lib/thor/options.rb
# This is a modified version of Daniel Berger's Getopt::Long class, # licensed under Ruby's license. class Thor class Options class Error < StandardError; end # simple Hash with indifferent access class Hash < ::Hash def initialize(hash) super() update hash end def [](key) super convert_key(key) end def values_at(*indices) indices.collect { |key| self[convert_key(key)] } end protected def convert_key(key) key.kind_of?(Symbol) ? key.to_s : key end # Magic predicates. For instance: # options.force? # => !!options['force'] def method_missing(method, *args, &block) method.to_s =~ /^(\w+)\?$/ ? !!self[$1] : super end end NUMERIC = /(\d*\.\d+|\d+)/ LONG_RE = /^(--\w+[-\w+]*)$/ SHORT_RE = /^(-[a-z])$/i EQ_RE = /^(--\w+[-\w+]*|-[a-z])=(.*)$/i SHORT_SQ_RE = /^-([a-z]{2,})$/i # Allow either -x -v or -xv style for single char args SHORT_NUM = /^(-[a-z])#{NUMERIC}$/i attr_reader :leading_non_opts, :trailing_non_opts def non_opts leading_non_opts + trailing_non_opts end # Takes an array of switches. Each array consists of up to three # elements that indicate the name and type of switch. Returns a hash # containing each switch name, minus the '-', as a key. The value # for each key depends on the type of switch and/or the value provided # by the user. # # The long switch _must_ be provided. The short switch defaults to the # first letter of the short switch. The default type is :boolean. # # Example: # # opts = Thor::Options.new( # "--debug" => true, # ["--verbose", "-v"] => true, # ["--level", "-l"] => :numeric # ).parse(args) # def initialize(switches) @defaults = {} @shorts = {} @leading_non_opts, @trailing_non_opts = [], [] @switches = switches.inject({}) do |mem, (name, type)| if name.is_a?(Array) name, *shorts = name else name = name.to_s shorts = [] end # we need both nice and dasherized form of switch name if name.index('-') == 0 nice_name = undasherize name else nice_name = name name = dasherize name end # if there are no shortcuts specified, generate one using the first character shorts << "-" + nice_name[0,1] if shorts.empty? and nice_name.length > 1 shorts.each { |short| @shorts[short] = name } # normalize type case type when TrueClass then type = :boolean when String @defaults[nice_name] = type type = :optional when Numeric @defaults[nice_name] = type type = :numeric end mem[name] = type mem end # remove shortcuts that happen to coincide with any of the main switches @shorts.keys.each do |short| @shorts.delete(short) if @switches.key?(short) end end def parse(args, skip_leading_non_opts = true) @args = args # start with Thor::Options::Hash pre-filled with defaults hash = Hash.new @defaults @leading_non_opts = [] if skip_leading_non_opts @leading_non_opts << shift until current_is_option? || @args.empty? end while current_is_option? case shift when SHORT_SQ_RE unshift $1.split('').map { |f| "-#{f}" } next when EQ_RE, SHORT_NUM unshift $2 switch = $1 when LONG_RE, SHORT_RE switch = $1 end switch = normalize_switch(switch) nice_name = undasherize(switch) type = switch_type(switch) case type when :required assert_value!(switch) raise Error, "cannot pass switch '#{peek}' as an argument" if valid?(peek) hash[nice_name] = shift when :optional hash[nice_name] = peek.nil? || valid?(peek) || shift when :boolean hash[nice_name] = true when :numeric assert_value!(switch) unless peek =~ NUMERIC and $& == peek raise Error, "expected numeric value for '#{switch}'; got #{peek.inspect}" end hash[nice_name] = $&.index('.') ? shift.to_f : shift.to_i end end @trailing_non_opts = @args check_required! hash hash.freeze hash end def formatted_usage return "" if @switches.empty? @switches.map do |opt, type| case type when :boolean "[#{opt}]" when :required opt + "=" + opt.gsub(/\-/, "").upcase else sample = @defaults[undasherize(opt)] sample ||= case type when :optional then undasherize(opt).gsub(/\-/, "_").upcase when :numeric then "N" end "[" + opt + "=" + sample.to_s + "]" end end.join(" ") end alias :to_s :formatted_usage private def assert_value!(switch) raise Error, "no value provided for argument '#{switch}'" if peek.nil? end def undasherize(str) str.sub(/^-{1,2}/, '') end def dasherize(str) (str.length > 1 ? "--" : "-") + str end def peek @args.first end def shift @args.shift end def unshift(arg) unless arg.kind_of?(Array) @args.unshift(arg) else @args = arg + @args end end def valid?(arg) @switches.key?(arg) or @shorts.key?(arg) end def current_is_option? case peek when LONG_RE, SHORT_RE, EQ_RE, SHORT_NUM valid?($1) when SHORT_SQ_RE $1.split('').any? { |f| valid?("-#{f}") } end end def normalize_switch(switch) @shorts.key?(switch) ? @shorts[switch] : switch end def switch_type(switch) @switches[switch] end def check_required!(hash) for name, type in @switches if type == :required and !hash[undasherize(name)] raise Error, "no value provided for required argument '#{name}'" end end end end end