Class: Commander::Runner

Inherits:
Object show all
Defined in:
lib/commander/runner.rb

Defined Under Namespace

Classes: CommandError, InvalidCommandError

Constant Summary collapse

DEFAULT_ERROR_HANDLER =
lambda do |runner, e|
  error_msg = "#{Paint[runner.program(:name), '#2794d8']}: #{Paint[e.to_s, :red, :bright]}"
  case e
  when OptionParser::InvalidOption,
       Commander::Runner::InvalidCommandError,
       Commander::Patches::CommandUsageError
    # Display the error message for a specific command. Most likely due to
    # invalid command syntax
    if cmd = runner.active_command
      $stderr.puts error_msg
      $stderr.puts "\nUsage:\n\n"
      runner.command('help').run(cmd.name)
    # Display the main app help text when called without `--help`
    elsif runner.args_without_command_name.empty?
      $stderr.puts "Usage:\n\n"
      runner.command('help').run(:error)
    # Display the main app help text when called with arguments. Mostly
    # likely an invalid syntax error
    else
      $stderr.puts error_msg
      $stderr.puts "\nUsage:\n\n"
      runner.command('help').run(:error)
    end
  # Display the help text for sub command groups when called without `--help`
  when SubCommandGroupError
    if cmd = runner.active_command
      $stderr.puts "Usage:\n\n"
      runner.command('help').run(cmd.name)
    end
  # Catch all error message for all other issues
  else
    $stderr.puts error_msg
  end
  exit(1)
end

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(args = ARGV) ⇒ Runner

Initialize a new command runner. Optionally supplying args for mocking, or arbitrary usage.



66
67
68
69
70
71
72
73
74
75
# File 'lib/commander/runner.rb', line 66

def initialize(args = ARGV)
  @args, @commands, @aliases, @options = args, {}, {}, []
  @help_formatter_aliases = help_formatter_alias_defaults
  @program = program_defaults
  @always_trace = false
  @never_trace = false
  @silent_trace = false
  @error_handler = DEFAULT_ERROR_HANDLER
  create_default_commands
end

Instance Attribute Details

#commandsObject (readonly)

Array of commands.



50
51
52
# File 'lib/commander/runner.rb', line 50

def commands
  @commands
end

#help_formatter_aliasesObject (readonly)

Hash of help formatter aliases.



60
61
62
# File 'lib/commander/runner.rb', line 60

def help_formatter_aliases
  @help_formatter_aliases
end

#optionsObject (readonly)

Global options.



55
56
57
# File 'lib/commander/runner.rb', line 55

def options
  @options
end

Class Method Details

.instanceObject

Return singleton Runner instance.



80
81
82
# File 'lib/commander/runner.rb', line 80

def self.instance
  @singleton ||= new
end

.separate_switches_from_description(*args) ⇒ Object

Return switches and description separated from the args passed.



506
507
508
509
510
# File 'lib/commander/runner.rb', line 506

def self.separate_switches_from_description(*args)
  switches = args.find_all { |arg| arg.to_s =~ /^-/ }
  description = args.last if args.last.is_a?(String) && !args.last.match(/^-/)
  [switches, description]
end

.switch_to_sym(switch) ⇒ Object

Attempts to generate a method name symbol from switch. For example:

-h                 # => :h
--trace            # => :trace
--some-switch      # => :some_switch
--[no-]feature     # => :feature
--file FILE        # => :file
--list of,things   # => :list


524
525
526
# File 'lib/commander/runner.rb', line 524

def self.switch_to_sym(switch)
  switch.scan(/[\-\]](\w+)/).join('_').to_sym rescue nil
end

Instance Method Details

#active_commandObject

Get active command within arguments passed to this runner.



292
293
294
# File 'lib/commander/runner.rb', line 292

def active_command
  @__active_command ||= command(command_name_from_args)
end

#add_command(command) ⇒ Object

Add a command object to this runner.



269
270
271
# File 'lib/commander/runner.rb', line 269

def add_command(command)
  @commands[command.name] = command
end

#alias?(name) ⇒ Boolean

Check if command name is an alias.

Returns:

  • (Boolean)


276
277
278
# File 'lib/commander/runner.rb', line 276

def alias?(name)
  @aliases.include? name.to_s
end

#alias_command(alias_name, name, *args) ⇒ Object

Alias command name with alias_name. Optionally args may be passed as if they were being passed straight to the original command via the command-line.



253
254
255
256
# File 'lib/commander/runner.rb', line 253

def alias_command(alias_name, name, *args)
  @commands[alias_name.to_s] = command name
  @aliases[alias_name.to_s] = args
end

#always_trace!Object

Enable tracing on all executions (bypasses –trace)



139
140
141
142
143
# File 'lib/commander/runner.rb', line 139

def always_trace!
  @always_trace = true
  @never_trace = false
  @silent_trace = false
end

#args_without_command_nameObject

Return arguments without the command name.



322
323
324
325
326
327
328
# File 'lib/commander/runner.rb', line 322

def args_without_command_name
  removed = []
  parts = command_name_from_args.split rescue []
  @args.dup.delete_if do |arg|
    removed << arg if parts.include?(arg) && !removed.include?(arg)
  end
end

#command(name) {|add_command(Commander::Command.new(name))| ... } ⇒ Object

Creates and yields a command instance when a block is passed. Otherwise attempts to return the command, raising InvalidCommandError when it does not exist.

Examples

command :my_command do |c|
  c.when_called do |args|
    # Code
  end
end

Yields:



230
231
232
233
# File 'lib/commander/runner.rb', line 230

def command(name, &block)
  yield add_command(Commander::Command.new(name)) if block
  @commands[name.to_s]
end

#command_exists?(name) ⇒ Boolean

Check if a command name exists.

Returns:

  • (Boolean)


283
284
285
# File 'lib/commander/runner.rb', line 283

def command_exists?(name)
  @commands[name.to_s]
end

#command_name_from_argsObject

Attempts to locate a command name from within the arguments. Supports multi-word commands, using the largest possible match.



300
301
302
# File 'lib/commander/runner.rb', line 300

def command_name_from_args
  @__command_name_from_args ||= (valid_command_names_from(*@args.dup).sort.last || @default_command)
end

#create_default_commandsObject

Creates default commands such as ‘help’ which is essentially the same as using the –help switch.



372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
# File 'lib/commander/runner.rb', line 372

def create_default_commands
  command :help do |c|
    c.syntax = "#{program(:name)} help [command]"
    c.description = 'Display global or [command] help documentation'
    c.example 'Display global help', "#{program(:name)} help"
    c.example "Display help for 'foo'", "#{program(:name)} help foo"
    c.when_called do |args, _options|
      UI.enable_paging if program(:help_paging)
      @help_commands = @commands.dup
      if args.empty? || args[0] == :error
        @help_options = @options.reject {|o| o[:switches].first == '--trace'}
        @help_commands.reject! { |k, v| !!v.hidden }
        old_wrap = $terminal.wrap_at
        $terminal.wrap_at = nil
        program(:nobanner, true) if args[0] == :error
        say help_formatter.render
        $terminal.wrap_at = old_wrap
      else
        command = command args.join(' ')
        begin
          require_valid_command command
        rescue InvalidCommandError => e
          error_handler&.call(self, e) ||
            abort("#{e}. Use --help for more information")
        end
        if command.sub_command_group?
          limit_commands_to_subcommands(command)
          say help_formatter.render_subcommand(command)
        else
          say help_formatter.render_command(command)
        end
      end
    end
  end
end

#default_command(name) ⇒ Object

Default command name to be used when no other command is found in the arguments.



262
263
264
# File 'lib/commander/runner.rb', line 262

def default_command(name)
  @default_command = name
end

#error_handler(&block) ⇒ Object

Set a handler to be used for advanced exception handling



166
167
168
169
# File 'lib/commander/runner.rb', line 166

def error_handler(&block)
  @error_handler = block if block
  @error_handler
end

#expand_optionally_negative_switches(switches) ⇒ Object

expand switches of the style ‘–[no-]blah’ into both their ‘–blah’ and ‘–no-blah’ variants, so that they can be properly detected and removed



450
451
452
453
454
455
456
457
458
459
# File 'lib/commander/runner.rb', line 450

def expand_optionally_negative_switches(switches)
  switches.reduce([]) do |memo, val|
    if val =~ /\[no-\]/
      memo << val.gsub(/\[no-\]/, '')
      memo << val.gsub(/\[no-\]/, 'no-')
    else
      memo << val
    end
  end
end

#global_option(*args, &block) ⇒ Object

Add a global option; follows the same syntax as Command#option This would be used for switches such as –version, –trace, etc.



239
240
241
242
243
244
245
246
247
# File 'lib/commander/runner.rb', line 239

def global_option(*args, &block)
  switches, description = Runner.separate_switches_from_description(*args)
  @options << {
    args: args,
    proc: block,
    switches: switches,
    description: description,
  }
end

#global_option_proc(switches, &block) ⇒ Object

Returns a proc allowing for commands to inherit global options. This functionality works whether a block is present for the global option or not, so simple switches such as –verbose can be used without a block, and used throughout all commands.



485
486
487
488
489
490
491
492
# File 'lib/commander/runner.rb', line 485

def global_option_proc(switches, &block)
  lambda do |value|
    unless active_command.nil?
      active_command.proxy_options << [Runner.switch_to_sym(switches.last), value]
    end
    yield value if block && !value.nil?
  end
end

#help_formatterObject

Help formatter instance.



315
316
317
# File 'lib/commander/runner.rb', line 315

def help_formatter
  @__help_formatter ||= program(:help_formatter).new self
end

#help_formatter_alias_defaultsObject

Returns hash of help formatter alias defaults.



333
334
335
336
337
# File 'lib/commander/runner.rb', line 333

def help_formatter_alias_defaults
  {
    compact: HelpFormatter::TerminalCompact,
  }
end

#limit_commands_to_subcommands(command) ⇒ Object

Limit commands to those which are subcommands of the one that is active



352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
# File 'lib/commander/runner.rb', line 352

def limit_commands_to_subcommands(command)
  commands.select! do |other_sym, _|
    other = other_sym.to_s
    # Do not match sub-sub commands (matches for a second space)
    if /\A#{command.name}\s.*\s/.match?(other)
      false
    # Do match regular sub commands
    elsif /\A#{command.name}\s/.match?(other)
      true
    # Do not match any other commands
    else
      false
    end
  end
end

#never_trace!Object

Hide the trace option from the help menus and don’t add it as a global option



148
149
150
151
152
# File 'lib/commander/runner.rb', line 148

def never_trace!
  @always_trace = false
  @never_trace = true
  @silent_trace = false
end

#parse_global_optionsObject

Parse global command options.



464
465
466
467
468
469
470
471
472
473
474
475
476
477
# File 'lib/commander/runner.rb', line 464

def parse_global_options
  parser = options.inject(OptionParser.new) do |options, option|
    options.on(*option[:args], &global_option_proc(option[:switches], &option[:proc]))
  end

  options = @args.dup
  begin
    parser.parse!(options)
  rescue OptionParser::InvalidOption => e
    # Remove the offending args and retry.
    options = options.reject { |o| e.args.include?(o) }
    retry
  end
end

#program(key, *args, &block) ⇒ Object

Assign program information.

Examples

# Set data
program :name, 'Commander'
program :version, Commander::VERSION
program :description, 'Commander utility program.'
program :help, 'Copyright', '2008 TJ Holowaychuk'
program :help, 'Anything', 'You want'
program :int_message 'Bye bye!'
program :help_formatter, :compact
program :help_formatter, Commander::HelpFormatter::TerminalCompact

# Get data
program :name # => 'Commander'

Keys

:version         (required) Program version triple, ex: '0.0.1'
:description     (required) Program description
:name            Program name, defaults to basename of executable
:help_formatter  Defaults to Commander::HelpFormatter::Terminal
:help            Allows addition of arbitrary global help blocks
:help_paging     Flag for toggling help paging
:int_message     Message to display when interrupted (CTRL + C)


200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
# File 'lib/commander/runner.rb', line 200

def program(key, *args, &block)
  if key == :help && !args.empty?
    @program[:help] ||= {}
    @program[:help][args.first] = args.at(1)
  elsif key == :help_formatter && !args.empty?
    @program[key] = (@help_formatter_aliases[args.first] || args.first)
  elsif block
    @program[key] = block
  else
    unless args.empty?
      @program[key] = args.count == 1 ? args[0] : args
    end
    @program[key]
  end
end

#program_defaultsObject

Returns hash of program defaults.



342
343
344
345
346
347
348
# File 'lib/commander/runner.rb', line 342

def program_defaults
  {
    help_formatter: HelpFormatter::Terminal,
    name: File.basename($PROGRAM_NAME),
    help_paging: true,
  }
end

#remove_global_options(options, args) ⇒ Object

Removes global options from args. This prevents an invalid option error from occurring when options are parsed again for the command.



420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
# File 'lib/commander/runner.rb', line 420

def remove_global_options(options, args)
  # TODO: refactor with flipflop, please TJ ! have time to refactor me !
  options.each do |option|
    switches = option[:switches].dup
    next if switches.empty?

    if (switch_has_arg = switches.any? { |s| s =~ /[ =]/ })
      switches.map! { |s| s[0, s.index('=') || s.index(' ') || s.length] }
    end

    switches = expand_optionally_negative_switches(switches)

    past_switch, arg_removed = false, false
    args.delete_if do |arg|
      if switches.any? { |s| s == arg }
        arg_removed = !switch_has_arg
        past_switch = true
      elsif past_switch && !arg_removed && arg !~ /^-/
        arg_removed = true
      else
        arg_removed = true
        false
      end
    end
  end
end

#require_program(*keys) ⇒ Object

Raises a CommandError when the program any of the keys are not present, or empty.



497
498
499
500
501
# File 'lib/commander/runner.rb', line 497

def require_program(*keys)
  keys.each do |key|
    fail CommandError, "program #{key} required" if program(key).nil? || program(key).empty?
  end
end

#require_valid_command(command = active_command) ⇒ Object

Raises InvalidCommandError when a command is not found.



411
412
413
# File 'lib/commander/runner.rb', line 411

def require_valid_command(command = active_command)
  fail InvalidCommandError, 'invalid command', caller if command.nil?
end

#run!Object

Run command parsing and execution process.



87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
# File 'lib/commander/runner.rb', line 87

def run!
  trace = @always_trace || false
  require_program :version, :description
  trap('INT') { abort program(:int_message) } if program(:int_message)
  trap('INT') { program(:int_block).call } if program(:int_block)
  global_option('-h', '--help', 'Display help documentation') do
    args = @args - %w(-h --help)
    command(:help).run(*args)
    return
  end
  global_option('--version', 'Display version information') do
    say version
    return
  end
  global_option('--trace', 'Display backtrace when an error occurs') { trace = true } unless @never_trace || @always_trace
  parse_global_options
  remove_global_options options, @args
  if trace
    run_active_command
  else
    begin
      run_active_command
    rescue InvalidCommandError => e
      error_handler&.call(self, e) ||
        abort("#{e}. Use --help for more information")
    rescue \
      OptionParser::InvalidOption,
      OptionParser::InvalidArgument,
      OptionParser::MissingArgument => e
      error_handler&.call(self, e) ||
        abort(e.to_s)
    rescue => e
      error_handler&.call(self, e) ||
        if @never_trace || @silent_trace
          abort("error: #{e}.")
        else
          abort("error: #{e}. Use --trace to view backtrace")
        end
    end
  end
end

#run_active_commandObject

Run the active command.



531
532
533
534
535
536
537
538
# File 'lib/commander/runner.rb', line 531

def run_active_command
  require_valid_command
  if alias? command_name_from_args
    active_command.run(*(@aliases[command_name_from_args.to_s] + args_without_command_name))
  else
    active_command.run(*args_without_command_name)
  end
end

#say(*args) ⇒ Object

:nodoc:



540
541
542
# File 'lib/commander/runner.rb', line 540

def say(*args) #:nodoc:
  $terminal.say(*args)
end

#silent_trace!Object

Includes the trace option in the help but not in the error message



157
158
159
160
161
# File 'lib/commander/runner.rb', line 157

def silent_trace!
  @always_trace = false
  @never_trace = false
  @silent_trace = true
end

#valid_command_names_from(*args) ⇒ Object

Returns array of valid command names found within args.



307
308
309
310
# File 'lib/commander/runner.rb', line 307

def valid_command_names_from(*args)
  arg_string = args.delete_if { |value| value =~ /^-/ }.join ' '
  commands.keys.find_all { |name| name if arg_string =~ /^#{name}\b/ }
end

#versionObject

Return program version.



132
133
134
# File 'lib/commander/runner.rb', line 132

def version
  format('%s %s', program(:name), program(:version))
end