class Commander::Runner
def self.instance
def self.instance @instance ||= new end
def self.separate_switches_from_description(*args)
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
def self.switch_to_sym(switch)
def self.switch_to_sym(switch) switch.scan(/[\-\]](\w+)/).join('_').to_sym rescue nil end
def active_command
def active_command @active_command ||= command(command_name_from_args) end
def add_command(command)
def add_command(command) @commands[command.name] = command end
def alias?(name)
def alias?(name) @aliases.include? name.to_s end
def alias_command(alias_name, name, *args)
def alias_command(alias_name, name, *args) @commands[alias_name.to_s] = command name @aliases[alias_name.to_s] = args end
def always_trace!
def always_trace! @always_trace = true @never_trace = false end
def args_without_command_name
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
def command(name, &block)
def command(name, &block) yield add_command(Commander::Command.new(name)) if block @commands[name.to_s] end
def command_exists?(name)
def command_exists?(name) @commands[name.to_s] end
def command_name_from_args
def command_name_from_args @command_name_from_args ||= (longest_valid_command_name_from(@args) || @default_command) end
def create_default_commands
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
def default_command(name)
def default_command(name) @default_command = name end
def expand_optionally_negative_switches(switches)
'--blah' and '--no-blah' variants, so that they can be
expand switches of the style '--[no-]blah' into both their
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
def global_option(*args, &block)
def global_option(*args, &block) switches, description = Runner.separate_switches_from_description(*args) @options << { args: args, proc: block, switches: switches, description: description, } end
def global_option_proc(switches, &block)
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
def help_formatter
def help_formatter @help_formatter ||= program(:help_formatter).new self end
def help_formatter_alias_defaults
def help_formatter_alias_defaults { compact: HelpFormatter::TerminalCompact, } end
def initialize(args = ARGV)
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
def longest_valid_command_name_from(args)
def longest_valid_command_name_from(args) valid_command_names_from(*args.dup).max end
def never_trace!
def never_trace! @never_trace = true @always_trace = false end
def parse_global_options
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
def program(key, *args, &block)
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
def program_defaults
def program_defaults { help_formatter: HelpFormatter::Terminal, name: File.basename($PROGRAM_NAME), help_paging: true, } end
def remove_global_options(options, args)
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
def require_program(*keys)
def require_program(*keys) keys.each do |key| fail CommandError, "program #{key} required" if program(key).nil? || program(key).empty? end end
def require_valid_command(command = active_command)
def require_valid_command(command = active_command) fail InvalidCommandError, 'invalid command', caller if command.nil? end
def run!
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
def run_active_command
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
def say(*args) #:nodoc:
def say(*args) #:nodoc: HighLine.default_instance.say(*args) end
def valid_command_names_from(*args)
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
def version
def version format('%s %s', program(:name), program(:version)) end