diff --git a/lib/puppet/string/option.rb b/lib/puppet/string/option.rb index 26b769c2e..70d62a01f 100644 --- a/lib/puppet/string/option.rb +++ b/lib/puppet/string/option.rb @@ -1,69 +1,78 @@ class Puppet::String::Option - attr_reader :string + attr_reader :parent attr_reader :name attr_reader :aliases attr_accessor :desc def takes_argument? !!@argument end def optional_argument? !!@optional_argument end - def initialize(string, *declaration, &block) - @string = string + def initialize(parent, *declaration, &block) + @parent = parent @optparse = [] # Collect and sort the arguments in the declaration. + dups = {} declaration.each do |item| if item.is_a? String and item.to_s =~ /^-/ then unless item =~ /^-[a-z]\b/ or item =~ /^--[^-]/ then raise ArgumentError, "#{item.inspect}: long options need two dashes (--)" end @optparse << item + + # Duplicate checking... + name = optparse_to_name(item) + if dup = dups[name] then + raise ArgumentError, "#{item.inspect}: duplicates existing alias #{dup.inspect} in #{@parent}" + else + dups[name] = item + end else raise ArgumentError, "#{item.inspect} is not valid for an option argument" end end if @optparse.empty? then raise ArgumentError, "No option declarations found while building" end # Now, infer the name from the options; we prefer the first long option as # the name, rather than just the first option. @name = optparse_to_name(@optparse.find do |a| a =~ /^--/ end || @optparse.first) @aliases = @optparse.map { |o| optparse_to_name(o) } # Do we take an argument? If so, are we consistent about it, because # incoherence here makes our life super-difficult, and we can more easily # relax this rule later if we find a valid use case for it. --daniel 2011-03-30 @argument = @optparse.any? { |o| o =~ /[ =]/ } if @argument and not @optparse.all? { |o| o =~ /[ =]/ } then raise ArgumentError, "Option #{@name} is inconsistent about taking an argument" end # Is our argument optional? The rules about consistency apply here, also, # just like they do to taking arguments at all. --daniel 2011-03-30 @optional_argument = @optparse.any? { |o| o.include? "[" } if @optional_argument and not @optparse.all? { |o| o.include? "[" } then raise ArgumentError, "Option #{@name} is inconsistent about the argument being optional" end end # to_s and optparse_to_name are roughly mirrored, because they are used to # transform strings to name symbols, and vice-versa. def to_s @name.to_s.tr('_', '-') end def optparse_to_name(declaration) unless found = declaration.match(/^-+([^= ]+)/) or found.length != 1 then raise ArgumentError, "Can't find a name in the declaration #{declaration.inspect}" end name = found.captures.first.tr('-', '_') raise "#{name.inspect} is an invalid option name" unless name.to_s =~ /^[a-z]\w*$/ name.to_sym end end diff --git a/spec/shared_behaviours/things_that_declare_options.rb b/spec/shared_behaviours/things_that_declare_options.rb index 6abce99e3..1b41c2279 100644 --- a/spec/shared_behaviours/things_that_declare_options.rb +++ b/spec/shared_behaviours/things_that_declare_options.rb @@ -1,120 +1,134 @@ # -*- coding: utf-8 -*- shared_examples_for "things that declare options" do it "should support options without arguments" do subject = add_options_to { option "--bar" } subject.should be_option :bar end it "should support options with an empty block" do subject = add_options_to do option "--foo" do # this section deliberately left blank end end subject.should be subject.should be_option :foo end it "should support option documentation" do text = "Sturm und Drang (German pronunciation: [ˈʃtʊʁm ʊnt ˈdʁaŋ]) …" subject = add_options_to do option "--foo" do desc text end end subject.get_option(:foo).desc.should == text end it "should list all the options" do subject = add_options_to do option "--foo" option "--bar" end subject.options.should =~ [:foo, :bar] end it "should detect conflicts in long options" do expect { add_options_to do option "--foo" option "--foo" end }.should raise_error ArgumentError, /Option foo conflicts with existing option foo/i end it "should detect conflicts in short options" do expect { add_options_to do option "-f" option "-f" end }.should raise_error ArgumentError, /Option f conflicts with existing option f/ end + ["-f", "--foo"].each do |option| + ["", " FOO", "=FOO", " [FOO]", "=[FOO]"].each do |argument| + input = option + argument + it "should detect conflicts within a single option like #{input.inspect}" do + expect { + add_options_to do + option input, input + end + }.should raise_error ArgumentError, /duplicates existing alias/ + end + end + end + + # Verify the range of interesting conflicts to check for ordering causing # the behaviour to change, or anything exciting like that. [ %w{--foo}, %w{-f}, %w{-f --foo}, %w{--baz -f}, %w{-f --baz}, %w{-b --foo}, %w{--foo -b} ].each do |conflict| base = %w{--foo -f} it "should detect conflicts between #{base.inspect} and #{conflict.inspect}" do expect { add_options_to do option *base option *conflict end }.should raise_error ArgumentError, /conflicts with existing option/ end end it "should fail if we are not consistent about taking an argument" do expect { add_options_to do option "--foo=bar", "--bar" end }. should raise_error ArgumentError, /inconsistent about taking an argument/ end it "should accept optional arguments" do subject = add_options_to do option "--foo=[baz]", "--bar=[baz]" end [:foo, :bar].each do |name| subject.should be_option name end end describe "#takes_argument?" do it "should detect an argument being absent" do subject = add_options_to do option "--foo" end subject.get_option(:foo).should_not be_takes_argument end ["=FOO", " FOO", "=[FOO]", " [FOO]"].each do |input| it "should detect an argument given #{input.inspect}" do subject = add_options_to do option "--foo#{input}" end subject.get_option(:foo).should be_takes_argument end end end describe "#optional_argument?" do it "should be false if no argument is present" do option = add_options_to do option "--foo" end.get_option(:foo) option.should_not be_takes_argument option.should_not be_optional_argument end ["=FOO", " FOO"].each do |input| it "should be false if the argument is mandatory (like #{input.inspect})" do option = add_options_to do option "--foo#{input}" end.get_option(:foo) option.should be_takes_argument option.should_not be_optional_argument end end ["=[FOO]", " [FOO]"].each do |input| it "should be true if the argument is optional (like #{input.inspect})" do option = add_options_to do option "--foo#{input}" end.get_option(:foo) option.should be_takes_argument option.should be_optional_argument end end end end