command_kit 0.1.0.pre1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. checksums.yaml +7 -0
  2. data/.document +3 -0
  3. data/.github/workflows/ruby.yml +29 -0
  4. data/.gitignore +7 -0
  5. data/.rspec +1 -0
  6. data/.yardopts +1 -0
  7. data/ChangeLog.md +29 -0
  8. data/Gemfile +14 -0
  9. data/LICENSE.txt +20 -0
  10. data/README.md +283 -0
  11. data/Rakefile +23 -0
  12. data/command_kit.gemspec +60 -0
  13. data/gemspec.yml +14 -0
  14. data/lib/command_kit.rb +1 -0
  15. data/lib/command_kit/arguments.rb +161 -0
  16. data/lib/command_kit/arguments/argument.rb +111 -0
  17. data/lib/command_kit/arguments/argument_value.rb +81 -0
  18. data/lib/command_kit/arguments/usage.rb +6 -0
  19. data/lib/command_kit/colors.rb +355 -0
  20. data/lib/command_kit/command.rb +42 -0
  21. data/lib/command_kit/command_name.rb +95 -0
  22. data/lib/command_kit/commands.rb +299 -0
  23. data/lib/command_kit/commands/auto_load.rb +153 -0
  24. data/lib/command_kit/commands/auto_load/subcommand.rb +90 -0
  25. data/lib/command_kit/commands/auto_require.rb +138 -0
  26. data/lib/command_kit/commands/command.rb +12 -0
  27. data/lib/command_kit/commands/help.rb +43 -0
  28. data/lib/command_kit/commands/parent_command.rb +21 -0
  29. data/lib/command_kit/commands/subcommand.rb +51 -0
  30. data/lib/command_kit/console.rb +141 -0
  31. data/lib/command_kit/description.rb +89 -0
  32. data/lib/command_kit/env.rb +43 -0
  33. data/lib/command_kit/env/home.rb +71 -0
  34. data/lib/command_kit/env/path.rb +71 -0
  35. data/lib/command_kit/examples.rb +99 -0
  36. data/lib/command_kit/exception_handler.rb +55 -0
  37. data/lib/command_kit/help.rb +62 -0
  38. data/lib/command_kit/help/man.rb +125 -0
  39. data/lib/command_kit/inflector.rb +84 -0
  40. data/lib/command_kit/main.rb +103 -0
  41. data/lib/command_kit/options.rb +179 -0
  42. data/lib/command_kit/options/option.rb +171 -0
  43. data/lib/command_kit/options/option_value.rb +90 -0
  44. data/lib/command_kit/options/parser.rb +227 -0
  45. data/lib/command_kit/options/quiet.rb +53 -0
  46. data/lib/command_kit/options/usage.rb +6 -0
  47. data/lib/command_kit/options/verbose.rb +55 -0
  48. data/lib/command_kit/options/version.rb +62 -0
  49. data/lib/command_kit/os.rb +47 -0
  50. data/lib/command_kit/pager.rb +115 -0
  51. data/lib/command_kit/printing.rb +32 -0
  52. data/lib/command_kit/printing/indent.rb +78 -0
  53. data/lib/command_kit/program_name.rb +57 -0
  54. data/lib/command_kit/stdio.rb +138 -0
  55. data/lib/command_kit/usage.rb +102 -0
  56. data/lib/command_kit/version.rb +4 -0
  57. data/lib/command_kit/xdg.rb +138 -0
  58. data/spec/arguments/argument_spec.rb +169 -0
  59. data/spec/arguments/argument_value_spec.rb +126 -0
  60. data/spec/arguments_spec.rb +213 -0
  61. data/spec/colors_spec.rb +470 -0
  62. data/spec/command_kit_spec.rb +8 -0
  63. data/spec/command_name_spec.rb +130 -0
  64. data/spec/command_spec.rb +49 -0
  65. data/spec/commands/auto_load/subcommand_spec.rb +82 -0
  66. data/spec/commands/auto_load_spec.rb +128 -0
  67. data/spec/commands/auto_require_spec.rb +142 -0
  68. data/spec/commands/fixtures/test_auto_load/cli/commands/test1.rb +10 -0
  69. data/spec/commands/fixtures/test_auto_load/cli/commands/test2.rb +10 -0
  70. data/spec/commands/fixtures/test_auto_require/lib/test_auto_require/cli/commands/test1.rb +10 -0
  71. data/spec/commands/help_spec.rb +66 -0
  72. data/spec/commands/parent_command_spec.rb +40 -0
  73. data/spec/commands/subcommand_spec.rb +99 -0
  74. data/spec/commands_spec.rb +767 -0
  75. data/spec/console_spec.rb +201 -0
  76. data/spec/description_spec.rb +203 -0
  77. data/spec/env/home_spec.rb +46 -0
  78. data/spec/env/path_spec.rb +78 -0
  79. data/spec/env_spec.rb +123 -0
  80. data/spec/examples_spec.rb +235 -0
  81. data/spec/exception_handler_spec.rb +103 -0
  82. data/spec/help_spec.rb +119 -0
  83. data/spec/inflector_spec.rb +104 -0
  84. data/spec/main_spec.rb +179 -0
  85. data/spec/options/option_spec.rb +258 -0
  86. data/spec/options/option_value_spec.rb +67 -0
  87. data/spec/options/parser_spec.rb +265 -0
  88. data/spec/options_spec.rb +137 -0
  89. data/spec/os_spec.rb +46 -0
  90. data/spec/pager_spec.rb +154 -0
  91. data/spec/printing/indent_spec.rb +130 -0
  92. data/spec/printing_spec.rb +76 -0
  93. data/spec/program_name_spec.rb +62 -0
  94. data/spec/spec_helper.rb +6 -0
  95. data/spec/stdio_spec.rb +264 -0
  96. data/spec/usage_spec.rb +237 -0
  97. data/spec/xdg_spec.rb +191 -0
  98. metadata +156 -0
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandKit
4
+ module Help
5
+ #
6
+ # Allows displaying a man-page instead of the usual `--help` output.
7
+ #
8
+ # ## Environment Variables
9
+ #
10
+ # * `TERM` - Specifies the type of terminal. When set to `DUMB`, it will
11
+ # disable man-page help output.
12
+ #
13
+ module Man
14
+ module ModuleMethods
15
+ #
16
+ # Extends {ClassMethods} or {ModuleMethods}, depending on whether
17
+ # {Help::Man} is being included into a class or a module.
18
+ #
19
+ # @param [Class, Module] context
20
+ # The class or module including {Man}.
21
+ #
22
+ def included(context)
23
+ super(context)
24
+
25
+ if context.class == Module
26
+ context.extend ModuleMethods
27
+ else
28
+ context.extend ClassMethods
29
+ end
30
+ end
31
+ end
32
+
33
+ extend ModuleMethods
34
+
35
+ #
36
+ # Defines class-level methods.
37
+ #
38
+ module ClassMethods
39
+ #
40
+ # Gets or sets the directory where man-pages are stored.
41
+ #
42
+ # @param [String, nil] new_man_dir
43
+ # If a String is given, it will set The class'es man-page directory.
44
+ #
45
+ # @return [String, nil]
46
+ # The class'es or superclass'es man-page directory.
47
+ #
48
+ # @example
49
+ # man_dir File.expand_path('../../../man',__FILE__)
50
+ #
51
+ def man_dir(new_man_dir=nil)
52
+ if new_man_dir
53
+ @man_dir = File.expand_path(new_man_dir)
54
+ else
55
+ @man_dir || (superclass.man_dir if superclass.kind_of?(ClassMethods))
56
+ end
57
+ end
58
+ end
59
+
60
+ #
61
+ # Determines if displaying man pages is supported.
62
+ #
63
+ # @return [Boolean]
64
+ # Indicates whether the `TERM` environment variable is not `dumb`
65
+ # and `$stdout` is a TTY.
66
+ #
67
+ def self.supported?
68
+ ENV['TERM'] != 'dumb' && $stdout.tty?
69
+ end
70
+
71
+ #
72
+ # Returns the man-page file name for the given command name.
73
+ #
74
+ # @param [String] command
75
+ # The given command name.
76
+ #
77
+ # @return [String]
78
+ # The man-page file name.
79
+ #
80
+ def man_page(command=command_name)
81
+ "#{command}.1"
82
+ end
83
+
84
+ #
85
+ # Displays the given man page.
86
+ #
87
+ # @param [String] page
88
+ # The man page file name.
89
+ #
90
+ # @return [Boolean, nil]
91
+ # Specifies whether the `man` command was successful or not.
92
+ # Returns `nil` when the `man` command is not installed.
93
+ #
94
+ def man(page=man_page)
95
+ system('man',page)
96
+ end
97
+
98
+ #
99
+ # Displays the {#man_page} instead of the usual `--help` output.
100
+ #
101
+ # @raise [NotImplementedError]
102
+ # {ClassMethods#man_dir man_dir} does not have a value.
103
+ #
104
+ # @note
105
+ # if `TERM` is `dumb` or `$stdout` is not a TTY, fallsback to printing
106
+ # the usual `--help` output.
107
+ #
108
+ def help
109
+ if Man.supported?
110
+ unless self.class.man_dir
111
+ raise(NotImplementedError,"#{self.class}.man_dir not set")
112
+ end
113
+
114
+ man_path = File.join(self.class.man_dir,man_page)
115
+
116
+ if man(man_path).nil?
117
+ super
118
+ end
119
+ else
120
+ super
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandKit
4
+ #
5
+ # A very simple inflector.
6
+ #
7
+ # @note
8
+ # If you need something more powerful, checkout
9
+ # [dry-inflector](https://dry-rb.org/gems/dry-inflector/0.1/)
10
+ #
11
+ module Inflector
12
+ #
13
+ # Removes the namespace from a constant name.
14
+ #
15
+ # @param [#to_s] name
16
+ # The constant name.
17
+ #
18
+ # @return [String]
19
+ # The class or module's name, without the namespace.
20
+ #
21
+ def self.demodularize(name)
22
+ name.to_s.split('::').last
23
+ end
24
+
25
+ #
26
+ # Converts a CamelCased name to an under_scored name.
27
+ #
28
+ # @param [#to_s] name
29
+ # The CamelCased name.
30
+ #
31
+ # @return [String]
32
+ # The resulting under_scored name.
33
+ #
34
+ def self.underscore(name)
35
+ # sourced from: https://github.com/dry-rb/dry-inflector/blob/c918f967ff82611da374eb0847a77b7e012d3fa8/lib/dry/inflector.rb#L286-L287
36
+ name = name.to_s.dup
37
+
38
+ name.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2')
39
+ name.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
40
+ name.tr!('-','_')
41
+ name.downcase!
42
+
43
+ name
44
+ end
45
+
46
+ #
47
+ # Replaces all underscores with dashes.
48
+ #
49
+ # @param [#to_s] name
50
+ # The under_scored name.
51
+ #
52
+ # @return [String]
53
+ # The dasherized name.
54
+ #
55
+ def self.dasherize(name)
56
+ name.to_s.tr('_','-')
57
+ end
58
+
59
+ #
60
+ # Converts an under_scored name to a CamelCased name.
61
+ #
62
+ # @param [String] name
63
+ # The under_scored name.
64
+ #
65
+ # @return [String]
66
+ # The CamelCased name.
67
+ #
68
+ def self.camelize(name)
69
+ name = name.to_s.dup
70
+
71
+ # sourced from: https://github.com/dry-rb/dry-inflector/blob/c918f967ff82611da374eb0847a77b7e012d3fa8/lib/dry/inflector.rb#L329-L334
72
+ name.sub!(/^[a-z\d]*/,&:capitalize)
73
+ name.gsub!(%r{(?:[_-]|(/))([a-z\d]*)}i) do |match|
74
+ slash = Regexp.last_match(1)
75
+ word = Regexp.last_match(2)
76
+
77
+ "#{slash}#{word.capitalize}"
78
+ end
79
+
80
+ name.gsub!('/','::')
81
+ name
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,103 @@
1
+ module CommandKit
2
+ #
3
+ # Defines a `main` method.
4
+ #
5
+ # ## Examples
6
+ #
7
+ # include CommandKit::Main
8
+ #
9
+ # def main(argv=[])
10
+ # # ...
11
+ # return 0
12
+ # end
13
+ #
14
+ module Main
15
+ module ModuleMethods
16
+ #
17
+ # Extends {ClassMethods} or {ModuleMethods}, depending on whether {Main}
18
+ # is being included into a class or a module.
19
+ #
20
+ # @param [Class, Module] context
21
+ # The class or module which is including {Main}.
22
+ #
23
+ def included(context)
24
+ super(context)
25
+
26
+ if context.class == Module
27
+ context.extend ModuleMethods
28
+ else
29
+ context.extend ClassMethods
30
+ end
31
+ end
32
+ end
33
+
34
+ extend ModuleMethods
35
+
36
+ #
37
+ # Class-level methods.
38
+ #
39
+ module ClassMethods
40
+ #
41
+ # Starts the command and then exits.
42
+ #
43
+ # @param [Array<String>] argv
44
+ # The Array of command-line arguments.
45
+ #
46
+ def start(argv=ARGV, **kwargs)
47
+ begin
48
+ exit main(argv, **kwargs)
49
+ rescue Interrupt
50
+ # https://tldp.org/LDP/abs/html/exitcodes.html
51
+ exit 130
52
+ rescue Errno::EPIPE
53
+ # STDOUT pipe broken
54
+ exit 0
55
+ end
56
+ end
57
+
58
+ #
59
+ # Initializes the command class with the given keyword arguments, then
60
+ # calls {Main#main main} with the given `argv`.
61
+ #
62
+ # @param [Array<String>] argv
63
+ # The Array of command-line arguments.
64
+ #
65
+ # @param [Hash{Symbol => Object}] kwargs
66
+ # Additional keyword arguments to initialize the command class with.
67
+ #
68
+ # @return [Integer]
69
+ # The exit status of the command.
70
+ #
71
+ def main(argv=[], **kwargs)
72
+ new(**kwargs).main(argv)
73
+ end
74
+ end
75
+
76
+ #
77
+ # Place-holder `main` method, which parses options, before calling {#run}.
78
+ #
79
+ # @param [Array<String>] argv
80
+ # The Array of command-line arguments.
81
+ #
82
+ # @return [Integer]
83
+ # The exit status code.
84
+ #
85
+ def main(argv=[])
86
+ run(*argv)
87
+ return 0
88
+ rescue SystemExit => system_exit
89
+ return system_exit.status
90
+ end
91
+
92
+ #
93
+ # Place-holder method for command business logic.
94
+ #
95
+ # @param [Array<Object>] args
96
+ # Additional arguments for the command.
97
+ #
98
+ # @abstract
99
+ #
100
+ def run(*args)
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,179 @@
1
+ require 'command_kit/options/option'
2
+ require 'command_kit/options/parser'
3
+
4
+ module CommandKit
5
+ #
6
+ # Provides a thin DSL for defining options as attributes.
7
+ #
8
+ # ## Examples
9
+ #
10
+ # include CommandKit::Options
11
+ #
12
+ # option :foo, type: String,
13
+ # short: '-f',
14
+ # desc: "Foo option"
15
+ #
16
+ # option :bar, type: String,
17
+ # short: '-b',
18
+ # usage: 'STR:STR:...',
19
+ # desc: "Bar option" do |arg|
20
+ # @bar = arg.split(':')
21
+ # end
22
+ #
23
+ # option :number, type: Integer,
24
+ # desc: 'Numbers' do |num|
25
+ # @numbers << num
26
+ # end
27
+ #
28
+ # def initialize
29
+ # super
30
+ #
31
+ # @numbers = []
32
+ # end
33
+ #
34
+ module Options
35
+ include Parser
36
+
37
+ module ModuleMethods
38
+ #
39
+ # Extends {ClassMethods} or {ModuleMethods}, depending on whether
40
+ # {Options} is being included into a class or a module.
41
+ #
42
+ # @param [Class, Module] context
43
+ # The class or module which is including {Options}.
44
+ #
45
+ def included(context)
46
+ super(context)
47
+
48
+ if context.class == Module
49
+ context.extend ModuleMethods
50
+ else
51
+ context.extend ClassMethods
52
+ end
53
+ end
54
+ end
55
+
56
+ extend ModuleMethods
57
+
58
+ #
59
+ # Defines class-level methods.
60
+ #
61
+ module ClassMethods
62
+ #
63
+ # All defined options for the command class.
64
+ #
65
+ # @return [Hash{Symbol => Option}]
66
+ #
67
+ def options
68
+ @options ||= if superclass.kind_of?(ClassMethods)
69
+ superclass.options.dup
70
+ else
71
+ {}
72
+ end
73
+ end
74
+
75
+ #
76
+ # Defines an {Option option} for the class.
77
+ #
78
+ # @param [Symbol] name
79
+ # The option name.
80
+ #
81
+ # @yield [(value)]
82
+ # If a block is given, it will be passed the parsed option value.
83
+ #
84
+ # @yieldparam [Object, nil] value
85
+ # The parsed option value.
86
+ #
87
+ # @return [Option]
88
+ #
89
+ # @example Define an option:
90
+ # option :foo, desc: "Foo option"
91
+ #
92
+ # @example With a custom short option:
93
+ # option :foo, short: '-f',
94
+ # desc: "Foo option"
95
+ #
96
+ # @example With a custom long option:
97
+ # option :foo, short: '--foo-opt',
98
+ # desc: "Foo option"
99
+ #
100
+ # @example With a custom usage string:
101
+ # option :foo, value: {usage: 'FOO'},
102
+ # desc: "Foo option"
103
+ #
104
+ # @example With a custom block:
105
+ # option :foo, desc: "Foo option" do |value|
106
+ # @foo = Foo.new(value)
107
+ # end
108
+ #
109
+ # @example With a custom type:
110
+ # option :foo, value: {type: Integer},
111
+ # desc: "Foo option"
112
+ #
113
+ # @example With a default value:
114
+ # option :foo, value: {type: Integer, default: 1},
115
+ # desc: "Foo option"
116
+ #
117
+ # @example With a required value:
118
+ # option :foo, value: {type: String, required: true},
119
+ # desc: "Foo option"
120
+ #
121
+ # @example With a custom option value Hash map:
122
+ # option :flag, value: {
123
+ # type: {
124
+ # 'enabled' => :enabled,
125
+ # 'yes' => :enabled,
126
+ # 'disabled' => :disabled,
127
+ # 'no' => :disabled
128
+ # }
129
+ # },
130
+ # desc: "Flag option"
131
+ #
132
+ # @example With a custom option value Array enum:
133
+ # option :enum, value: {type: %w[yes no]},
134
+ # desc: "Enum option"
135
+ #
136
+ # @example With a custom option value Regexp:
137
+ # option :date, value: {type: /(\d+)-(\d+)-(\d{2,4})/},
138
+ # desc: "Regexp optin" do |date,d,m,y|
139
+ # # ...
140
+ # end
141
+ #
142
+ def option(name,**kwargs,&block)
143
+ options[name] = Option.new(name,**kwargs,&block)
144
+ end
145
+ end
146
+
147
+ # Hash of parsed option values.
148
+ #
149
+ # @return [Hash{Symbol => Object}]
150
+ attr_reader :options
151
+
152
+ #
153
+ # Initializes {#options} and populates the
154
+ # {Parser#option_parser option parser}.
155
+ #
156
+ # @param [Hash{Symbol => Object}] options
157
+ # Optional pre-populated options hash.
158
+ #
159
+ def initialize(options: {}, **kwargs)
160
+ @options = options
161
+
162
+ super(**kwargs)
163
+
164
+ self.class.options.each_value do |option|
165
+ option_parser.on(*option.usage,option.type,option.desc) do |arg,*captures|
166
+ @options[option.name] = if arg.nil?
167
+ option.default_value
168
+ else
169
+ arg
170
+ end
171
+
172
+ if option.block
173
+ instance_exec(*arg,*captures,&option.block)
174
+ end
175
+ end
176
+ end
177
+ end
178
+ end
179
+ end