require 'optparse' module Commander class Runner #-- # Exceptions #++ class CommandError < StandardError; end class InvalidCommandError < CommandError; end ## # Array of commands. attr_reader :commands ## # Global options. attr_reader :options ## # Hash of help formatter aliases. attr_reader :help_formatter_aliases ## # Initialize a new command runner. Optionally # supplying _args_ for mocking, or arbitrary usage. def initialize args = ARGV @args, @commands, @aliases, @options = args, {}, {}, [] @help_formatter_aliases = help_formatter_alias_defaults @program = program_defaults create_default_commands end ## # Return singleton Runner instance. def self.instance @singleton ||= new end ## # Run command parsing and execution process. 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') { say version; return } global_option('-t', '--trace', 'Display backtrace when an error occurs') { trace = true } unless @never_trace parse_global_options remove_global_options options, @args unless trace 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 => e if @never_trace abort "error: #{e}." else abort "error: #{e}. Use --trace to view backtrace" end end else run_active_command end end ## # Return program version. def version '%s %s' % [program(:name), program(:version)] end ## # Enable tracing on all executions (bypasses --trace) def always_trace! @always_trace = true @never_trace = false end ## # Hide the trace option from the help menus and don't add it as a global option def never_trace! @never_trace = true @always_trace = false end ## # 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 # :int_message Message to display when interrupted (CTRL + C) # def program key, *args, &block if key == :help and !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 ## # 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 # def command name, &block yield add_command(Commander::Command.new(name)) if block @commands[name.to_s] end ## # Add a global option; follows the same syntax as Command#option # This would be used for switches such as --version, --trace, etc. def global_option *args, &block switches, description = Runner.separate_switches_from_description *args @options << { :args => args, :proc => block, :switches => switches, :description => description, } end ## # 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. def alias_command alias_name, name, *args @commands[alias_name.to_s] = command name @aliases[alias_name.to_s] = args end ## # Default command _name_ to be used when no other # command is found in the arguments. def default_command name @default_command = name end ## # Add a command object to this runner. def add_command command @commands[command.name] = command end ## # Check if command _name_ is an alias. def alias? name @aliases.include? name.to_s end ## # Check if a command _name_ exists. def command_exists? name @commands[name.to_s] end #:stopdoc: ## # Get active command within arguments passed to this runner. def active_command @__active_command ||= command(command_name_from_args) end ## # Attempts to locate a command name from within the arguments. # Supports multi-word commands, using the largest possible match. def command_name_from_args @__command_name_from_args ||= (valid_command_names_from(*@args.dup).sort.last || @default_command) end ## # Returns array of valid command names found within _args_. def valid_command_names_from *args arg_string = args.delete_if { |value| value =~ /^-/ }.join ' ' commands.keys.find_all { |name| name if /^#{name}\b/.match arg_string } end ## # Help formatter instance. def help_formatter @__help_formatter ||= program(:help_formatter).new self end ## # Return arguments without the 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) and not removed.include?(arg) end end ## # Returns hash of help formatter alias defaults. def help_formatter_alias_defaults return :compact => HelpFormatter::TerminalCompact end ## # Returns hash of program defaults. def program_defaults return :help_formatter => HelpFormatter::Terminal, :name => File.basename($0) end ## # Creates default commands such as 'help' which is # essentially the same as using the --help switch. 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| enable_paging if args.empty? say help_formatter.render else command = command args.join(' ') 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 ## # Raises InvalidCommandError when a _command_ is not found. def require_valid_command command = active_command raise InvalidCommandError, 'invalid command', caller if command.nil? end ## # Removes global _options_ from _args_. This prevents an invalid # option error from occurring when options are parsed # again for the command. 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 switchHasArg = switches.any? { |s| s =~ /[ =]/ } switches.map! { |s| s[0, s.index('=') || s.index(' ') || s.length] } end past_switch, arg_removed = false, false args.delete_if do |arg| if switches.any? { |s| arg[0, s.length] == s } arg_removed = !switchHasArg past_switch = true elsif past_switch && !arg_removed && arg !~ /^-/ arg_removed = true else arg_removed = true false end end end end ## # Parse global command 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 ## # 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. 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 and !value.nil? end end ## # Raises a CommandError when the program any of the _keys_ are not present, or empty. def require_program *keys keys.each do |key| raise CommandError, "program #{key} required" if program(key).nil? or program(key).empty? end end ## # Return switches and description separated from the _args_ passed. def self.separate_switches_from_description *args switches = args.find_all { |arg| arg.to_s =~ /^-/ } description = args.last unless !args.last.is_a? String or args.last.match(/^-/) return switches, description end ## # 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 # def self.switch_to_sym switch switch.scan(/[\-\]](\w+)/).join('_').to_sym rescue nil end ## # Run the 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: $terminal.say *args end end end