cmdparse 2.0.6 → 3.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.
@@ -1,43 +1,43 @@
1
1
  #
2
2
  #--
3
3
  # cmdparse: advanced command line parser supporting commands
4
- # Copyright (C) 2004-2014 Thomas Leitner
5
- #
6
- # This file is part of cmdparse.
7
- #
8
- # cmdparse is free software: you can redistribute it and/or modify it under the terms of the GNU
9
- # Lesser General Public License as published by the Free Software Foundation, either version 3 of
10
- # the License, or (at your option) any later version.
11
- #
12
- # cmdparse is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
13
- # the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
14
- # General Public License for more details.
15
- #
16
- # You should have received a copy of the GNU Lesser General Public License along with cmdparse. If
17
- # not, see <http://www.gnu.org/licenses/>.
4
+ # Copyright (C) 2004-2015 Thomas Leitner
18
5
  #
6
+ # This file is part of cmdparse which is licensed under the MIT.
19
7
  #++
20
8
  #
21
9
 
22
- # Namespace module for cmdparse.
10
+ require 'optparse'
11
+
12
+ OptionParser::Officious.delete('version')
13
+ OptionParser::Officious.delete('help')
14
+
15
+ # Namespace module for cmdparse.
16
+ #
17
+ # See CmdParse::CommandParser and CmdParse::Command for the two important classes.
23
18
  module CmdParse
24
19
 
25
20
  # The version of this cmdparse implemention
26
- VERSION = [2, 0, 6]
21
+ VERSION = '3.0.0'
27
22
 
28
23
 
29
24
  # Base class for all cmdparse errors.
30
- class ParseError < RuntimeError
25
+ class ParseError < StandardError
26
+
27
+ # Sets the error reason for the subclass.
28
+ def self.reason(reason)
29
+ @reason = reason
30
+ end
31
31
 
32
- # Set the reason for a subclass.
33
- def self.reason(reason, has_arguments = true)
34
- (@@reason ||= {})[self] = [reason, has_arguments]
32
+ # Returns the error reason or 'CmdParse error' if it has not been set.
33
+ def self.get_reason
34
+ @reason ||= 'CmdParse error'
35
35
  end
36
36
 
37
- # Return the reason plus the message.
37
+ # Returns the reason plus the original message.
38
38
  def message
39
- data = @@reason[self.class] || ['Unknown error', true]
40
- data[0] + (data[1] ? ": " + super : '')
39
+ str = super
40
+ self.class.get_reason + (str.empty? ? "" : ": #{str}")
41
41
  end
42
42
 
43
43
  end
@@ -59,418 +59,809 @@ module CmdParse
59
59
 
60
60
  # This error is thrown when no command was given and no default command was specified.
61
61
  class NoCommandGivenError < ParseError
62
- reason 'No command given', false
62
+ reason 'No command given'
63
+
64
+ def initialize #:nodoc:
65
+ super('')
66
+ end
63
67
  end
64
68
 
65
69
  # This error is thrown when a command is added to another command which does not support commands.
66
70
  class TakesNoCommandError < ParseError
67
- reason 'This command takes no other commands', false
71
+ reason 'This command takes no other commands'
68
72
  end
69
73
 
74
+ # This error is thrown when not enough arguments are provided for the command.
75
+ class NotEnoughArgumentsError < ParseError
76
+ reason 'Not enough arguments provided, minimum is'
77
+ end
78
+
79
+ # Command Hash - will return partial key matches as well if there is a single non-ambigous
80
+ # matching key
81
+ class CommandHash < Hash #:nodoc:
82
+
83
+ def key?(name) #:nodoc:
84
+ !self[name].nil?
85
+ end
86
+
87
+ def [](cmd_name) #:nodoc:
88
+ super or begin
89
+ possible = keys.select {|key| key[0, cmd_name.length] == cmd_name }
90
+ fetch(possible[0]) if possible.size == 1
91
+ end
92
+ end
93
+
94
+ end
70
95
 
71
- # Base class for all parser wrappers.
72
- class ParserWrapper
96
+ # Container for multiple OptionParser::List objects.
97
+ #
98
+ # This is needed for providing what's equivalent to stacked OptionParser instances and the global
99
+ # options implementation.
100
+ class MultiList #:nodoc:
73
101
 
74
- # Return the parser instance for the object and, if a block is a given, yield the instance.
75
- def instance
76
- yield @instance if block_given?
77
- @instance
102
+ def initialize(list) #:nodoc:
103
+ @list = list
78
104
  end
79
105
 
80
- # Parse the arguments in order, i.e. stops at the first non-option argument, and returns all
81
- # remaining arguments.
82
- def order(args)
83
- raise InvalidOptionError.new(args[0]) if args[0] =~ /^-/
84
- args
106
+ def summarize(*args, &block) #:nodoc:
107
+ # We don't want summary information of the global options to automatically appear.
85
108
  end
86
109
 
87
- # Permute the arguments so that all options anywhere on the command line are parsed and the
88
- # remaining non-options are returned.
89
- def permute(args)
90
- raise InvalidOptionError.new(args[0]) if args.any? {|a| a =~ /^-/}
91
- args
110
+ [:accept, :reject, :prepend, :append].each do |mname|
111
+ module_eval <<-EOF
112
+ def #{mname}(*args, &block)
113
+ @list[-1].#{mname}(*args, &block)
114
+ end
115
+ EOF
92
116
  end
93
117
 
94
- # Return a summary string of the options.
95
- def summarize
96
- ""
118
+ [:search, :complete, :each_option, :add_banner, :compsys].each do |mname|
119
+ module_eval <<-EOF
120
+ def #{mname}(*args, &block) #:nodoc:
121
+ @list.reverse_each {|list| list.#{mname}(*args, &block)}
122
+ end
123
+ EOF
97
124
  end
98
125
 
99
126
  end
100
127
 
101
- # Require default option parser wrapper
102
- require 'cmdparse/wrappers/optparse'
128
+ # Extension for OptionParser objects to allow access to some internals.
129
+ class ::OptionParser #:nodoc:
103
130
 
104
- # Command Hash - will return partial key matches as well if there is a single
105
- # non-ambigous matching key
106
- class CommandHash < Hash
131
+ # Access the option list stack.
132
+ attr_reader :stack
107
133
 
108
- def [](cmd_name)
109
- super or begin
110
- possible = keys.select {|key| key[0, cmd_name.length] == cmd_name }
111
- fetch(possible[0]) if possible.size == 1
134
+ # Returns +true+ if at least one local option is defined.
135
+ #
136
+ # The zeroth stack element is not respected when doing the query because it contains either the
137
+ # OptionParser::DefaultList or a CmdParse::MultiList with the global options of the
138
+ # CmdParse::CommandParser.
139
+ def options_defined?
140
+ stack[1..-1].each do |list|
141
+ list.each_option do |switch|
142
+ return true if ::OptionParser::Switch === switch && (switch.short || switch.long)
143
+ end
112
144
  end
145
+ false
146
+ end
147
+
148
+ # Returns +true+ if a banner has been set.
149
+ def banner?
150
+ !@banner.nil?
113
151
  end
114
152
 
115
153
  end
116
154
 
117
- # Base class for the commands. This class implements all needed methods so that it can be used by
118
- # the +CommandParser+ class.
155
+ # === Base class for commands
156
+ #
157
+ # This class implements all needed methods so that it can be used by the CommandParser class.
158
+ #
159
+ # Commands can either be created by sub-classing or on the fly when using the #add_command method.
160
+ # The latter allows for a more terse specification of a command while the sub-class approach
161
+ # allows to customize all aspects of a command by overriding methods.
162
+ #
163
+ # Basic example for sub-classing:
164
+ #
165
+ # class TestCommand < CmdParse::Command
166
+ # def initialize
167
+ # super('test', takes_commands: false)
168
+ # options.on('-m', '--my-opt', 'My option') { 'Do something' }
169
+ # end
170
+ # end
171
+ #
172
+ # parser = CmdParse::CommandParser.new
173
+ # parser.add_command(TestCommand.new)
174
+ # parser.parse
175
+ #
176
+ # Basic example for on the fly creation:
177
+ #
178
+ # parser = CmdParse::CommandParser.new
179
+ # parser.add_command('test') do |cmd|
180
+ # takes_commands(false)
181
+ # options.on('-m', '--my-opt', 'My option') { 'Do something' }
182
+ # end
183
+ # parser.parse
184
+ #
185
+ # === Basic Properties
186
+ #
187
+ # The only thing that is mandatory to set for a Command is its #name. If the command does not take
188
+ # any sub-commands, then additionally an #action block needs to be specified or the #execute
189
+ # method overridden.
190
+ #
191
+ # However, there are several other methods that can be used to configure the behavior of a
192
+ # command:
193
+ #
194
+ # #takes_commands:: For specifying whether sub-commands are allowed.
195
+ # #options:: For specifying command specific options.
196
+ # #add_command:: For specifying sub-commands if the command takes them.
197
+ #
198
+ # === Help Related Methods
199
+ #
200
+ # Many of this class' methods are related to providing useful help output. While the most common
201
+ # methods can directly be invoked to set or retrieve information, many other methods compute the
202
+ # needed information dynamically and therefore need to be overridden to customize their return
203
+ # value.
204
+ #
205
+ # #short_desc::
206
+ # For a short description of the command (getter/setter).
207
+ # #long_desc::
208
+ # For a detailed description of the command (getter/setter).
209
+ # #argument_desc::
210
+ # For describing command arguments (setter).
211
+ # #help, #help_banner, #help_short_desc, #help_long_desc, #help_commands, #help_arguments, #help_options::
212
+ # For outputting the general command help or individual sections of the command help (getter).
213
+ # #usage, #usage_options, #usage_arguments, #usage_commands::
214
+ # For outputting the usage line or individual parts of it (getter).
215
+ #
216
+ # === Built-in Commands
217
+ #
218
+ # cmdparse ships with two built-in commands:
219
+ # * HelpCommand (for showing help messages) and
220
+ # * VersionCommand (for showing version information).
119
221
  class Command
120
222
 
121
- # The name of the command
223
+ # The name of the command.
122
224
  attr_reader :name
123
225
 
124
- # A short description of the command. Should ideally be smaller than 60 characters.
125
- attr_accessor :short_desc
126
-
127
- # A detailed description of the command. Maybe a single string or an array of strings for
128
- # multiline description. Each string should ideally be smaller than 76 characters.
129
- attr_accessor :description
130
-
131
- # The wrapper for parsing the command line options.
132
- attr_accessor :options
133
-
134
- # Returns the name of the default command.
226
+ # Returns the name of the default sub-command or +nil+ if there isn't any.
135
227
  attr_reader :default_command
136
228
 
137
- # Sets or returns the super command of this command. The super command is either a +Command+
138
- # instance for normal commands or a +CommandParser+ instance for the root command.
229
+ # Sets or returns the super-command of this command. The super-command is either a Command
230
+ # instance for normal commands or a CommandParser instance for the main command (ie.
231
+ # CommandParser#main_command).
139
232
  attr_accessor :super_command
140
233
 
141
- # Returns the list of commands for this command.
234
+ # Returns the mapping of command name to command for all sub-commands of this command.
142
235
  attr_reader :commands
143
236
 
144
- # Initialize the command called +name+.
145
- #
146
- # Parameters:
147
- #
148
- # [has_commands]
149
- # Specifies if this command takes other commands as argument.
150
- # [partial_commands (optional)]
151
- # Specifies whether partial command matching should be used.
152
- # [has_args (optional)]
153
- # Specifies whether this command takes arguments
154
- def initialize(name, has_commands, partial_commands = false, has_args = true)
155
- @name = name
156
- @options = ParserWrapper.new
157
- @has_commands = has_commands
158
- @has_args = has_args
159
- @commands = Hash.new
237
+ # A data store (initially an empty Hash) that can be used for storing anything. For example, it
238
+ # can be used to store option values. cmdparse itself doesn't do anything with it.
239
+ attr_accessor :data
240
+
241
+ # Initializes the command called +name+.
242
+ #
243
+ # Options:
244
+ #
245
+ # takes_commands:: Specifies whether this command can take sub-commands.
246
+ def initialize(name, takes_commands: true)
247
+ @name = name.freeze
248
+ @options = OptionParser.new
249
+ @commands = CommandHash.new
160
250
  @default_command = nil
161
- use_partial_commands(partial_commands)
251
+ @action = nil
252
+ @argument_desc ||= {}
253
+ @data = {}
254
+ takes_commands(takes_commands)
162
255
  end
163
256
 
164
- # Define whether partial command matching should be used.
165
- def use_partial_commands(use_partial)
166
- temp = (use_partial ? CommandHash.new : Hash.new)
167
- temp.update(@commands)
168
- @commands = temp
257
+ # Sets whether this command can take sub-command.
258
+ #
259
+ # The argument +val+ needs to be +true+ or +false+.
260
+ def takes_commands(val)
261
+ if !val && commands.size > 0
262
+ raise Error, "Can't change value of takes_commands to false because there are already sub-commands"
263
+ else
264
+ @takes_commands = val
265
+ end
169
266
  end
267
+ alias takes_commands= takes_commands
170
268
 
171
- # Return +true+ if this command supports sub commands.
172
- def has_commands?
173
- @has_commands
269
+ # Return +true+ if this command can take sub-commands.
270
+ def takes_commands?
271
+ @takes_commands
174
272
  end
175
273
 
176
- # Return +true+ if this command uses arguments.
177
- def has_args?
178
- @has_args
274
+ # :call-seq:
275
+ # command.options {|opts| ...} -> opts
276
+ # command.options -> opts
277
+ #
278
+ # Yields the OptionParser instance that is used for parsing the options of this command (if a
279
+ # block is given) and returns it.
280
+ def options #:yields: options
281
+ yield(@options) if block_given?
282
+ @options
179
283
  end
180
284
 
181
- # Add a command to the command list if this command takes other commands as argument.
285
+ # :call-seq:
286
+ # command.add_command(other_command, default: false) {|cmd| ... } -> command
287
+ # command.add_command('other', default: false) {|cmd| ...} -> command
288
+ #
289
+ # Adds a command to the command list.
290
+ #
291
+ # The argument +command+ can either be a Command object or a String in which case a new Command
292
+ # object is created. In both cases the Command object is yielded.
182
293
  #
183
- # If the optional parameter +default+ is true, then this command is used when no command is
184
- # specified on the command line.
185
- def add_command(command, default = false)
186
- raise TakesNoCommandError.new(@name) if !has_commands?
294
+ # If the optional argument +default+ is +true+, then the command is used when no other
295
+ # sub-command is specified on the command line.
296
+ #
297
+ # If this command takes no other commands, an error is raised.
298
+ def add_command(command, default: false) # :yields: command_object
299
+ raise TakesNoCommandError.new(name) unless takes_commands?
300
+
301
+ command = Command.new(command) if command.kind_of?(String)
302
+ command.super_command = self
187
303
  @commands[command.name] = command
188
304
  @default_command = command.name if default
189
- command.super_command = self
190
- command.init
191
- end
192
-
193
- # For sorting commands by name.
194
- def <=>(other)
195
- @name <=> other.name
196
- end
305
+ command.fire_hook_after_add
306
+ yield(command) if block_given?
197
307
 
198
- # Return the +CommandParser+ instance for this command or +nil+ if this command was not assigned
199
- # to a +CommandParser+ instance.
200
- def commandparser
201
- cmd = super_command
202
- cmd = cmd.super_command while !cmd.nil? && !cmd.kind_of?(CommandParser)
203
- cmd
308
+ self
204
309
  end
205
310
 
206
- # Return a list of super commands, ie.:
207
- # [command, super_command, super_super_command, ...]
208
- def super_commands
311
+ # :call-seq:
312
+ # command.command_chain -> [top_level_command, super_command, ..., command]
313
+ #
314
+ # Returns the command chain, i.e. a list containing this command and all of its super-commands,
315
+ # starting at the top level command.
316
+ def command_chain
209
317
  cmds = []
210
318
  cmd = self
211
319
  while !cmd.nil? && !cmd.super_command.kind_of?(CommandParser)
212
- cmds << cmd
320
+ cmds.unshift(cmd)
213
321
  cmd = cmd.super_command
214
322
  end
215
323
  cmds
216
324
  end
217
325
 
218
- # This method is called when the command is added to a +Command+ instance.
219
- def init; end
326
+ # Returns the associated CommandParser instance for this command or +nil+ if no command parser
327
+ # is associated.
328
+ def command_parser
329
+ cmd = super_command
330
+ cmd = cmd.super_command while !cmd.nil? && !cmd.kind_of?(CommandParser)
331
+ cmd
332
+ end
220
333
 
221
- # Set the given +block+ as execution block. See also: +execute+.
222
- def set_execution_block(&block)
223
- @exec_block = block
334
+ # Sets the given +block+ as the action block that is used on when executing this command.
335
+ #
336
+ # If a sub-class is created for specifying a command, then the #execute method should be
337
+ # overridden instead of setting an action block.
338
+ #
339
+ # See also: #execute
340
+ def action(&block)
341
+ @action = block
224
342
  end
225
343
 
226
- # Invoke the block set by +set_execution_block+.
344
+ # Invokes the action block with the parsed arguments.
345
+ #
346
+ # This method is called by the CommandParser instance if this command was specified on the
347
+ # command line to be executed.
227
348
  #
228
- # This method is called by the +CommandParser+ instance if this command was specified on the
229
- # command line.
230
- def execute(args)
231
- @exec_block.call(args)
349
+ # Sub-classes can either specify an action block or directly override this method (the latter is
350
+ # preferred).
351
+ def execute(*args)
352
+ @action.call(*args)
232
353
  end
233
354
 
234
- # Define the usage line for the command.
235
- def usage
236
- tmp = "Usage: #{commandparser.program_name}"
237
- tmp << " [global options]" if !commandparser.options.instance_of?(ParserWrapper)
238
- tmp << super_commands.reverse.collect do |c|
239
- t = " #{c.name}"
240
- t << " [options]" if !c.options.instance_of?(ParserWrapper)
241
- t
242
- end.join('')
243
- tmp << " COMMAND [options]" if has_commands?
244
- tmp << " [ARGS]" if has_args?
245
- tmp
355
+ # Sets the short description of the command if an argument is given. Always returns the short
356
+ # description.
357
+ #
358
+ # The short description is ideally shorter than 60 characters.
359
+ def short_desc(*val)
360
+ @short_desc = val[0] unless val.empty?
361
+ @short_desc
246
362
  end
363
+ alias short_desc= short_desc
247
364
 
248
- # Default method for showing the help for the command.
249
- def show_help
250
- puts commandparser.banner + "\n" if commandparser.banner
251
- puts usage
252
- puts
253
- if short_desc && !short_desc.empty?
254
- puts short_desc
255
- puts
256
- end
257
- if description && !description.empty?
258
- puts " " + [description].flatten.join("\n ")
259
- puts
365
+ # Sets the detailed description of the command if an argument is given. Always returns the
366
+ # detailed description.
367
+ #
368
+ # This may be a single string or an array of strings for multiline description. Each string
369
+ # is ideally shorter than 76 characters.
370
+ def long_desc(*val)
371
+ @long_desc = val.flatten unless val.empty?
372
+ @long_desc
373
+ end
374
+ alias long_desc= long_desc
375
+
376
+ # :call-seq:
377
+ # cmd.argument_desc(name => desc, ...)
378
+ #
379
+ # Sets the descriptions for one or more arguments using name-description pairs.
380
+ #
381
+ # The used names should correspond to the names used in #usage_arguments.
382
+ def argument_desc(hash)
383
+ @argument_desc.update(hash)
384
+ end
385
+
386
+ # Returns the number of arguments required for the execution of the command, i.e. the number of
387
+ # arguments the #action block or the #execute method takes.
388
+ #
389
+ # If the returned number is negative, it means that the minimum number of arguments is -n-1.
390
+ #
391
+ # See: Method#arity, Proc#arity
392
+ def arity
393
+ (@action || method(:execute)).arity
394
+ end
395
+
396
+ # Returns +true+ if the command can take one or more arguments.
397
+ def takes_arguments?
398
+ arity.abs > 0
399
+ end
400
+
401
+ # Returns a string containing the help message for the command.
402
+ def help
403
+ output = ''
404
+ output << help_banner
405
+ output << help_short_desc
406
+ output << help_long_desc
407
+ output << help_commands
408
+ output << help_arguments
409
+ output << help_options('Options (take precedence over global options)', options)
410
+ output << help_options('Global Options', command_parser.global_options)
411
+ end
412
+
413
+ # Returns the banner (including the usage line) of the command.
414
+ #
415
+ # The usage line is command specific but the rest is the same for all commands and can be set
416
+ # via +command_parser.main_options.banner+.
417
+ def help_banner
418
+ output = ''
419
+ if command_parser.main_options.banner?
420
+ output << format(command_parser.main_options.banner, indent: 0) << "\n\n"
260
421
  end
261
- if has_commands?
262
- list_commands
263
- puts
422
+ output << format(usage, indent: 7) << "\n\n"
423
+ end
424
+
425
+ # Returns the usage line for the command.
426
+ #
427
+ # The usage line is automatically generated from the available information. If this is not
428
+ # suitable, override this method to provide a command specific usage line.
429
+ #
430
+ # Typical usage lines looks like the following:
431
+ #
432
+ # Usage: program [options] command [options] {sub_command1 | sub_command2}
433
+ # Usage: program [options] command [options] ARG1 [ARG2] [REST...]
434
+ #
435
+ # See: #usage_options, #usage_arguments, #usage_commands
436
+ def usage
437
+ tmp = "Usage: #{command_parser.main_options.program_name}"
438
+ tmp << command_parser.main_command.usage_options
439
+ tmp << command_chain.map {|cmd| " #{cmd.name}#{cmd.usage_options}"}.join('')
440
+ if takes_commands?
441
+ tmp << " #{usage_commands}"
442
+ elsif takes_arguments?
443
+ tmp << " #{usage_arguments}"
264
444
  end
265
- if !(summary = options.summarize).empty?
266
- puts summary
267
- puts
445
+ tmp
446
+ end
447
+
448
+ # Returns a string describing the options of the command for use in the usage line.
449
+ #
450
+ # If there are any options, the resulting string also includes a leading space!
451
+ #
452
+ # A typical return value would look like the following:
453
+ #
454
+ # [options]
455
+ #
456
+ # See: #usage
457
+ def usage_options
458
+ (options.options_defined? ? ' [options]' : '')
459
+ end
460
+
461
+ # Returns a string describing the arguments for the command for use in the usage line.
462
+ #
463
+ # By default the names of the action block or #execute method arguments are used (done via
464
+ # Ruby's reflection API). If this is not wanted, override this method.
465
+ #
466
+ # A typical return value would look like the following:
467
+ #
468
+ # ARG1 [ARG2] [REST...]
469
+ #
470
+ # See: #usage, #argument_desc
471
+ def usage_arguments
472
+ (@action || method(:execute)).parameters.map do |type, name|
473
+ case type
474
+ when :req then name.to_s
475
+ when :opt then "[#{name}]"
476
+ when :rest then "[#{name}...]"
477
+ end
478
+ end.join(" ").upcase
479
+ end
480
+
481
+ # Returns a string describing the sub-commands of the commands for use in the usage line.
482
+ #
483
+ # Override this method for providing a command specific specialization.
484
+ #
485
+ # A typical return value would look like the following:
486
+ #
487
+ # {command | other_command | another_command }
488
+ def usage_commands
489
+ (commands.size > 0 ? "{#{commands.keys.join(" | ")}}" : '')
490
+ end
491
+
492
+ # Returns the formatted short description.
493
+ #
494
+ # For the output format see #cond_format_help_section
495
+ def help_short_desc
496
+ cond_format_help_section("Summary", "#{name} - #{short_desc}",
497
+ condition: short_desc && !short_desc.empty?)
498
+ end
499
+
500
+ # Returns the formatted detailed description.
501
+ #
502
+ # For the output format see #cond_format_help_section
503
+ def help_long_desc
504
+ cond_format_help_section("Description", [long_desc].flatten,
505
+ condition: long_desc && !long_desc.empty?)
506
+ end
507
+
508
+ # Returns the formatted sub-commands of this command.
509
+ #
510
+ # For the output format see #cond_format_help_section
511
+ def help_commands
512
+ describe_commands = lambda do |command, level = 0|
513
+ command.commands.sort.collect do |name, cmd|
514
+ str = " "*level << name << (name == command.default_command ? " (*)" : '')
515
+ str = str.ljust(command_parser.help_desc_indent) << cmd.short_desc.to_s
516
+ str = format(str, width: command_parser.help_line_width - command_parser.help_indent,
517
+ indent: command_parser.help_desc_indent)
518
+ str << "\n" << (cmd.takes_commands? ? describe_commands.call(cmd, level + 1) : "")
519
+ end.join('')
268
520
  end
269
- if self != commandparser.main_command &&
270
- !(summary = commandparser.main_command.options.summarize).empty?
271
- puts summary
272
- puts
521
+ cond_format_help_section("Available commands", describe_commands.call(self),
522
+ condition: takes_commands?)
523
+ end
524
+
525
+ # Returns the formatted arguments of this command.
526
+ #
527
+ # For the output format see #cond_format_help_section
528
+ def help_arguments
529
+ desc = @argument_desc.map {|k, v| k.to_s.ljust(command_parser.help_desc_indent) << v.to_s}
530
+ cond_format_help_section('Arguments', desc, condition: @argument_desc.size > 0)
531
+ end
532
+
533
+ # Returns the formatted option descriptions for the given OptionParser instance.
534
+ #
535
+ # The section title needs to be specified with the +title+ argument.
536
+ #
537
+ # For the output format see #cond_format_help_section
538
+ def help_options(title, options)
539
+ summary = ''
540
+ summary_width = command_parser.main_options.summary_width
541
+ options.summarize([], summary_width, summary_width - 1, '') do |line|
542
+ summary << format(line, width: command_parser.help_line_width - command_parser.help_indent,
543
+ indent: summary_width + 1, indent_first_line: false) << "\n"
273
544
  end
545
+ cond_format_help_section(title, summary, condition: !summary.empty?)
274
546
  end
275
547
 
276
- #######
277
- private
278
- #######
548
+ # This hook method is called when the command (or one of its super-commands) is added to another
549
+ # Command instance that has an associated command parser (#see command_parser).
550
+ #
551
+ # It can be used, for example, to add global options.
552
+ def on_after_add
553
+ end
279
554
 
280
- def list_commands(command = self)
281
- puts "Available commands:"
282
- puts " " + collect_commands_info(command).join("\n ")
555
+ # For sorting commands by name.
556
+ def <=>(other)
557
+ self.name <=> other.name
283
558
  end
284
559
 
285
- def collect_commands_info(command, level = 1)
286
- command.commands.sort.collect do |name, cmd|
287
- str = " "*level + name
288
- str = str.ljust(18) + cmd.short_desc.to_s
289
- str += " (default command)" if name == command.default_command
290
- [str] + (cmd.has_commands? ? collect_commands_info(cmd, level + 1) : [])
291
- end.flatten
560
+ protected
561
+
562
+ # Conditionally formats a help section.
563
+ #
564
+ # Returns either the formatted help section if the condition is +true+ or an empty string
565
+ # otherwise.
566
+ #
567
+ # The help section starts with a title and the given lines are indented to easily distinguish
568
+ # different sections.
569
+ #
570
+ # A typical help section would look like the following:
571
+ #
572
+ # Summary:
573
+ # help - Provide help for individual commands
574
+ def cond_format_help_section(title, *lines, condition: true, indent: true)
575
+ if condition
576
+ "#{title}:\n" << format(lines.flatten.join("\n"),
577
+ indent: (indent ? command_parser.help_indent : 0),
578
+ indent_first_line: true) << "\n\n"
579
+ else
580
+ ''
581
+ end
582
+ end
583
+
584
+ # Returns the text in +content+ formatted so that no line is longer than +width+ characters.
585
+ #
586
+ # Options:
587
+ #
588
+ # width:: The maximum width of a line. If not specified, the CommandParser#help_line_width value
589
+ # is used.
590
+ #
591
+ # indent:: This option specifies the amount of spaces prepended to each line. If not specified,
592
+ # the CommandParser#help_indent value is used.
593
+ #
594
+ # indent_first_line:: If this option is +true+, then the first line is also indented.
595
+ def format(content, width: command_parser.help_line_width,
596
+ indent: command_parser.help_indent, indent_first_line: false)
597
+ content = (content || '').dup
598
+ line_length = width - indent
599
+ first_line_pattern = other_lines_pattern = /\A.{1,#{line_length}}\z|\A.{1,#{line_length}}[ \n]/
600
+ (first_line_pattern = /\A.{1,#{width}}\z|\A.{1,#{width}}[ \n]/) unless indent_first_line
601
+ pattern = first_line_pattern
602
+
603
+ content.split(/\n\n/).map do |paragraph|
604
+ lines = []
605
+ while paragraph.length > 0
606
+ unless (str = paragraph.slice!(pattern).sub(/[ \n]\z/, ''))
607
+ str = paragraph.slice!(0, line_length)
608
+ end
609
+ lines << (lines.empty? && !indent_first_line ? '' : ' '*indent) + str.gsub(/\n/, ' ')
610
+ pattern = other_lines_pattern
611
+ end
612
+ lines.join("\n")
613
+ end.join("\n\n")
614
+ end
615
+
616
+ def fire_hook_after_add #:nodoc:
617
+ return unless command_parser
618
+ @options.stack[0] = MultiList.new(command_parser.global_options.stack)
619
+ on_after_add
620
+ @commands.each_value {|cmd| cmd.fire_hook_after_add}
292
621
  end
293
622
 
294
623
  end
295
624
 
296
- # The default help command. It adds the options "-h" and "--help" to the global options of the
297
- # associated +CommandParser+. When the command is specified on the command line, it can show the
298
- # main help or individual command help.
625
+ # The default help Command.
626
+ #
627
+ # It adds the options "-h" and "--help" to the CommandParser#global_options.
628
+ #
629
+ # When the command is specified on the command line (or one of the above mentioned options), it
630
+ # shows the main help or individual command help.
299
631
  class HelpCommand < Command
300
632
 
301
- def initialize
302
- super('help', false)
303
- self.short_desc = 'Provide help for individual commands'
304
- self.description = ['This command prints the program help if no arguments are given. If one or',
305
- 'more command names are given as arguments, these arguments are interpreted',
306
- 'as a hierachy of commands and the help for the right most command is show.']
633
+ def initialize #:nodoc:
634
+ super('help', takes_commands: false)
635
+ short_desc('Provide help for individual commands')
636
+ long_desc('This command prints the program help if no arguments are given. If one or ' <<
637
+ 'more command names are given as arguments, these arguments are interpreted ' <<
638
+ 'as a hierachy of commands and the help for the right most command is show.')
639
+ argument_desc(COMMAND: 'The name of a command or sub-command')
307
640
  end
308
641
 
309
- def init
310
- case commandparser.main_command.options
311
- when OptionParserWrapper
312
- commandparser.main_command.options.instance do |opt|
313
- opt.on_tail("-h", "--help", "Show help") do
314
- execute([])
315
- end
316
- end
642
+ def on_after_add #:nodoc:
643
+ command_parser.global_options.on_tail("-h", "--help", "Show help") do
644
+ execute(*command_parser.current_command.command_chain.map(&:name))
645
+ exit
317
646
  end
318
647
  end
319
648
 
320
- def usage
321
- "Usage: #{commandparser.program_name} help [COMMAND SUBCOMMAND ...]"
649
+ def usage_arguments #:nodoc:
650
+ "[COMMAND COMMAND...]"
322
651
  end
323
652
 
324
- def execute(args)
653
+ def execute(*args) #:nodoc:
325
654
  if args.length > 0
326
- cmd = commandparser.main_command
655
+ cmd = command_parser.main_command
327
656
  arg = args.shift
328
- while !arg.nil? && cmd.commands[arg]
657
+ while !arg.nil? && cmd.commands.key?(arg)
329
658
  cmd = cmd.commands[arg]
330
659
  arg = args.shift
331
660
  end
332
661
  if arg.nil?
333
- cmd.show_help
662
+ puts cmd.help
334
663
  else
335
664
  raise InvalidArgumentError, args.unshift(arg).join(' ')
336
665
  end
337
666
  else
338
- commandparser.main_command.show_help
667
+ puts command_parser.main_command.help
339
668
  end
340
- exit
341
669
  end
342
670
 
343
671
  end
344
672
 
345
673
 
346
- # The default version command. It adds the options "-v" and "--version" to the global options of
347
- # the associated +CommandParser+. When specified on the command line, it shows the version of the
348
- # program.
674
+ # The default version command.
675
+ #
676
+ # It adds the options "-v" and "--version" to the CommandParser#global_options.
677
+ #
678
+ # When the command is specified on the command line (or one of the above mentioned options), it
679
+ # shows the version of the program configured by the settings
680
+ #
681
+ # * command_parser.main_options.program_name
682
+ # * command_parser.main_options.version
349
683
  class VersionCommand < Command
350
684
 
351
- def initialize
352
- super('version', false, false, false)
353
- self.short_desc = "Show the version of the program"
685
+ def initialize #:nodoc:
686
+ super('version', takes_commands: false)
687
+ short_desc("Show the version of the program")
354
688
  end
355
689
 
356
- def init
357
- case commandparser.main_command.options
358
- when OptionParserWrapper
359
- commandparser.main_command.options.instance do |opt|
360
- opt.on_tail("--version", "-v", "Show the version of the program") do
361
- execute([])
362
- end
363
- end
690
+ def on_after_add #:nodoc:
691
+ command_parser.main_options.on_tail("--version", "-v", "Show the version of the program") do
692
+ execute
364
693
  end
365
694
  end
366
695
 
367
- def usage
368
- "Usage: #{commandparser.program_name} version"
369
- end
370
-
371
- def execute(args)
372
- version = commandparser.program_version
373
- version = version.join('.') if version.instance_of?(Array)
374
- puts commandparser.banner + "\n" if commandparser.banner
375
- puts "#{commandparser.program_name} #{version}"
696
+ def execute #:nodoc:
697
+ version = command_parser.main_options.version
698
+ version = version.join('.') if version.kind_of?(Array)
699
+ puts command_parser.main_options.banner + "\n" if command_parser.main_options.banner?
700
+ puts "#{command_parser.main_options.program_name} #{version}"
376
701
  exit
377
702
  end
378
703
 
379
704
  end
380
705
 
381
706
 
382
- # The main class for creating a command based CLI program.
707
+ # === Main Class for Creating a Command Based CLI Program
708
+ #
709
+ # This class can directly be used (or sub-classed, if need be) to create a command based CLI
710
+ # program.
711
+ #
712
+ # The CLI program itself is represented by the #main_command, a Command instance (as are all
713
+ # commands and sub-commands). This main command can either hold sub-commands (the normal use case)
714
+ # which represent the programs top level commands or take no commands in which case it acts
715
+ # similar to a simple OptionParser based program (albeit with better help functionality).
716
+ #
717
+ # Parsing the command line for commands is done by this class, option parsing is delegated to the
718
+ # battle tested OptionParser of the Ruby standard library.
719
+ #
720
+ # === Usage
721
+ #
722
+ # After initialization some optional information is expected to be set on the Command#options of
723
+ # the #main_command:
724
+ #
725
+ # banner:: A banner that appears in the help output before anything else.
726
+ # program_name:: The name of the program. If not set, this value is computed from $0.
727
+ # version:: The version string of the program.
728
+ #
729
+ # In addition to the main command's options instance (which represents the top level options that
730
+ # need to be specified before any command name), there is also a #global_options instance which
731
+ # represents options that can be specified anywhere on the command line.
732
+ #
733
+ # Top level commands can be added to the main command by using the #add_command method.
734
+ #
735
+ # Once everything is set up, the #parse method is used for parsing the command line.
383
736
  class CommandParser
384
737
 
385
- # A standard banner for help & version screens
386
- attr_accessor :banner
387
-
388
738
  # The top level command representing the program itself.
389
739
  attr_reader :main_command
390
740
 
391
- # The name of the program.
392
- attr_accessor :program_name
741
+ # The command that is being executed. Only available during parsing of the command line
742
+ # arguments.
743
+ attr_reader :current_command
393
744
 
394
- # The version of the program.
395
- attr_accessor :program_version
745
+ # A data store (initially an empty Hash) that can be used for storing anything. For example, it
746
+ # can be used to store global option values. cmdparse itself doesn't do anything with it.
747
+ attr_accessor :data
396
748
 
397
749
  # Should exceptions be handled gracefully? I.e. by printing error message and the help screen?
750
+ #
751
+ # See ::new for possible values.
398
752
  attr_reader :handle_exceptions
399
753
 
400
- # Create a new CommandParser object.
401
- #
402
- # [handleExceptions (optional)]
403
- # Specifies if the object should handle exceptions gracefully.
404
- # [partial_commands (optional)]
405
- # Specifies if you want partial command matching for the top level commands.
406
- # [has_args (optional)]
407
- # Specifies whether the command parser takes arguments (only used when no sub commands are
408
- # defined).
409
- def initialize(handleExceptions = false, partial_commands = false, has_args = true)
410
- @main_command = Command.new('mainCommand', true, partial_commands, has_args)
754
+ # The maximum width of the help lines.
755
+ attr_accessor :help_line_width
756
+
757
+ # The amount of spaces to indent the content of help sections.
758
+ attr_accessor :help_indent
759
+
760
+ # The indentation used for, among other things, command descriptions.
761
+ attr_accessor :help_desc_indent
762
+
763
+ # Creates a new CommandParser object.
764
+ #
765
+ # Options:
766
+ #
767
+ # handle_exceptions:: Set to +true+ if exceptions should be handled gracefully by showing the
768
+ # error and a help message, or to +false+ if exception should not be handled
769
+ # at all. If this options is set to :no_help, the exception is handled but no
770
+ # help message is shown.
771
+ #
772
+ # takes_commands:: Specifies whether the main program takes any commands.
773
+ def initialize(handle_exceptions: false, takes_commands: true)
774
+ @global_options = OptionParser.new
775
+ @main_command = Command.new('main', takes_commands: takes_commands)
411
776
  @main_command.super_command = self
412
- @program_name = $0
413
- @program_version = "0.0.0"
414
- @handle_exceptions = handleExceptions
777
+ @main_command.options.stack[0] = MultiList.new(@global_options.stack)
778
+ @handle_exceptions = handle_exceptions
779
+ @help_line_width = 80
780
+ @help_indent = 4
781
+ @help_desc_indent = 18
782
+ @data = {}
415
783
  end
416
784
 
417
- # Return the wrapper for parsing the global options.
418
- def options
785
+ # :call-seq:
786
+ # cmdparse.main_options -> OptionParser instance
787
+ # cmdparse.main_options {|opts| ...} -> opts (OptionParser instance)
788
+ #
789
+ # Yields the main options (that are only available directly after the program name) if a block
790
+ # is given and returns them.
791
+ #
792
+ # The main options are also used for setting the program name, version and banner.
793
+ def main_options
794
+ yield(@main_command.options) if block_given?
419
795
  @main_command.options
420
796
  end
421
797
 
422
- # Set the wrapper for parsing the global options.
423
- def options=(wrapper)
424
- @main_command.options = wrapper
798
+ # :call-seq:
799
+ # cmdparse.global_options -> OptionParser instance
800
+ # cmdparse.gloabl_options {|opts| ...} -> opts (OptionParser instance)
801
+ #
802
+ # Yields the global options if a block is given and returns them.
803
+ #
804
+ # The global options are those options that can be used on the top level and with any
805
+ # command.
806
+ def global_options
807
+ yield(@global_options) if block_given?
808
+ @global_options
425
809
  end
426
810
 
427
- # Add a top level command.
428
- def add_command(*args)
429
- @main_command.add_command(*args)
811
+ # Adds a top level command.
812
+ #
813
+ # See Command#add_command for detailed invocation information.
814
+ def add_command(*args, &block)
815
+ @main_command.add_command(*args, &block)
430
816
  end
431
817
 
432
- # Parse the command line arguments.
818
+ # Parses the command line arguments.
433
819
  #
434
- # If a block is specified, the current hierarchy level and the name of the current command is
820
+ # If a block is given, the current hierarchy level and the name of the current command is
435
821
  # yielded after the option parsing is done but before a command is executed.
436
- def parse(argv = ARGV) # :yields: level, commandName
822
+ def parse(argv = ARGV) # :yields: level, command_name
437
823
  level = 0
438
- command = @main_command
824
+ @current_command = @main_command
439
825
 
440
- while !command.nil?
441
- argv = if command.has_commands? || ENV.include?('POSIXLY_CORRECT')
442
- command.options.order(argv)
826
+ while true
827
+ argv = if @current_command.takes_commands? || ENV.include?('POSIXLY_CORRECT')
828
+ @current_command.options.order(argv)
443
829
  else
444
- command.options.permute(argv)
830
+ @current_command.options.permute(argv)
445
831
  end
446
- yield(level, command.name) if block_given?
447
-
448
- if command.has_commands?
449
- cmdName, argv = argv[0], argv[1..-1] || []
450
-
451
- if cmdName.nil?
452
- if command.default_command.nil?
453
- raise NoCommandGivenError
454
- else
455
- cmdName = command.default_command
456
- end
457
- else
458
- raise InvalidCommandError.new(cmdName) unless command.commands[ cmdName ]
832
+ yield(level, @current_command.name) if block_given?
833
+
834
+ if @current_command.takes_commands?
835
+ cmd_name = argv.shift || @current_command.default_command
836
+
837
+ if cmd_name.nil?
838
+ raise NoCommandGivenError.new
839
+ elsif !@current_command.commands.key?(cmd_name)
840
+ raise InvalidCommandError.new(cmd_name)
459
841
  end
460
842
 
461
- command = command.commands[cmdName]
843
+ @current_command = @current_command.commands[cmd_name]
462
844
  level += 1
463
845
  else
464
- command.execute(argv)
465
- command = nil
846
+ original_n = @current_command.arity
847
+ n = (original_n < 0 ? -original_n - 1 : original_n)
848
+ raise NotEnoughArgumentsError.new(n) if argv.size < n
849
+
850
+ argv.slice!(n..-1) unless original_n < 0
851
+ @current_command.execute(*argv)
852
+ break
466
853
  end
467
854
  end
468
855
  rescue ParseError, OptionParser::ParseError => e
469
- raise if !@handle_exceptions
856
+ raise unless @handle_exceptions
470
857
  puts "Error while parsing command line:\n " + e.message
471
- puts
472
- @main_command.commands['help'].execute(command.super_commands.reverse.collect {|c| c.name}) if @main_command.commands['help']
473
- exit
858
+ if @handle_exceptions != :no_help && @main_command.commands.key?('help')
859
+ puts
860
+ @main_command.commands['help'].execute(*@current_command.command_chain.map(&:name))
861
+ end
862
+ exit(64) # FreeBSD standard exit error for "command was used incorrectly"
863
+ ensure
864
+ @current_command = nil
474
865
  end
475
866
 
476
867
  end