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.
Files changed (7) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG +16 -2
  3. data/README.md +94 -14
  4. data/VERSION +1 -1
  5. data/lib/cheetah.rb +178 -81
  6. data/lib/cheetah/version.rb +1 -0
  7. metadata +15 -39
@@ -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 of Ruby 1.8.7.
8
- * Added support of Ruby 2.0.0.
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", :stdout => :capture)
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", :stdout => stdout)
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 ouptut: #{e.stderr}"
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", :stdin => source_code)
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", :stdin => stdin)
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", :stdout => :capture)
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", ".", :stdout => :capture, :stderr => :capture)
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", :stdout => stdout)
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"], :stdout => :capture)
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 a non-zero exit
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 ouptut: #{e.stderr}"
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", :logger => logger)
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 = my_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 1.9.3 or 2.0.0. Non-Unix
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.4.0
1
+ 0.5.0
@@ -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", :stdout => :capture)
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", :stdout => stdout)
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(commands); end
129
- def record_stdin(stdin); end
130
- def record_stdout(stdout); end
131
- def record_stderr(stderr); end
132
- def record_status(status); end
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
- :stdin => { :name => "Standard input", :method => :info },
142
- :stdout => { :name => "Standard output", :method => :info },
143
- :stderr => { :name => "Error output", :method => :error }
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 = { :stdin => false, :stdout => false, :stderr => false }
150
- @stream_buffer = { :stdin => "", :stdout => "", :stderr => "" }
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
- "Status: #{status.exitstatus}"
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, rest = $1, $2
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 @stream_used[stream] && !@stream_buffer[stream].empty?
196
- log_stream_line(stream, @stream_buffer[stream])
197
- end
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
- :stdin => "",
211
- :stdout => nil,
212
- :stderr => nil,
213
- :logger => nil
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 = { :logger = my_logger }
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 they are
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"], :stdout => :capture)
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", :stdout => capture)
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", :stdout => stdout)
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
- pid, status = Process.wait2(pid)
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
- :stdin => !options[:stdin].is_a?(String),
382
- :stdout => ![nil, :capture].include?(options[:stdout]),
383
- :stderr => ![nil, :capture].include?(options[:stderr])
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
- :stdin => streamed[:stdin] ? options[:stdin] : StringIO.new(options[:stdin]),
390
- :stdout => streamed[:stdout] ? options[:stdout] : StringIO.new(""),
391
- :stderr => streamed[:stderr] ? options[:stderr] : StringIO.new("")
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
- def fork_commands_recursive(commands, pipes)
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][WRITE].close
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
- :stdin => pipes[:stdin],
436
- :stdout => pipe_to_child,
437
- :stderr => pipes[:stderr]
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[WRITE].close
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][READ].close
449
- STDOUT.reopen(pipes[:stdout][WRITE])
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
- exec([command, command], *args)
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 = { :stdin => IO.pipe, :stdout => IO.pipe, :stderr => IO.pipe }
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
- pipes_readable + pipes_writable)
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
- " (error output streamed away)"
548
- elsif streams[:stderr].string.empty?
549
- " (no error output)"
550
- else
551
- lines = streams[:stderr].string.split("\n")
552
- ": " + lines.first + (lines.size > 1 ? " (...)" : "")
553
- end
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
- when [false, false]
568
- nil
569
- when [true, false]
570
- streams[:stdout].string
571
- when [false, true]
572
- streams[:stderr].string
573
- when [true, true]
574
- [streams[:stdout].string, streams[:stderr].string]
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
-
@@ -1,3 +1,4 @@
1
+ # Cheetah namespace
1
2
  module Cheetah
2
3
  # Cheetah version (uses [semantic versioning](http://semver.org/)).
3
4
  VERSION = File.read(File.dirname(__FILE__) + "/../../VERSION").strip
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.4.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: 2013-11-21 00:00:00.000000000 Z
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: '0'
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: '0'
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: 1.8.23
88
+ rubygems_version: 2.4.5.1
113
89
  signing_key:
114
- specification_version: 3
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: []