cmdparse 2.0.6 → 3.0.0

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