bolt 2.4.0 → 2.5.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of bolt might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/bolt-modules/boltlib/lib/puppet/datatypes/target.rb +3 -1
- data/lib/bolt/bolt_option_parser.rb +1 -1
- data/lib/bolt/executor.rb +3 -3
- data/lib/bolt/inventory/target.rb +0 -6
- data/lib/bolt/result.rb +3 -3
- data/lib/bolt/shell.rb +88 -0
- data/lib/bolt/shell/bash.rb +431 -0
- data/lib/bolt/{transport/sudoable → shell/bash}/tmpdir.rb +10 -10
- data/lib/bolt/target.rb +5 -4
- data/lib/bolt/task.rb +3 -0
- data/lib/bolt/transport/base.rb +6 -9
- data/lib/bolt/transport/docker.rb +2 -2
- data/lib/bolt/transport/local.rb +8 -11
- data/lib/bolt/transport/local/connection.rb +58 -0
- data/lib/bolt/transport/local_windows.rb +3 -3
- data/lib/bolt/transport/orch.rb +1 -1
- data/lib/bolt/transport/remote.rb +1 -1
- data/lib/bolt/transport/simple.rb +48 -0
- data/lib/bolt/transport/ssh.rb +2 -14
- data/lib/bolt/transport/ssh/connection.rb +66 -111
- data/lib/bolt/transport/winrm.rb +2 -2
- data/lib/bolt/util.rb +17 -1
- data/lib/bolt/version.rb +1 -1
- data/lib/bolt_spec/bolt_context.rb +206 -0
- data/lib/bolt_spec/plans.rb +7 -94
- metadata +8 -6
- data/lib/bolt/transport/local/shell.rb +0 -216
- data/lib/bolt/transport/sudoable.rb +0 -150
- data/lib/bolt/transport/sudoable/connection.rb +0 -118
@@ -1,216 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'open3'
|
4
|
-
require 'fileutils'
|
5
|
-
require 'bolt/node/output'
|
6
|
-
require 'bolt/util'
|
7
|
-
|
8
|
-
module Bolt
|
9
|
-
module Transport
|
10
|
-
class Local < Sudoable
|
11
|
-
class Shell < Sudoable::Connection
|
12
|
-
attr_accessor :user, :logger, :target
|
13
|
-
attr_writer :run_as
|
14
|
-
|
15
|
-
CHUNK_SIZE = 4096
|
16
|
-
|
17
|
-
def initialize(target)
|
18
|
-
@target = target
|
19
|
-
# The familiar problem: Etc.getlogin is broken on osx
|
20
|
-
@user = ENV['USER'] || Etc.getlogin
|
21
|
-
@run_as = target.options['run-as']
|
22
|
-
@logger = Logging.logger[self]
|
23
|
-
@sudo_id = SecureRandom.uuid
|
24
|
-
end
|
25
|
-
|
26
|
-
# If prompted for sudo password, send password to stdin and return an
|
27
|
-
# empty string. Otherwise, check for sudo errors and raise Bolt error.
|
28
|
-
# If sudo_id is detected, that means the task needs to have stdin written.
|
29
|
-
# If error is not sudo-related, return the stderr string to be added to
|
30
|
-
# node output
|
31
|
-
def handle_sudo(stdin, err, pid, sudo_stdin)
|
32
|
-
if err.include?(Sudoable.sudo_prompt)
|
33
|
-
# A wild sudo prompt has appeared!
|
34
|
-
if @target.options['sudo-password']
|
35
|
-
stdin.write("#{@target.options['sudo-password']}\n")
|
36
|
-
''
|
37
|
-
else
|
38
|
-
raise Bolt::Node::EscalateError.new(
|
39
|
-
"Sudo password for user #{@user} was not provided for localhost",
|
40
|
-
'NO_PASSWORD'
|
41
|
-
)
|
42
|
-
end
|
43
|
-
elsif err =~ /^#{@sudo_id}/
|
44
|
-
if sudo_stdin
|
45
|
-
stdin.write("#{sudo_stdin}\n")
|
46
|
-
stdin.close
|
47
|
-
end
|
48
|
-
''
|
49
|
-
else
|
50
|
-
handle_sudo_errors(err, pid)
|
51
|
-
end
|
52
|
-
end
|
53
|
-
|
54
|
-
def handle_sudo_errors(err, pid)
|
55
|
-
if err =~ /^#{@user} is not in the sudoers file\./
|
56
|
-
@logger.debug { err }
|
57
|
-
raise Bolt::Node::EscalateError.new(
|
58
|
-
"User #{@user} does not have sudo permission on localhost",
|
59
|
-
'SUDO_DENIED'
|
60
|
-
)
|
61
|
-
elsif err =~ /^Sorry, try again\./
|
62
|
-
@logger.debug { err }
|
63
|
-
# CODEREVIEW can we kill a sudo process without sudo password?
|
64
|
-
Process.kill('TERM', pid)
|
65
|
-
raise Bolt::Node::EscalateError.new(
|
66
|
-
"Sudo password for user #{@user} not recognized on localhost",
|
67
|
-
'BAD_PASSWORD'
|
68
|
-
)
|
69
|
-
else
|
70
|
-
# No need to raise an error - just return the string
|
71
|
-
err
|
72
|
-
end
|
73
|
-
end
|
74
|
-
|
75
|
-
def copy_file(source, dest)
|
76
|
-
@logger.debug { "Uploading #{source}, to #{dest}" }
|
77
|
-
if source.is_a?(StringIO)
|
78
|
-
File.open("tempfile", "w") { |f| f.write(source.read) }
|
79
|
-
execute(['mv', 'tempfile', dest])
|
80
|
-
else
|
81
|
-
# Mimic the behavior of `cp --remove-destination`
|
82
|
-
# since the flag isn't supported on MacOS
|
83
|
-
result = execute(['rm', '-rf', dest])
|
84
|
-
if result.exit_code != 0
|
85
|
-
message = "Could not remove existing file #{dest}: #{result.stderr.string}"
|
86
|
-
raise Bolt::Node::FileError.new(message, 'REMOVE_ERROR')
|
87
|
-
end
|
88
|
-
|
89
|
-
result = execute(['cp', '-r', source, dest])
|
90
|
-
if result.exit_code != 0
|
91
|
-
message = "Could not copy file to #{dest}: #{result.stderr.string}"
|
92
|
-
raise Bolt::Node::FileError.new(message, 'COPY_ERROR')
|
93
|
-
end
|
94
|
-
end
|
95
|
-
end
|
96
|
-
|
97
|
-
def with_tmpscript(script)
|
98
|
-
with_tempdir do |dir|
|
99
|
-
dest = File.join(dir.to_s, File.basename(script))
|
100
|
-
copy_file(script, dest)
|
101
|
-
yield dest, dir
|
102
|
-
end
|
103
|
-
end
|
104
|
-
|
105
|
-
# See if there's a sudo prompt in the output
|
106
|
-
# If not, return the output
|
107
|
-
def check_sudo(out, inp, pid, stdin)
|
108
|
-
buffer = out.readpartial(CHUNK_SIZE)
|
109
|
-
# Split on newlines, including the newline
|
110
|
-
lines = buffer.split(/(?<=[\n])/)
|
111
|
-
# handle_sudo will return the line if it is not a sudo prompt or error
|
112
|
-
lines.map! { |line| handle_sudo(inp, line, pid, stdin) }
|
113
|
-
lines.join("")
|
114
|
-
# If stream has reached EOF, no password prompt is expected
|
115
|
-
# return an empty string
|
116
|
-
rescue EOFError
|
117
|
-
''
|
118
|
-
end
|
119
|
-
|
120
|
-
def execute(command, sudoable: true, **options)
|
121
|
-
run_as = options[:run_as] || self.run_as
|
122
|
-
escalate = sudoable && run_as && @user != run_as
|
123
|
-
use_sudo = escalate && @target.options['run-as-command'].nil?
|
124
|
-
|
125
|
-
command_str = inject_interpreter(options[:interpreter], command)
|
126
|
-
|
127
|
-
if escalate
|
128
|
-
if use_sudo
|
129
|
-
sudo_exec = target.options['sudo-executable'] || "sudo"
|
130
|
-
sudo_flags = [sudo_exec, "-k", "-S", "-u", run_as, "-p", Sudoable.sudo_prompt]
|
131
|
-
sudo_flags += ["-E"] if options[:environment]
|
132
|
-
sudo_str = Shellwords.shelljoin(sudo_flags)
|
133
|
-
else
|
134
|
-
sudo_str = Shellwords.shelljoin(@target.options['run-as-command'] + [run_as])
|
135
|
-
end
|
136
|
-
command_str = build_sudoable_command_str(command_str, sudo_str, @sudo_id, options)
|
137
|
-
end
|
138
|
-
|
139
|
-
command_arr = options[:environment].nil? ? [command_str] : [options[:environment], command_str]
|
140
|
-
|
141
|
-
# Prepare the variables!
|
142
|
-
result_output = Bolt::Node::Output.new
|
143
|
-
# Sudo handler will pass stdin if needed.
|
144
|
-
in_buffer = !use_sudo && options[:stdin] ? options[:stdin] : ''
|
145
|
-
# Chunks of this size will be read in one iteration
|
146
|
-
index = 0
|
147
|
-
timeout = 0.1
|
148
|
-
|
149
|
-
inp, out, err, t = Open3.popen3(*command_arr)
|
150
|
-
read_streams = { out => String.new,
|
151
|
-
err => String.new }
|
152
|
-
write_stream = in_buffer.empty? ? [] : [inp]
|
153
|
-
|
154
|
-
# See if there's a sudo prompt
|
155
|
-
if use_sudo
|
156
|
-
ready_read = select([err], nil, nil, timeout * 5)
|
157
|
-
read_streams[err] << check_sudo(err, inp, t.pid, options[:stdin]) if ready_read
|
158
|
-
end
|
159
|
-
|
160
|
-
# True while the process is running or waiting for IO input
|
161
|
-
while t.alive?
|
162
|
-
# See if we can read from out or err, or write to in
|
163
|
-
ready_read, ready_write, = select(read_streams.keys, write_stream, nil, timeout)
|
164
|
-
|
165
|
-
# Read from out and err
|
166
|
-
ready_read&.each do |stream|
|
167
|
-
# Check for sudo prompt
|
168
|
-
read_streams[stream] << if use_sudo
|
169
|
-
check_sudo(stream, inp, t.pid, options[:stdin])
|
170
|
-
else
|
171
|
-
stream.readpartial(CHUNK_SIZE)
|
172
|
-
end
|
173
|
-
rescue EOFError
|
174
|
-
end
|
175
|
-
|
176
|
-
# select will either return an empty array if there are no
|
177
|
-
# writable streams or nil if no IO object is available before the
|
178
|
-
# timeout is reached.
|
179
|
-
writable = if ready_write.respond_to?(:empty?)
|
180
|
-
!ready_write.empty?
|
181
|
-
else
|
182
|
-
!ready_write.nil?
|
183
|
-
end
|
184
|
-
|
185
|
-
begin
|
186
|
-
if writable && index < in_buffer.length
|
187
|
-
to_print = in_buffer[index..-1]
|
188
|
-
written = inp.write_nonblock to_print
|
189
|
-
index += written
|
190
|
-
|
191
|
-
if index >= in_buffer.length && !write_stream.empty?
|
192
|
-
inp.close
|
193
|
-
write_stream = []
|
194
|
-
end
|
195
|
-
end
|
196
|
-
# If a task has stdin as an input_method but doesn't actually
|
197
|
-
# read from stdin, the task may return and close the input stream
|
198
|
-
rescue Errno::EPIPE
|
199
|
-
write_stream = []
|
200
|
-
end
|
201
|
-
end
|
202
|
-
# Read any remaining data in the pipe. Do not wait for
|
203
|
-
# EOF in case the pipe is inherited by a child process.
|
204
|
-
read_streams.each do |stream, _|
|
205
|
-
loop { read_streams[stream] << stream.read_nonblock(CHUNK_SIZE) }
|
206
|
-
rescue Errno::EAGAIN, EOFError
|
207
|
-
end
|
208
|
-
result_output.stdout << read_streams[out]
|
209
|
-
result_output.stderr << read_streams[err]
|
210
|
-
result_output.exit_code = t.value.exitstatus
|
211
|
-
result_output
|
212
|
-
end
|
213
|
-
end
|
214
|
-
end
|
215
|
-
end
|
216
|
-
end
|
@@ -1,150 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'shellwords'
|
4
|
-
require 'bolt/transport/base'
|
5
|
-
|
6
|
-
module Bolt
|
7
|
-
module Transport
|
8
|
-
class Sudoable < Base
|
9
|
-
def self.sudo_prompt
|
10
|
-
'[sudo] Bolt needs to run as another user, password: '
|
11
|
-
end
|
12
|
-
|
13
|
-
def run_command(target, command, options = {})
|
14
|
-
with_connection(target) do |conn|
|
15
|
-
conn.running_as(options[:run_as]) do
|
16
|
-
output = conn.execute(command, sudoable: true)
|
17
|
-
Bolt::Result.for_command(target,
|
18
|
-
output.stdout.string,
|
19
|
-
output.stderr.string,
|
20
|
-
output.exit_code,
|
21
|
-
'command', command)
|
22
|
-
end
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
26
|
-
def upload(target, source, destination, options = {})
|
27
|
-
with_connection(target) do |conn|
|
28
|
-
conn.running_as(options[:run_as]) do
|
29
|
-
conn.with_tempdir do |dir|
|
30
|
-
basename = File.basename(destination)
|
31
|
-
tmpfile = File.join(dir.to_s, basename)
|
32
|
-
conn.copy_file(source, tmpfile)
|
33
|
-
# pass over file ownership if we're using run-as to be a different user
|
34
|
-
dir.chown(conn.run_as)
|
35
|
-
result = conn.execute(['mv', '-f', tmpfile, destination], sudoable: true)
|
36
|
-
if result.exit_code != 0
|
37
|
-
message = "Could not move temporary file '#{tmpfile}' to #{destination}: #{result.stderr.string}"
|
38
|
-
raise Bolt::Node::FileError.new(message, 'MV_ERROR')
|
39
|
-
end
|
40
|
-
end
|
41
|
-
Bolt::Result.for_upload(target, source, destination)
|
42
|
-
end
|
43
|
-
end
|
44
|
-
end
|
45
|
-
|
46
|
-
def run_script(target, script, arguments, options = {})
|
47
|
-
# unpack any Sensitive data
|
48
|
-
arguments = unwrap_sensitive_args(arguments)
|
49
|
-
|
50
|
-
with_connection(target) do |conn|
|
51
|
-
conn.running_as(options[:run_as]) do
|
52
|
-
conn.with_tempdir do |dir|
|
53
|
-
path = conn.write_executable(dir.to_s, script)
|
54
|
-
dir.chown(conn.run_as)
|
55
|
-
output = conn.execute([path, *arguments], sudoable: true)
|
56
|
-
Bolt::Result.for_command(target,
|
57
|
-
output.stdout.string,
|
58
|
-
output.stderr.string,
|
59
|
-
output.exit_code,
|
60
|
-
'script', script)
|
61
|
-
end
|
62
|
-
end
|
63
|
-
end
|
64
|
-
end
|
65
|
-
|
66
|
-
def run_task(target, task, arguments, options = {})
|
67
|
-
implementation = select_implementation(target, task)
|
68
|
-
executable = implementation['path']
|
69
|
-
input_method = implementation['input_method']
|
70
|
-
extra_files = implementation['files']
|
71
|
-
|
72
|
-
with_connection(target) do |conn|
|
73
|
-
conn.running_as(options[:run_as]) do
|
74
|
-
stdin, output = nil
|
75
|
-
execute_options = {}
|
76
|
-
execute_options[:interpreter] = select_interpreter(executable, target.options['interpreters'])
|
77
|
-
interpreter_debug = if execute_options[:interpreter]
|
78
|
-
" using '#{execute_options[:interpreter]}' interpreter"
|
79
|
-
end
|
80
|
-
# log the arguments with sensitive data redacted, do NOT log unwrapped_arguments
|
81
|
-
logger.debug("Running '#{executable}' with #{arguments.to_json}#{interpreter_debug}")
|
82
|
-
# unpack any Sensitive data
|
83
|
-
arguments = unwrap_sensitive_args(arguments)
|
84
|
-
|
85
|
-
conn.with_tempdir do |dir|
|
86
|
-
if extra_files.empty?
|
87
|
-
task_dir = dir
|
88
|
-
else
|
89
|
-
# TODO: optimize upload of directories
|
90
|
-
arguments['_installdir'] = dir.to_s
|
91
|
-
task_dir = File.join(dir.to_s, task.tasks_dir)
|
92
|
-
dir.mkdirs([task.tasks_dir] + extra_files.map { |file| File.dirname(file['name']) })
|
93
|
-
extra_files.each do |file|
|
94
|
-
conn.copy_file(file['path'], File.join(dir.to_s, file['name']))
|
95
|
-
end
|
96
|
-
end
|
97
|
-
|
98
|
-
if STDIN_METHODS.include?(input_method)
|
99
|
-
stdin = JSON.dump(arguments)
|
100
|
-
end
|
101
|
-
|
102
|
-
if ENVIRONMENT_METHODS.include?(input_method)
|
103
|
-
execute_options[:environment] = envify_params(arguments)
|
104
|
-
end
|
105
|
-
|
106
|
-
remote_task_path = conn.write_executable(task_dir, executable)
|
107
|
-
|
108
|
-
# Avoid the horrors of passing data on stdin via a tty on multiple platforms
|
109
|
-
# by writing a wrapper script that directs stdin to the task.
|
110
|
-
if stdin && target.options['tty']
|
111
|
-
wrapper = make_wrapper_stringio(remote_task_path, stdin, execute_options[:interpreter])
|
112
|
-
execute_options.delete(:interpreter)
|
113
|
-
execute_options[:wrapper] = true
|
114
|
-
remote_task_path = conn.write_executable(dir, wrapper, 'wrapper.sh')
|
115
|
-
end
|
116
|
-
|
117
|
-
dir.chown(conn.run_as)
|
118
|
-
|
119
|
-
execute_options[:stdin] = stdin
|
120
|
-
execute_options[:sudoable] = true if conn.run_as
|
121
|
-
output = conn.execute(remote_task_path, execute_options)
|
122
|
-
end
|
123
|
-
Bolt::Result.for_task(target, output.stdout.string,
|
124
|
-
output.stderr.string,
|
125
|
-
output.exit_code,
|
126
|
-
task.name)
|
127
|
-
end
|
128
|
-
end
|
129
|
-
end
|
130
|
-
|
131
|
-
def make_wrapper_stringio(task_path, stdin, interpreter = nil)
|
132
|
-
if interpreter
|
133
|
-
StringIO.new(<<~SCRIPT)
|
134
|
-
#!/bin/sh
|
135
|
-
'#{interpreter}' '#{task_path}' <<'EOF'
|
136
|
-
#{stdin}
|
137
|
-
EOF
|
138
|
-
SCRIPT
|
139
|
-
else
|
140
|
-
StringIO.new(<<~SCRIPT)
|
141
|
-
#!/bin/sh
|
142
|
-
'#{task_path}' <<'EOF'
|
143
|
-
#{stdin}
|
144
|
-
EOF
|
145
|
-
SCRIPT
|
146
|
-
end
|
147
|
-
end
|
148
|
-
end
|
149
|
-
end
|
150
|
-
end
|
@@ -1,118 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'bolt/transport/sudoable/tmpdir'
|
4
|
-
|
5
|
-
module Bolt
|
6
|
-
module Transport
|
7
|
-
class Sudoable < Base
|
8
|
-
class Connection
|
9
|
-
attr_accessor :target
|
10
|
-
|
11
|
-
def initialize(target)
|
12
|
-
@target = target
|
13
|
-
@run_as = nil
|
14
|
-
@logger = Logging.logger[@target.safe_name]
|
15
|
-
end
|
16
|
-
|
17
|
-
# This method allows the @run_as variable to be used as a per-operation
|
18
|
-
# override for the user to run as. When @run_as is unset, the user
|
19
|
-
# specified on the target will be used.
|
20
|
-
def run_as
|
21
|
-
@run_as || target.options['run-as']
|
22
|
-
end
|
23
|
-
|
24
|
-
# Run as the specified user for the duration of the block.
|
25
|
-
def running_as(user)
|
26
|
-
@run_as = user
|
27
|
-
yield
|
28
|
-
ensure
|
29
|
-
@run_as = nil
|
30
|
-
end
|
31
|
-
|
32
|
-
def make_executable(path)
|
33
|
-
result = execute(['chmod', 'u+x', path])
|
34
|
-
if result.exit_code != 0
|
35
|
-
message = "Could not make file '#{path}' executable: #{result.stderr.string}"
|
36
|
-
raise Bolt::Node::FileError.new(message, 'CHMOD_ERROR')
|
37
|
-
end
|
38
|
-
end
|
39
|
-
|
40
|
-
def make_tempdir
|
41
|
-
tmpdir = @target.options.fetch('tmpdir', '/tmp')
|
42
|
-
script_dir = @target.options.fetch('script-dir', SecureRandom.uuid)
|
43
|
-
tmppath = File.join(tmpdir, script_dir)
|
44
|
-
command = ['mkdir', '-m', 700, tmppath]
|
45
|
-
|
46
|
-
result = execute(command)
|
47
|
-
if result.exit_code != 0
|
48
|
-
raise Bolt::Node::FileError.new("Could not make tempdir: #{result.stderr.string}", 'TEMPDIR_ERROR')
|
49
|
-
end
|
50
|
-
path = tmppath || result.stdout.string.chomp
|
51
|
-
Sudoable::Tmpdir.new(self, path)
|
52
|
-
end
|
53
|
-
|
54
|
-
def write_executable(dir, file, filename = nil)
|
55
|
-
filename ||= File.basename(file)
|
56
|
-
remote_path = File.join(dir.to_s, filename)
|
57
|
-
copy_file(file, remote_path)
|
58
|
-
make_executable(remote_path)
|
59
|
-
remote_path
|
60
|
-
end
|
61
|
-
|
62
|
-
# A helper to create and delete a tempdir on the remote system. Yields the
|
63
|
-
# directory name.
|
64
|
-
def with_tempdir
|
65
|
-
dir = make_tempdir
|
66
|
-
yield dir
|
67
|
-
ensure
|
68
|
-
dir&.delete
|
69
|
-
end
|
70
|
-
|
71
|
-
def execute(*_args)
|
72
|
-
message = "#{self.class.name} must implement #{method} to execute commands"
|
73
|
-
raise NotImplementedError, message
|
74
|
-
end
|
75
|
-
|
76
|
-
# In the case where a task is run with elevated privilege and needs stdin
|
77
|
-
# a random string is echoed to stderr indicating that the stdin is available
|
78
|
-
# for task input data because the sudo password has already either been
|
79
|
-
# provided on stdin or was not needed.
|
80
|
-
def prepend_sudo_success(sudo_id, command_str, reset_cwd)
|
81
|
-
command_str = "cd && #{command_str}" if reset_cwd
|
82
|
-
"sh -c 'echo #{sudo_id} 1>&2; #{command_str}'"
|
83
|
-
end
|
84
|
-
|
85
|
-
def prepend_chdir(command_str)
|
86
|
-
"sh -c 'cd && #{command_str}'"
|
87
|
-
end
|
88
|
-
|
89
|
-
# A helper to build up a single string that contains all of the options for
|
90
|
-
# privilege escalation. A wrapper script is used to direct task input to stdin
|
91
|
-
# when a tty is allocated and thus we do not need to prepend_sudo_success when
|
92
|
-
# using the wrapper or when the task does not require stdin data.
|
93
|
-
def build_sudoable_command_str(command_str, sudo_str, sudo_id, options)
|
94
|
-
if options[:stdin] && !options[:wrapper]
|
95
|
-
"#{sudo_str} #{prepend_sudo_success(sudo_id, command_str, options[:reset_cwd])}"
|
96
|
-
elsif options[:reset_cwd]
|
97
|
-
"#{sudo_str} #{prepend_chdir(command_str)}"
|
98
|
-
else
|
99
|
-
"#{sudo_str} #{command_str}"
|
100
|
-
end
|
101
|
-
end
|
102
|
-
|
103
|
-
# Returns string with the interpreter conditionally prepended
|
104
|
-
def inject_interpreter(interpreter, command)
|
105
|
-
if interpreter
|
106
|
-
if command.is_a?(Array)
|
107
|
-
command.unshift(interpreter)
|
108
|
-
else
|
109
|
-
command = [interpreter, command]
|
110
|
-
end
|
111
|
-
end
|
112
|
-
|
113
|
-
command.is_a?(String) ? command : Shellwords.shelljoin(command)
|
114
|
-
end
|
115
|
-
end
|
116
|
-
end
|
117
|
-
end
|
118
|
-
end
|