rodish 1.1.0 → 2.0.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 26579ef8c44c72bdb1176ab1d1916704a7c98526e65280c2380ea2eb266865eb
4
- data.tar.gz: 9920dad59ac41bec67cbafcd5a6d4267a4d83055b095066f5f7d9020e7333a67
3
+ metadata.gz: fe9eb4dc1a9f6cc1e7582a9b24dcbc3491f47e3a3aa24def4ff3f94a241d1dbc
4
+ data.tar.gz: 90748c4035108090d6576a824e6a6e1699079a755bbf405ce0484d238cd6ee1b
5
5
  SHA512:
6
- metadata.gz: 3cb162c995baa0e74b483430173019b155710de995d684c8af21bdb583d2f14a129b55b09b871c87d33a94148be807f6841185428648acf1db562286fe8fbe3d
7
- data.tar.gz: 83c8d247e109d3c8cd400713d1080f8e1bcc598028043ca21c31db11e2736c4087b918d97d0dd7cd5b42974fc65ff1be56f8b96cf9a5b21737f710aba227a568
6
+ metadata.gz: 01030a76bc9a5db02ae2b78ddd9bcdb24b18d0b32d9421dffc41f92041700a062a2f2c13260656cce79521f07abccb3ed5da7b4efeb3e93b5de06c246f2dfdfa
7
+ data.tar.gz: fe7ea63452c88a46dbe528c33b8f2a54d965f93c47ebc9a4984cd985c5a4ec73c777c4f4fc8c08973783ee0f6eb0957fc0b9804485d91d35dc41fee0e51620f3
@@ -1,35 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "option_parser"
4
- require_relative "skip_option_parser"
5
4
  require_relative "errors"
6
5
 
7
6
  module Rodish
8
7
  # Rodish::Command is the main object in Rodish's processing.
9
8
  # It handles a single command, and may have one or more
10
- # subcommands and/or post subcommands, forming a tree.
9
+ # subcommands, forming a tree.
11
10
  #
12
11
  # Rodish's argv processing starts with the root command,
13
12
  # processing options and deleting appropriately to subcommands,
14
13
  # until the requested command or subcommand is located,
15
14
  # which is then executed.
16
15
  class Command
17
- option_parser = OptionParser.new
18
- option_parser.set_banner("")
19
- option_parser.freeze
20
-
21
- # The default option parser if no options are given for
22
- # the command.
23
- DEFAULT_OPTION_PARSER = option_parser
24
-
25
16
  # A hash of subcommands for the command. Keys are
26
17
  # subcommand name strings.
27
18
  attr_reader :subcommands
28
19
 
29
- # A hash of post subcommands for the command. Keys are
30
- # post subcommand name strings.
31
- attr_reader :post_subcommands
32
-
33
20
  # The block to execute if this command is the requested
34
21
  # subcommand. May be nil if this subcommand cannot be
35
22
  # executed, and can only dispatch to subcommands.
@@ -48,36 +35,20 @@ module Rodish
48
35
  # are placed directly in the options hash.
49
36
  attr_accessor :option_key
50
37
 
51
- # The post option parser for the current command. Called
52
- # only before dispatching to post subcommands.
53
- attr_accessor :post_option_parser
54
-
55
- # Similar to +option_key+, but for post options instead
56
- # of normal subcommands.
57
- attr_accessor :post_option_key
58
-
59
- # A hook to execute after parsing options for the command.
60
- attr_accessor :after_options
61
-
62
- # A hook to execute before executing the current
63
- # command or dispatching to subcommands. This will not
64
- # be called if an invalid subcommand is given and no
65
- # run block is present.
66
- attr_accessor :before
67
-
68
38
  # The number of arguments the run block will accept.
69
39
  # Should be either an integer or a range of integers.
70
40
  attr_accessor :num_args
71
41
 
72
- # The error message to use if an invalid number of
73
- # arguments is provided.
74
- attr_accessor :invalid_args_message
42
+ # A description for the command
43
+ attr_accessor :desc
44
+
45
+ # A usage banner for the command or subcommands.
46
+ attr_accessor :banner
75
47
 
76
48
  def initialize(command_path)
77
49
  @command_path = command_path
78
- @command_name = command_path.join(" ").freeze
50
+ @command_name = _command_name(command_path)
79
51
  @subcommands = {}
80
- @post_subcommands = {}
81
52
  @num_args = 0
82
53
  end
83
54
 
@@ -86,93 +57,72 @@ module Rodish
86
57
  def freeze
87
58
  @subcommands.each_value(&:freeze)
88
59
  @subcommands.freeze
89
- @post_subcommands.each_value(&:freeze)
90
- @post_subcommands.freeze
91
60
  @option_parser.freeze
92
- @post_option_parser.freeze
93
61
  super
94
62
  end
95
63
 
96
- # Run a post subcommand using the given context (generally self),
97
- # options, and argv. Usually called inside a run block, after
98
- # shifting one or more values off the given argv:
99
- #
100
- # run do |argv, opts, command|
101
- # @name = argv.shift
102
- # command.run(self, opts, argv)
103
- # end
104
- def run(context, options, argv)
105
- begin
106
- process_options(argv, options, @post_option_key, @post_option_parser)
107
- rescue ::OptionParser::InvalidOption => e
108
- raise CommandFailure.new(e.message, @post_option_parser)
109
- end
64
+ # Return a help string for the command.
65
+ def help
66
+ help_lines.join("\n")
67
+ end
110
68
 
111
- arg = argv[0]
112
- if arg && @post_subcommands[arg]
113
- process_subcommand(@post_subcommands, context, options, argv)
114
- else
115
- process_command_failure(arg, @post_subcommands, @post_option_parser, "post ")
69
+ # Return an array of help strings for the command.
70
+ def help_lines
71
+ output = []
72
+ help_order.each do |type|
73
+ send(:"_help_#{type}", output)
116
74
  end
75
+ output
76
+ end
77
+
78
+ # Process options for the command using the option key and parser.
79
+ def process_command_options(context, options, argv)
80
+ process_options(argv, options, @option_key, @option_parser)
117
81
  end
118
- alias run_post_subcommand run
119
82
 
120
83
  # Process the current command. This first processes the options.
121
84
  # After processing the options, it checks if the first argument
122
85
  # in the remaining argv is a subcommand. If so, it dispatches to
123
86
  # that subcommand. If not, it dispatches to the run block.
124
87
  def process(context, options, argv)
125
- process_options(argv, options, @option_key, @option_parser)
126
- context.instance_exec(argv, options, &after_options) if after_options
88
+ process_command_options(context, options, argv)
127
89
 
128
90
  arg = argv[0]
129
91
  if argv && @subcommands[arg]
130
92
  process_subcommand(@subcommands, context, options, argv)
131
93
  elsif run_block
132
94
  if valid_args?(argv)
133
- context.instance_exec(argv, options, &before) if before
134
-
135
95
  if @num_args.is_a?(Integer)
136
96
  context.instance_exec(*argv, options, self, &run_block)
137
97
  else
138
98
  context.instance_exec(argv, options, self, &run_block)
139
99
  end
140
- elsif @invalid_args_message
141
- raise_failure("invalid arguments#{subcommand_name} (#{@invalid_args_message})")
142
100
  else
143
- raise_failure("invalid number of arguments#{subcommand_name} (accepts: #{@num_args}, given: #{argv.length})")
101
+ raise_invalid_args_failure(argv)
144
102
  end
145
103
  else
146
- process_command_failure(arg, @subcommands, @option_parser, "")
104
+ process_command_failure(arg, @subcommands, "")
147
105
  end
148
106
  rescue ::OptionParser::InvalidOption => e
149
- if @option_parser || @post_option_parser
150
- raise_failure(e.message)
151
- else
152
- raise
153
- end
107
+ raise_failure(e.message)
154
108
  end
155
109
 
156
- # This yields the current command and all subcommands and
157
- # post subcommands, recursively.
110
+ # This yields the current command and all subcommands, recursively.
158
111
  def each_subcommand(names = [].freeze, &block)
159
112
  yield names, self
160
113
  _each_subcommand(names, @subcommands, &block)
161
- _each_subcommand(names, @post_subcommands, &block)
162
114
  end
163
115
 
164
- # Raise a CommandFailure with the given error and the given
165
- # option parsers.
166
- def raise_failure(message, option_parsers = self.option_parsers)
167
- raise CommandFailure.new(message, option_parsers)
116
+ # Yield each banner string (if any) to the block.
117
+ def each_banner
118
+ yield banner if banner
119
+ nil
168
120
  end
169
121
 
170
- # Returns a string of options text for the command's option parsers.
171
- def options_text
172
- option_parsers = self.option_parsers
173
- unless option_parsers.empty?
174
- _options_text(option_parsers)
175
- end
122
+ # Raise a CommandFailure with the given error and the given
123
+ # option parsers.
124
+ def raise_failure(message)
125
+ raise CommandFailure.new(message, self)
176
126
  end
177
127
 
178
128
  # Returns a Command instance for the named subcommand.
@@ -181,19 +131,114 @@ module Rodish
181
131
  _subcommand(@subcommands, name)
182
132
  end
183
133
 
184
- # Returns a Command instance for the named post subcommand.
185
- # This will autoload the post subcommand if not already loaded.
186
- def post_subcommand(name)
187
- _subcommand(@post_subcommands, name)
134
+ private
135
+
136
+ # The string to use for the usage heading in help output.
137
+ def help_usage_heading
138
+ "Usage:"
188
139
  end
189
140
 
190
- # An array of option parsers for the command. May be empty
191
- # if the command has no option parsers.
192
- def option_parsers
193
- [@option_parser, @post_option_parser].compact
141
+ # The string to use for the command heading in help output.
142
+ def help_command_heading
143
+ "Commands:"
194
144
  end
195
145
 
196
- private
146
+ # The string to use for the options heading in help output.
147
+ def help_options_heading
148
+ "Options:"
149
+ end
150
+
151
+ # Use default help order by default.
152
+ def help_order
153
+ default_help_order
154
+ end
155
+
156
+ # The default order of help sections
157
+ def default_help_order
158
+ [:desc, :banner, :commands, :options]
159
+ end
160
+
161
+ # Add description to help output.
162
+ def _help_desc(output)
163
+ if desc
164
+ output << desc << ""
165
+ end
166
+ end
167
+
168
+ # Add banner to help output.
169
+ def _help_banner(output)
170
+ if each_banner{break true}
171
+ output << help_usage_heading
172
+ each_banner do |banner|
173
+ output << " #{banner}"
174
+ end
175
+ output << ""
176
+ end
177
+ end
178
+
179
+ # Add commands to help output.
180
+ def _help_commands(output)
181
+ name_len = 0
182
+ each_local_subcommand do |name|
183
+ len = name.length
184
+ name_len = len if len > name_len
185
+ end
186
+
187
+ __help_command_hashes.each do |heading, hash|
188
+ next if hash.empty?
189
+ output << heading
190
+ command_output = []
191
+ _each_local_subcommand(hash) do |name, command|
192
+ command_output << " #{name.ljust(name_len)} #{command.desc}"
193
+ end
194
+ command_output.sort!
195
+ output.concat(command_output)
196
+ output << ""
197
+ end
198
+ end
199
+
200
+ # Hash with hash of subcommand values to potentially show help output for.
201
+ def __help_command_hashes
202
+ {help_command_heading => @subcommands}
203
+ end
204
+
205
+ # Add options to help output.
206
+ def _help_options(output)
207
+ __help_option_parser_hashes.each do |heading, parser|
208
+ next if omit_option_parser_from_help?(parser)
209
+ output << heading
210
+ output << parser.summarize(String.new)
211
+ end
212
+ end
213
+
214
+ # Hash with option parser values to potentially show help output for.
215
+ def __help_option_parser_hashes
216
+ {help_options_heading => @option_parser}
217
+ end
218
+
219
+ # Whether the given option parser should be ommitted from the
220
+ # command help output.
221
+ def omit_option_parser_from_help?(parser)
222
+ parser.nil?
223
+ end
224
+
225
+ # Raise a error when an invalid number of arguments has been provided.
226
+ def raise_invalid_args_failure(argv)
227
+ raise_failure(invalid_num_args_failure_error_message(argv))
228
+ end
229
+
230
+ # Yield each local subcommand to the block. This does not
231
+ # yield the current command or nested subcommands.
232
+ def each_local_subcommand(&block)
233
+ _each_local_subcommand(@subcommands, &block)
234
+ end
235
+
236
+ # Internals of each_local_subcommand.
237
+ def _each_local_subcommand(subcommands)
238
+ subcommands.each_key do |name|
239
+ yield name, _subcommand(subcommands, name)
240
+ end
241
+ end
197
242
 
198
243
  # Yield to the block for each subcommand in the given
199
244
  # subcommands. Internals of #each_subcommand.
@@ -220,32 +265,37 @@ module Rodish
220
265
  subcommand
221
266
  end
222
267
 
223
- # Return a string containing all option parser text.
224
- def _options_text(option_parsers)
225
- option_parsers.join("\n\n")
226
- end
227
-
228
- # Handle command failures for both subcommands and post subcommands.
229
- def process_command_failure(arg, subcommands, option_parser, prefix)
268
+ # Handle command failures for subcommands.
269
+ def process_command_failure(arg, subcommands, prefix)
230
270
  if subcommands.empty?
231
271
  raise ProgramBug, "program bug, no run block or #{prefix}subcommands defined#{subcommand_name}"
232
272
  elsif arg
233
- raise_failure("invalid #{prefix}subcommand: #{arg}", option_parser)
273
+ raise_failure(invalid_subcommand_error_message(arg, subcommands, prefix))
234
274
  else
235
- raise_failure("no #{prefix}subcommand provided", option_parser)
275
+ raise_failure(no_subcommand_provided_error_message(arg, subcommands, prefix))
236
276
  end
237
277
  end
238
278
 
279
+ # The error message to use when an invalid number of arguments is provided.
280
+ def invalid_num_args_failure_error_message(argv)
281
+ "invalid number of arguments#{subcommand_name} (requires: #{@num_args}, given: #{argv.length})"
282
+ end
283
+
284
+ # Error message for cases where an invalid subcommand is provided.
285
+ def invalid_subcommand_error_message(arg, subcommands, prefix)
286
+ "invalid #{prefix}subcommand: #{arg}"
287
+ end
288
+
289
+ # Error message for cases where a subcommand is required and not provided.
290
+ def no_subcommand_provided_error_message(arg, subcommands, prefix)
291
+ "no #{prefix}subcommand provided"
292
+ end
293
+
239
294
  # Process options for the given command. If option_key is set,
240
295
  # parsed options are added as a options subhash under the given key.
241
296
  # Otherwise, parsed options placed directly into options.
242
297
  def process_options(argv, options, option_key, option_parser)
243
- case option_parser
244
- when SkipOptionParser
245
- # do nothing
246
- when nil
247
- DEFAULT_OPTION_PARSER.order!(argv)
248
- else
298
+ if option_parser
249
299
  command_options = option_key ? {} : options
250
300
 
251
301
  option_parser.order!(argv, into: command_options)
@@ -253,6 +303,8 @@ module Rodish
253
303
  if option_key
254
304
  options[option_key] = command_options
255
305
  end
306
+ else
307
+ self.class::DEFAULT_OPTION_PARSER.order!(argv)
256
308
  end
257
309
  end
258
310
 
@@ -261,7 +313,6 @@ module Rodish
261
313
  def process_subcommand(subcommands, context, options, argv)
262
314
  subcommand = _subcommand(subcommands, argv[0])
263
315
  argv.shift
264
- context.instance_exec(argv, options, &before) if before
265
316
  subcommand.process(context, options, argv)
266
317
  end
267
318
 
@@ -274,6 +325,12 @@ module Rodish
274
325
  end
275
326
  end
276
327
 
328
+ # Set the command name for the command. The command name is used
329
+ # in error messages.
330
+ def _command_name(command_path)
331
+ command_path.join(" ").freeze
332
+ end
333
+
277
334
  # Return whether the given argv has a valid number of arguments.
278
335
  def valid_args?(argv)
279
336
  if @num_args.is_a?(Integer)
data/lib/rodish/dsl.rb CHANGED
@@ -2,7 +2,6 @@
2
2
 
3
3
  require_relative "command"
4
4
  require_relative "option_parser"
5
- require_relative "skip_option_parser"
6
5
 
7
6
  module Rodish
8
7
  # The Rodish::DSL class implements Rodish's DSL. Blocks
@@ -18,8 +17,8 @@ module Rodish
18
17
  # the given block in the context of a new instance using
19
18
  # that command.
20
19
  def self.command(command_path, &block)
21
- command = Command.new(command_path)
22
- new(command).instance_exec(&block)
20
+ command = self::Command.new(command_path)
21
+ new(command).instance_exec(&block) if block
23
22
  command
24
23
  end
25
24
 
@@ -27,16 +26,14 @@ module Rodish
27
26
  @command = command
28
27
  end
29
28
 
30
- # Skip option parsing for the command. This is different
31
- # then the default option parsing, which will error if any
32
- # options are given. A banner must be provided, setting
33
- # the usage for the command.
34
- #
35
- # The main reason to use this is if you are going to pass
36
- # the entire remaining argv as the argv to another
37
- # program.
38
- def skip_option_parsing(banner)
39
- @command.option_parser = SkipOptionParser.new(banner)
29
+ # Set the description for the command.
30
+ def desc(description)
31
+ @command.desc = description
32
+ end
33
+
34
+ # Set the banner for the command execution and subcommand usage.
35
+ def banner(banner)
36
+ @command.banner = banner
40
37
  end
41
38
 
42
39
  # Set the option parser for the command to based on the
@@ -49,46 +46,18 @@ module Rodish
49
46
  #
50
47
  # If +key+ is given, parsed options
51
48
  # will be placed in a subhash using that key.
52
- #
53
- # The block is optional, allowing you to set a usage banner for
54
- # commands without allowing any options.
55
49
  def options(banner, key: nil, &block)
50
+ @command.banner = banner
56
51
  @command.option_key = key
57
- @command.option_parser = create_option_parser(banner, @command.subcommands, &block)
58
- end
59
-
60
- # Similar to +options+, but sets the option parser for post
61
- # subcommands. This option parser is only used when the
62
- # command is executed and chooses to run a post subcommand.
63
- def post_options(banner, key: nil, &block)
64
- @command.post_option_key = key
65
- @command.post_option_parser = create_option_parser(banner, @command.post_subcommands, &block)
66
- end
67
-
68
- # Sets the after_options block. This block is executed in the same
69
- # context as the run block would be executed, directly after
70
- # option parsing.
71
- def after_options(&block)
72
- @command.after_options = block
73
- end
74
-
75
- # Sets the before block. This block is executed in the same
76
- # context as the run block would be executed, before either
77
- # subcommand execution or execution of the current command.
78
- # It is not called on invalid or missing subcommands.
79
- def before(&block)
80
- @command.before = block
52
+ @command.option_parser = create_option_parser(&block)
81
53
  end
82
54
 
83
55
  # Set the number of arguments supported by this command.
84
56
  # The default is 0. To support a fixed number of arguments,
85
57
  # pass an Integer. To support a variable number of arguments,
86
- # pass a Range. The +invalid_args_message+ argument sets the
87
- # error message to use if an invalid number of arguments is
88
- # passed.
89
- def args(args, invalid_args_message: nil)
58
+ # pass a Range.
59
+ def args(args)
90
60
  @command.num_args = args
91
- @command.invalid_args_message = invalid_args_message
92
61
  end
93
62
 
94
63
  # Autoload subcommands from the given directory. Filenames
@@ -101,24 +70,12 @@ module Rodish
101
70
  _autoload_subcommand_dir(@command.subcommands, dir)
102
71
  end
103
72
 
104
- # Similar to +autoload_subcommand_dir+, but for post
105
- # subcommands instead of normal subcommands.
106
- def autoload_post_subcommand_dir(dir)
107
- _autoload_subcommand_dir(@command.post_subcommands, dir)
108
- end
109
-
110
73
  # Create a new subcommand with the given name and yield to
111
74
  # the block to configure the subcommand.
112
75
  def on(command_name, &block)
113
76
  _on(@command.subcommands, command_name, &block)
114
77
  end
115
78
 
116
- # Same as +on+, but for post subcommands instead of normal
117
- # subcommands.
118
- def run_on(command_name, &block)
119
- _on(@command.post_subcommands, command_name, &block)
120
- end
121
-
122
79
  # Set the block to run for subcommand execution. Commands
123
80
  # should have subcommands and/or a run block, otherwise it
124
81
  # is not possible to use the command successfully.
@@ -126,38 +83,9 @@ module Rodish
126
83
  @command.run_block = block
127
84
  end
128
85
 
129
- # A shortcut for calling +on+ and +run+.
130
- #
131
- # is "hello" do
132
- # :world
133
- # end
134
- #
135
- # is equivalent to:
136
- #
137
- # on "hello" do
138
- # run do
139
- # :world
140
- # end
141
- # end
142
- #
143
- # The +args+ argument sets the number of arguments supported by
144
- # the command.
145
- #
146
- # The +invalid_args_message+ arguments set the error message to
147
- # use if an invalid number of arguments is provided.
148
- def is(command_name, args: 0, invalid_args_message: nil, &block)
149
- _is(:on, command_name, args:, invalid_args_message:, &block)
150
- end
151
-
152
- # Similar to +is+, but for post subcommands instead of normal
153
- # subcommands.
154
- def run_is(command_name, args: 0, invalid_args_message: nil, &block)
155
- _is(:run_on, command_name, args:, invalid_args_message:, &block)
156
- end
157
-
158
86
  private
159
87
 
160
- # Internals of autoloading of normal and post subcommands.
88
+ # Internals of autoloading of subcommands.
161
89
  # This sets the value of the subcommand as a string instead of a
162
90
  # Command instance, and the Command#_subcommand method recognizes
163
91
  # this and handles the autoloading.
@@ -167,30 +95,17 @@ module Rodish
167
95
  end
168
96
  end
169
97
 
170
- # Internals of +is+ and +run_is+.
171
- def _is(meth, command_name, args:, invalid_args_message: nil, &block)
172
- public_send(meth, command_name) do
173
- args(args, invalid_args_message:)
174
- run(&block)
175
- end
176
- end
177
-
178
- # Internals of +on+ and +run_on+.
98
+ # Internals of +on+.
179
99
  def _on(hash, command_name, &block)
180
100
  command_path = @command.command_path + [command_name]
181
- hash[command_name] = DSL.command(command_path.freeze, &block)
101
+ hash[command_name] = self.class.command(command_path.freeze, &block)
182
102
  end
183
103
 
184
- # Internals of +options+ and +post_options+.
185
- def create_option_parser(banner, subcommands, &block)
186
- option_parser = OptionParser.new
187
- option_parser.set_banner("Usage: #{banner}")
188
- if block
189
- option_parser.separator ""
190
- option_parser.separator "Options:"
191
- option_parser.instance_exec(&block)
192
- end
193
- option_parser.subcommands = subcommands
104
+ # Internals of +options+.
105
+ def create_option_parser(&block)
106
+ option_parser = self.class::OptionParser.new
107
+ option_parser.banner = "" # Avoids issues when parser is frozen
108
+ option_parser.instance_exec(&block)
194
109
  option_parser
195
110
  end
196
111
  end
data/lib/rodish/errors.rb CHANGED
@@ -26,9 +26,10 @@ module Rodish
26
26
  # * Invalid subcommands
27
27
  # * No subcommand given for a command that only supports subcommands
28
28
  class CommandFailure < CommandExit
29
- def initialize(message, option_parsers = [])
30
- option_parsers = [option_parsers] unless option_parsers.is_a?(Array)
31
- @option_parsers = option_parsers.compact
29
+ attr_reader :command
30
+
31
+ def initialize(message, command=nil)
32
+ @command = command
32
33
  super(message)
33
34
  end
34
35
 
@@ -41,10 +42,11 @@ module Rodish
41
42
  # parsers. This can be used to show usage an options along with
42
43
  # error messages for failing commands.
43
44
  def message_with_usage
44
- if @option_parsers.empty?
45
+ help = @command&.help || ''
46
+ if help.empty?
45
47
  message
46
48
  else
47
- "#{message}\n\n#{@option_parsers.join("\n\n")}"
49
+ "#{message}\n\n#{help}"
48
50
  end
49
51
  end
50
52
  end