cheetah 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG +16 -2
- data/README.md +94 -14
- data/VERSION +1 -1
- data/lib/cheetah.rb +178 -81
- data/lib/cheetah/version.rb +1 -0
- metadata +15 -39
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 4af5244a54ce65d8ca375a0fd9b868db3cddbaa2
|
4
|
+
data.tar.gz: fd2c672c6ee3a7b19802004b127e26e0f08747f3
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 843e3a2137117843ce28075d7278092fc682daa1b01860283aaa2504810b725e36b0a35e342b4f14ebcf4bac09757a0f54dcafe2c7f7057fb57f9310113b80bc
|
7
|
+
data.tar.gz: c51c8b5c470afb34ee59e58ca715515f533aad699ab8dab0ad1e416a71b36a4ed53e01b642e67b4dc778254ceb4be9cdde62d5e82614ba63ffbdbd5c8a72fdcc
|
data/CHANGELOG
CHANGED
@@ -1,11 +1,25 @@
|
|
1
|
+
0.5.0 (2015-12-18)
|
2
|
+
------------------
|
3
|
+
|
4
|
+
* Added chroot option for executing in different system root.
|
5
|
+
* Added ENV overwrite option.
|
6
|
+
* Allowed to specify known exit codes that are not errors.
|
7
|
+
* Documented how to execute in different working directory.
|
8
|
+
* Allowed passing nil as :stdin to be same as :stdout and :strerr.
|
9
|
+
* Converted parameters for command to strings with `.to_s`.
|
10
|
+
* Adapted testsuite to new rspec.
|
11
|
+
* Updated documentation with various fixes.
|
12
|
+
* Dropped support for Ruby 1.9.3.
|
13
|
+
* Added support for Ruby 2.1 and 2.2.
|
14
|
+
|
1
15
|
0.4.0 (2013-11-21)
|
2
16
|
------------------
|
3
17
|
|
4
18
|
* Implemented incremental logging. The input and both outputs of the executed
|
5
19
|
command are now logged one-by-line by the default recorder. A custom recorder
|
6
20
|
can record them on even finer granularity.
|
7
|
-
* Dropped support
|
8
|
-
* Added support
|
21
|
+
* Dropped support for Ruby 1.8.7.
|
22
|
+
* Added support for Ruby 2.0.0.
|
9
23
|
* Internal code improvements.
|
10
24
|
|
11
25
|
0.3.0 (2012-06-21)
|
data/README.md
CHANGED
@@ -1,5 +1,9 @@
|
|
1
1
|
Cheetah
|
2
2
|
=======
|
3
|
+
[](https://travis-ci.org/openSUSE/cheetah)
|
4
|
+
[](https://codeclimate.com/github/openSUSE/cheetah)
|
5
|
+
[](https://coveralls.io/r/openSUSE/cheetah?branch=master)
|
6
|
+
|
3
7
|
|
4
8
|
Your swiss army knife for executing external commands in Ruby safely and
|
5
9
|
conveniently.
|
@@ -9,11 +13,11 @@ Examples
|
|
9
13
|
|
10
14
|
```ruby
|
11
15
|
# Run a command and capture its output
|
12
|
-
files = Cheetah.run("ls", "-la", :
|
16
|
+
files = Cheetah.run("ls", "-la", stdout: :capture)
|
13
17
|
|
14
18
|
# Run a command and capture its output into a stream
|
15
19
|
File.open("files.txt", "w") do |stdout|
|
16
|
-
Cheetah.run("ls", "-la", :
|
20
|
+
Cheetah.run("ls", "-la", stdout: stdout)
|
17
21
|
end
|
18
22
|
|
19
23
|
# Run a command and handle errors
|
@@ -22,7 +26,7 @@ begin
|
|
22
26
|
rescue Cheetah::ExecutionFailed => e
|
23
27
|
puts e.message
|
24
28
|
puts "Standard output: #{e.stdout}"
|
25
|
-
puts "Error
|
29
|
+
puts "Error output: #{e.stderr}"
|
26
30
|
end
|
27
31
|
```
|
28
32
|
|
@@ -34,7 +38,11 @@ Features
|
|
34
38
|
* Piping commands together
|
35
39
|
* 100% secure (shell expansion is impossible by design)
|
36
40
|
* Raises exceptions on errors (no more manual status code checks)
|
41
|
+
but allows to specify which non-zero codes are not an error
|
42
|
+
* Thread-safety
|
43
|
+
* Allows overriding environment variables
|
37
44
|
* Optional logging for easy debugging
|
45
|
+
* Running on changed root ( requires chroot permission )
|
38
46
|
|
39
47
|
Non-features
|
40
48
|
------------
|
@@ -63,13 +71,16 @@ To run a command, just specify it together with its arguments:
|
|
63
71
|
|
64
72
|
```ruby
|
65
73
|
Cheetah.run("tar", "xzf", "foo.tar.gz")
|
74
|
+
|
75
|
+
Cheetah converts each argument to a string using `#to_s`.
|
76
|
+
|
66
77
|
```
|
67
78
|
### Passing Input
|
68
79
|
|
69
80
|
Using the `:stdin` option you can pass a string to command's standard input:
|
70
81
|
|
71
82
|
```ruby
|
72
|
-
Cheetah.run("python", :
|
83
|
+
Cheetah.run("python", stdin: source_code)
|
73
84
|
```
|
74
85
|
|
75
86
|
If the input is big you may want to avoid passing it in one huge string. In that
|
@@ -78,7 +89,7 @@ input from it gradually.
|
|
78
89
|
|
79
90
|
```ruby
|
80
91
|
File.open("huge_program.py") do |stdin|
|
81
|
-
Cheetah.run("python", :
|
92
|
+
Cheetah.run("python", stdin: stdin)
|
82
93
|
end
|
83
94
|
```
|
84
95
|
|
@@ -88,7 +99,7 @@ To capture command's standard output, set the `:stdout` option to `:capture`.
|
|
88
99
|
You will receive the output as a return value of the call:
|
89
100
|
|
90
101
|
```ruby
|
91
|
-
files = Cheetah.run("ls", "-la", :
|
102
|
+
files = Cheetah.run("ls", "-la", stdout: :capture)
|
92
103
|
```
|
93
104
|
|
94
105
|
The same technique works with the error output — just use the `:stderr` option.
|
@@ -96,7 +107,7 @@ If you specify capturing of both outputs, the return value will be a two-element
|
|
96
107
|
array:
|
97
108
|
|
98
109
|
```ruby
|
99
|
-
results, errors = Cheetah.run("grep", "-r", "User", ".", :
|
110
|
+
results, errors = Cheetah.run("grep", "-r", "User", ".", stdout: => :capture, stderr: => :capture)
|
100
111
|
```
|
101
112
|
|
102
113
|
If the output is big you may want to avoid capturing it into a huge string. In
|
@@ -105,7 +116,7 @@ command will write its output into it gradually.
|
|
105
116
|
|
106
117
|
```ruby
|
107
118
|
File.open("files.txt", "w") do |stdout|
|
108
|
-
Cheetah.run("ls", "-la", :
|
119
|
+
Cheetah.run("ls", "-la", stdout: stdout)
|
109
120
|
end
|
110
121
|
```
|
111
122
|
|
@@ -115,12 +126,12 @@ You can pipe multiple commands together and execute them as one. Just specify
|
|
115
126
|
the commands together with their arguments as arrays:
|
116
127
|
|
117
128
|
```ruby
|
118
|
-
processes = Cheetah.run(["ps", "aux"], ["grep", "ruby"], :
|
129
|
+
processes = Cheetah.run(["ps", "aux"], ["grep", "ruby"], stdout: :capture)
|
119
130
|
```
|
120
131
|
|
121
132
|
### Error Handling
|
122
133
|
|
123
|
-
If the command can't be executed for some reason or returns
|
134
|
+
If the command can't be executed for some reason or returns an unexpected non-zero exit
|
124
135
|
status, Cheetah raises an exception with detailed information about the failure:
|
125
136
|
|
126
137
|
```ruby
|
@@ -130,7 +141,8 @@ begin
|
|
130
141
|
rescue Cheetah::ExecutionFailed => e
|
131
142
|
puts e.message
|
132
143
|
puts "Standard output: #{e.stdout}"
|
133
|
-
puts "Error
|
144
|
+
puts "Error output: #{e.stderr}"
|
145
|
+
puts "Exit status: #{e.status.exitstatus}"
|
134
146
|
end
|
135
147
|
```
|
136
148
|
### Logging
|
@@ -139,7 +151,55 @@ For debugging purposes, you can use a logger. Cheetah will log the command, its
|
|
139
151
|
status, input and both outputs to it:
|
140
152
|
|
141
153
|
```ruby
|
142
|
-
Cheetah.run("ls -l", :
|
154
|
+
Cheetah.run("ls -l", logger: logger)
|
155
|
+
```
|
156
|
+
|
157
|
+
### Overwriting env
|
158
|
+
|
159
|
+
If the command needs adapted environment variables, use the :env option.
|
160
|
+
Passed hash is used to update existing env (for details see ENV.update).
|
161
|
+
Nil value means unset variable. Environment is restored to its original state after
|
162
|
+
running the command.
|
163
|
+
|
164
|
+
```ruby
|
165
|
+
Cheetah.run("env", env: { "LC_ALL" => "C" })
|
166
|
+
```
|
167
|
+
|
168
|
+
### Expecting Non-zero Exit Status
|
169
|
+
|
170
|
+
If command is expected to return valid a non-zero exit status like `grep` command
|
171
|
+
which return `1` if given regexp is not found, then option `:allowed_exitstatus`
|
172
|
+
can be used:
|
173
|
+
|
174
|
+
```ruby
|
175
|
+
# Run a command, handle exitstatus and handle errors
|
176
|
+
begin
|
177
|
+
exitstatus = Cheetah.run("grep", "userA", "/etc/passwd", allowed_exitstatus: 1)
|
178
|
+
if exitstates == 0
|
179
|
+
puts "found"
|
180
|
+
else
|
181
|
+
puts "not found"
|
182
|
+
end
|
183
|
+
rescue Cheetah::ExecutionFailed => e
|
184
|
+
puts e.message
|
185
|
+
puts "Standard output: #{e.stdout}"
|
186
|
+
puts "Error output: #{e.stderr}"
|
187
|
+
puts "Exit status: #{e.status.exitstatus}"
|
188
|
+
end
|
189
|
+
```
|
190
|
+
|
191
|
+
Exit status is returned as last element of result. If it is only captured thing,
|
192
|
+
then it is return without array.
|
193
|
+
Supported input for `allowed_exitstatus` are anything supporting include, fixnum
|
194
|
+
or nil for no allowed existatus.
|
195
|
+
|
196
|
+
```ruby
|
197
|
+
# allowed inputs
|
198
|
+
allowed_exitstatus: 1
|
199
|
+
allowed_exitstatus: 1..5
|
200
|
+
allowed_exitstatus: [1, 2]
|
201
|
+
allowed_exitstatus: object_with_include_method
|
202
|
+
allowed_exitstatus: nil
|
143
203
|
```
|
144
204
|
|
145
205
|
### Setting Defaults
|
@@ -149,13 +209,33 @@ To avoid repetition, you can set global default value of any option passed too
|
|
149
209
|
|
150
210
|
```ruby
|
151
211
|
# If you're tired of passing the :logger option all the time...
|
152
|
-
Cheetah.default_options = { :logger
|
212
|
+
Cheetah.default_options = { :logger => my_logger }
|
153
213
|
Cheetah.run("./configure")
|
154
214
|
Cheetah.run("make")
|
155
215
|
Cheetah.run("make", "install")
|
156
216
|
Cheetah.default_options = {}
|
157
217
|
```
|
158
218
|
|
219
|
+
### Changing Working Directory
|
220
|
+
|
221
|
+
If diferent working directory is needed for running program, then suggested
|
222
|
+
usage is to enclose call into `Dir.chdir` method.
|
223
|
+
|
224
|
+
```ruby
|
225
|
+
Dir.chdir("/workspace") do
|
226
|
+
Cheetah.run("make")
|
227
|
+
end
|
228
|
+
```
|
229
|
+
|
230
|
+
### Changing System Root
|
231
|
+
|
232
|
+
If a command needs to be executed in different system root then the `:chroot`
|
233
|
+
option can be used:
|
234
|
+
|
235
|
+
```ruby
|
236
|
+
Cheetah.run("/usr/bin/inspect", chroot: "/mnt/target_system")
|
237
|
+
```
|
238
|
+
|
159
239
|
### More Information
|
160
240
|
|
161
241
|
For more information, see the
|
@@ -164,7 +244,7 @@ For more information, see the
|
|
164
244
|
Compatibility
|
165
245
|
-------------
|
166
246
|
|
167
|
-
Cheetah should run well on any Unix system with Ruby
|
247
|
+
Cheetah should run well on any Unix system with Ruby 2.0.0, 2.1 and 2.2. Non-Unix
|
168
248
|
systems and different Ruby implementations/versions may work too but they were
|
169
249
|
not tested.
|
170
250
|
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.5.0
|
data/lib/cheetah.rb
CHANGED
@@ -22,11 +22,11 @@ require File.expand_path(File.dirname(__FILE__) + "/cheetah/version")
|
|
22
22
|
# * Handling of interactive commands
|
23
23
|
#
|
24
24
|
# @example Run a command and capture its output
|
25
|
-
# files = Cheetah.run("ls", "-la", :
|
25
|
+
# files = Cheetah.run("ls", "-la", stdout: :capture)
|
26
26
|
#
|
27
27
|
# @example Run a command and capture its output into a stream
|
28
28
|
# File.open("files.txt", "w") do |stdout|
|
29
|
-
# Cheetah.run("ls", "-la", :
|
29
|
+
# Cheetah.run("ls", "-la", stdout: stdout)
|
30
30
|
# end
|
31
31
|
#
|
32
32
|
# @example Run a command and handle errors
|
@@ -125,11 +125,15 @@ module Cheetah
|
|
125
125
|
# A recorder that does not record anyting. Used by {Cheetah.run} when no
|
126
126
|
# logger is passed.
|
127
127
|
class NullRecorder < Recorder
|
128
|
-
def record_commands(
|
129
|
-
|
130
|
-
def
|
131
|
-
|
132
|
-
def
|
128
|
+
def record_commands(_commands); end
|
129
|
+
|
130
|
+
def record_stdin(_stdin); end
|
131
|
+
|
132
|
+
def record_stdout(_stdout); end
|
133
|
+
|
134
|
+
def record_stderr(_stderr); end
|
135
|
+
|
136
|
+
def record_status(_status); end
|
133
137
|
end
|
134
138
|
|
135
139
|
# A default recorder. It uses the `Logger::INFO` level for normal messages and
|
@@ -138,16 +142,16 @@ module Cheetah
|
|
138
142
|
class DefaultRecorder < Recorder
|
139
143
|
# @private
|
140
144
|
STREAM_INFO = {
|
141
|
-
:
|
142
|
-
:
|
143
|
-
:
|
145
|
+
stdin: { name: "Standard input", method: :info },
|
146
|
+
stdout: { name: "Standard output", method: :info },
|
147
|
+
stderr: { name: "Error output", method: :error }
|
144
148
|
}
|
145
149
|
|
146
150
|
def initialize(logger)
|
147
151
|
@logger = logger
|
148
152
|
|
149
|
-
@stream_used = { :
|
150
|
-
@stream_buffer = { :
|
153
|
+
@stream_used = { stdin: false, stdout: false, stderr: false }
|
154
|
+
@stream_buffer = { stdin: "", stdout: "", stderr: "" }
|
151
155
|
end
|
152
156
|
|
153
157
|
def record_commands(commands)
|
@@ -172,7 +176,7 @@ module Cheetah
|
|
172
176
|
log_stream_remainder(:stderr)
|
173
177
|
|
174
178
|
@logger.send status.success? ? :info : :error,
|
175
|
-
|
179
|
+
"Status: #{status.exitstatus}"
|
176
180
|
end
|
177
181
|
|
178
182
|
protected
|
@@ -183,7 +187,8 @@ module Cheetah
|
|
183
187
|
|
184
188
|
def log_stream_increment(stream, data)
|
185
189
|
@stream_buffer[stream] + data =~ /\A((?:.*\n)*)(.*)\z/
|
186
|
-
lines
|
190
|
+
lines = Regexp.last_match(1)
|
191
|
+
rest = Regexp.last_match(2)
|
187
192
|
|
188
193
|
lines.each_line { |l| log_stream_line(stream, l) }
|
189
194
|
|
@@ -192,9 +197,9 @@ module Cheetah
|
|
192
197
|
end
|
193
198
|
|
194
199
|
def log_stream_remainder(stream)
|
195
|
-
if
|
196
|
-
|
197
|
-
|
200
|
+
return if !@stream_used[stream] || @stream_buffer[stream].empty?
|
201
|
+
|
202
|
+
log_stream_line(stream, @stream_buffer[stream])
|
198
203
|
end
|
199
204
|
|
200
205
|
def log_stream_line(stream, line)
|
@@ -207,10 +212,12 @@ module Cheetah
|
|
207
212
|
|
208
213
|
# @private
|
209
214
|
BUILTIN_DEFAULT_OPTIONS = {
|
210
|
-
:
|
211
|
-
:
|
212
|
-
:
|
213
|
-
:
|
215
|
+
stdin: "",
|
216
|
+
stdout: nil,
|
217
|
+
stderr: nil,
|
218
|
+
logger: nil,
|
219
|
+
env: {},
|
220
|
+
chroot: "/"
|
214
221
|
}
|
215
222
|
|
216
223
|
READ = 0 # @private
|
@@ -225,7 +232,7 @@ module Cheetah
|
|
225
232
|
# By default, no values are specified here.
|
226
233
|
#
|
227
234
|
# @example Setting a logger once for execution of multiple commands
|
228
|
-
# Cheetah.default_options = { :
|
235
|
+
# Cheetah.default_options = { logger: my_logger }
|
229
236
|
# Cheetah.run("./configure")
|
230
237
|
# Cheetah.run("make")
|
231
238
|
# Cheetah.run("make", "install")
|
@@ -244,7 +251,7 @@ module Cheetah
|
|
244
251
|
# multiple command case, the execution succeeds if the last command can be
|
245
252
|
# executed and returns a zero exit status.)
|
246
253
|
#
|
247
|
-
# Commands and their arguments never undergo shell expansion
|
254
|
+
# Commands and their arguments never undergo shell expansion - they are
|
248
255
|
# passed directly to the operating system. While this may create some
|
249
256
|
# inconvenience in certain cases, it eliminates a whole class of security
|
250
257
|
# bugs.
|
@@ -296,6 +303,14 @@ module Cheetah
|
|
296
303
|
# execution
|
297
304
|
# @option options [Recorder, nil] :recorder (DefaultRecorder.new) recorder
|
298
305
|
# to handle the command execution logging
|
306
|
+
# @option options [Fixnum, .include?, nil] :allowed_exitstatus (nil)
|
307
|
+
# Allows to specify allowed exit codes that do not cause exception. It
|
308
|
+
# adds as last element of result exitstatus.
|
309
|
+
# @option options [Hash] :env ({})
|
310
|
+
# Allows to update ENV for the time of running the command. if key maps to nil value it
|
311
|
+
# is deleted from ENV.
|
312
|
+
# @option options [String] :chroot ("/")
|
313
|
+
# Allows to run on different system root.
|
299
314
|
#
|
300
315
|
# @example
|
301
316
|
# Cheetah.run("tar", "xzf", "foo.tar.gz")
|
@@ -325,16 +340,16 @@ module Cheetah
|
|
325
340
|
# in the first variant
|
326
341
|
#
|
327
342
|
# @example
|
328
|
-
# processes = Cheetah.run(["ps", "aux"], ["grep", "ruby"], :
|
343
|
+
# processes = Cheetah.run(["ps", "aux"], ["grep", "ruby"], stdout: :capture)
|
329
344
|
#
|
330
345
|
# @raise [ExecutionFailed] when the execution fails
|
331
346
|
#
|
332
347
|
# @example Run a command and capture its output
|
333
|
-
# files = Cheetah.run("ls", "-la", :
|
348
|
+
# files = Cheetah.run("ls", "-la", stdout: :capture)
|
334
349
|
#
|
335
350
|
# @example Run a command and capture its output into a stream
|
336
351
|
# File.open("files.txt", "w") do |stdout|
|
337
|
-
# Cheetah.run("ls", "-la", :
|
352
|
+
# Cheetah.run("ls", "-la", stdout: stdout)
|
338
353
|
# end
|
339
354
|
#
|
340
355
|
# @example Run a command and handle errors
|
@@ -345,10 +360,35 @@ module Cheetah
|
|
345
360
|
# puts "Standard output: #{e.stdout}"
|
346
361
|
# puts "Error ouptut: #{e.stderr}"
|
347
362
|
# end
|
363
|
+
#
|
364
|
+
# @example Run a command with expected false and handle errors
|
365
|
+
# begin
|
366
|
+
# # exit code 1 for grep mean not found
|
367
|
+
# result = Cheetah.run("grep", "userA", "/etc/passwd", allowed_exitstatus: 1)
|
368
|
+
# if result == 0
|
369
|
+
# puts "found"
|
370
|
+
# else
|
371
|
+
# puts "not found"
|
372
|
+
# end
|
373
|
+
# rescue Cheetah::ExecutionFailed => e
|
374
|
+
# puts e.message
|
375
|
+
# puts "Standard output: #{e.stdout}"
|
376
|
+
# puts "Error ouptut: #{e.stderr}"
|
377
|
+
# end
|
378
|
+
#
|
379
|
+
# @example more complex example with allowed_exitstatus
|
380
|
+
# stdout, exitcode = Cheetah.run("cmd", stdout: :capture, allowed_exitstatus: 1..5)
|
381
|
+
#
|
382
|
+
|
348
383
|
def run(*args)
|
349
384
|
options = args.last.is_a?(Hash) ? args.pop : {}
|
350
385
|
options = BUILTIN_DEFAULT_OPTIONS.merge(@default_options).merge(options)
|
351
386
|
|
387
|
+
options[:stdin] ||= "" # allow passing nil stdin see issue gh#11
|
388
|
+
if !options[:allowed_exitstatus].respond_to?(:include?)
|
389
|
+
options[:allowed_exitstatus] = Array(options[:allowed_exitstatus])
|
390
|
+
end
|
391
|
+
|
352
392
|
streamed = compute_streamed(options)
|
353
393
|
streams = build_streams(options, streamed)
|
354
394
|
commands = build_commands(args)
|
@@ -356,39 +396,47 @@ module Cheetah
|
|
356
396
|
|
357
397
|
recorder.record_commands(commands)
|
358
398
|
|
359
|
-
pid, pipes = fork_commands(commands)
|
399
|
+
pid, pipes = fork_commands(commands, options)
|
360
400
|
select_loop(streams, pipes, recorder)
|
361
|
-
|
401
|
+
_pid, status = Process.wait2(pid)
|
362
402
|
|
363
403
|
begin
|
364
|
-
check_errors(commands, status, streams, streamed)
|
404
|
+
check_errors(commands, status, streams, streamed, options)
|
365
405
|
ensure
|
366
406
|
recorder.record_status(status)
|
367
407
|
end
|
368
408
|
|
369
|
-
build_result(streams, options)
|
409
|
+
build_result(streams, status, options)
|
370
410
|
end
|
371
411
|
|
372
412
|
private
|
373
413
|
|
374
414
|
# Parts of Cheetah.run
|
375
415
|
|
416
|
+
def with_env(env, &block)
|
417
|
+
old_env = ENV.to_hash
|
418
|
+
ENV.update(env)
|
419
|
+
block.call
|
420
|
+
ensure
|
421
|
+
ENV.replace(old_env)
|
422
|
+
end
|
423
|
+
|
376
424
|
def compute_streamed(options)
|
377
425
|
# The assumption for :stdout and :stderr is that anything except :capture
|
378
426
|
# and nil is an IO-like object. We avoid detecting it directly to allow
|
379
427
|
# passing StringIO, mocks, etc.
|
380
428
|
{
|
381
|
-
:
|
382
|
-
:
|
383
|
-
:
|
429
|
+
stdin: !options[:stdin].is_a?(String),
|
430
|
+
stdout: ![nil, :capture].include?(options[:stdout]),
|
431
|
+
stderr: ![nil, :capture].include?(options[:stderr])
|
384
432
|
}
|
385
433
|
end
|
386
434
|
|
387
435
|
def build_streams(options, streamed)
|
388
436
|
{
|
389
|
-
:
|
390
|
-
:
|
391
|
-
:
|
437
|
+
stdin: streamed[:stdin] ? options[:stdin] : StringIO.new(options[:stdin]),
|
438
|
+
stdout: streamed[:stdout] ? options[:stdout] : StringIO.new(""),
|
439
|
+
stderr: streamed[:stderr] ? options[:stderr] : StringIO.new("")
|
392
440
|
}
|
393
441
|
end
|
394
442
|
|
@@ -410,7 +458,8 @@ module Cheetah
|
|
410
458
|
# The following code ensures that the result consistently (in all three
|
411
459
|
# cases) contains an array of arrays specifying commands and their
|
412
460
|
# arguments.
|
413
|
-
args.all? { |a| a.is_a?(Array) } ? args : [args]
|
461
|
+
commands = args.all? { |a| a.is_a?(Array) } ? args : [args]
|
462
|
+
commands.map { |c| c.map(&:to_s) }
|
414
463
|
end
|
415
464
|
|
416
465
|
def build_recorder(options)
|
@@ -421,54 +470,90 @@ module Cheetah
|
|
421
470
|
end
|
422
471
|
end
|
423
472
|
|
424
|
-
|
473
|
+
# Reopen *stream* to write **into** the writing half of *pipe*
|
474
|
+
# and close the reading half of *pipe*.
|
475
|
+
# @param pipe [Array<IO>] a pair of IOs as returned from IO.pipe
|
476
|
+
# @param stream [IO]
|
477
|
+
def into_pipe(stream, pipe)
|
478
|
+
stream.reopen(pipe[WRITE])
|
479
|
+
pipe[WRITE].close
|
480
|
+
pipe[READ].close
|
481
|
+
end
|
482
|
+
|
483
|
+
# Reopen *stream* to read **from** the reading half of *pipe*
|
484
|
+
# and close the writing half of *pipe*.
|
485
|
+
# @param pipe [Array<IO>] a pair of IOs as returned from IO.pipe
|
486
|
+
# @param stream [IO]
|
487
|
+
def from_pipe(stream, pipe)
|
488
|
+
stream.reopen(pipe[READ])
|
489
|
+
pipe[READ].close
|
490
|
+
pipe[WRITE].close
|
491
|
+
end
|
492
|
+
|
493
|
+
def chroot_step(options)
|
494
|
+
return options if [nil, "/"].include?(options[:chroot])
|
495
|
+
|
496
|
+
options = options.dup
|
497
|
+
# delete chroot option otherwise in pipe will chroot each fork recursivelly
|
498
|
+
root = options.delete(:chroot)
|
499
|
+
Dir.chroot(root)
|
500
|
+
# curdir can be outside chroot which is considered as security problem
|
501
|
+
Dir.chdir("/")
|
502
|
+
|
503
|
+
options
|
504
|
+
end
|
505
|
+
|
506
|
+
def fork_commands_recursive(commands, pipes, options)
|
425
507
|
fork do
|
426
508
|
begin
|
509
|
+
# support chrooting
|
510
|
+
options = chroot_step(options)
|
511
|
+
|
427
512
|
if commands.size == 1
|
428
|
-
pipes[:stdin]
|
429
|
-
STDIN.reopen(pipes[:stdin][READ])
|
430
|
-
pipes[:stdin][READ].close
|
513
|
+
from_pipe(STDIN, pipes[:stdin])
|
431
514
|
else
|
432
515
|
pipe_to_child = IO.pipe
|
433
516
|
|
434
|
-
fork_commands_recursive(commands[0..-2],
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
517
|
+
fork_commands_recursive(commands[0..-2],
|
518
|
+
{
|
519
|
+
stdin: pipes[:stdin],
|
520
|
+
stdout: pipe_to_child,
|
521
|
+
stderr: pipes[:stderr]
|
522
|
+
},
|
523
|
+
options
|
524
|
+
)
|
439
525
|
|
440
526
|
pipes[:stdin][READ].close
|
441
527
|
pipes[:stdin][WRITE].close
|
442
528
|
|
443
|
-
pipe_to_child
|
444
|
-
STDIN.reopen(pipe_to_child[READ])
|
445
|
-
pipe_to_child[READ].close
|
529
|
+
from_pipe(STDIN, pipe_to_child)
|
446
530
|
end
|
447
531
|
|
448
|
-
pipes[:stdout]
|
449
|
-
|
450
|
-
pipes[:stdout][WRITE].close
|
451
|
-
|
452
|
-
pipes[:stderr][READ].close
|
453
|
-
STDERR.reopen(pipes[:stderr][WRITE])
|
454
|
-
pipes[:stderr][WRITE].close
|
532
|
+
into_pipe(STDOUT, pipes[:stdout])
|
533
|
+
into_pipe(STDERR, pipes[:stderr])
|
455
534
|
|
456
535
|
# All file descriptors from 3 above should be closed here, but since I
|
457
536
|
# don't know about any way how to detect the maximum file descriptor
|
458
537
|
# number portably in Ruby, I didn't implement it. Patches welcome.
|
459
538
|
|
460
539
|
command, *args = commands.last
|
461
|
-
|
540
|
+
with_env(options[:env]) do
|
541
|
+
exec([command, command], *args)
|
542
|
+
end
|
462
543
|
rescue SystemCallError => e
|
544
|
+
# depends when failed, if pipe is already redirected or not, so lets find it
|
545
|
+
output = pipes[:stderr][WRITE].closed? ? STDERR : pipes[:stderr][WRITE]
|
546
|
+
output.puts e.message
|
547
|
+
|
463
548
|
exit!(127)
|
464
549
|
end
|
465
550
|
end
|
466
551
|
end
|
467
552
|
|
468
|
-
def fork_commands(commands)
|
469
|
-
pipes = { :
|
553
|
+
def fork_commands(commands, options)
|
554
|
+
pipes = { stdin: IO.pipe, stdout: IO.pipe, stderr: IO.pipe }
|
470
555
|
|
471
|
-
pid = fork_commands_recursive(commands, pipes)
|
556
|
+
pid = fork_commands_recursive(commands, pipes, options)
|
472
557
|
|
473
558
|
[
|
474
559
|
pipes[:stdin][READ],
|
@@ -508,7 +593,7 @@ module Cheetah
|
|
508
593
|
break if pipes_readable.empty? && pipes_writable.empty?
|
509
594
|
|
510
595
|
ios_read, ios_write, ios_error = select(pipes_readable, pipes_writable,
|
511
|
-
|
596
|
+
pipes_readable + pipes_writable)
|
512
597
|
|
513
598
|
if !ios_error.empty?
|
514
599
|
raise IOError, "Error when communicating with executed program."
|
@@ -540,39 +625,52 @@ module Cheetah
|
|
540
625
|
end
|
541
626
|
end
|
542
627
|
|
543
|
-
def check_errors(commands, status, streams, streamed)
|
628
|
+
def check_errors(commands, status, streams, streamed, options)
|
544
629
|
return if status.success?
|
630
|
+
return if options[:allowed_exitstatus].include?(status.exitstatus)
|
545
631
|
|
546
632
|
stderr_part = if streamed[:stderr]
|
547
|
-
|
548
|
-
|
549
|
-
|
550
|
-
|
551
|
-
|
552
|
-
|
553
|
-
|
633
|
+
" (error output streamed away)"
|
634
|
+
elsif streams[:stderr].string.empty?
|
635
|
+
" (no error output)"
|
636
|
+
else
|
637
|
+
lines = streams[:stderr].string.split("\n")
|
638
|
+
": " + lines.first + (lines.size > 1 ? " (...)" : "")
|
639
|
+
end
|
554
640
|
|
555
641
|
raise ExecutionFailed.new(
|
556
642
|
commands,
|
557
643
|
status,
|
558
644
|
streamed[:stdout] ? nil : streams[:stdout].string,
|
559
645
|
streamed[:stderr] ? nil : streams[:stderr].string,
|
560
|
-
"Execution of #{format_commands(commands)} "
|
646
|
+
"Execution of #{format_commands(commands)} " \
|
561
647
|
"failed with status #{status.exitstatus}#{stderr_part}."
|
562
648
|
)
|
563
649
|
end
|
564
650
|
|
565
|
-
def build_result(streams, options)
|
566
|
-
case [options[:stdout] == :capture, options[:stderr] == :capture]
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
571
|
-
|
572
|
-
|
573
|
-
|
574
|
-
|
651
|
+
def build_result(streams, status, options)
|
652
|
+
res = case [options[:stdout] == :capture, options[:stderr] == :capture]
|
653
|
+
when [false, false]
|
654
|
+
nil
|
655
|
+
when [true, false]
|
656
|
+
streams[:stdout].string
|
657
|
+
when [false, true]
|
658
|
+
streams[:stderr].string
|
659
|
+
when [true, true]
|
660
|
+
[streams[:stdout].string, streams[:stderr].string]
|
661
|
+
end
|
662
|
+
|
663
|
+
# do not capture only for empty array or nil converted to empty array
|
664
|
+
if !options[:allowed_exitstatus].is_a?(Array) || !options[:allowed_exitstatus].empty?
|
665
|
+
if res.nil?
|
666
|
+
res = status.exitstatus
|
667
|
+
else
|
668
|
+
res = Array(res)
|
669
|
+
res << status.exitstatus
|
670
|
+
end
|
575
671
|
end
|
672
|
+
|
673
|
+
res
|
576
674
|
end
|
577
675
|
|
578
676
|
def format_commands(commands)
|
@@ -582,4 +680,3 @@ module Cheetah
|
|
582
680
|
|
583
681
|
self.default_options = {}
|
584
682
|
end
|
585
|
-
|
data/lib/cheetah/version.rb
CHANGED
metadata
CHANGED
@@ -1,78 +1,55 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: cheetah
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
5
|
-
prerelease:
|
4
|
+
version: 0.5.0
|
6
5
|
platform: ruby
|
7
6
|
authors:
|
8
7
|
- David Majda
|
9
8
|
autorequire:
|
10
9
|
bindir: bin
|
11
10
|
cert_chain: []
|
12
|
-
date:
|
11
|
+
date: 2015-12-18 00:00:00.000000000 Z
|
13
12
|
dependencies:
|
14
13
|
- !ruby/object:Gem::Dependency
|
15
14
|
name: abstract_method
|
16
15
|
requirement: !ruby/object:Gem::Requirement
|
17
|
-
none: false
|
18
16
|
requirements:
|
19
|
-
- - ~>
|
17
|
+
- - "~>"
|
20
18
|
- !ruby/object:Gem::Version
|
21
19
|
version: '1.2'
|
22
20
|
type: :runtime
|
23
21
|
prerelease: false
|
24
22
|
version_requirements: !ruby/object:Gem::Requirement
|
25
|
-
none: false
|
26
23
|
requirements:
|
27
|
-
- - ~>
|
24
|
+
- - "~>"
|
28
25
|
- !ruby/object:Gem::Version
|
29
26
|
version: '1.2'
|
30
27
|
- !ruby/object:Gem::Dependency
|
31
28
|
name: rspec
|
32
29
|
requirement: !ruby/object:Gem::Requirement
|
33
|
-
none: false
|
34
30
|
requirements:
|
35
|
-
- -
|
31
|
+
- - "~>"
|
36
32
|
- !ruby/object:Gem::Version
|
37
|
-
version: '
|
33
|
+
version: '3.3'
|
38
34
|
type: :development
|
39
35
|
prerelease: false
|
40
36
|
version_requirements: !ruby/object:Gem::Requirement
|
41
|
-
none: false
|
42
37
|
requirements:
|
43
|
-
- -
|
38
|
+
- - "~>"
|
44
39
|
- !ruby/object:Gem::Version
|
45
|
-
version: '
|
46
|
-
- !ruby/object:Gem::Dependency
|
47
|
-
name: redcarpet
|
48
|
-
requirement: !ruby/object:Gem::Requirement
|
49
|
-
none: false
|
50
|
-
requirements:
|
51
|
-
- - ! '>='
|
52
|
-
- !ruby/object:Gem::Version
|
53
|
-
version: '0'
|
54
|
-
type: :development
|
55
|
-
prerelease: false
|
56
|
-
version_requirements: !ruby/object:Gem::Requirement
|
57
|
-
none: false
|
58
|
-
requirements:
|
59
|
-
- - ! '>='
|
60
|
-
- !ruby/object:Gem::Version
|
61
|
-
version: '0'
|
40
|
+
version: '3.3'
|
62
41
|
- !ruby/object:Gem::Dependency
|
63
42
|
name: yard
|
64
43
|
requirement: !ruby/object:Gem::Requirement
|
65
|
-
none: false
|
66
44
|
requirements:
|
67
|
-
- -
|
45
|
+
- - "~>"
|
68
46
|
- !ruby/object:Gem::Version
|
69
47
|
version: '0'
|
70
48
|
type: :development
|
71
49
|
prerelease: false
|
72
50
|
version_requirements: !ruby/object:Gem::Requirement
|
73
|
-
none: false
|
74
51
|
requirements:
|
75
|
-
- -
|
52
|
+
- - "~>"
|
76
53
|
- !ruby/object:Gem::Version
|
77
54
|
version: '0'
|
78
55
|
description: Your swiss army knife for executing external commands in Ruby safely
|
@@ -91,27 +68,26 @@ files:
|
|
91
68
|
homepage: https://github.com/openSUSE/cheetah
|
92
69
|
licenses:
|
93
70
|
- MIT
|
71
|
+
metadata: {}
|
94
72
|
post_install_message:
|
95
73
|
rdoc_options: []
|
96
74
|
require_paths:
|
97
75
|
- lib
|
98
76
|
required_ruby_version: !ruby/object:Gem::Requirement
|
99
|
-
none: false
|
100
77
|
requirements:
|
101
|
-
- -
|
78
|
+
- - ">="
|
102
79
|
- !ruby/object:Gem::Version
|
103
80
|
version: '0'
|
104
81
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
105
|
-
none: false
|
106
82
|
requirements:
|
107
|
-
- -
|
83
|
+
- - ">="
|
108
84
|
- !ruby/object:Gem::Version
|
109
85
|
version: '0'
|
110
86
|
requirements: []
|
111
87
|
rubyforge_project:
|
112
|
-
rubygems_version:
|
88
|
+
rubygems_version: 2.4.5.1
|
113
89
|
signing_key:
|
114
|
-
specification_version:
|
90
|
+
specification_version: 4
|
115
91
|
summary: Your swiss army knife for executing external commands in Ruby safely and
|
116
92
|
conveniently.
|
117
93
|
test_files: []
|