diff --git a/lib/puppet/util/command_line/puppet_option_parser.rb b/lib/puppet/util/command_line/puppet_option_parser.rb index 7bb2f57a9..2068d31f0 100644 --- a/lib/puppet/util/command_line/puppet_option_parser.rb +++ b/lib/puppet/util/command_line/puppet_option_parser.rb @@ -1,98 +1,90 @@ 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 + # 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 => 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 def pass_only_last_value_on_to(block) lambda { |values| block.call(values.is_a?(Array) ? values.last : values) } end end end end 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 630b636ed..40a498291 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,123 +1,127 @@ require 'spec_helper' require 'puppet/util/command_line/puppet_option_parser' 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 "parses a 'short' option with a value" do parses( :option => ["--angry", "-a", "Angry", :REQUIRED], :from_arguments => ["-a", "foo"], :expects => "foo" ) end 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 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 ) 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"] 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