diff --git a/lib/puppet/util/command_line/puppet_option_parser.rb b/lib/puppet/util/command_line/puppet_option_parser.rb index 0fbf9a264..2b59d6ab3 100644 --- a/lib/puppet/util/command_line/puppet_option_parser.rb +++ b/lib/puppet/util/command_line/puppet_option_parser.rb @@ -1,95 +1,86 @@ module Puppet module Util class CommandLine class PuppetOptionError < Puppet::Error end - class PuppetUnrecognizedOptionError < PuppetOptionError - end - # This is a command line option parser. It is intended to have an API that is very similar to # the ruby stdlib 'OptionParser' API, for ease of integration into our existing code... however, # However, we've removed the OptionParser-based implementation and are only maintaining the # it's impilemented based on the third-party "trollop" library. This was done because there # are places where the stdlib OptionParser is not flexible enough to meet our needs. class PuppetOptionParser def initialize(usage_msg = nil) require "puppet/util/command_line/trollop" @create_default_short_options = false - @wrapped_parser = ::Trollop::Parser.new do + @parser = ::Trollop::Parser.new do banner usage_msg create_default_short_options = false handle_help_and_version = false end end # This parameter, if set, will tell the underlying option parser not to throw an # exception if we pass it options that weren't explicitly registered. We need this # capability because we need to be able to pass all of the command-line options before # we know which application/face they are going to be running, but the app/face # may specify additional command-line arguments that are valid for that app/face. attr_reader :ignore_invalid_options def ignore_invalid_options=(value) - @wrapped_parser.ignore_invalid_options = value + @parser.ignore_invalid_options = value end - def on(*args, &block) - # This is ugly and I apologize :) - # I wanted to keep the API for this class compatible with how we were previously - # interacting with the ruby stdlib OptionParser. Unfortunately, that means that - # you can specify options as an array, with three or four elements. The 2nd element - # is an optional "short" representation. This series of shift/pop operations seemed - # the easiest way to avoid breaking compatibility with that syntax. - - # The first argument is always the "--long" representation... - long = args.shift - - # The last argument is always the "type" - type = args.pop - # The second-to-last argument is always the "description" - desc = args.pop - - # if there is anything left, it's the "short" representation. - short = args.shift + # The 2nd element is an optional "short" representation. + if args.length == 3 + long, desc, type = args + elsif args.length == 4 + long, short, desc, type = args + else + raise ArgumentError, "this method only takes 3 or 4 arguments. Given: #{args.inspect}" + end options = { :long => long, :short => short, :required => false, - :callback => block, + :callback => pass_only_last_value_on_to(block), + :multi => true, } case type when :REQUIRED options[:type] = :string when :NONE options[:type] = :flag else raise PuppetOptionError.new("Unsupported type: '#{type}'") end - @wrapped_parser.opt long.sub("^--", "").intern, desc, options + @parser.opt long.sub("^--", "").intern, desc, options end def parse(*args) args = args[0] if args.size == 1 and Array === args[0] args_copy = args.dup begin - @wrapped_parser.parse args_copy + @parser.parse args_copy rescue ::Trollop::CommandlineError => err - raise PuppetUnrecognizedOptionError.new(err) if err.message =~ /^unknown argument/ + raise PuppetOptionError.new("Error parsing arguments", err) end end - end - + def pass_only_last_value_on_to(block) + lambda { |values| block.call(values.is_a?(Array) ? values.last : values) } + end + private :pass_only_last_value_on_to + end end end -end \ No newline at end of file +end diff --git a/spec/unit/util/command_line_utils/puppet_option_parser_spec.rb b/spec/unit/util/command_line_utils/puppet_option_parser_spec.rb index 9f530fe00..a00c025bb 100644 --- a/spec/unit/util/command_line_utils/puppet_option_parser_spec.rb +++ b/spec/unit/util/command_line_utils/puppet_option_parser_spec.rb @@ -1,99 +1,127 @@ require 'spec_helper' require 'puppet/util/command_line/puppet_option_parser' -class Puppet::Util::CommandLine - describe PuppetOptionParser do - let(:option_parser) { PuppetOptionParser.new } - - - it "should trigger callback for a 'long' option with a value" do - self.expects(:handle_option).with("--angry", "foo") - option_parser.on(*["--angry", "Angry", :REQUIRED]) do |val| - handle_option("--angry", val) - end - expect { option_parser.parse(["--angry", "foo"]) }.not_to raise_error +describe Puppet::Util::CommandLine::PuppetOptionParser do + let(:option_parser) { described_class.new } + + describe "an option with a value" do + it "parses a 'long' option with a value" do + parses( + :option => ["--angry", "Angry", :REQUIRED], + :from_arguments => ["--angry", "foo"], + :expects => "foo" + ) end - it "should trigger callback for a 'short' option with a value" do - self.expects(:handle_option).with("--angry", "foo") - option_parser.on(*["--angry", "-a", "Angry", :REQUIRED]) do |val| - handle_option("--angry", val) - end - expect { option_parser.parse(["-a", "foo"]) }.not_to raise_error + it "parses a 'short' option with a value" do + parses( + :option => ["--angry", "-a", "Angry", :REQUIRED], + :from_arguments => ["-a", "foo"], + :expects => "foo" + ) end - it "should trigger callback for a 'long' option without a value" do - self.expects(:handle_option).with("--angry", true) - option_parser.on(*["--angry", "Angry", :NONE]) do |val| - handle_option("--angry", val) - end - expect { option_parser.parse(["--angry"]) }.not_to raise_error + it "overrides a previous argument with a later one" do + parses( + :option => ["--later", "Later", :REQUIRED], + :from_arguments => ["--later", "tomorrow", "--later", "morgen"], + :expects => "morgen" + ) end + end - it "should trigger callback for a 'short' option without a value" do - self.expects(:handle_option).with("--angry", true) - option_parser.on(*["--angry", "-a", "Angry", :NONE]) do |val| - handle_option("--angry", val) - end - expect { option_parser.parse(["-a"]) }.not_to raise_error + describe "an option without a value" do + it "parses a 'long' option" do + parses( + :option => ["--angry", "Angry", :NONE], + :from_arguments => ["--angry"], + :expects => true + ) end - it "should support the '--no-blah' syntax" do - self.expects(:handle_option).with("--rage", false) - option_parser.on(*["--[no-]rage", "Rage", :NONE]) do |val| - handle_option("--rage", val) - end - expect { option_parser.parse(["--no-rage"]) }.not_to raise_error + it "parses a 'short' option" do + parses( + :option => ["--angry", "-a", "Angry", :NONE], + :from_arguments => ["-a"], + :expects => true + ) end - describe "#parse" do - it "should not modify the original argument array" do - self.expects(:handle_option).with("--foo", true) - option_parser.on(*["--foo", "Foo", :NONE]) do |val| - handle_option("--foo", val) - end - args = ["--foo"] - expect { option_parser.parse(args) }.not_to raise_error - args.length.should == 1 - end + it "supports the '--no-blah' syntax" do + parses( + :option => ["--[no-]rage", "Rage", :NONE], + :from_arguments => ["--no-rage"], + :expects => false + ) end + it "overrides a previous argument with a later one" do + parses( + :option => ["--[no-]rage", "Rage", :NONE], + :from_arguments => ["--rage", "--no-rage"], + :expects => false + ) + end + end + it "does not accept an unknown option specification" do + expect { option_parser.on("not", "enough") }.should raise_error(ArgumentError) + end + it "does not modify the original argument array" do + option_parser.on("--foo", "Foo", :NONE) { |val| } + args = ["--foo"] - # The ruby stdlib OptionParser has an awesome "feature" that you cannot disable, whereby if - # it sees a short option that you haven't specifically registered with it (e.g., "-r"), it - # will automatically attempt to expand it out to whatever long options that you might have - # registered. Since we need to do our option parsing in two passes (one pass against only - # the global/puppet-wide settings definitions, and then a second pass that includes the - # application or face settings--because we can't load the app/face until we've determined - # the libdir), it is entirely possible that we intend to define our "short" option as part - # of the second pass. Therefore, if the option parser attempts to expand it out into a - # long option during the first pass, terrible things will happen. - # - # A long story short: we need to have the ability to control this kind of behavior in our - # option parser, and this test simply affirms that we do. - it "should not try to expand short options that weren't explicitly registered" do + option_parser.parse(args) - [ - ["--ridiculous", "This is ridiculous", :REQUIRED], - ["--rage-inducing", "This is rage-inducing", :REQUIRED] - ].each do |option| - option_parser.on(*option) {} - end + args.length.should == 1 + end - expect { option_parser.parse(["-r"]) }.to raise_error(PuppetUnrecognizedOptionError) + # The ruby stdlib OptionParser has an awesome "feature" that you cannot disable, whereby if + # it sees a short option that you haven't specifically registered with it (e.g., "-r"), it + # will automatically attempt to expand it out to whatever long options that you might have + # registered. Since we need to do our option parsing in two passes (one pass against only + # the global/puppet-wide settings definitions, and then a second pass that includes the + # application or face settings--because we can't load the app/face until we've determined + # the libdir), it is entirely possible that we intend to define our "short" option as part + # of the second pass. Therefore, if the option parser attempts to expand it out into a + # long option during the first pass, terrible things will happen. + # + # A long story short: we need to have the ability to control this kind of behavior in our + # option parser, and this test simply affirms that we do. + it "does not try to expand short options that weren't explicitly registered" do + + [ + ["--ridiculous", "This is ridiculous", :REQUIRED], + ["--rage-inducing", "This is rage-inducing", :REQUIRED] + ].each do |option| + option_parser.on(*option) {} end - it "should respect :ignore_invalid_options" do - option_parser.ignore_invalid_options = true - expect { option_parser.parse(["--foo"]) }.not_to raise_error - end + expect { option_parser.parse(["-r"]) }.to raise_error(Puppet::Util::CommandLine::PuppetOptionError) + end + + it "respects :ignore_invalid_options" do + option_parser.ignore_invalid_options = true + expect { option_parser.parse(["--unknown-option"]) }.not_to raise_error + end + + it "raises if there is an invalid option and :ignore_invalid_options is not set" do + expect { option_parser.parse(["--unknown-option"]) }.to raise_error(Puppet::Util::CommandLine::PuppetOptionError) + end + + def parses(option_case) + option = option_case[:option] + expected_value = option_case[:expects] + arguments = option_case[:from_arguments] - it "should raise if there is an invalid option and :ignore_invalid_options is not set" do - expect { option_parser.parse(["--foo"]) }.to raise_error(PuppetOptionError) + seen_value = nil + option_parser.on(*option) do |val| + seen_value = val end + option_parser.parse(arguments) + seen_value.should == expected_value end end