mixlib-shellout 2.1.0-universal-mingw32

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ module Mixlib
2
+ class ShellOut
3
+ class ShellCommandFailed < RuntimeError; end
4
+ class CommandTimeout < RuntimeError; end
5
+ class InvalidCommandOption < RuntimeError; end
6
+ end
7
+ end
@@ -0,0 +1,415 @@
1
+ #--
2
+ # Author:: Daniel DeLeo (<dan@opscode.com>)
3
+ # Copyright:: Copyright (c) 2010, 2011 Opscode, Inc.
4
+ # License:: Apache License, Version 2.0
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ module Mixlib
20
+ class ShellOut
21
+ module Unix
22
+
23
+ # "1.8.7" as a frozen string. We use this with a hack that disables GC to
24
+ # avoid segfaults on Ruby 1.8.7, so we need to allocate the fewest
25
+ # objects we possibly can.
26
+ ONE_DOT_EIGHT_DOT_SEVEN = "1.8.7".freeze
27
+
28
+ # Option validation that is unix specific
29
+ def validate_options(opts)
30
+ # No options to validate, raise exceptions here if needed
31
+ end
32
+
33
+ # Whether we're simulating a login shell
34
+ def using_login?
35
+ return login && user
36
+ end
37
+
38
+ # Helper method for sgids
39
+ def all_seconderies
40
+ ret = []
41
+ Etc.endgrent
42
+ while ( g = Etc.getgrent ) do
43
+ ret << g
44
+ end
45
+ Etc.endgrent
46
+ return ret
47
+ end
48
+
49
+ # The secondary groups that the subprocess will switch to.
50
+ # Currently valid only if login is used, and is set
51
+ # to the user's secondary groups
52
+ def sgids
53
+ return nil unless using_login?
54
+ user_name = Etc.getpwuid(uid).name
55
+ all_seconderies.select{|g| g.mem.include?(user_name)}.map{|g|g.gid}
56
+ end
57
+
58
+ # The environment variables that are deduced from simulating logon
59
+ # Only valid if login is used
60
+ def logon_environment
61
+ return {} unless using_login?
62
+ entry = Etc.getpwuid(uid)
63
+ # According to `man su`, the set fields are:
64
+ # $HOME, $SHELL, $USER, $LOGNAME, $PATH, and $IFS
65
+ # Values are copied from "shadow" package in Ubuntu 14.10
66
+ {'HOME'=>entry.dir, 'SHELL'=>entry.shell, 'USER'=>entry.name, 'LOGNAME'=>entry.name, 'PATH'=>'/sbin:/bin:/usr/sbin:/usr/bin', 'IFS'=>"\t\n"}
67
+ end
68
+
69
+ # Merges the two environments for the process
70
+ def process_environment
71
+ logon_environment.merge(self.environment)
72
+ end
73
+
74
+ # Run the command, writing the command's standard out and standard error
75
+ # to +stdout+ and +stderr+, and saving its exit status object to +status+
76
+ # === Returns
77
+ # returns +self+; +stdout+, +stderr+, +status+, and +exitstatus+ will be
78
+ # populated with results of the command.
79
+ # === Raises
80
+ # * Errno::EACCES when you are not privileged to execute the command
81
+ # * Errno::ENOENT when the command is not available on the system (or not
82
+ # in the current $PATH)
83
+ # * Chef::Exceptions::CommandTimeout when the command does not complete
84
+ # within +timeout+ seconds (default: 600s). When this happens, ShellOut
85
+ # will send a TERM and then KILL to the entire process group to ensure
86
+ # that any grandchild processes are terminated. If the invocation of
87
+ # the child process spawned multiple child processes (which commonly
88
+ # happens if the command is passed as a single string to be interpreted
89
+ # by bin/sh, and bin/sh is not bash), the exit status object may not
90
+ # contain the correct exit code of the process (of course there is no
91
+ # exit code if the command is killed by SIGKILL, also).
92
+ def run_command
93
+ @child_pid = fork_subprocess
94
+ @reaped = false
95
+
96
+ configure_parent_process_file_descriptors
97
+
98
+ # Ruby 1.8.7 and 1.8.6 from mid 2009 try to allocate objects during GC
99
+ # when calling IO.select and IO#read. Disabling GC works around the
100
+ # segfault, but obviously it's a bad workaround. We no longer support
101
+ # 1.8.6 so we only need this hack for 1.8.7.
102
+ GC.disable if RUBY_VERSION == ONE_DOT_EIGHT_DOT_SEVEN
103
+
104
+ # CHEF-3390: Marshall.load on Ruby < 1.8.7p369 also has a GC bug related
105
+ # to Marshall.load, so try disabling GC first.
106
+ propagate_pre_exec_failure
107
+
108
+ @status = nil
109
+ @result = nil
110
+ @execution_time = 0
111
+
112
+ write_to_child_stdin
113
+
114
+ until @status
115
+ ready_buffers = attempt_buffer_read
116
+ unless ready_buffers
117
+ @execution_time += READ_WAIT_TIME
118
+ if @execution_time >= timeout && !@result
119
+ # kill the bad proccess
120
+ reap_errant_child
121
+ # read anything it wrote when we killed it
122
+ attempt_buffer_read
123
+ # raise
124
+ raise CommandTimeout, "Command timed out after #{@execution_time.to_i}s:\n#{format_for_exception}"
125
+ end
126
+ end
127
+
128
+ attempt_reap
129
+ end
130
+
131
+ self
132
+ rescue Errno::ENOENT
133
+ # When ENOENT happens, we can be reasonably sure that the child process
134
+ # is going to exit quickly, so we use the blocking variant of waitpid2
135
+ reap
136
+ raise
137
+ ensure
138
+ reap_errant_child if should_reap?
139
+ # make one more pass to get the last of the output after the
140
+ # child process dies
141
+ attempt_buffer_read
142
+ # no matter what happens, turn the GC back on, and hope whatever busted
143
+ # version of ruby we're on doesn't allocate some objects during the next
144
+ # GC run.
145
+ GC.enable
146
+ close_all_pipes
147
+ end
148
+
149
+ private
150
+
151
+ def set_user
152
+ if user
153
+ Process.uid = uid
154
+ Process.euid = uid
155
+ end
156
+ end
157
+
158
+ def set_group
159
+ if group
160
+ Process.egid = gid
161
+ Process.gid = gid
162
+ end
163
+ end
164
+
165
+ def set_secondarygroups
166
+ if sgids
167
+ Process.groups = sgids
168
+ end
169
+ end
170
+
171
+ def set_environment
172
+ # user-set variables should override the login ones
173
+ process_environment.each do |env_var,value|
174
+ ENV[env_var] = value
175
+ end
176
+ end
177
+
178
+ def set_umask
179
+ File.umask(umask) if umask
180
+ end
181
+
182
+ def set_cwd
183
+ Dir.chdir(cwd) if cwd
184
+ end
185
+
186
+ # Since we call setsid the child_pgid will be the child_pid, set to negative here
187
+ # so it can be directly used in arguments to kill, wait, etc.
188
+ def child_pgid
189
+ -@child_pid
190
+ end
191
+
192
+ def initialize_ipc
193
+ @stdin_pipe, @stdout_pipe, @stderr_pipe, @process_status_pipe = IO.pipe, IO.pipe, IO.pipe, IO.pipe
194
+ @process_status_pipe.last.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
195
+ end
196
+
197
+ def child_stdin
198
+ @stdin_pipe[1]
199
+ end
200
+
201
+ def child_stdout
202
+ @stdout_pipe[0]
203
+ end
204
+
205
+ def child_stderr
206
+ @stderr_pipe[0]
207
+ end
208
+
209
+ def child_process_status
210
+ @process_status_pipe[0]
211
+ end
212
+
213
+ def close_all_pipes
214
+ child_stdin.close unless child_stdin.closed?
215
+ child_stdout.close unless child_stdout.closed?
216
+ child_stderr.close unless child_stderr.closed?
217
+ child_process_status.close unless child_process_status.closed?
218
+ end
219
+
220
+ # Replace stdout, and stderr with pipes to the parent, and close the
221
+ # reader side of the error marshaling side channel.
222
+ #
223
+ # If there is no input, close STDIN so when we exec,
224
+ # the new program will know it's never getting input ever.
225
+ def configure_subprocess_file_descriptors
226
+ process_status_pipe.first.close
227
+
228
+ # HACK: for some reason, just STDIN.close isn't good enough when running
229
+ # under ruby 1.9.2, so make it good enough:
230
+ stdin_pipe.last.close
231
+ STDIN.reopen stdin_pipe.first
232
+ stdin_pipe.first.close unless input
233
+
234
+ stdout_pipe.first.close
235
+ STDOUT.reopen stdout_pipe.last
236
+ stdout_pipe.last.close
237
+
238
+ stderr_pipe.first.close
239
+ STDERR.reopen stderr_pipe.last
240
+ stderr_pipe.last.close
241
+
242
+ STDOUT.sync = STDERR.sync = true
243
+ STDIN.sync = true if input
244
+ end
245
+
246
+ def configure_parent_process_file_descriptors
247
+ # Close the sides of the pipes we don't care about
248
+ stdin_pipe.first.close
249
+ stdin_pipe.last.close unless input
250
+ stdout_pipe.last.close
251
+ stderr_pipe.last.close
252
+ process_status_pipe.last.close
253
+ # Get output as it happens rather than buffered
254
+ child_stdin.sync = true if input
255
+ child_stdout.sync = true
256
+ child_stderr.sync = true
257
+
258
+ true
259
+ end
260
+
261
+ # Some patch levels of ruby in wide use (in particular the ruby 1.8.6 on OSX)
262
+ # segfault when you IO.select a pipe that's reached eof. Weak sauce.
263
+ def open_pipes
264
+ @open_pipes ||= [child_stdout, child_stderr, child_process_status]
265
+ end
266
+
267
+ # Keep this unbuffered for now
268
+ def write_to_child_stdin
269
+ return unless input
270
+ child_stdin << input
271
+ child_stdin.close # Kick things off
272
+ end
273
+
274
+ def attempt_buffer_read
275
+ ready = IO.select(open_pipes, nil, nil, READ_WAIT_TIME)
276
+ if ready
277
+ read_stdout_to_buffer if ready.first.include?(child_stdout)
278
+ read_stderr_to_buffer if ready.first.include?(child_stderr)
279
+ read_process_status_to_buffer if ready.first.include?(child_process_status)
280
+ end
281
+ ready
282
+ end
283
+
284
+ def read_stdout_to_buffer
285
+ while chunk = child_stdout.read_nonblock(READ_SIZE)
286
+ @stdout << chunk
287
+ @live_stdout << chunk if @live_stdout
288
+ end
289
+ rescue Errno::EAGAIN
290
+ rescue EOFError
291
+ open_pipes.delete(child_stdout)
292
+ end
293
+
294
+ def read_stderr_to_buffer
295
+ while chunk = child_stderr.read_nonblock(READ_SIZE)
296
+ @stderr << chunk
297
+ @live_stderr << chunk if @live_stderr
298
+ end
299
+ rescue Errno::EAGAIN
300
+ rescue EOFError
301
+ open_pipes.delete(child_stderr)
302
+ end
303
+
304
+ def read_process_status_to_buffer
305
+ while chunk = child_process_status.read_nonblock(READ_SIZE)
306
+ @process_status << chunk
307
+ end
308
+ rescue Errno::EAGAIN
309
+ rescue EOFError
310
+ open_pipes.delete(child_process_status)
311
+ end
312
+
313
+ def fork_subprocess
314
+ initialize_ipc
315
+
316
+ fork do
317
+ # Child processes may themselves fork off children. A common case
318
+ # is when the command is given as a single string (instead of
319
+ # command name plus Array of arguments) and /bin/sh does not
320
+ # support the "ONESHOT" optimization (where sh -c does exec without
321
+ # forking). To support cleaning up all the children, we need to
322
+ # ensure they're in a unique process group.
323
+ #
324
+ # We use setsid here to abandon our controlling tty and get a new session
325
+ # and process group that are set to the pid of the child process.
326
+ Process.setsid
327
+
328
+ configure_subprocess_file_descriptors
329
+
330
+ set_secondarygroups
331
+ set_group
332
+ set_user
333
+ set_environment
334
+ set_umask
335
+ set_cwd
336
+
337
+ begin
338
+ command.kind_of?(Array) ? exec(*command, :close_others=>true) : exec(command, :close_others=>true)
339
+
340
+ raise 'forty-two' # Should never get here
341
+ rescue Exception => e
342
+ Marshal.dump(e, process_status_pipe.last)
343
+ process_status_pipe.last.flush
344
+ end
345
+ process_status_pipe.last.close unless (process_status_pipe.last.closed?)
346
+ exit!
347
+ end
348
+ end
349
+
350
+ # Attempt to get a Marshaled error from the side-channel.
351
+ # If it's there, un-marshal it and raise. If it's not there,
352
+ # assume everything went well.
353
+ def propagate_pre_exec_failure
354
+ begin
355
+ attempt_buffer_read until child_process_status.eof?
356
+ e = Marshal.load(@process_status)
357
+ raise(Exception === e ? e : "unknown failure: #{e.inspect}")
358
+ rescue ArgumentError # If we get an ArgumentError error, then the exec was successful
359
+ true
360
+ ensure
361
+ child_process_status.close
362
+ open_pipes.delete(child_process_status)
363
+ end
364
+ end
365
+
366
+ def reap_errant_child
367
+ return if attempt_reap
368
+ @terminate_reason = "Command exceeded allowed execution time, process terminated"
369
+ logger.error("Command exceeded allowed execution time, sending TERM") if logger
370
+ Process.kill(:TERM, child_pgid)
371
+ sleep 3
372
+ attempt_reap
373
+ logger.error("Command exceeded allowed execution time, sending KILL") if logger
374
+ Process.kill(:KILL, child_pgid)
375
+ reap
376
+
377
+ # Should not hit this but it's possible if something is calling waitall
378
+ # in a separate thread.
379
+ rescue Errno::ESRCH
380
+ nil
381
+ end
382
+
383
+ def should_reap?
384
+ # if we fail to fork, no child pid so nothing to reap
385
+ @child_pid && !@reaped
386
+ end
387
+
388
+ # Unconditionally reap the child process. This is used in scenarios where
389
+ # we can be confident the child will exit quickly, and has not spawned
390
+ # and grandchild processes.
391
+ def reap
392
+ results = Process.waitpid2(@child_pid)
393
+ @reaped = true
394
+ @status = results.last
395
+ rescue Errno::ECHILD
396
+ # When cleaning up timed-out processes, we might send SIGKILL to the
397
+ # whole process group after we've cleaned up the direct child. In that
398
+ # case the grandchildren will have been adopted by init so we can't
399
+ # reap them even if we wanted to (we don't).
400
+ nil
401
+ end
402
+
403
+ # Try to reap the child process but don't block if it isn't dead yet.
404
+ def attempt_reap
405
+ if results = Process.waitpid2(@child_pid, Process::WNOHANG)
406
+ @reaped = true
407
+ @status = results.last
408
+ else
409
+ nil
410
+ end
411
+ end
412
+
413
+ end
414
+ end
415
+ end
@@ -0,0 +1,5 @@
1
+ module Mixlib
2
+ class ShellOut
3
+ VERSION = "2.1.0"
4
+ end
5
+ end
@@ -0,0 +1,320 @@
1
+ #--
2
+ # Author:: Daniel DeLeo (<dan@opscode.com>)
3
+ # Author:: John Keiser (<jkeiser@opscode.com>)
4
+ # Author:: Ho-Sheng Hsiao (<hosh@opscode.com>)
5
+ # Copyright:: Copyright (c) 2011, 2012 Opscode, Inc.
6
+ # License:: Apache License, Version 2.0
7
+ #
8
+ # Licensed under the Apache License, Version 2.0 (the "License");
9
+ # you may not use this file except in compliance with the License.
10
+ # You may obtain a copy of the License at
11
+ #
12
+ # http://www.apache.org/licenses/LICENSE-2.0
13
+ #
14
+ # Unless required by applicable law or agreed to in writing, software
15
+ # distributed under the License is distributed on an "AS IS" BASIS,
16
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17
+ # See the License for the specific language governing permissions and
18
+ # limitations under the License.
19
+ #
20
+
21
+ require 'win32/process'
22
+ require 'windows/handle'
23
+ require 'windows/process'
24
+ require 'windows/synchronize'
25
+
26
+ require 'mixlib/shellout/windows/core_ext'
27
+
28
+ module Mixlib
29
+ class ShellOut
30
+ module Windows
31
+
32
+ include ::Windows::Handle
33
+ include ::Windows::Process
34
+ include ::Windows::Synchronize
35
+
36
+ TIME_SLICE = 0.05
37
+
38
+ # Option validation that is windows specific
39
+ def validate_options(opts)
40
+ if opts[:user]
41
+ unless opts[:password]
42
+ raise InvalidCommandOption, "You must supply both a username and password when supplying a user in windows"
43
+ end
44
+ end
45
+ end
46
+
47
+ #--
48
+ # Missing lots of features from the UNIX version, such as
49
+ # uid, etc.
50
+ def run_command
51
+
52
+ #
53
+ # Create pipes to capture stdout and stderr,
54
+ #
55
+ stdout_read, stdout_write = IO.pipe
56
+ stderr_read, stderr_write = IO.pipe
57
+ stdin_read, stdin_write = IO.pipe
58
+ open_streams = [ stdout_read, stderr_read ]
59
+
60
+ begin
61
+
62
+ #
63
+ # Set cwd, environment, appname, etc.
64
+ #
65
+ app_name, command_line = command_to_run(self.command)
66
+ create_process_args = {
67
+ :app_name => app_name,
68
+ :command_line => command_line,
69
+ :startup_info => {
70
+ :stdout => stdout_write,
71
+ :stderr => stderr_write,
72
+ :stdin => stdin_read
73
+ },
74
+ :environment => inherit_environment.map { |k,v| "#{k}=#{v}" },
75
+ :close_handles => false
76
+ }
77
+ create_process_args[:cwd] = cwd if cwd
78
+ # default to local account database if domain is not specified
79
+ create_process_args[:domain] = domain.nil? ? "." : domain
80
+ create_process_args[:with_logon] = with_logon if with_logon
81
+ create_process_args[:password] = password if password
82
+
83
+ #
84
+ # Start the process
85
+ #
86
+ process = Process.create(create_process_args)
87
+ begin
88
+ # Start pushing data into input
89
+ stdin_write << input if input
90
+
91
+ # Close pipe to kick things off
92
+ stdin_write.close
93
+
94
+ #
95
+ # Wait for the process to finish, consuming output as we go
96
+ #
97
+ start_wait = Time.now
98
+ while true
99
+ wait_status = WaitForSingleObject(process.process_handle, 0)
100
+ case wait_status
101
+ when WAIT_OBJECT_0
102
+ # Get process exit code
103
+ exit_code = [0].pack('l')
104
+ unless GetExitCodeProcess(process.process_handle, exit_code)
105
+ raise get_last_error
106
+ end
107
+ @status = ThingThatLooksSortOfLikeAProcessStatus.new
108
+ @status.exitstatus = exit_code.unpack('l').first
109
+
110
+ return self
111
+ when WAIT_TIMEOUT
112
+ # Kill the process
113
+ if (Time.now - start_wait) > timeout
114
+ begin
115
+ Process.kill(:KILL, process.process_id)
116
+ rescue Errno::EIO
117
+ logger.warn("Failed to kill timed out process #{process.process_id}") if logger
118
+ end
119
+
120
+ raise Mixlib::ShellOut::CommandTimeout, "command timed out:\n#{format_for_exception}"
121
+ end
122
+
123
+ consume_output(open_streams, stdout_read, stderr_read)
124
+ else
125
+ raise "Unknown response from WaitForSingleObject(#{process.process_handle}, #{timeout*1000}): #{wait_status}"
126
+ end
127
+
128
+ end
129
+
130
+ ensure
131
+ CloseHandle(process.thread_handle) if process.thread_handle
132
+ CloseHandle(process.process_handle) if process.process_handle
133
+ end
134
+
135
+ ensure
136
+ #
137
+ # Consume all remaining data from the pipes until they are closed
138
+ #
139
+ stdout_write.close
140
+ stderr_write.close
141
+
142
+ while consume_output(open_streams, stdout_read, stderr_read)
143
+ end
144
+ end
145
+ end
146
+
147
+ private
148
+
149
+ class ThingThatLooksSortOfLikeAProcessStatus
150
+ attr_accessor :exitstatus
151
+ def success?
152
+ exitstatus == 0
153
+ end
154
+ end
155
+
156
+ def consume_output(open_streams, stdout_read, stderr_read)
157
+ return false if open_streams.length == 0
158
+ ready = IO.select(open_streams, nil, nil, READ_WAIT_TIME)
159
+ return true if ! ready
160
+
161
+ if ready.first.include?(stdout_read)
162
+ begin
163
+ next_chunk = stdout_read.readpartial(READ_SIZE)
164
+ @stdout << next_chunk
165
+ @live_stdout << next_chunk if @live_stdout
166
+ rescue EOFError
167
+ stdout_read.close
168
+ open_streams.delete(stdout_read)
169
+ end
170
+ end
171
+
172
+ if ready.first.include?(stderr_read)
173
+ begin
174
+ next_chunk = stderr_read.readpartial(READ_SIZE)
175
+ @stderr << next_chunk
176
+ @live_stderr << next_chunk if @live_stderr
177
+ rescue EOFError
178
+ stderr_read.close
179
+ open_streams.delete(stderr_read)
180
+ end
181
+ end
182
+
183
+ return true
184
+ end
185
+
186
+ IS_BATCH_FILE = /\.bat"?$|\.cmd"?$/i
187
+
188
+ def command_to_run(command)
189
+ return _run_under_cmd(command) if Utils.should_run_under_cmd?(command)
190
+
191
+ candidate = candidate_executable_for_command(command)
192
+
193
+ # Don't do searching for empty commands. Let it fail when it runs.
194
+ return [ nil, command ] if candidate.length == 0
195
+
196
+ # Check if the exe exists directly. Otherwise, search PATH.
197
+ exe = Utils.find_executable(candidate)
198
+ exe = Utils.which(unquoted_executable_path(command)) if exe.nil? && exe !~ /[\\\/]/
199
+
200
+ # Batch files MUST use cmd; and if we couldn't find the command we're looking for,
201
+ # we assume it must be a cmd builtin.
202
+ if exe.nil? || exe =~ IS_BATCH_FILE
203
+ _run_under_cmd(command)
204
+ else
205
+ _run_directly(command, exe)
206
+ end
207
+ end
208
+
209
+ # cmd does not parse multiple quotes well unless the whole thing is wrapped up in quotes.
210
+ # https://github.com/opscode/mixlib-shellout/pull/2#issuecomment-4837859
211
+ # http://ss64.com/nt/syntax-esc.html
212
+ def _run_under_cmd(command)
213
+ [ ENV['COMSPEC'], "cmd /c \"#{command}\"" ]
214
+ end
215
+
216
+ def _run_directly(command, exe)
217
+ [ exe, command ]
218
+ end
219
+
220
+ def unquoted_executable_path(command)
221
+ command[0,command.index(/\s/) || command.length]
222
+ end
223
+
224
+ def candidate_executable_for_command(command)
225
+ if command =~ /^\s*"(.*?)"/
226
+ # If we have quotes, do an exact match
227
+ $1
228
+ else
229
+ # Otherwise check everything up to the first space
230
+ unquoted_executable_path(command).strip
231
+ end
232
+ end
233
+
234
+ def inherit_environment
235
+ result = {}
236
+ ENV.each_pair do |k,v|
237
+ result[k] = v
238
+ end
239
+
240
+ environment.each_pair do |k,v|
241
+ if v == nil
242
+ result.delete(k)
243
+ else
244
+ result[k] = v
245
+ end
246
+ end
247
+ result
248
+ end
249
+
250
+ module Utils
251
+ # api: semi-private
252
+ # If there are special characters parsable by cmd.exe (such as file redirection), then
253
+ # this method should return true.
254
+ #
255
+ # This parser is based on
256
+ # https://github.com/ruby/ruby/blob/9073db5cb1d3173aff62be5b48d00f0fb2890991/win32/win32.c#L1437
257
+ def self.should_run_under_cmd?(command)
258
+ return true if command =~ /^@/
259
+
260
+ quote = nil
261
+ env = false
262
+ env_first_char = false
263
+
264
+ command.dup.each_char do |c|
265
+ case c
266
+ when "'", '"'
267
+ if (!quote)
268
+ quote = c
269
+ elsif quote == c
270
+ quote = nil
271
+ end
272
+ next
273
+ when '>', '<', '|', '&', "\n"
274
+ return true unless quote
275
+ when '%'
276
+ return true if env
277
+ env = env_first_char = true
278
+ next
279
+ else
280
+ next unless env
281
+ if env_first_char
282
+ env_first_char = false
283
+ env = false and next if c !~ /[A-Za-z_]/
284
+ end
285
+ env = false if c !~ /[A-Za-z1-9_]/
286
+ end
287
+ end
288
+ return false
289
+ end
290
+
291
+ def self.pathext
292
+ @pathext ||= ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') + [''] : ['']
293
+ end
294
+
295
+ # which() mimicks the Unix which command
296
+ # FIXME: it is not working
297
+ def self.which(cmd)
298
+ ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
299
+ exe = find_executable("#{path}/#{cmd}")
300
+ return exe if exe
301
+ end
302
+ return nil
303
+ end
304
+
305
+ # Windows has a different notion of what "executable" means
306
+ # The OS will search through valid the extensions and look
307
+ # for a binary there.
308
+ def self.find_executable(path)
309
+ return path if File.executable? path
310
+
311
+ pathext.each do |ext|
312
+ exe = "#{path}#{ext}"
313
+ return exe if File.executable? exe
314
+ end
315
+ return nil
316
+ end
317
+ end
318
+ end # class
319
+ end
320
+ end