cheetah 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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: []