cheetah 0.4.0 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Travis Build](https://travis-ci.org/openSUSE/cheetah.svg?branch=master)](https://travis-ci.org/openSUSE/cheetah)
|
4
|
+
[![Code Climate](https://codeclimate.com/github/openSUSE/cheetah/badges/gpa.svg)](https://codeclimate.com/github/openSUSE/cheetah)
|
5
|
+
[![Coverage Status](https://img.shields.io/coveralls/openSUSE/cheetah.svg)](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: []
|