module CommandLine class Option attr_reader :name, :alias attr_reader :parameter_count attr_reader :default_value # Rewrites a command line keyword by replacing the underscores with dashes # sym The symbol to rewrite def self.rewrite(sym) sym.to_s.gsub(/_/, '-').to_sym end # Initialize new CommandLine::Option # name The name of the flag # definition The definition of the flag. def initialize(name, definition = {}) @name = CommandLine::Option.rewrite(name) @alias = definition[:alias].to_sym if definition[:alias] @required = definition.has_key?(:required) && definition[:required] == true @parameter_count = definition[:parameters] || 1 @multiple = definition[:multiple] || false @default_value = definition[:default] || false end def parse(arguments_parser) if @parameter_count == 0 return true elsif @parameter_count == 1 parameter = arguments_parser.next_parameter raise CommandLine::ParameterExpected, self if parameter.nil? return parameter elsif @parameter_count == :any parameters = [] while parameter = arguments_parser.next_parameter && parameter != '--' parameters << parameter end return parameters else parameters = [] @parameter_count.times do |n| parameter = arguments_parser.next_parameter raise CommandLine::ParameterExpected, self if parameter.nil? parameters << parameter end return parameters end end def =~(test) [@name, @alias].include?(CommandLine::Option.rewrite(test)) end # Argument representation of the flag (--fast) def to_option "--#{@name}" end # Argument alias representation of the flag (-f) def to_alias "-#{@alias}" end # Check if flag has an alias def has_alias? !@alias.nil? end # Check if flag is required def required? @required end # Check if flag is optional def optional? !@required end def multiple? @multiple end def has_default? !@default_value.nil? end end class Arguments class Definition ENDLESS_PARAMETERS = 99999 attr_reader :commands, :options, :parameters def initialize(parent) @parent = parent @options = {} @commands = {} @parameters = nil end def [](option_name) option_symbol = CommandLine::Option.rewrite(option_name) if the_option = @options.detect { |(name, odef)| odef =~ option_symbol } the_option[1] else raise CommandLine::UnknownOption, option_name end end def minimum_parameters=(count_specifier) @parameters = count_specifier..ENDLESS_PARAMETERS end def parameters=(count_specifier) @parameters = count_specifier end alias :files= :parameters= def option(name, options = {}) clo = CommandLine::Option.new(name, options) @options[clo.name] = clo end def switch(name, switch_alias = nil) option(name, :alias => switch_alias, :parameters => 0) end def command(name, &block) command_definition = Definition.new(self) yield(command_definition) if block_given? @commands[CommandLine::Option.rewrite(name)] = command_definition end def has_command?(command) @commands[CommandLine::Option.rewrite(command)] end end OPTION_REGEXP = /^\-\-([A-Za-z0-9-]+)$/; ALIASES_REGEXP = /^\-([A-Aa-z0-9]+)$/ attr_reader :definition attr_reader :tokens attr_reader :command, :options, :parameters def self.parse(tokens = $*, &block) cla = Arguments.new cla.define(&block) return cla.parse!(tokens) end def initialize @tokens = [] @definition = Definition.new(self) @current_definition = @definition end def define(&block) yield(@definition) end def [](option) if the_option = @options.detect { |(key, value)| key =~ option } the_option[1] else @current_definition[option].default_value end end def next_token @current_token = @tokens.shift return @current_token end def next_parameter parameter_candidate = @tokens.first parameter = (parameter_candidate.nil? || OPTION_REGEXP =~ parameter_candidate || ALIASES_REGEXP =~ parameter_candidate) ? nil : @tokens.shift return parameter end def parse!(tokens) @current_definition = @definition @first_token = true @tokens = tokens.clone @options = {} @parameters = [] @command = nil prepare_result! while next_token if @first_token && command_definition = @definition.has_command?(@current_token) @current_definition = command_definition @command = CommandLine::Option.rewrite(@current_token) else case @current_token when ALIASES_REGEXP; handle_alias_expansion($1) when OPTION_REGEXP; handle_option($1) else; handle_other_parameter(@current_token) end @first_token = false end end validate_arguments! return self end protected def prepare_result! multiple_options = Hash[*@current_definition.options.select { |name, o| o.multiple? }.flatten] multiple_options.each { |name, definition| @options[definition] = [] } end def validate_arguments! if @current_definition.parameters && !(@current_definition.parameters === @parameters.length) raise CommandLine::ParametersOutOfRange.new(@current_definition.parameters, @parameters.length) end required_options = Hash[*@current_definition.options.select { |name, o| o.required? }.flatten] required_options.each do |name, definition| raise CommandLine::RequiredOptionMissing, definition unless self[name] end end def handle_alias_expansion(aliases) aliases.reverse.scan(/./) do |alias_char| if option_definition = @current_definition[alias_char] @tokens.unshift(option_definition.to_option) else raise CommandLine::UnknownOption, alias_char end end end def handle_other_parameter(parameter) @parameters << parameter end def handle_option(option_name) option_definition = @current_definition[option_name] raise CommandLine::UnknownOption, option_name if option_definition.nil? if option_definition.multiple? @options[option_definition] << option_definition.parse(self) else @options[option_definition] = option_definition.parse(self) end end end # Commandline parsing errors and exceptions class Error < Exception end # Missing a required flag class RequiredOptionMissing < CommandLine::Error def initialize(option) super("You have to provide the #{option.name} option!") end end # Missing a required file class ParametersOutOfRange < CommandLine::Error def initialize(expected, actual) if expected.kind_of?(Range) if expected.end == CommandLine::Arguments::Definition::ENDLESS_PARAMETERS super("The command expected at least #{expected.begin} parameters, but found #{actual}!") else super("The command expected between #{expected.begin} and #{expected.end} parameters, but found #{actual}!") end else super("The command expected #{expected} parameters, but found #{actual}!") end end end # Missing a required flag argument class ParameterExpected < CommandLine::Error def initialize(option) super("The option #{option.inspect} expects a parameter!") end end # Encountered an unkown flag class UnknownOption < CommandLine::Error def initialize(option_identifier) super("#{option_identifier.inspect} not recognized as a valid option!") end end end