cheetah 0.4.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG +37 -2
- data/README.md +94 -14
- data/VERSION +1 -1
- data/lib/cheetah/version.rb +4 -1
- data/lib/cheetah.rb +242 -107
- metadata +18 -43
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: bd4cbe464be1d5ee56434c1a380d958cf0cbcb40095d8f21fcfb3cf7b2320db2
|
4
|
+
data.tar.gz: 3572805d086b24911c1feb6169b5911723a918fcf1ca0547d2f28984e0827ade
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 032f86dbf253379e1663e01d03f8a5a013a13829e4bb5b9c3f880d73611a2c92176e51086402b604cfd20a30d28dfa1571766b9fa2e4eb5377e4c12a6bda1b39
|
7
|
+
data.tar.gz: 1ecf8c2ff66d2eef73b658f925bbb5a7c6292861ce4fd52a9f2ed6fab91e49f3ba25fe5cd84f8da294b6340887872c9ef5100f2799fa8f8c319afbcead8ced3e
|
data/CHANGELOG
CHANGED
@@ -1,11 +1,46 @@
|
|
1
|
+
1.0.0 (2021-11-30)
|
2
|
+
------------------
|
3
|
+
|
4
|
+
* Add support for ruby 3.0
|
5
|
+
As side effect now Recorder#record_status receive additional parameter
|
6
|
+
|
7
|
+
0.5.2 (2020-01-06)
|
8
|
+
------------------
|
9
|
+
|
10
|
+
* If listed in allowed_exitstatus, log exit code as Info, not as Error
|
11
|
+
(bsc#1153749)
|
12
|
+
* Added support for ruby 2.7
|
13
|
+
|
14
|
+
0.5.1 (2019-10-16)
|
15
|
+
------------------
|
16
|
+
|
17
|
+
* Implement closing open fds after call to fork (bsc#1151960). This will work
|
18
|
+
only in linux system with mounted /proc. For other Unixes it works as before.
|
19
|
+
* drop support for ruby that is EOL (2.3 and lower)
|
20
|
+
* Added support for ruby 2.4, 2.5, 2.6
|
21
|
+
|
22
|
+
0.5.0 (2015-12-18)
|
23
|
+
------------------
|
24
|
+
|
25
|
+
* Added chroot option for executing in different system root.
|
26
|
+
* Added ENV overwrite option.
|
27
|
+
* Allowed to specify known exit codes that are not errors.
|
28
|
+
* Documented how to execute in different working directory.
|
29
|
+
* Allowed passing nil as :stdin to be same as :stdout and :strerr.
|
30
|
+
* Converted parameters for command to strings with `.to_s`.
|
31
|
+
* Adapted testsuite to new rspec.
|
32
|
+
* Updated documentation with various fixes.
|
33
|
+
* Dropped support for Ruby 1.9.3.
|
34
|
+
* Added support for Ruby 2.1 and 2.2.
|
35
|
+
|
1
36
|
0.4.0 (2013-11-21)
|
2
37
|
------------------
|
3
38
|
|
4
39
|
* Implemented incremental logging. The input and both outputs of the executed
|
5
40
|
command are now logged one-by-line by the default recorder. A custom recorder
|
6
41
|
can record them on even finer granularity.
|
7
|
-
* Dropped support
|
8
|
-
* Added support
|
42
|
+
* Dropped support for Ruby 1.8.7.
|
43
|
+
* Added support for Ruby 2.0.0.
|
9
44
|
* Internal code improvements.
|
10
45
|
|
11
46
|
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
|
+
1.0.0
|
data/lib/cheetah/version.rb
CHANGED
@@ -1,4 +1,7 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Cheetah namespace
|
1
4
|
module Cheetah
|
2
5
|
# Cheetah version (uses [semantic versioning](http://semver.org/)).
|
3
|
-
VERSION = File.read(File.dirname(__FILE__)
|
6
|
+
VERSION = File.read("#{File.dirname(__FILE__)}/../../VERSION").strip
|
4
7
|
end
|
data/lib/cheetah.rb
CHANGED
@@ -1,9 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "abstract_method"
|
2
4
|
require "logger"
|
3
5
|
require "shellwords"
|
4
6
|
require "stringio"
|
5
7
|
|
6
|
-
require File.expand_path(File.dirname(__FILE__)
|
8
|
+
require File.expand_path("#{File.dirname(__FILE__)}/cheetah/version")
|
7
9
|
|
8
10
|
# Your swiss army knife for executing external commands in Ruby safely and
|
9
11
|
# conveniently.
|
@@ -22,11 +24,11 @@ require File.expand_path(File.dirname(__FILE__) + "/cheetah/version")
|
|
22
24
|
# * Handling of interactive commands
|
23
25
|
#
|
24
26
|
# @example Run a command and capture its output
|
25
|
-
# files = Cheetah.run("ls", "-la", :
|
27
|
+
# files = Cheetah.run("ls", "-la", stdout: :capture)
|
26
28
|
#
|
27
29
|
# @example Run a command and capture its output into a stream
|
28
30
|
# File.open("files.txt", "w") do |stdout|
|
29
|
-
# Cheetah.run("ls", "-la", :
|
31
|
+
# Cheetah.run("ls", "-la", stdout: stdout)
|
30
32
|
# end
|
31
33
|
#
|
32
34
|
# @example Run a command and handle errors
|
@@ -119,17 +121,22 @@ module Cheetah
|
|
119
121
|
#
|
120
122
|
# @abstract
|
121
123
|
# @param [Process::Status] status the executed command exit status
|
124
|
+
# @param [Boolean] allowed_status whether the exit code is in the list of allowed exit codes
|
122
125
|
abstract_method :record_status
|
123
126
|
end
|
124
127
|
|
125
128
|
# A recorder that does not record anyting. Used by {Cheetah.run} when no
|
126
129
|
# logger is passed.
|
127
130
|
class NullRecorder < Recorder
|
128
|
-
def record_commands(
|
129
|
-
|
130
|
-
def
|
131
|
-
|
132
|
-
def
|
131
|
+
def record_commands(_commands); end
|
132
|
+
|
133
|
+
def record_stdin(_stdin); end
|
134
|
+
|
135
|
+
def record_stdout(_stdout); end
|
136
|
+
|
137
|
+
def record_stderr(_stderr); end
|
138
|
+
|
139
|
+
def record_status(_status, _allowed_status); end
|
133
140
|
end
|
134
141
|
|
135
142
|
# A default recorder. It uses the `Logger::INFO` level for normal messages and
|
@@ -138,16 +145,18 @@ module Cheetah
|
|
138
145
|
class DefaultRecorder < Recorder
|
139
146
|
# @private
|
140
147
|
STREAM_INFO = {
|
141
|
-
:
|
142
|
-
:
|
143
|
-
:
|
144
|
-
}
|
148
|
+
stdin: { name: "Standard input", method: :info },
|
149
|
+
stdout: { name: "Standard output", method: :info },
|
150
|
+
stderr: { name: "Error output", method: :error }
|
151
|
+
}.freeze
|
145
152
|
|
146
153
|
def initialize(logger)
|
154
|
+
super()
|
155
|
+
|
147
156
|
@logger = logger
|
148
157
|
|
149
|
-
@stream_used = { :
|
150
|
-
@stream_buffer = { :
|
158
|
+
@stream_used = { stdin: false, stdout: false, stderr: false }
|
159
|
+
@stream_buffer = { stdin: +"", stdout: +"", stderr: +"" }
|
151
160
|
end
|
152
161
|
|
153
162
|
def record_commands(commands)
|
@@ -166,24 +175,25 @@ module Cheetah
|
|
166
175
|
log_stream_increment(:stderr, stderr)
|
167
176
|
end
|
168
177
|
|
169
|
-
def record_status(status)
|
178
|
+
def record_status(status, allowed_status)
|
170
179
|
log_stream_remainder(:stdin)
|
171
180
|
log_stream_remainder(:stdout)
|
172
181
|
log_stream_remainder(:stderr)
|
173
182
|
|
174
|
-
@logger.send
|
175
|
-
|
183
|
+
@logger.send allowed_status ? :info : :error,
|
184
|
+
"Status: #{status.exitstatus}"
|
176
185
|
end
|
177
186
|
|
178
187
|
protected
|
179
188
|
|
180
189
|
def format_commands(commands)
|
181
|
-
|
190
|
+
"\"#{commands.map { |c| Shellwords.join(c) }.join(' | ')}\""
|
182
191
|
end
|
183
192
|
|
184
193
|
def log_stream_increment(stream, data)
|
185
194
|
@stream_buffer[stream] + data =~ /\A((?:.*\n)*)(.*)\z/
|
186
|
-
lines
|
195
|
+
lines = Regexp.last_match(1)
|
196
|
+
rest = Regexp.last_match(2)
|
187
197
|
|
188
198
|
lines.each_line { |l| log_stream_line(stream, l) }
|
189
199
|
|
@@ -192,9 +202,9 @@ module Cheetah
|
|
192
202
|
end
|
193
203
|
|
194
204
|
def log_stream_remainder(stream)
|
195
|
-
if
|
196
|
-
|
197
|
-
|
205
|
+
return if !@stream_used[stream] || @stream_buffer[stream].empty?
|
206
|
+
|
207
|
+
log_stream_line(stream, @stream_buffer[stream])
|
198
208
|
end
|
199
209
|
|
200
210
|
def log_stream_line(stream, line)
|
@@ -207,11 +217,13 @@ module Cheetah
|
|
207
217
|
|
208
218
|
# @private
|
209
219
|
BUILTIN_DEFAULT_OPTIONS = {
|
210
|
-
:
|
211
|
-
:
|
212
|
-
:
|
213
|
-
:
|
214
|
-
|
220
|
+
stdin: "",
|
221
|
+
stdout: nil,
|
222
|
+
stderr: nil,
|
223
|
+
logger: nil,
|
224
|
+
env: {},
|
225
|
+
chroot: "/"
|
226
|
+
}.freeze
|
215
227
|
|
216
228
|
READ = 0 # @private
|
217
229
|
WRITE = 1 # @private
|
@@ -225,7 +237,7 @@ module Cheetah
|
|
225
237
|
# By default, no values are specified here.
|
226
238
|
#
|
227
239
|
# @example Setting a logger once for execution of multiple commands
|
228
|
-
# Cheetah.default_options = { :
|
240
|
+
# Cheetah.default_options = { logger: my_logger }
|
229
241
|
# Cheetah.run("./configure")
|
230
242
|
# Cheetah.run("make")
|
231
243
|
# Cheetah.run("make", "install")
|
@@ -244,7 +256,7 @@ module Cheetah
|
|
244
256
|
# multiple command case, the execution succeeds if the last command can be
|
245
257
|
# executed and returns a zero exit status.)
|
246
258
|
#
|
247
|
-
# Commands and their arguments never undergo shell expansion
|
259
|
+
# Commands and their arguments never undergo shell expansion - they are
|
248
260
|
# passed directly to the operating system. While this may create some
|
249
261
|
# inconvenience in certain cases, it eliminates a whole class of security
|
250
262
|
# bugs.
|
@@ -296,6 +308,14 @@ module Cheetah
|
|
296
308
|
# execution
|
297
309
|
# @option options [Recorder, nil] :recorder (DefaultRecorder.new) recorder
|
298
310
|
# to handle the command execution logging
|
311
|
+
# @option options [Fixnum, .include?, nil] :allowed_exitstatus (nil)
|
312
|
+
# Allows to specify allowed exit codes that do not cause exception. It
|
313
|
+
# adds as last element of result exitstatus.
|
314
|
+
# @option options [Hash] :env ({})
|
315
|
+
# Allows to update ENV for the time of running the command. if key maps to nil value it
|
316
|
+
# is deleted from ENV.
|
317
|
+
# @option options [String] :chroot ("/")
|
318
|
+
# Allows to run on different system root.
|
299
319
|
#
|
300
320
|
# @example
|
301
321
|
# Cheetah.run("tar", "xzf", "foo.tar.gz")
|
@@ -325,16 +345,16 @@ module Cheetah
|
|
325
345
|
# in the first variant
|
326
346
|
#
|
327
347
|
# @example
|
328
|
-
# processes = Cheetah.run(["ps", "aux"], ["grep", "ruby"], :
|
348
|
+
# processes = Cheetah.run(["ps", "aux"], ["grep", "ruby"], stdout: :capture)
|
329
349
|
#
|
330
350
|
# @raise [ExecutionFailed] when the execution fails
|
331
351
|
#
|
332
352
|
# @example Run a command and capture its output
|
333
|
-
# files = Cheetah.run("ls", "-la", :
|
353
|
+
# files = Cheetah.run("ls", "-la", stdout: :capture)
|
334
354
|
#
|
335
355
|
# @example Run a command and capture its output into a stream
|
336
356
|
# File.open("files.txt", "w") do |stdout|
|
337
|
-
# Cheetah.run("ls", "-la", :
|
357
|
+
# Cheetah.run("ls", "-la", stdout: stdout)
|
338
358
|
# end
|
339
359
|
#
|
340
360
|
# @example Run a command and handle errors
|
@@ -345,10 +365,35 @@ module Cheetah
|
|
345
365
|
# puts "Standard output: #{e.stdout}"
|
346
366
|
# puts "Error ouptut: #{e.stderr}"
|
347
367
|
# end
|
368
|
+
#
|
369
|
+
# @example Run a command with expected false and handle errors
|
370
|
+
# begin
|
371
|
+
# # exit code 1 for grep mean not found
|
372
|
+
# result = Cheetah.run("grep", "userA", "/etc/passwd", allowed_exitstatus: 1)
|
373
|
+
# if result == 0
|
374
|
+
# puts "found"
|
375
|
+
# else
|
376
|
+
# puts "not found"
|
377
|
+
# end
|
378
|
+
# rescue Cheetah::ExecutionFailed => e
|
379
|
+
# puts e.message
|
380
|
+
# puts "Standard output: #{e.stdout}"
|
381
|
+
# puts "Error ouptut: #{e.stderr}"
|
382
|
+
# end
|
383
|
+
#
|
384
|
+
# @example more complex example with allowed_exitstatus
|
385
|
+
# stdout, exitcode = Cheetah.run("cmd", stdout: :capture, allowed_exitstatus: 1..5)
|
386
|
+
#
|
387
|
+
|
348
388
|
def run(*args)
|
349
389
|
options = args.last.is_a?(Hash) ? args.pop : {}
|
350
390
|
options = BUILTIN_DEFAULT_OPTIONS.merge(@default_options).merge(options)
|
351
391
|
|
392
|
+
options[:stdin] ||= "" # allow passing nil stdin see issue gh#11
|
393
|
+
if !options[:allowed_exitstatus].respond_to?(:include?)
|
394
|
+
options[:allowed_exitstatus] = Array(options[:allowed_exitstatus])
|
395
|
+
end
|
396
|
+
|
352
397
|
streamed = compute_streamed(options)
|
353
398
|
streams = build_streams(options, streamed)
|
354
399
|
commands = build_commands(args)
|
@@ -356,39 +401,63 @@ module Cheetah
|
|
356
401
|
|
357
402
|
recorder.record_commands(commands)
|
358
403
|
|
359
|
-
pid, pipes = fork_commands(commands)
|
404
|
+
pid, pipes = fork_commands(commands, options)
|
360
405
|
select_loop(streams, pipes, recorder)
|
361
|
-
|
406
|
+
_pid, status = Process.wait2(pid)
|
407
|
+
|
408
|
+
# when more exit status are allowed, then pass it below that it did
|
409
|
+
# not fail (bsc#1153749)
|
410
|
+
success = allowed_status?(status, options)
|
362
411
|
|
363
412
|
begin
|
364
|
-
|
413
|
+
report_errors(commands, status, streams, streamed) if !success
|
365
414
|
ensure
|
366
|
-
|
415
|
+
# backward compatibility for recorders with just single parameter
|
416
|
+
if recorder.method(:record_status).arity == 1
|
417
|
+
recorder.record_status(status)
|
418
|
+
else
|
419
|
+
recorder.record_status(status, success)
|
420
|
+
end
|
367
421
|
end
|
368
422
|
|
369
|
-
build_result(streams, options)
|
423
|
+
build_result(streams, status, options)
|
370
424
|
end
|
371
425
|
|
372
426
|
private
|
373
427
|
|
428
|
+
def allowed_status?(status, options)
|
429
|
+
exit_status = status.exitstatus
|
430
|
+
return exit_status.zero? unless allowed_exitstatus?(options)
|
431
|
+
|
432
|
+
options[:allowed_exitstatus].include?(exit_status)
|
433
|
+
end
|
434
|
+
|
374
435
|
# Parts of Cheetah.run
|
375
436
|
|
437
|
+
def with_env(env, &block)
|
438
|
+
old_env = ENV.to_hash
|
439
|
+
ENV.update(env)
|
440
|
+
block.call
|
441
|
+
ensure
|
442
|
+
ENV.replace(old_env)
|
443
|
+
end
|
444
|
+
|
376
445
|
def compute_streamed(options)
|
377
446
|
# The assumption for :stdout and :stderr is that anything except :capture
|
378
447
|
# and nil is an IO-like object. We avoid detecting it directly to allow
|
379
448
|
# passing StringIO, mocks, etc.
|
380
449
|
{
|
381
|
-
:
|
382
|
-
:
|
383
|
-
:
|
450
|
+
stdin: !options[:stdin].is_a?(String),
|
451
|
+
stdout: ![nil, :capture].include?(options[:stdout]),
|
452
|
+
stderr: ![nil, :capture].include?(options[:stderr])
|
384
453
|
}
|
385
454
|
end
|
386
455
|
|
387
456
|
def build_streams(options, streamed)
|
388
457
|
{
|
389
|
-
:
|
390
|
-
:
|
391
|
-
:
|
458
|
+
stdin: streamed[:stdin] ? options[:stdin] : StringIO.new(options[:stdin]),
|
459
|
+
stdout: streamed[:stdout] ? options[:stdout] : StringIO.new(+""),
|
460
|
+
stderr: streamed[:stderr] ? options[:stderr] : StringIO.new(+"")
|
392
461
|
}
|
393
462
|
end
|
394
463
|
|
@@ -410,7 +479,8 @@ module Cheetah
|
|
410
479
|
# The following code ensures that the result consistently (in all three
|
411
480
|
# cases) contains an array of arrays specifying commands and their
|
412
481
|
# arguments.
|
413
|
-
args.all? { |a| a.is_a?(Array) } ? args : [args]
|
482
|
+
commands = args.all? { |a| a.is_a?(Array) } ? args : [args]
|
483
|
+
commands.map { |c| c.map(&:to_s) }
|
414
484
|
end
|
415
485
|
|
416
486
|
def build_recorder(options)
|
@@ -421,54 +491,106 @@ module Cheetah
|
|
421
491
|
end
|
422
492
|
end
|
423
493
|
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
end
|
494
|
+
# Reopen *stream* to write **into** the writing half of *pipe*
|
495
|
+
# and close the reading half of *pipe*.
|
496
|
+
# @param pipe [Array<IO>] a pair of IOs as returned from IO.pipe
|
497
|
+
# @param stream [IO]
|
498
|
+
def into_pipe(stream, pipe)
|
499
|
+
stream.reopen(pipe[WRITE])
|
500
|
+
pipe[WRITE].close
|
501
|
+
pipe[READ].close
|
502
|
+
end
|
503
|
+
|
504
|
+
# Reopen *stream* to read **from** the reading half of *pipe*
|
505
|
+
# and close the writing half of *pipe*.
|
506
|
+
# @param pipe [Array<IO>] a pair of IOs as returned from IO.pipe
|
507
|
+
# @param stream [IO]
|
508
|
+
def from_pipe(stream, pipe)
|
509
|
+
stream.reopen(pipe[READ])
|
510
|
+
pipe[READ].close
|
511
|
+
pipe[WRITE].close
|
512
|
+
end
|
513
|
+
|
514
|
+
def chroot_step(options)
|
515
|
+
return options if [nil, "/"].include?(options[:chroot])
|
447
516
|
|
448
|
-
|
449
|
-
|
450
|
-
|
517
|
+
options = options.dup
|
518
|
+
# delete chroot option otherwise in pipe will chroot each fork recursively
|
519
|
+
root = options.delete(:chroot)
|
520
|
+
Dir.chroot(root)
|
521
|
+
# curdir can be outside chroot which is considered as security problem
|
522
|
+
Dir.chdir("/")
|
451
523
|
|
452
|
-
|
453
|
-
|
454
|
-
|
524
|
+
options
|
525
|
+
end
|
526
|
+
|
527
|
+
def fork_commands_recursive(commands, pipes, options)
|
528
|
+
fork do
|
529
|
+
# support chrooting
|
530
|
+
options = chroot_step(options)
|
531
|
+
|
532
|
+
if commands.size == 1
|
533
|
+
from_pipe($stdin, pipes[:stdin])
|
534
|
+
else
|
535
|
+
pipe_to_child = IO.pipe
|
536
|
+
|
537
|
+
fork_commands_recursive(commands[0..-2],
|
538
|
+
{
|
539
|
+
stdin: pipes[:stdin],
|
540
|
+
stdout: pipe_to_child,
|
541
|
+
stderr: pipes[:stderr]
|
542
|
+
},
|
543
|
+
options)
|
544
|
+
|
545
|
+
pipes[:stdin][READ].close
|
546
|
+
pipes[:stdin][WRITE].close
|
547
|
+
|
548
|
+
from_pipe($stdin, pipe_to_child)
|
549
|
+
end
|
455
550
|
|
456
|
-
|
457
|
-
|
458
|
-
# number portably in Ruby, I didn't implement it. Patches welcome.
|
551
|
+
into_pipe($stdout, pipes[:stdout])
|
552
|
+
into_pipe($stderr, pipes[:stderr])
|
459
553
|
|
460
|
-
|
554
|
+
close_fds
|
555
|
+
|
556
|
+
command, *args = commands.last
|
557
|
+
with_env(options[:env]) do
|
461
558
|
exec([command, command], *args)
|
462
|
-
rescue SystemCallError => e
|
463
|
-
exit!(127)
|
464
559
|
end
|
560
|
+
rescue SystemCallError => e
|
561
|
+
# depends when failed, if pipe is already redirected or not, so lets find it
|
562
|
+
output = pipes[:stderr][WRITE].closed? ? $stderr : pipes[:stderr][WRITE]
|
563
|
+
output.puts e.message
|
564
|
+
|
565
|
+
exit!(127)
|
465
566
|
end
|
466
567
|
end
|
467
568
|
|
468
|
-
|
469
|
-
|
569
|
+
# closes all open fds starting with 3 and above
|
570
|
+
def close_fds
|
571
|
+
# NOTE: this will work only if unix has /proc filesystem. If it does not
|
572
|
+
# have it, it won't close other fds.
|
573
|
+
Dir.glob("/proc/self/fd/*").each do |path|
|
574
|
+
fd = File.basename(path).to_i
|
575
|
+
next if (0..2).include?(fd)
|
470
576
|
|
471
|
-
|
577
|
+
# here we intentionally ignore some failures when fd close failed
|
578
|
+
# rubocop:disable Lint/SuppressedException
|
579
|
+
begin
|
580
|
+
IO.new(fd).close
|
581
|
+
# Ruby reserves some fds for its VM and it result in this exception
|
582
|
+
rescue ArgumentError
|
583
|
+
# Ignore if close failed with invalid FD
|
584
|
+
rescue Errno::EBADF
|
585
|
+
end
|
586
|
+
# rubocop:enable Lint/SuppressedException
|
587
|
+
end
|
588
|
+
end
|
589
|
+
|
590
|
+
def fork_commands(commands, options)
|
591
|
+
pipes = { stdin: IO.pipe, stdout: IO.pipe, stderr: IO.pipe }
|
592
|
+
|
593
|
+
pid = fork_commands_recursive(commands, pipes, options)
|
472
594
|
|
473
595
|
[
|
474
596
|
pipes[:stdin][READ],
|
@@ -508,11 +630,9 @@ module Cheetah
|
|
508
630
|
break if pipes_readable.empty? && pipes_writable.empty?
|
509
631
|
|
510
632
|
ios_read, ios_write, ios_error = select(pipes_readable, pipes_writable,
|
511
|
-
|
633
|
+
pipes_readable + pipes_writable)
|
512
634
|
|
513
|
-
if !ios_error.empty?
|
514
|
-
raise IOError, "Error when communicating with executed program."
|
515
|
-
end
|
635
|
+
raise IOError, "Error when communicating with executed program." if !ios_error.empty?
|
516
636
|
|
517
637
|
ios_read.each do |pipe|
|
518
638
|
begin
|
@@ -540,46 +660,61 @@ module Cheetah
|
|
540
660
|
end
|
541
661
|
end
|
542
662
|
|
543
|
-
def
|
663
|
+
def report_errors(commands, status, streams, streamed)
|
544
664
|
return if status.success?
|
545
665
|
|
546
666
|
stderr_part = if streamed[:stderr]
|
547
|
-
|
548
|
-
|
549
|
-
|
550
|
-
|
551
|
-
|
552
|
-
|
553
|
-
|
667
|
+
" (error output streamed away)"
|
668
|
+
elsif streams[:stderr].string.empty?
|
669
|
+
" (no error output)"
|
670
|
+
else
|
671
|
+
lines = streams[:stderr].string.split("\n")
|
672
|
+
": #{lines.first}#{lines.size > 1 ? ' (...)' : ''}"
|
673
|
+
end
|
554
674
|
|
555
675
|
raise ExecutionFailed.new(
|
556
676
|
commands,
|
557
677
|
status,
|
558
678
|
streamed[:stdout] ? nil : streams[:stdout].string,
|
559
679
|
streamed[:stderr] ? nil : streams[:stderr].string,
|
560
|
-
"Execution of #{format_commands(commands)} "
|
680
|
+
"Execution of #{format_commands(commands)} " \
|
561
681
|
"failed with status #{status.exitstatus}#{stderr_part}."
|
562
682
|
)
|
563
683
|
end
|
564
684
|
|
565
|
-
def build_result(streams, options)
|
566
|
-
case [options[:stdout] == :capture, options[:stderr] == :capture]
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
571
|
-
|
572
|
-
|
573
|
-
|
574
|
-
|
685
|
+
def build_result(streams, status, options)
|
686
|
+
res = case [options[:stdout] == :capture, options[:stderr] == :capture]
|
687
|
+
when [false, false]
|
688
|
+
nil
|
689
|
+
when [true, false]
|
690
|
+
streams[:stdout].string
|
691
|
+
when [false, true]
|
692
|
+
streams[:stderr].string
|
693
|
+
when [true, true]
|
694
|
+
[streams[:stdout].string, streams[:stderr].string]
|
695
|
+
end
|
696
|
+
|
697
|
+
if allowed_exitstatus?(options)
|
698
|
+
if res.nil?
|
699
|
+
res = status.exitstatus
|
700
|
+
else
|
701
|
+
res = Array(res)
|
702
|
+
res << status.exitstatus
|
703
|
+
end
|
575
704
|
end
|
705
|
+
|
706
|
+
res
|
707
|
+
end
|
708
|
+
|
709
|
+
def allowed_exitstatus?(options)
|
710
|
+
# more exit status allowed for non array or non empty array
|
711
|
+
!options[:allowed_exitstatus].is_a?(Array) || !options[:allowed_exitstatus].empty?
|
576
712
|
end
|
577
713
|
|
578
714
|
def format_commands(commands)
|
579
|
-
|
715
|
+
"\"#{commands.map { |c| Shellwords.join(c) }.join(' | ')}\""
|
580
716
|
end
|
581
717
|
end
|
582
718
|
|
583
719
|
self.default_options = {}
|
584
720
|
end
|
585
|
-
|
metadata
CHANGED
@@ -1,80 +1,57 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: cheetah
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
5
|
-
prerelease:
|
4
|
+
version: 1.0.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: 2021-12-01 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
|
-
version:
|
47
|
+
version: 0.9.11
|
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
|
-
version:
|
54
|
+
version: 0.9.11
|
78
55
|
description: Your swiss army knife for executing external commands in Ruby safely
|
79
56
|
and conveniently.
|
80
57
|
email: dmajda@suse.de
|
@@ -91,28 +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
|
-
version: '
|
80
|
+
version: '2.5'
|
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.7.6.3
|
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: []
|
118
|
-
has_rdoc:
|