lib/thor/options.rb in mislav-thor-0.9.5 vs lib/thor/options.rb in mislav-thor-0.9.10

- old
+ new

@@ -1,59 +1,57 @@ -# This is a modified version of Daniel Berger's Getopt::ong class, +# This is a modified version of Daniel Berger's Getopt::Long class, # licensed under Ruby's license. -require 'set' - class Thor class Options class Error < StandardError; end - - LONG_RE = /^(--\w+[-\w+]*)$/ - SHORT_RE = /^(-\w)$/ - LONG_EQ_RE = /^(--\w+[-\w+]*)=(.*?)$|(-\w?)=(.*?)$/ - SHORT_SQ_RE = /^-(\w\S+?)$/ # Allow either -x -v or -xv style for single char args - - attr_accessor :args - - def initialize(args, switches) - @args = args - @defaults = {} - - switches = switches.map do |names, type| - case type - when TrueClass then type = :boolean - when String - @defaults[names] = type - type = :optional - end - - if names.is_a?(String) - if names =~ LONG_RE - names = [names, "-" + names[2].chr] - else - names = [names] - end - end - - [names, type] + + # simple Hash with indifferent access + class Hash < ::Hash + def initialize(hash) + super() + update hash end - - @valid = switches.map {|s| s.first}.flatten.to_set - @types = switches.inject({}) do |h, (forms,v)| - forms.each {|f| h[f] ||= v} - h + + def [](key) + super convert_key(key) end - @syns = switches.inject({}) do |h, (forms,_)| - forms.each {|f| h[f] ||= forms} - h + + 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 = method.to_s + if method =~ /^(\w+)=$/ + self[$1] = args.first + elsif method =~ /^(\w+)\?$/ + !!self[$1] + else + self[method] + end + end end - def skip_non_opts - non_opts = [] - non_opts << pop until looking_at_opt? || @args.empty? - non_opts + 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 @@ -63,94 +61,207 @@ # 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(args, - # "--debug" => true, - # ["--verbose", "-v"] => true, - # ["--level", "-l"] => :numeric - # ).getopts + # opts = Thor::Options.new( + # "--debug" => true, + # ["--verbose", "-v"] => true, + # ["--level", "-l"] => :numeric + # ).parse(args) # - def getopts(check_required = true) - hash = @defaults.dup + def initialize(switches) + @defaults = {} + @shorts = {} + + @leading_non_opts, @trailing_non_opts = [], [] - while looking_at_opt? - case pop + @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 + @defaults[nice_name] = true + type = :boolean + when FalseClass + @defaults[nice_name] = false + 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 - push(*$1.split("").map {|s| s = "-#{s}"}) + unshift $1.split('').map { |f| "-#{f}" } next - when LONG_EQ_RE - push($1, $2) - next + when EQ_RE, SHORT_NUM + unshift $2 + switch = $1 when LONG_RE, SHORT_RE switch = $1 end - - case @types[switch] + + switch = normalize_switch(switch) + nice_name = undasherize(switch) + type = switch_type(switch) + + case type when :required - raise Error, "no value provided for required argument '#{switch}'" if peek.nil? - raise Error, "cannot pass switch '#{peek}' as an argument" if @valid.include?(peek) - hash[switch] = pop - when :boolean - hash[switch] = true + assert_value!(switch) + raise Error, "cannot pass switch '#{peek}' as an argument" if valid?(peek) + hash[nice_name] = shift when :optional - # For optional arguments, there may be an argument. If so, it - # cannot be another switch. If not, it is set to true. - hash[switch] = @valid.include?(peek) || peek.nil? || pop + hash[nice_name] = peek.nil? || valid?(peek) || shift + when :boolean + if !@switches.key?(switch) && nice_name =~ /^no-(\w+)$/ + hash[$1] = false + else + hash[nice_name] = true + end + + 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 - hash = normalize_hash hash - check_required_args hash if check_required + check_required! hash + hash.freeze hash end - - def check_required_args(hash) - @types.select {|k,v| v == :required}.map {|k,v| @syns[k]}.uniq.each do |syns| - unless syns.map {|s| s.gsub(/^-+/, '')}.any? {|s| hash[s]} - raise Error, "no value provided for required argument '#{syns.first}'" + + 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 + 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 pop - arg = peek - @args = @args[1..-1] || [] - arg + def shift + @args.shift end - def push(*args) - @args = args + @args + def unshift(arg) + unless arg.kind_of?(Array) + @args.unshift(arg) + else + @args = arg + @args + end end + + def valid?(arg) + if arg.to_s =~ /^--no-(\w+)$/ + @switches.key?(arg) or (@switches["--#{$1}"] == :boolean) + else + @switches.key?(arg) or @shorts.key?(arg) + end + end - def looking_at_opt? + def current_is_option? case peek - when LONG_RE, SHORT_RE, LONG_EQ_RE - @valid.include? $1 + when LONG_RE, SHORT_RE, EQ_RE, SHORT_NUM + valid?($1) when SHORT_SQ_RE - $1.split("").any? {|f| @valid.include? "-#{f}"} + $1.split('').any? { |f| valid?("-#{f}") } end end - - # Set synonymous switches to the same value, e.g. if -t is a synonym - # for --test, and the user passes "--test", then set "-t" to the same - # value that "--test" was set to. - # - # This allows users to refer to the long or short switch and get - # the same value - def normalize_hash(hash) - hash.map do |switch, val| - @syns[switch].map {|key| [key, val]} - end.inject([]) {|a, v| a + v}.map do |key, value| - [key.sub(/^-+/, ''), value] - end.inject({}) {|h, (k,v)| h[k] = v; h[k.to_sym] = v; h} + + def normalize_switch(switch) + @shorts.key?(switch) ? @shorts[switch] : switch end - + + def switch_type(switch) + if switch =~ /^--no-(\w+)$/ + @switches[switch] || @switches["--#{$1}"] + else + @switches[switch] + end + 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