command_kit 0.1.0.pre1

Sign up to get free protection for your applications and to get access to all the features.
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