diff --git a/lib/puppet/application/string_base.rb b/lib/puppet/application/string_base.rb index ffd49e8c0..1169a45a4 100644 --- a/lib/puppet/application/string_base.rb +++ b/lib/puppet/application/string_base.rb @@ -1,117 +1,125 @@ require 'puppet/application' require 'puppet/string' class Puppet::Application::StringBase < Puppet::Application should_parse_config run_mode :agent - def preinit - super - trap(:INT) do - $stderr.puts "Cancelling String" - exit(0) - end - end - option("--debug", "-d") do |arg| Puppet::Util::Log.level = :debug end option("--verbose", "-v") do Puppet::Util::Log.level = :info end option("--format FORMAT") do |arg| @format = arg.to_sym end option("--mode RUNMODE", "-r") do |arg| raise "Invalid run mode #{arg}; supported modes are user, agent, master" unless %w{user agent master}.include?(arg) self.class.run_mode(arg.to_sym) set_run_mode self.class.run_mode end - attr_accessor :string, :type, :verb, :arguments, :format + attr_accessor :string, :action, :type, :arguments, :format attr_writer :exit_code # This allows you to set the exit code if you don't want to just exit # immediately but you need to indicate a failure. def exit_code @exit_code || 0 end - def main - # Call the method associated with the provided action (e.g., 'find'). - if result = string.send(verb, *arguments) - puts render(result) - end - exit(exit_code) - end - # Override this if you need custom rendering. def render(result) render_method = Puppet::Network::FormatHandler.format(format).render_method if render_method == "to_pson" jj result exit(0) else result.send(render_method) end end def preinit + super + trap(:INT) do + $stderr.puts "Cancelling String" + exit(0) + end + # We need to parse enough of the command line out early, to identify what # the action is, so that we can obtain the full set of options to parse. - # - # This requires a partial parse first, and removing the options that we - # understood, then identification of the next item, then another round of - # the same until we have the string and action all set. --daniel 2011-03-29 - # - # NOTE: We can't use the Puppet::Application implementation of option - # parsing because it is (*ahem*) going to puts on $stderr and exit when it - # hits a parse problem, not actually let us reuse stuff. --daniel 2011-03-29 # TODO: These should be configurable versions, through a global # '--version' option, but we don't implement that yet... --daniel 2011-03-29 @type = self.class.name.to_s.sub(/.+:/, '').downcase.to_sym @string = Puppet::String[@type, :current] @format = @string.default_format - # Now, collect the global and string options and parse the command line. - begin - @string.options.inject OptionParser.new do |options, option| - option = @string.get_option option # turn it into the object, bleh - options.on(*option.to_optparse) do |value| - puts "REVISIT: do something with #{value.inspect}" + # Now, walk the command line and identify the action. We skip over + # arguments based on introspecting the action and all, and find the first + # non-option word to use as the action. + action = nil + cli = command_line.args.dup # we destroy this copy, but... + while @action.nil? and not cli.empty? do + item = cli.shift + if item =~ /^-/ then + option = @string.options.find { |a| item =~ /^-+#{a}\b/ } + if option then + if @string.get_option(option).takes_argument? then + # We don't validate if the argument is optional or mandatory, + # because it doesn't matter here. We just assume that errors will + # be caught later. --daniel 2011-03-30 + cli.shift unless cli.first =~ /^-/ + end + else + raise ArgumentError, "Unknown option #{item.sub(/=.*$/, '').inspect}" end - end.parse! command_line.args.dup - rescue OptionParser::InvalidOption => e - puts e.inspect # ...and ignore?? + else + action = @string.get_action(item.to_sym) + if action.nil? then + raise ArgumentError, "#{@string} does not have an #{item.inspect} action!" + end + @action = action + end end - fail "REVISIT: Finish this code, eh..." + @action or raise ArgumentError, "No action given on the command line!" + + # Finally, we can interact with the default option code to build behaviour + # around the full set of options we now know we support. + @action.options.each do |option| + option = @action.get_option(option) # make it the object. + self.class.option(*option.optparse) # ...and make the CLI parse it. + end end def setup Puppet::Util::Log.newdestination :console # We copy all of the app options to the end of the call; This allows each # action to read in the options. This replaces the older model where we # would invoke the action with options set as global state in the # interface object. --daniel 2011-03-28 - @verb = command_line.args.shift @arguments = Array(command_line.args) << options validate end + + def main + # Call the method associated with the provided action (e.g., 'find'). + if result = @string.send(@action.name, *arguments) + puts render(result) + end + exit(exit_code) + end def validate - unless verb + unless @action raise "You must specify #{string.actions.join(", ")} as a verb; 'save' probably does not work right now" end - - unless string.action?(verb) - raise "Command '#{verb}' not found for #{type}" - end end end diff --git a/lib/puppet/string/option.rb b/lib/puppet/string/option.rb index 70d62a01f..e7b6f187c 100644 --- a/lib/puppet/string/option.rb +++ b/lib/puppet/string/option.rb @@ -1,78 +1,79 @@ class Puppet::String::Option attr_reader :parent attr_reader :name attr_reader :aliases + attr_reader :optparse attr_accessor :desc def takes_argument? !!@argument end def optional_argument? !!@optional_argument end 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/unit/application/string_base_spec.rb b/spec/unit/application/string_base_spec.rb index 65cadb8fd..1072d9be9 100755 --- a/spec/unit/application/string_base_spec.rb +++ b/spec/unit/application/string_base_spec.rb @@ -1,75 +1,135 @@ #!/usr/bin/env ruby require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper.rb') require 'puppet/application/string_base' require 'tmpdir' class Puppet::Application::StringBase::Basetest < Puppet::Application::StringBase option("--[no-]foo") end describe Puppet::Application::StringBase do before :all do @dir = Dir.mktmpdir $LOAD_PATH.push(@dir) FileUtils.mkdir_p(File.join @dir, 'puppet', 'string') File.open(File.join(@dir, 'puppet', 'string', 'basetest.rb'), 'w') do |f| f.puts "Puppet::String.define(:basetest, '0.0.1')" end end after :all do FileUtils.remove_entry_secure @dir $LOAD_PATH.pop end - before do - @app = Puppet::Application::StringBase::Basetest.new - @app.stubs(:exit) - @app.stubs(:puts) + let :app do + app = Puppet::Application::StringBase::Basetest.new + app.stubs(:exit) + app.stubs(:puts) + app.command_line.stubs(:subcommand_name).returns 'subcommand' + app.command_line.stubs(:args).returns [] Puppet::Util::Log.stubs(:newdestination) + app + end + + describe "#preinit" do + before :each do + app.command_line.stubs(:args).returns %w{} + end + + it "should set the string based on the type" + it "should set the format based on the string default" + + describe "parsing the command line" do + before :all do + Puppet::String[:basetest, '0.0.1'].action :foo do + option "--foo" + invoke do |options| + options + end + end + end + + it "should find the action" do + app.command_line.stubs(:args).returns %w{foo} + app.preinit + app.action.should be + app.action.name.should == :foo + end + + it "should fail if no action is given" do + expect { app.preinit }. + should raise_error ArgumentError, /No action given/ + end + + it "should report a sensible error when options with = fail" do + app.command_line.stubs(:args).returns %w{--foo=bar foo} + expect { app.preinit }. + should raise_error ArgumentError, /Unknown option "--foo"/ + end + + it "should fail if an action option is before the action" do + app.command_line.stubs(:args).returns %w{--foo foo} + expect { app.preinit }. + should raise_error ArgumentError, /Unknown option "--foo"/ + end + + it "should fail if an unknown option is before the action" do + app.command_line.stubs(:args).returns %w{--bar foo} + expect { app.preinit }. + should raise_error ArgumentError, /Unknown option "--bar"/ + end + + it "should not fail if an unknown option is after the action" do + app.command_line.stubs(:args).returns %w{foo --bar} + app.preinit + app.action.name.should == :foo + app.string.should_not be_option :bar + app.action.should_not be_option :bar + end + end end describe "when calling main" do before do @app.verb = :find @app.arguments = ["myname", "myarg"] @app.string.stubs(:find) end it "should send the specified verb and name to the string" do @app.string.expects(:find).with("myname", "myarg") - - @app.main + app.main end it "should use its render method to render any result" it "should exit with the current exit code" end describe "during setup" do before do - @app.command_line.stubs(:args).returns(["find", "myname", "myarg"]) - @app.stubs(:validate) + app.command_line.stubs(:args).returns(["find", "myname", "myarg"]) + app.stubs(:validate) end it "should set the verb from the command line arguments" do @app.setup @app.verb.should == "find" end it "should make sure arguments are an array" do @app.command_line.stubs(:args).returns(["find", "myname", "myarg"]) @app.setup @app.arguments.should == ["myname", "myarg", {}] end it "should pass options as the last argument" do @app.command_line.stubs(:args).returns(["find", "myname", "myarg", "--foo"]) @app.parse_options @app.setup @app.arguments.should == ["myname", "myarg", { :foo => true }] end end end diff --git a/spec/unit/string/action_spec.rb b/spec/unit/string/action_spec.rb index 258ad5aa6..e5fefdbdc 100755 --- a/spec/unit/string/action_spec.rb +++ b/spec/unit/string/action_spec.rb @@ -1,135 +1,157 @@ #!/usr/bin/env ruby require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper.rb') require 'puppet/string/action' describe Puppet::String::Action do describe "when validating the action name" do [nil, '', 'foo bar', '-foobar'].each do |input| it "should treat #{input.inspect} as an invalid name" do expect { Puppet::String::Action.new(nil, input) }. should raise_error(/is an invalid action name/) end end end describe "when invoking" do it "should be able to call other actions on the same object" do string = Puppet::String.new(:my_string, '0.0.1') do action(:foo) do invoke { 25 } end action(:bar) do invoke { "the value of foo is '#{foo}'" } end end string.foo.should == 25 string.bar.should == "the value of foo is '25'" end # bar is a class action calling a class action # quux is a class action calling an instance action # baz is an instance action calling a class action # qux is an instance action calling an instance action it "should be able to call other actions on the same object when defined on a class" do class Puppet::String::MyStringBaseClass < Puppet::String action(:foo) do invoke { 25 } end action(:bar) do invoke { "the value of foo is '#{foo}'" } end action(:quux) do invoke { "qux told me #{qux}" } end end string = Puppet::String::MyStringBaseClass.new(:my_inherited_string, '0.0.1') do action(:baz) do invoke { "the value of foo in baz is '#{foo}'" } end action(:qux) do invoke { baz } end end string.foo.should == 25 string.bar.should == "the value of foo is '25'" string.quux.should == "qux told me the value of foo in baz is '25'" string.baz.should == "the value of foo in baz is '25'" string.qux.should == "the value of foo in baz is '25'" end + + context "when calling the Ruby API" do + let :string do + Puppet::String.new(:ruby_api, '1.0.0') do + action :bar do + invoke do |options| + options + end + end + end + end + + it "should work when no options are supplied" do + options = string.bar + options.should == {} + end + + it "should work when options are supplied" do + options = string.bar :bar => "beer" + options.should == { :bar => "beer" } + end + end end describe "with action-level options" do it "should support options with an empty block" do string = Puppet::String.new(:action_level_options, '0.0.1') do action :foo do option "--bar" do # this line left deliberately blank end end end string.should_not be_option :bar string.get_action(:foo).should be_option :bar end it "should return only action level options when there are no string options" do string = Puppet::String.new(:action_level_options, '0.0.1') do action :foo do option "--bar" end end string.get_action(:foo).options.should =~ [:bar] end describe "with both string and action options" do let :string do Puppet::String.new(:action_level_options, '0.0.1') do action :foo do option "--bar" end action :baz do option "--bim" end option "--quux" end end it "should return combined string and action options" do string.get_action(:foo).options.should =~ [:bar, :quux] end it "should get an action option when asked" do string.get_action(:foo).get_option(:bar). should be_an_instance_of Puppet::String::Option end it "should get a string option when asked" do string.get_action(:foo).get_option(:quux). should be_an_instance_of Puppet::String::Option end it "should return options only for this action" do string.get_action(:baz).options.should =~ [:bim, :quux] end end it_should_behave_like "things that declare options" do def add_options_to(&block) string = Puppet::String.new(:with_options, '0.0.1') do action(:foo, &block) end string.get_action(:foo) end end it "should fail when a string option duplicates an action option" do expect { Puppet::String.new(:action_level_options, '0.0.1') do option "--foo" action :bar do option "--foo" end end }.should raise_error ArgumentError, /Option foo conflicts with existing option foo/i end end end