diff --git a/lib/puppet/util/command_line/puppet_option_parser.rb b/lib/puppet/util/command_line/puppet_option_parser.rb index 0fbf9a264..7bb2f57a9 100644 --- a/lib/puppet/util/command_line/puppet_option_parser.rb +++ b/lib/puppet/util/command_line/puppet_option_parser.rb @@ -1,95 +1,98 @@ 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 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 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 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 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 rescue ::Trollop::CommandlineError => err raise PuppetUnrecognizedOptionError.new(err) if err.message =~ /^unknown argument/ end end - end - + def pass_only_last_value_on_to(block) + lambda { |values| block.call(values.is_a?(Array) ? values.last : values) } + end + 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 1b5251ba9..630b636ed 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,103 +1,123 @@ require 'spec_helper' require 'puppet/util/command_line/puppet_option_parser' describe Puppet::Util::CommandLine::PuppetOptionParser do let(:option_parser) { described_class.new } - it "parses a 'long' option with a value" do - parses( - :option => ["--angry", "Angry", :REQUIRED], - :from_arguments => ["--angry", "foo"], - :expects => "foo" - ) - end + 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 "parses a 'short' option with a value" do - parses( - :option => ["--angry", "-a", "Angry", :REQUIRED], - :from_arguments => ["-a", "foo"], - :expects => "foo" - ) - end + it "parses a 'short' option with a value" do + parses( + :option => ["--angry", "-a", "Angry", :REQUIRED], + :from_arguments => ["-a", "foo"], + :expects => "foo" + ) + end - it "parses a 'long' option without a value" do - parses( - :option => ["--angry", "Angry", :NONE], - :from_arguments => ["--angry"], - :expects => true - ) + 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 "parses a 'short' option without a value" do - parses( - :option => ["--angry", "-a", "Angry", :NONE], - :from_arguments => ["-a"], - :expects => true - ) - end + 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 "parses a 'short' option" do + parses( + :option => ["--angry", "-a", "Angry", :NONE], + :from_arguments => ["-a"], + :expects => true + ) + end - it "supports the '--no-blah' syntax" do - parses( - :option => ["--[no-]rage", "Rage", :NONE], - :from_arguments => ["--no-rage"], - :expects => false - ) + 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 modify the original argument array" do option_parser.on("--foo", "Foo", :NONE) { |val| } args = ["--foo"] option_parser.parse(args) args.length.should == 1 end # 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 expect { option_parser.parse(["-r"]) }.to raise_error(Puppet::Util::CommandLine::PuppetUnrecognizedOptionError) 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] seen_value = nil option_parser.on(*option) do |val| seen_value = val end option_parser.parse(arguments) seen_value.should == expected_value end end