diff --git a/lib/puppet/provider/exec.rb b/lib/puppet/provider/exec.rb index 03f547280..111339178 100644 --- a/lib/puppet/provider/exec.rb +++ b/lib/puppet/provider/exec.rb @@ -1,79 +1,84 @@ +require 'puppet/provider' +require 'puppet/util/execution' + class Puppet::Provider::Exec < Puppet::Provider include Puppet::Util::Execution def run(command, check = false) output = nil status = nil dir = nil checkexe(command) if dir = resource[:cwd] unless File.directory?(dir) if check dir = nil else self.fail "Working directory '#{dir}' does not exist" end end end dir ||= Dir.pwd debug "Executing#{check ? " check": ""} '#{command}'" begin # Do our chdir Dir.chdir(dir) do environment = {} environment[:PATH] = resource[:path].join(File::PATH_SEPARATOR) if resource[:path] if envlist = resource[:environment] envlist = [envlist] unless envlist.is_a? Array envlist.each do |setting| if setting =~ /^(\w+)=((.|\n)+)$/ env_name = $1 value = $2 if environment.include?(env_name) || environment.include?(env_name.to_sym) warning "Overriding environment setting '#{env_name}' with '#{value}'" end environment[env_name] = value else warning "Cannot understand environment setting #{setting.inspect}" end end end withenv environment do Timeout::timeout(resource[:timeout]) do output, status = Puppet::Util::SUIDManager. run_and_capture(command, resource[:user], resource[:group]) end # The shell returns 127 if the command is missing. if status.exitstatus == 127 raise ArgumentError, output end end end rescue Errno::ENOENT => detail self.fail detail.to_s end return output, status end def extractexe(command) - # easy case: command was quoted - if command =~ /^"([^"]+)"/ - $1 + if command.is_a? Array + command.first + elsif match = /^"([^"]+)"|^'([^']+)'/.match(command) + # extract whichever of the two sides matched the content. + match[1] or match[2] else command.split(/ /)[0] end end def validatecmd(command) exe = extractexe(command) # if we're not fully qualified, require a path self.fail "'#{command}' is not qualified and no path was specified. Please qualify the command or specify a path." if !absolute_path?(exe) and resource[:path].nil? end end diff --git a/lib/puppet/provider/exec/shell.rb b/lib/puppet/provider/exec/shell.rb index ad2171005..3dbf75f9d 100644 --- a/lib/puppet/provider/exec/shell.rb +++ b/lib/puppet/provider/exec/shell.rb @@ -1,26 +1,25 @@ Puppet::Type.type(:exec).provide :shell, :parent => :posix do include Puppet::Util::Execution confine :feature => :posix desc <<-EOT Passes the provided command through `/bin/sh`; only available on POSIX systems. This allows the use of shell globbing and built-ins, and does not require that the path to a command be fully-qualified. Although this can be more convenient than the `posix` provider, it also means that you need to be more careful with escaping; as ever, with great power comes etc. etc. This provider closely resembles the behavior of the `exec` type in Puppet 0.25.x. EOT def run(command, check = false) - command = %Q{/bin/sh -c "#{command.gsub(/"/,'\"')}"} - super(command, check) + super(['/bin/sh', '-c', command], check) end def validatecmd(command) true end end diff --git a/spec/unit/provider/exec/shell_spec.rb b/spec/unit/provider/exec/shell_spec.rb index c3ed37532..aafb572ee 100755 --- a/spec/unit/provider/exec/shell_spec.rb +++ b/spec/unit/provider/exec/shell_spec.rb @@ -1,46 +1,53 @@ #!/usr/bin/env rspec require 'spec_helper' describe Puppet::Type.type(:exec).provider(:shell), :unless => Puppet.features.microsoft_windows? do let :resource do Puppet::Resource.new(:exec, 'foo') end let :provider do described_class.new(resource) end describe "#run" do it "should be able to run builtin shell commands" do output, status = provider.run("if [ 1 = 1 ]; then echo 'blah'; fi") status.exitstatus.should == 0 output.should == "blah\n" end it "should be able to run commands with single quotes in them" do output, status = provider.run("echo 'foo bar'") status.exitstatus.should == 0 output.should == "foo bar\n" end it "should be able to run commands with double quotes in them" do output, status = provider.run('echo "foo bar"') status.exitstatus.should == 0 output.should == "foo bar\n" end it "should be able to run multiple commands separated by a semicolon" do output, status = provider.run("echo 'foo' ; echo 'bar'") status.exitstatus.should == 0 output.should == "foo\nbar\n" end it "should be able to read values from the environment parameter" do resource[:environment] = "FOO=bar" output, status = provider.run("echo $FOO") status.exitstatus.should == 0 output.should == "bar\n" end + + it "#14060: should interpolate inside the subshell, not outside it" do + resource[:environment] = "foo=outer" + output, status = provider.run("foo=inner; echo \"foo is $foo\"") + status.exitstatus.should == 0 + output.should == "foo is inner\n" + end end describe "#validatecmd" do it "should always return true because builtins don't need path or to be fully qualified" do provider.validatecmd('whateverdoesntmatter').should == true end end end diff --git a/spec/unit/provider/exec_spec.rb b/spec/unit/provider/exec_spec.rb new file mode 100755 index 000000000..deea7748e --- /dev/null +++ b/spec/unit/provider/exec_spec.rb @@ -0,0 +1,35 @@ +#!/usr/bin/env rspec +require 'spec_helper' +require 'puppet/provider/exec' + +describe Puppet::Provider::Exec do + describe "#extractexe" do + it "should return the first element of an array" do + subject.extractexe(['one', 'two']).should == 'one' + end + + { + # double-quoted commands + %q{"/has whitespace"} => "/has whitespace", + %q{"/no/whitespace"} => "/no/whitespace", + # singe-quoted commands + %q{'/has whitespace'} => "/has whitespace", + %q{'/no/whitespace'} => "/no/whitespace", + # combinations + %q{"'/has whitespace'"} => "'/has whitespace'", + %q{'"/has whitespace"'} => '"/has whitespace"', + %q{"/has 'special' characters"} => "/has 'special' characters", + %q{'/has "special" characters'} => '/has "special" characters', + # whitespace split commands + %q{/has whitespace} => "/has", + %q{/no/whitespace} => "/no/whitespace", + }.each do |base_command, exe| + ['', ' and args', ' "and args"', " 'and args'"].each do |args| + command = base_command + args + it "should extract #{exe.inspect} from #{command.inspect}" do + subject.extractexe(command).should == exe + end + end + end + end +end