diff --git a/lib/puppet/util/command_line.rb b/lib/puppet/util/command_line.rb index 2351360c1..22fd3532a 100644 --- a/lib/puppet/util/command_line.rb +++ b/lib/puppet/util/command_line.rb @@ -1,190 +1,199 @@ # Bundler and rubygems maintain a set of directories from which to # load gems. If Bundler is loaded, let it determine what can be # loaded. If it's not loaded, then use rubygems. But do this before # loading any puppet code, so that our gem loading system is sane. if not defined? ::Bundler begin require 'rubygems' rescue LoadError end end require 'puppet' require 'puppet/util' require "puppet/util/plugins" require "puppet/util/rubygems" require "puppet/util/limits" +require 'puppet/util/colors' module Puppet module Util # This is the main entry point for all puppet applications / faces; it # is basically where the bootstrapping process / lifecycle of an app # begins. class CommandLine include Puppet::Util::Limits OPTION_OR_MANIFEST_FILE = /^-|\.pp$|\.rb$/ # @param zero [String] the name of the executable # @param argv [Array] the arguments passed on the command line # @param stdin [IO] (unused) def initialize(zero = $0, argv = ARGV, stdin = STDIN) @command = File.basename(zero, '.rb') @argv = argv Puppet::Plugins.on_commandline_initialization(:command_line_object => self) end # @return [String] name of the subcommand is being executed # @api public def subcommand_name return @command if @command != 'puppet' if @argv.first =~ OPTION_OR_MANIFEST_FILE nil else @argv.first end end # @return [Array] the command line arguments being passed to the subcommand # @api public def args return @argv if @command != 'puppet' if subcommand_name.nil? @argv else @argv[1..-1] end end # @api private # @deprecated def self.available_subcommands Puppet.deprecation_warning('Puppet::Util::CommandLine.available_subcommands is deprecated; please use Puppet::Application.available_application_names instead.') Puppet::Application.available_application_names end # available_subcommands was previously an instance method, not a class # method, and we have an unknown number of user-implemented applications # that depend on that behaviour. Forwarding allows us to preserve a # backward compatible API. --daniel 2011-04-11 # @api private # @deprecated def available_subcommands Puppet.deprecation_warning('Puppet::Util::CommandLine#available_subcommands is deprecated; please use Puppet::Application.available_application_names instead.') Puppet::Application.available_application_names end # Run the puppet subcommand. If the subcommand is determined to be an # external executable, this method will never return and the current # process will be replaced via {Kernel#exec}. # # @return [void] def execute Puppet::Util.exit_on_fail("initialize global default settings") do Puppet.initialize_settings(args) end setpriority(Puppet[:priority]) find_subcommand.run end # @api private def external_subcommand Puppet::Util.which("puppet-#{subcommand_name}") end private def find_subcommand if subcommand_name.nil? NilSubcommand.new(self) elsif Puppet::Application.available_application_names.include?(subcommand_name) ApplicationSubcommand.new(subcommand_name, self) elsif path_to_subcommand = external_subcommand ExternalSubcommand.new(path_to_subcommand, self) else UnknownSubcommand.new(subcommand_name, self) end end # @api private class ApplicationSubcommand def initialize(subcommand_name, command_line) @subcommand_name = subcommand_name @command_line = command_line end def run # For most applications, we want to be able to load code from the modulepath, # such as apply, describe, resource, and faces. # For agent, we only want to load pluginsync'ed code from libdir. # For master, we shouldn't ever be loading per-enviroment code into the master's # ruby process, but that requires fixing (#17210, #12173, #8750). So for now # we try to restrict to only code that can be autoloaded from the node's # environment. # PUP-2114 - at this point in the bootstrapping process we do not # have an appropriate application-wide current_environment set. # If we cannot find the configured environment, which may not exist, # we do not attempt to add plugin directories to the load path. # if @subcommand_name != 'master' and @subcommand_name != 'agent' if configured_environment = Puppet.lookup(:environments).get(Puppet[:environment]) configured_environment.each_plugin_directory do |dir| $LOAD_PATH << dir unless $LOAD_PATH.include?(dir) end end end app = Puppet::Application.find(@subcommand_name).new(@command_line) Puppet::Plugins.on_application_initialization(:application_object => @command_line) app.run end end # @api private class ExternalSubcommand def initialize(path_to_subcommand, command_line) @path_to_subcommand = path_to_subcommand @command_line = command_line end def run Kernel.exec(@path_to_subcommand, *@command_line.args) end end # @api private class NilSubcommand + include Puppet::Util::Colors + def initialize(command_line) @command_line = command_line end def run - if @command_line.args.include? "--version" or @command_line.args.include? "-V" + args = @command_line.args + if args.include? "--version" or args.include? "-V" puts Puppet.version + elsif @command_line.subcommand_name.nil? && args.count > 0 + # If the subcommand is truly nil and there is an arg, it's an option; print out the invalid option message + puts colorize(:hred, "Error: Could not parse application options: invalid option: #{args[0]}") + exit 1 else puts "See 'puppet help' for help on available puppet subcommands" end end end # @api private class UnknownSubcommand < NilSubcommand def initialize(subcommand_name, command_line) @subcommand_name = subcommand_name super(command_line) end def run - puts "Error: Unknown Puppet subcommand '#{@subcommand_name}'" + puts colorize(:hred, "Error: Unknown Puppet subcommand '#{@subcommand_name}'") super + exit 1 end end end end end diff --git a/spec/lib/puppet_spec/matchers.rb b/spec/lib/puppet_spec/matchers.rb index b53b23436..c01d8e89d 100644 --- a/spec/lib/puppet_spec/matchers.rb +++ b/spec/lib/puppet_spec/matchers.rb @@ -1,128 +1,147 @@ require 'stringio' ######################################################################## # Backward compatibility for Jenkins outdated environment. module RSpec module Matchers module BlockAliases alias_method :to, :should unless method_defined? :to alias_method :to_not, :should_not unless method_defined? :to_not alias_method :not_to, :should_not unless method_defined? :not_to end end end ######################################################################## # Custom matchers... RSpec::Matchers.define :have_matching_element do |expected| match do |actual| actual.any? { |item| item =~ expected } end end RSpec::Matchers.define :exit_with do |expected| actual = nil match do |block| begin block.call rescue SystemExit => e actual = e.status end actual and actual == expected end failure_message_for_should do |block| "expected exit with code #{expected} but " + (actual.nil? ? " exit was not called" : "we exited with #{actual} instead") end failure_message_for_should_not do |block| "expected that exit would not be called with #{expected}" end description do "expect exit with #{expected}" end end -class HavePrintedMatcher - attr_accessor :expected, :actual - def initialize(expected) - case expected +RSpec::Matchers.define :have_printed do |expected| + + case expected when String, Regexp - @expected = expected + expected = expected else - @expected = expected.to_s + expected = expected.to_s + end + + chain :and_exit_with do |code| + @expected_exit_code = code + end + + define_method :matches_exit_code? do |actual| + @expected_exit_code.nil? || @expected_exit_code == actual + end + + define_method :matches_output? do |actual| + return false unless actual + case expected + when String + actual.include?(expected) + when Regexp + expected.match(actual) + else + raise ArgumentError, "No idea how to match a #{actual.class.name}" end end - def matches?(block) + match do |block| + $stderr = $stdout = StringIO.new + $stdout.set_encoding('UTF-8') if $stdout.respond_to?(:set_encoding) + begin - $stderr = $stdout = StringIO.new - $stdout.set_encoding('UTF-8') if $stdout.respond_to?(:set_encoding) block.call + rescue SystemExit => e + raise unless @expected_exit_code + @actual_exit_code = e.status + ensure $stdout.rewind @actual = $stdout.read - ensure + $stdout = STDOUT $stderr = STDERR end - if @actual then - case @expected - when String - @actual.include? @expected - when Regexp - @expected.match @actual - end - else - false - end + matches_output?(@actual) && matches_exit_code?(@actual_exit_code) end - def failure_message_for_should - if @actual.nil? then - "expected #{@expected.inspect}, but nothing was printed" + failure_message_for_should do |actual| + if actual.nil? then + "expected #{expected.inspect}, but nothing was printed" else - "expected #{@expected.inspect} to be printed; got:\n#{@actual}" + if !@expected_exit_code.nil? && matches_output?(actual) + "expected exit with code #{@expected_exit_code} but " + + (@actual_exit_code.nil? ? " exit was not called" : "exited with #{@actual_exit_code} instead") + else + "expected #{expected.inspect} to be printed; got:\n#{actual}" + end end end - def failure_message_for_should_not - "expected #{@expected.inspect} to not be printed; got:\n#{@actual}" + failure_message_for_should_not do |actual| + if @expected_exit_code && matches_exit_code?(@actual_exit_code) + "expected exit code to not be #{@actual_exit_code}" + else + "expected #{expected.inspect} to not be printed; got:\n#{actual}" + end end - def description - "expect #{@expected.inspect} to be printed" + description do + "expect #{expected.inspect} to be printed" + (@expected_exit_code.nil ? '' : " with exit code #{@expected_exit_code}") end end -def have_printed(what) - HavePrintedMatcher.new(what) -end - RSpec::Matchers.define :equal_attributes_of do |expected| match do |actual| actual.instance_variables.all? do |attr| actual.instance_variable_get(attr) == expected.instance_variable_get(attr) end end end RSpec::Matchers.define :equal_resource_attributes_of do |expected| match do |actual| actual.keys do |attr| actual[attr] == expected[attr] end end end RSpec::Matchers.define :be_one_of do |*expected| match do |actual| expected.include? actual end failure_message_for_should do |actual| "expected #{actual.inspect} to be one of #{expected.map(&:inspect).join(' or ')}" end end diff --git a/spec/unit/util/command_line_spec.rb b/spec/unit/util/command_line_spec.rb index 6ba8077c2..bb14ce9a0 100755 --- a/spec/unit/util/command_line_spec.rb +++ b/spec/unit/util/command_line_spec.rb @@ -1,188 +1,192 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet/face' require 'puppet/util/command_line' describe Puppet::Util::CommandLine do include PuppetSpec::Files context "#initialize" do it "should pull off the first argument if it looks like a subcommand" do command_line = Puppet::Util::CommandLine.new("puppet", %w{ client --help whatever.pp }) command_line.subcommand_name.should == "client" command_line.args.should == %w{ --help whatever.pp } end it "should return nil if the first argument looks like a .pp file" do command_line = Puppet::Util::CommandLine.new("puppet", %w{ whatever.pp }) command_line.subcommand_name.should == nil command_line.args.should == %w{ whatever.pp } end it "should return nil if the first argument looks like a .rb file" do command_line = Puppet::Util::CommandLine.new("puppet", %w{ whatever.rb }) command_line.subcommand_name.should == nil command_line.args.should == %w{ whatever.rb } end it "should return nil if the first argument looks like a flag" do command_line = Puppet::Util::CommandLine.new("puppet", %w{ --debug }) command_line.subcommand_name.should == nil command_line.args.should == %w{ --debug } end it "should return nil if the first argument is -" do command_line = Puppet::Util::CommandLine.new("puppet", %w{ - }) command_line.subcommand_name.should == nil command_line.args.should == %w{ - } end it "should return nil if the first argument is --help" do command_line = Puppet::Util::CommandLine.new("puppet", %w{ --help }) command_line.subcommand_name.should == nil end it "should return nil if there are no arguments" do command_line = Puppet::Util::CommandLine.new("puppet", []) command_line.subcommand_name.should == nil command_line.args.should == [] end it "should pick up changes to the array of arguments" do args = %w{subcommand} command_line = Puppet::Util::CommandLine.new("puppet", args) args[0] = 'different_subcommand' command_line.subcommand_name.should == 'different_subcommand' end end context "#execute" do %w{--version -V}.each do |arg| it "should print the version and exit if #{arg} is given" do expect do described_class.new("puppet", [arg]).execute end.to have_printed(/^#{Puppet.version}$/) end end end describe "when dealing with puppet commands" do it "should return the executable name if it is not puppet" do command_line = Puppet::Util::CommandLine.new("puppetmasterd", []) command_line.subcommand_name.should == "puppetmasterd" end describe "when the subcommand is not implemented" do it "should find and invoke an executable with a hyphenated name" do commandline = Puppet::Util::CommandLine.new("puppet", ['whatever', 'argument']) Puppet::Util.expects(:which).with('puppet-whatever'). returns('/dev/null/puppet-whatever') Kernel.expects(:exec).with('/dev/null/puppet-whatever', 'argument') commandline.execute end describe "and an external implementation cannot be found" do + before :each do + Puppet::Util::CommandLine::UnknownSubcommand.any_instance.stubs(:console_has_color?).returns false + end + it "should abort and show the usage message" do - commandline = Puppet::Util::CommandLine.new("puppet", ['whatever', 'argument']) Puppet::Util.expects(:which).with('puppet-whatever').returns(nil) + commandline = Puppet::Util::CommandLine.new("puppet", ['whatever', 'argument']) commandline.expects(:exec).never expect { commandline.execute - }.to have_printed(/Unknown Puppet subcommand 'whatever'/) + }.to have_printed(/Unknown Puppet subcommand 'whatever'/).and_exit_with(1) end it "should abort and show the help message" do - commandline = Puppet::Util::CommandLine.new("puppet", ['whatever', 'argument']) Puppet::Util.expects(:which).with('puppet-whatever').returns(nil) + commandline = Puppet::Util::CommandLine.new("puppet", ['whatever', 'argument']) commandline.expects(:exec).never expect { commandline.execute - }.to have_printed(/See 'puppet help' for help on available puppet subcommands/) + }.to have_printed(/See 'puppet help' for help on available puppet subcommands/).and_exit_with(1) end %w{--version -V}.each do |arg| it "should abort and display #{arg} information" do - commandline = Puppet::Util::CommandLine.new("puppet", ['whatever', arg]) Puppet::Util.expects(:which).with('puppet-whatever').returns(nil) + commandline = Puppet::Util::CommandLine.new("puppet", ['whatever', arg]) commandline.expects(:exec).never expect { commandline.execute - }.to have_printed(/^#{Puppet.version}$/) + }.to have_printed(/^#{Puppet.version}$/).and_exit_with(1) end end end end describe 'when loading commands' do it "should deprecate the available_subcommands instance method" do Puppet::Application.expects(:available_application_names) Puppet.expects(:deprecation_warning).with("Puppet::Util::CommandLine#available_subcommands is deprecated; please use Puppet::Application.available_application_names instead.") command_line = Puppet::Util::CommandLine.new("foo", %w{ client --help whatever.pp }) command_line.available_subcommands end it "should deprecate the available_subcommands class method" do Puppet::Application.expects(:available_application_names) Puppet.expects(:deprecation_warning).with("Puppet::Util::CommandLine.available_subcommands is deprecated; please use Puppet::Application.available_application_names instead.") Puppet::Util::CommandLine.available_subcommands end end describe 'when setting process priority' do let(:command_line) do Puppet::Util::CommandLine.new("puppet", %w{ agent }) end before :each do Puppet::Util::CommandLine::ApplicationSubcommand.any_instance.stubs(:run) end it 'should never set priority by default' do Process.expects(:setpriority).never command_line.execute end it 'should lower the process priority if one has been specified' do Puppet[:priority] = 10 Process.expects(:setpriority).with(0, Process.pid, 10) command_line.execute end it 'should warn if trying to raise priority, but not privileged user' do Puppet[:priority] = -10 Process.expects(:setpriority).raises(Errno::EACCES, 'Permission denied') Puppet.expects(:warning).with("Failed to set process priority to '-10'") command_line.execute end it "should warn if the platform doesn't support `Process.setpriority`" do Puppet[:priority] = 15 Process.expects(:setpriority).raises(NotImplementedError, 'NotImplementedError: setpriority() function is unimplemented on this machine') Puppet.expects(:warning).with("Failed to set process priority to '15'") command_line.execute end end end end