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,42 @@
1
+ require 'command_kit/main'
2
+ require 'command_kit/env'
3
+ require 'command_kit/stdio'
4
+ require 'command_kit/printing'
5
+ require 'command_kit/usage'
6
+ require 'command_kit/arguments'
7
+ require 'command_kit/options'
8
+ require 'command_kit/examples'
9
+ require 'command_kit/description'
10
+ require 'command_kit/exception_handler'
11
+
12
+ module CommandKit
13
+ #
14
+ # The command class base-class.
15
+ #
16
+ # ## Examples
17
+ #
18
+ # class MyCmd < CommandKit::Command
19
+ #
20
+ # # ...
21
+ #
22
+ # end
23
+ #
24
+ # @note Command classes are not required to inherit from {Command}. This class
25
+ # only exists as a convenience.
26
+ #
27
+ class Command
28
+
29
+ include Main
30
+ include Env
31
+ include Stdio
32
+ include Printing
33
+ include Help
34
+ include Usage
35
+ include Arguments
36
+ include Options
37
+ include Examples
38
+ include Description
39
+ include ExceptionHandler
40
+
41
+ end
42
+ end
@@ -0,0 +1,95 @@
1
+ require 'command_kit/inflector'
2
+
3
+ module CommandKit
4
+ #
5
+ # Defines or derives a command class'es command-name.
6
+ #
7
+ # ## Examples
8
+ #
9
+ # ### Implicit
10
+ #
11
+ # class MyCmd
12
+ #
13
+ # include CommandKit::CommandName
14
+ #
15
+ # end
16
+ #
17
+ # MyCmd.command_name
18
+ # # => "my_cmd"
19
+ #
20
+ # ### Explicit
21
+ #
22
+ # class MyCmd
23
+ #
24
+ # include CommandKit::CommandName
25
+ #
26
+ # commnad_name 'foo-cmd'
27
+ #
28
+ # end
29
+ #
30
+ # MyCmd.command_name
31
+ # # => "foo-cmd"
32
+ #
33
+ module CommandName
34
+ module ModuleMethods
35
+ #
36
+ # Extends {ClassMethods} or {ModuleMethods}, depending on whether
37
+ # {CommandName} is being included into a class or a module.
38
+ #
39
+ # @param [Class, Module] context
40
+ # The class or module which is including {CommandName}.
41
+ #
42
+ def included(context)
43
+ super(context)
44
+
45
+ if context.class == Module
46
+ context.extend ModuleMethods
47
+ else
48
+ context.extend ClassMethods
49
+ end
50
+ end
51
+ end
52
+
53
+ extend ModuleMethods
54
+
55
+ #
56
+ # Defines class-level methods.
57
+ #
58
+ module ClassMethods
59
+ #
60
+ # Derives the command name from the class name.
61
+ #
62
+ # @param [String, nil] new_command_name
63
+ # If given a command name argument, it will override the derived
64
+ # command name.
65
+ #
66
+ # @return [String]
67
+ #
68
+ def command_name(new_command_name=nil)
69
+ if new_command_name
70
+ @command_name = new_command_name.to_s
71
+ else
72
+ @command_name || Inflector.underscore(Inflector.demodularize(name))
73
+ end
74
+ end
75
+ end
76
+
77
+ # The commands name.
78
+ #
79
+ # @return [String]
80
+ attr_reader :command_name
81
+
82
+ #
83
+ # Initializes command_name.
84
+ #
85
+ # @param [String] command_name
86
+ # Overrides the command name. Defaults to
87
+ # {ClassMethods#command_name self.class.command_name}.
88
+ #
89
+ def initialize(command_name: self.class.command_name, **kwargs)
90
+ @command_name = command_name
91
+
92
+ super(**kwargs)
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,299 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'command_kit/commands/subcommand'
4
+ require 'command_kit/commands/parent_command'
5
+ require 'command_kit/commands/help'
6
+ require 'command_kit/command_name'
7
+ require 'command_kit/usage'
8
+ require 'command_kit/options'
9
+ require 'command_kit/stdio'
10
+ require 'command_kit/env'
11
+
12
+ module CommandKit
13
+ #
14
+ # Adds sub-commands to a command.
15
+ #
16
+ # ## Examples
17
+ #
18
+ # class CLI
19
+ #
20
+ # include CommandKit::Commands
21
+ #
22
+ # command_name :foo
23
+ #
24
+ # class Foo < CommandKit::Command
25
+ # # ...
26
+ # end
27
+ #
28
+ # class FooBar < CommandKit::Command
29
+ # # ...
30
+ # end
31
+ #
32
+ # command Foo
33
+ # command 'foo-bar', FooBar
34
+ #
35
+ # end
36
+ #
37
+ module Commands
38
+ include CommandName
39
+ include Usage
40
+ include Options
41
+ include Stdio
42
+ include Env
43
+
44
+ module ModuleMethods
45
+ #
46
+ # Extends {ClassMethods} or {ModuleMethods}, depending on whether
47
+ # {Commands} is being included into a class or a module.
48
+ #
49
+ # @param [Class, Module] context
50
+ # The class or module which is including {Commands}.
51
+ #
52
+ def included(context)
53
+ super(context)
54
+
55
+ if context.class == Module
56
+ context.extend ModuleMethods
57
+ else
58
+ context.usage "[options] [COMMAND [ARGS...]]"
59
+ context.extend ClassMethods
60
+ context.command Help
61
+ end
62
+ end
63
+ end
64
+
65
+ extend ModuleMethods
66
+
67
+ #
68
+ # Class-level methods.
69
+ #
70
+ module ClassMethods
71
+ #
72
+ # The registered sub-commands.
73
+ #
74
+ # @return [Hash{String => Subcommand}]
75
+ # The Hash of sub-command names and command classes.
76
+ #
77
+ def commands
78
+ @commands ||= if superclass.kind_of?(ClassMethods)
79
+ superclass.commands.dup
80
+ else
81
+ {}
82
+ end
83
+ end
84
+
85
+ #
86
+ # The registered command aliases.
87
+ #
88
+ # @return [Hash{String => String}]
89
+ # The Hash of command aliases to primary command names.
90
+ #
91
+ def command_aliases
92
+ @command_aliases ||= if superclass.kind_of?(ClassMethods)
93
+ superclass.command_aliases.dup
94
+ else
95
+ {}
96
+ end
97
+ end
98
+
99
+ #
100
+ # Mounts a command as a sub-command.
101
+ #
102
+ # @param [#to_s] name
103
+ # The optional name to mount the command as. Defaults to the command's
104
+ # {CommandName::ClassMethods#command_name command_name}.
105
+ #
106
+ # @param [Class#main] command_class
107
+ # The sub-command class.
108
+ #
109
+ # @param [Hash{Symbol => Object}] kwargs
110
+ # Keyword arguments.
111
+ #
112
+ # @option kwargs [String, nil] summary
113
+ # A short summary for the subcommand. Defaults to the first sentence
114
+ # of the command.
115
+ #
116
+ # @option kwags [Array<String>] aliases
117
+ # Optional alias names for the subcommand.
118
+ #
119
+ # @return [Subcommand]
120
+ # The registered sub-command class.
121
+ #
122
+ # @example
123
+ # command Foo
124
+ #
125
+ # @example
126
+ # command 'foo-bar', FooBar
127
+ #
128
+ def command(name=nil, command_class, **kwargs)
129
+ name = if name then name.to_s
130
+ else command_class.command_name
131
+ end
132
+
133
+ subcommand = Subcommand.new(command_class,**kwargs)
134
+
135
+ commands[name] = subcommand
136
+
137
+ subcommand.aliases.each do |command_alias|
138
+ command_aliases[command_alias] = name
139
+ end
140
+
141
+ return subcommand
142
+ end
143
+
144
+ #
145
+ # Gets the command.
146
+ #
147
+ # @param [String] name
148
+ #
149
+ # @return [Class#main, nil]
150
+ #
151
+ def get_command(name)
152
+ name = name.to_s
153
+ name = command_aliases.fetch(name,name)
154
+
155
+ if (subcommand = commands[name])
156
+ subcommand.command
157
+ end
158
+ end
159
+ end
160
+
161
+ def initialize(**kwargs)
162
+ super(**kwargs)
163
+
164
+ @option_parser.on(/^[^-].*$/) do |command|
165
+ OptionParser.terminate(command)
166
+ end
167
+ end
168
+
169
+ #
170
+ # Looks up the given command name and initializes a subcommand.
171
+ #
172
+ # @param [#to_s] name
173
+ # The given command name.
174
+ #
175
+ # @return [Object#main, nil]
176
+ # The initialized subcommand.
177
+ #
178
+ def command(name)
179
+ unless (command_class = self.class.get_command(name))
180
+ return
181
+ end
182
+
183
+ kwargs = {}
184
+
185
+ if command_class.include?(ParentCommand)
186
+ kwargs[:parent_command] = self
187
+ end
188
+
189
+ if command_class.include?(CommandName)
190
+ kwargs[:command_name] = "#{command_name} #{command_class.command_name}"
191
+ end
192
+
193
+ if command_class.include?(Stdio)
194
+ kwargs[:stdin] = stdin
195
+ kwargs[:stdout] = stdout
196
+ kwargs[:stderr] = stderr
197
+ end
198
+
199
+ if command_class.include?(Env)
200
+ kwargs[:env] = env.dup
201
+ end
202
+
203
+ if command_class.include?(Options)
204
+ kwargs[:options] = options.dup
205
+ end
206
+
207
+ return command_class.new(**kwargs)
208
+ end
209
+
210
+ #
211
+ # Invokes the command with the given argv.
212
+ #
213
+ # @param [String] name
214
+ # The name of the command to invoke.
215
+ #
216
+ # @param [Array<String>] argv
217
+ # The additional arguments to pass to the command.
218
+ #
219
+ # @return [Integer]
220
+ # The exit status of the command.
221
+ #
222
+ def invoke(name,*argv)
223
+ if (command = command(name))
224
+ command.main(argv)
225
+ else
226
+ on_unknown_command(name,argv)
227
+ end
228
+ end
229
+
230
+ #
231
+ # Prints an error about an unknown command and exits with an error code.
232
+ #
233
+ # @param [String] name
234
+ #
235
+ def command_not_found(name)
236
+ print_error "'#{name}' is not a #{command_name} command. See `#{command_name} help`"
237
+ exit(1)
238
+ end
239
+
240
+ #
241
+ # Place-holder method that is called when the subcommand is not known.
242
+ #
243
+ # @param [String] name
244
+ # The given sub-command name.
245
+ #
246
+ # @param [Array<String>] argv
247
+ # Additional argv.
248
+ #
249
+ # @abstract
250
+ #
251
+ # @see command_not_found
252
+ #
253
+ def on_unknown_command(name,argv=[])
254
+ command_not_found(name)
255
+ end
256
+
257
+ #
258
+ # Runs the command or specified subcommand.
259
+ #
260
+ # @note If no subcommand is given, {#help} will be called.
261
+ #
262
+ def run(command=nil,*argv)
263
+ if command
264
+ exit invoke(command,*argv)
265
+ else
266
+ help
267
+ end
268
+ end
269
+
270
+ #
271
+ # Prints the available commands and their summaries.
272
+ #
273
+ def help_commands
274
+ unless self.class.commands.empty?
275
+ puts
276
+ puts "Commands:"
277
+
278
+ self.class.commands.sort.each do |name,subcommand|
279
+ names = [name, *subcommand.aliases].join(', ')
280
+
281
+ if subcommand.summary
282
+ puts " #{names}\t#{subcommand.summary}"
283
+ else
284
+ puts " #{names}"
285
+ end
286
+ end
287
+ end
288
+ end
289
+
290
+ #
291
+ # Prints help information and available commands.
292
+ #
293
+ def help
294
+ super
295
+
296
+ help_commands
297
+ end
298
+ end
299
+ end
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'command_kit/commands'
4
+ require 'command_kit/commands/auto_load/subcommand'
5
+ require 'command_kit/inflector'
6
+
7
+ module CommandKit
8
+ module Commands
9
+ #
10
+ # Provides lazy-loading access to a directory / module namespace of
11
+ # command classes.
12
+ #
13
+ # ## Examples
14
+ #
15
+ # class CLI
16
+ #
17
+ # include CommandKit::Commands::AutoLoad.new(
18
+ # dir: "#{__dir__}/cli/commands",
19
+ # namespace: 'CLI::Commands'
20
+ # )
21
+ #
22
+ # end
23
+ #
24
+ # ### Explicit Mapping
25
+ #
26
+ # class CLI
27
+ #
28
+ # include CommandKit::Commands::AutoLoad.new(
29
+ # dir: "#{__dir__}/cli/commands",
30
+ # namespace: 'CLI::Commands'
31
+ # ) { |autoload|
32
+ # autoload.command 'foo', 'Foo', 'foo.rb', summary: 'Foo command'
33
+ # autoload.command 'bar', 'Bar', 'bar.rb', summary: 'Bar command'
34
+ # }
35
+ #
36
+ # end
37
+ #
38
+ class AutoLoad < Module
39
+
40
+ # The auto-load subcommands.
41
+ #
42
+ # @return [Hash{String => Subcommand}]
43
+ attr_reader :commands
44
+
45
+ # The path to the directory containing the command files.
46
+ #
47
+ # @return [String]
48
+ attr_reader :dir
49
+
50
+ # The namespace that the will contain the command classes.
51
+ #
52
+ # @return [String]
53
+ attr_reader :namespace
54
+
55
+ #
56
+ # Initializes the namespace.
57
+ #
58
+ # @param [String] dir
59
+ # The path to the directory containing the command files.
60
+ #
61
+ # @param [Module, Class, String] namespace
62
+ # The namespace constant that contains the command classes.
63
+ #
64
+ # @yield [self]
65
+ # If a block is given, it will be used to explicitly map the files
66
+ # within {#dir} as commands.
67
+ #
68
+ def initialize(dir: , namespace: )
69
+ @commands = {}
70
+
71
+ @dir = dir
72
+ @namespace = namespace
73
+
74
+ if block_given?
75
+ yield self
76
+ else
77
+ files.each do |path|
78
+ base_name = File.basename(path)
79
+ file_name = base_name.chomp('.rb')
80
+ command_name = Inflector.dasherize(file_name)
81
+ class_name = Inflector.camelize(file_name)
82
+
83
+ command command_name, class_name, base_name
84
+ end
85
+ end
86
+ end
87
+
88
+ #
89
+ # Defines an auto-loaded command mapping.
90
+ #
91
+ # @param [#to_s] name
92
+ # The name of the command.
93
+ #
94
+ # @param [String] constant
95
+ # The constant name of the command class.
96
+ #
97
+ # @param [String] file
98
+ # The file name of the command class.
99
+ #
100
+ # @param [Hash{Symbol => Object}] kwargs
101
+ # Keyword arguments.
102
+ #
103
+ # @option kwargs [String, nil] summary
104
+ # An optional summary for the command.
105
+ #
106
+ # @option kwargs [Array<String>] aliases
107
+ # Optional alias names for the subcommand.
108
+ #
109
+ def command(name, constant, file, **kwargs)
110
+ @commands[name.to_s] = Subcommand.new(
111
+ "#{@namespace}::#{constant}",
112
+ join(file),
113
+ **kwargs
114
+ )
115
+ end
116
+
117
+ #
118
+ # Joins a relative path with {#dir}.
119
+ #
120
+ # @param [String] path
121
+ # The relative path.
122
+ #
123
+ # @return [String]
124
+ # The joined absolute path.
125
+ #
126
+ def join(path)
127
+ File.join(@dir,path)
128
+ end
129
+
130
+ #
131
+ # Returns the files within given directory.
132
+ #
133
+ # @return [Array<String>]
134
+ # The paths to the `.rb` files in the directory.
135
+ #
136
+ def files
137
+ Dir.glob(join('*.rb'))
138
+ end
139
+
140
+ #
141
+ # Includes {Commands} and registers all files within the namespace
142
+ # as lazy-loaded subcommands.
143
+ #
144
+ # @param [Class] command
145
+ # The command class including {AutoLoad}.
146
+ #
147
+ def included(command)
148
+ command.include Commands
149
+ command.commands.merge!(@commands)
150
+ end
151
+ end
152
+ end
153
+ end