Class: Commander::Runner

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

Overview

Runner

The engine behind Commander's DSL: holds the registered commands, global options, and program metadata, parses ARGV, and dispatches to the matched Commander::Command. Accessed as a singleton (Runner.instance) by Commander::Delegates so that the top-level +command+/+program+/+run!+ methods all operate on the same runner.

Defined Under Namespace

Classes: CommandError, InvalidCommandError

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.



30
31
32
33
34
35
36
37
# File 'lib/commander/runner.rb', line 30

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
  create_default_commands
end

Instance Attribute Details

#commandsObject (readonly)

Returns the value of attribute commands.



25
26
27
# File 'lib/commander/runner.rb', line 25

def commands
  @commands
end

#help_formatter_aliasesObject (readonly)

Returns the value of attribute help_formatter_aliases.



25
26
27
# File 'lib/commander/runner.rb', line 25

def help_formatter_aliases
  @help_formatter_aliases
end

#optionsObject (readonly)

Returns the value of attribute options.



25
26
27
# File 'lib/commander/runner.rb', line 25

def options
  @options
end

Class Method Details

.instanceObject

Return singleton Runner instance.



41
42
43
# File 'lib/commander/runner.rb', line 41

def self.instance
  @instance ||= new
end

.separate_switches_from_description(*args) ⇒ Object

Return switches and description separated from the args passed.



396
397
398
399
400
# File 'lib/commander/runner.rb', line 396

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


413
414
415
# File 'lib/commander/runner.rb', line 413

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.



218
219
220
# File 'lib/commander/runner.rb', line 218

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

#add_command(command) ⇒ Object

Add a command object to this runner.



198
199
200
# File 'lib/commander/runner.rb', line 198

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

#alias?(name) ⇒ Boolean

Check if command name is an alias.

Returns:

  • (Boolean)


204
205
206
# File 'lib/commander/runner.rb', line 204

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.



184
185
186
187
# File 'lib/commander/runner.rb', line 184

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)



94
95
96
97
# File 'lib/commander/runner.rb', line 94

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

#args_without_command_nameObject

Return arguments without the command name.



246
247
248
249
250
251
252
# File 'lib/commander/runner.rb', line 246

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:



163
164
165
166
# File 'lib/commander/runner.rb', line 163

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)


210
211
212
# File 'lib/commander/runner.rb', line 210

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. Returns the default command, if no valid commands found in the args.



226
227
228
# File 'lib/commander/runner.rb', line 226

def command_name_from_args
  @command_name_from_args ||= longest_valid_command_name_from(@args) || @default_command
end

#create_default_commandsObject

Creates default commands such as 'help' which is essentially the same as using the --help switch.



275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
# File 'lib/commander/runner.rb', line 275

def create_default_commands
  command :help do |c|
    c.syntax = 'commander help [command]'
    c.description = 'Display global or [command] help documentation'
    c.example 'Display global help', 'command help'
    c.example "Display help for 'foo'", 'command help foo'
    c.when_called do |args, _options|
      UI.enable_paging if program(:help_paging)
      if args.empty?
        say help_formatter.render
      else
        command = command(longest_valid_command_name_from(args))
        begin
          require_valid_command command
        rescue InvalidCommandError => e
          abort "#{e}. Use --help for more information"
        end
        say help_formatter.render_command(command)
      end
    end
  end
end

#default_command(name) ⇒ Object

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



192
193
194
# File 'lib/commander/runner.rb', line 192

def default_command(name)
  @default_command = name
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



344
345
346
347
348
349
350
351
352
353
# File 'lib/commander/runner.rb', line 344

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.



171
172
173
174
175
176
177
178
179
# File 'lib/commander/runner.rb', line 171

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.



377
378
379
380
381
382
383
384
# File 'lib/commander/runner.rb', line 377

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

#help_formatterObject

Help formatter instance.



240
241
242
# File 'lib/commander/runner.rb', line 240

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

#help_formatter_alias_defaultsObject

Returns hash of help formatter alias defaults.



256
257
258
259
260
# File 'lib/commander/runner.rb', line 256

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

#never_trace!Object

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



101
102
103
104
# File 'lib/commander/runner.rb', line 101

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

#parse_global_optionsObject

Parse global command options.



357
358
359
360
361
362
363
364
365
366
367
368
369
370
# File 'lib/commander/runner.rb', line 357

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)


134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
# File 'lib/commander/runner.rb', line 134

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.length == 1 ? args[0] : args
    end
    @program[key]
  end
end

#program_defaultsObject

Returns hash of program defaults.



264
265
266
267
268
269
270
# File 'lib/commander/runner.rb', line 264

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.



308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
# File 'lib/commander/runner.rb', line 308

def remove_global_options(options, args)
  options.each do |option|
    switches = option[:switches]
    next if switches.empty?

    option_takes_argument = switches.any? { |s| s =~ /[ =]/ }
    switches = expand_optionally_negative_switches(switches)

    option_argument_needs_removal = false
    args.delete_if do |token|
      break if token == '--'

      # Use just the portion of the token before the = when
      # comparing switches.
      index_of_equals = token.index('=') if option_takes_argument
      token = token[0, index_of_equals] if index_of_equals
      token_contains_option_argument = !index_of_equals.nil?

      if switches.any? { |s| s[0, token.length] == token }
        option_argument_needs_removal =
          option_takes_argument && !token_contains_option_argument
        true
      elsif option_argument_needs_removal && token !~ /^-/
        option_argument_needs_removal = false
        true
      else
        option_argument_needs_removal = false
        false
      end
    end
  end
end

#require_program(*keys) ⇒ Object

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



388
389
390
391
392
# File 'lib/commander/runner.rb', line 388

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.



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

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

#run!Object

Run command parsing and execution process.



47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
# File 'lib/commander/runner.rb', line 47

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('-v', '--version', 'Display version information') do
    say version
    return
  end
  global_option('-t', '--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
      abort "#{e}. Use --help for more information"
    rescue \
      OptionParser::InvalidOption,
      OptionParser::InvalidArgument,
      OptionParser::MissingArgument => e
      abort e.to_s
    rescue StandardError => e
      if @never_trace
        abort "error: #{e}."
      else
        abort "error: #{e}. Use --trace to view backtrace"
      end
    end
  end
end

#run_active_commandObject

Run the active command.



419
420
421
422
423
424
425
426
# File 'lib/commander/runner.rb', line 419

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:



428
429
430
# File 'lib/commander/runner.rb', line 428

def say(*args) # :nodoc:
  HighLine.default_instance.say(*args)
end

#valid_command_names_from(*args) ⇒ Object

Returns array of valid command names found within args.



232
233
234
235
236
# File 'lib/commander/runner.rb', line 232

def valid_command_names_from(*args)
  remove_global_options options, 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.



88
89
90
# File 'lib/commander/runner.rb', line 88

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