cheetah 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (6) hide show
  1. data/CHANGELOG +18 -0
  2. data/README.md +97 -17
  3. data/VERSION +1 -1
  4. data/lib/cheetah.rb +401 -114
  5. data/lib/cheetah/version.rb +4 -0
  6. metadata +29 -13
data/CHANGELOG CHANGED
@@ -1,3 +1,21 @@
1
+ 0.3.0 (2012-06-21)
2
+ ------------------
3
+
4
+ * Allowed passing an IO in the :stdin option.
5
+ * Replaced the :capture option with :stdout and :stderr and allowed streaming
6
+ standard and error output into an IO.
7
+ * Implemented support for piped commands.
8
+ * Implemented the :recorder option allowing to customize logging.
9
+ * Replaced Cheetah.logger with more generic Cheetah.default_options.
10
+ * Commands in logs and exception messages are now directly copy-pastable into
11
+ the shell.
12
+ * Officially supports Ruby 1.8.7 and 1.9.3 on Unix systems.
13
+ * Added Travis CI integration.
14
+ * Various internal code improvements and fixes.
15
+ * Improved gem description and summary.
16
+ * Improved documentation.
17
+ * Improved README.md.
18
+
1
19
  0.2.1 (2012-04-12)
2
20
  ------------------
3
21
 
data/README.md CHANGED
@@ -1,14 +1,20 @@
1
1
  Cheetah
2
2
  =======
3
3
 
4
- Cheetah is a simple library for executing external commands safely and conveniently.
4
+ Your swiss army knife for executing external commands in Ruby safely and
5
+ conveniently.
5
6
 
6
7
  Examples
7
8
  --------
8
9
 
9
10
  ```ruby
10
11
  # Run a command and capture its output
11
- files = Cheetah.run("ls", "-la", :capture => :stdout)
12
+ files = Cheetah.run("ls", "-la", :stdout => :capture)
13
+
14
+ # Run a command and capture its output into a stream
15
+ File.open("files.txt", "w") do |stdout|
16
+ Cheetah.run("ls", "-la", :stdout => stdout)
17
+ end
12
18
 
13
19
  # Run a command and handle errors
14
20
  begin
@@ -25,6 +31,7 @@ Features
25
31
 
26
32
  * Easy passing of command input
27
33
  * Easy capturing of command output (standard, error, or both)
34
+ * Piping commands together
28
35
  * 100% secure (shell expansion is impossible by design)
29
36
  * Raises exceptions on errors (no more manual status code checks)
30
37
  * Optional logging for easy debugging
@@ -32,7 +39,6 @@ Features
32
39
  Non-features
33
40
  ------------
34
41
 
35
- * Handling of commands producing big outputs
36
42
  * Handling of interactive commands
37
43
 
38
44
  Installation
@@ -49,23 +55,73 @@ First, require the library:
49
55
  require "cheetah"
50
56
  ```
51
57
 
52
- You can now use the `Cheetah.run` method to run commands, pass them an input and capture their output:
58
+ You can now use the `Cheetah.run` method to run commands.
59
+
60
+ ### Running Commands
61
+
62
+ To run a command, just specify it together with its arguments:
53
63
 
54
64
  ```ruby
55
- # Run a command with arguments
56
65
  Cheetah.run("tar", "xzf", "foo.tar.gz")
66
+ ```
67
+ ### Passing Input
57
68
 
58
- # Pass an input
69
+ Using the `:stdin` option you can pass a string to command's standard input:
70
+
71
+ ```ruby
59
72
  Cheetah.run("python", :stdin => source_code)
73
+ ```
74
+
75
+ If the input is big you may want to avoid passing it in one huge string. In that
76
+ case, pass an `IO` as a value of the `:stdin` option. The command will read its
77
+ input from it gradually.
78
+
79
+ ```ruby
80
+ File.open("huge_program.py") do |stdin|
81
+ Cheetah.run("python", :stdin => stdin)
82
+ end
83
+ ```
84
+
85
+ ### Capturing Output
86
+
87
+ To capture command's standard output, set the `:stdout` option to `:capture`.
88
+ You will receive the output as a return value of the call:
60
89
 
61
- # Capture standard output
62
- files = Cheetah.run("ls", "-la", :capture => :stdout)
90
+ ```ruby
91
+ files = Cheetah.run("ls", "-la", :stdout => :capture)
92
+ ```
93
+
94
+ The same technique works with the error output — just use the `:stderr` option.
95
+ If you specify capturing of both outputs, the return value will be a two-element
96
+ array:
63
97
 
64
- # Capture both standard and error output
65
- results, errors = Cheetah.run("grep", "-r", "User", ".", :capture => [:stdout, :stderr))
98
+ ```ruby
99
+ results, errors = Cheetah.run("grep", "-r", "User", ".", :stdout => :capture, :stderr => :capture)
66
100
  ```
67
101
 
68
- If the command can't be executed for some reason or returns a non-zero exit status, the method raises an exception with detailed information about the failure:
102
+ If the output is big you may want to avoid capturing it into a huge string. In
103
+ that case, pass an `IO` as a value of the `:stdout` or `:stderr` option. The
104
+ command will write its output into it gradually.
105
+
106
+ ```ruby
107
+ File.open("files.txt", "w") do |stdout|
108
+ Cheetah.run("ls", "-la", :stdout => stdout)
109
+ end
110
+ ```
111
+
112
+ ### Piping Commands
113
+
114
+ You can pipe multiple commands together and execute them as one. Just specify
115
+ the commands together with their arguments as arrays:
116
+
117
+ ```ruby
118
+ processes = Cheetah.run(["ps", "aux"], ["grep", "ruby"], :stdout => :capture)
119
+ ```
120
+
121
+ ### Error Handling
122
+
123
+ If the command can't be executed for some reason or returns a non-zero exit
124
+ status, Cheetah raises an exception with detailed information about the failure:
69
125
 
70
126
  ```ruby
71
127
  # Run a command and handle errors
@@ -77,19 +133,43 @@ rescue Cheetah::ExecutionFailed => e
77
133
  puts "Error ouptut: #{e.stderr}"
78
134
  end
79
135
  ```
136
+ ### Logging
80
137
 
81
- For debugging purposes, you can also use a logger. Cheetah will log the command, its status, input and both outputs to it. The `:info` level will be used for normal messages, the `:error` level for messages about errors (non-zero exit status or non-empty error output):
138
+ For debugging purposes, you can use a logger. Cheetah will log the command, its
139
+ status, input and both outputs to it:
82
140
 
83
141
  ```ruby
84
- # Log the execution
85
142
  Cheetah.run("ls -l", :logger => logger)
143
+ ```
144
+
145
+ ### Setting Defaults
146
+
147
+ To avoid repetition, you can set global default value of any option passed too
148
+ `Cheetah.run`:
86
149
 
87
- # Or, if you're tired of passing the :logger option all the time...
88
- Cheetah.logger = my_logger
150
+ ```ruby
151
+ # If you're tired of passing the :logger option all the time...
152
+ Cheetah.default_options = { :logger = my_logger }
89
153
  Cheetah.run("./configure")
90
154
  Cheetah.run("make")
91
155
  Cheetah.run("make", "install")
92
- Cheetah.logger = nil
156
+ Cheetah.default_options = {}
93
157
  ```
94
158
 
95
- For more information, see the [API documentation](http://rubydoc.info/github/openSUSE/cheetah/frames).
159
+ ### More Information
160
+
161
+ For more information, see the
162
+ [API documentation](http://rubydoc.info/github/openSUSE/cheetah/frames).
163
+
164
+ Compatibility
165
+ -------------
166
+
167
+ Cheetah should run well on any Unix system with Ruby 1.8.7 or 1.9.3. Non-Unix
168
+ systems and different Ruby implementations/versions may work too but they were
169
+ not tested.
170
+
171
+ Authors
172
+ -------
173
+
174
+ * [David Majda](http://github.com/dmajda)
175
+ * [Josef Reidinger](http://github.com/jreidinger)
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.2.1
1
+ 0.3.0
@@ -1,20 +1,33 @@
1
- # A simple library for executing external commands safely and conveniently.
1
+ require "abstract_method"
2
+ require "logger"
3
+ require "shellwords"
4
+ require "stringio"
5
+
6
+ require File.expand_path(File.dirname(__FILE__) + "/cheetah/version")
7
+
8
+ # Your swiss army knife for executing external commands in Ruby safely and
9
+ # conveniently.
2
10
  #
3
11
  # ## Features
4
12
  #
5
13
  # * Easy passing of command input
6
14
  # * Easy capturing of command output (standard, error, or both)
15
+ # * Piping commands together
7
16
  # * 100% secure (shell expansion is impossible by design)
8
17
  # * Raises exceptions on errors (no more manual status code checks)
9
18
  # * Optional logging for easy debugging
10
19
  #
11
20
  # ## Non-features
12
21
  #
13
- # * Handling of commands producing big outputs
14
22
  # * Handling of interactive commands
15
23
  #
16
24
  # @example Run a command and capture its output
17
- # files = Cheetah.run("ls", "-la", :capture => :stdout)
25
+ # files = Cheetah.run("ls", "-la", :stdout => :capture)
26
+ #
27
+ # @example Run a command and capture its output into a stream
28
+ # File.open("files.txt", "w") do |stdout|
29
+ # Cheetah.run("ls", "-la", :stdout => stdout)
30
+ # end
18
31
  #
19
32
  # @example Run a command and handle errors
20
33
  # begin
@@ -25,102 +38,271 @@
25
38
  # puts "Error ouptut: #{e.stderr}"
26
39
  # end
27
40
  module Cheetah
28
- # Cheetah version (uses [semantic versioning](http://semver.org/)).
29
- VERSION = File.read(File.dirname(__FILE__) + "/../VERSION").strip
30
-
31
41
  # Exception raised when a command execution fails.
32
42
  class ExecutionFailed < StandardError
33
- # @return [String] the executed command
34
- attr_reader :command
35
-
36
- # @return [Array<String>] the executed command arguments
37
- attr_reader :args
43
+ # @return [Array<Array<String>>] the executed commands as an array where
44
+ # each item is again an array containing an executed command in the first
45
+ # element and its arguments in the remaining ones
46
+ attr_reader :commands
38
47
 
39
48
  # @return [Process::Status] the executed command exit status
40
49
  attr_reader :status
41
50
 
42
- # @return [String] the output the executed command wrote to stdout
51
+ # @return [String, nil] the output the executed command wrote to stdout; can
52
+ # be `nil` if stdout was captured into a stream
43
53
  attr_reader :stdout
44
54
 
45
- # @return [String] the output the executed command wrote to stderr
55
+ # @return [String, nil] the output the executed command wrote to stderr; can
56
+ # be `nil` if stderr was captured into a stream
46
57
  attr_reader :stderr
47
58
 
48
59
  # Initializes a new {ExecutionFailed} instance.
49
60
  #
50
- # @param [String] command the executed command
51
- # @param [Array<String>] args the executed command arguments
61
+ # @param [Array<Array<String>>] commands the executed commands as an array
62
+ # where each item is again an array containing an executed command in the
63
+ # first element and its arguments in the remaining ones
52
64
  # @param [Process::Status] status the executed command exit status
53
- # @param [String] stdout the output the executed command wrote to stdout
54
- # @param [String] stderr the output the executed command wrote to stderr
65
+ # @param [String, nil] stdout the output the executed command wrote to stdout
66
+ # @param [String, nil] stderr the output the executed command wrote to stderr
55
67
  # @param [String, nil] message the exception message
56
- def initialize(command, args, status, stdout, stderr, message = nil)
68
+ def initialize(commands, status, stdout, stderr, message = nil)
57
69
  super(message)
58
- @command = command
59
- @args = args
60
- @status = status
61
- @stdout = stdout
62
- @stderr = stderr
70
+ @commands = commands
71
+ @status = status
72
+ @stdout = stdout
73
+ @stderr = stderr
74
+ end
75
+ end
76
+
77
+ # Defines a recorder interface. Recorder is an object that handles recording
78
+ # of the command execution into a logger. It decides what exactly gets logged,
79
+ # at what level and using what messages.
80
+ #
81
+ # @abstract
82
+ class Recorder
83
+ # @!method record_commands(commands)
84
+ # Called to record the executed commands.
85
+ #
86
+ # @abstract
87
+ # @param [Array<Array<String>>] commands the executed commands as an array
88
+ # where each item is again an array containing an executed command in
89
+ # the first element and its arguments in the remaining ones
90
+ abstract_method :record_commands
91
+
92
+ # @!method record_stdin(stdin)
93
+ # Called to record the executed command input (if it wasn't read from a
94
+ # stream).
95
+ #
96
+ # @abstract
97
+ # @param [String] stdin the executed command input
98
+ abstract_method :record_stdin
99
+
100
+ # @!method record_status(status)
101
+ # Called to record the executed command exit status.
102
+ #
103
+ # @abstract
104
+ # @param [Process::Status] status the executed command exit status
105
+ abstract_method :record_status
106
+
107
+ # @!method record_stdout(stdout)
108
+ # Called to record the output the executed command wrote to stdout (if it
109
+ # wasn't captured into a stream).
110
+ #
111
+ # @abstract
112
+ # @param [String] stdout the output the executed command wrote to stdout
113
+ abstract_method :record_stdout
114
+
115
+ # @!method record_stderr(stderr)
116
+ # Called to record the output the executed command wrote to stderr (if it
117
+ # wasn't captured into a stream).
118
+ #
119
+ # @abstract
120
+ # @param [String] stderr the output the executed command wrote to stderr
121
+ abstract_method :record_stderr
122
+ end
123
+
124
+ # A recorder that does not record anyting. Used by {Cheetah.run} when no
125
+ # logger is passed.
126
+ class NullRecorder < Recorder
127
+ def record_commands(commands); end
128
+ def record_stdin(stdin); end
129
+ def record_status(status); end
130
+ def record_stdout(stdout); end
131
+ def record_stderr(stderr); end
132
+ end
133
+
134
+ # A default recorder. It uses the `Logger::INFO` level for normal messages and
135
+ # the `Logger::ERROR` level for messages about errors (non-zero exit status or
136
+ # non-empty error output). Used by {Cheetah.run} when a logger is passed.
137
+ class DefaultRecorder < Recorder
138
+ def initialize(logger)
139
+ @logger = logger
140
+ end
141
+
142
+ def record_commands(commands)
143
+ @logger.info "Executing #{format_commands(commands)}."
144
+ end
145
+
146
+ def record_stdin(stdin)
147
+ @logger.info "Standard input: #{format_input_output(stdin)}"
148
+ end
149
+
150
+ def record_status(status)
151
+ @logger.send status.success? ? :info : :error,
152
+ "Status: #{status.exitstatus}"
153
+ end
154
+
155
+ def record_stdout(stdout)
156
+ @logger.info "Standard output: #{format_input_output(stdout)}"
157
+ end
158
+
159
+ def record_stderr(stderr)
160
+ @logger.send stderr.empty? ? :info : :error,
161
+ "Error output: #{format_input_output(stderr)}"
162
+ end
163
+
164
+ protected
165
+
166
+ def format_input_output(s)
167
+ s.empty? ? "(none)" : s
168
+ end
169
+
170
+ def format_commands(commands)
171
+ '"' + commands.map { |c| Shellwords.join(c) }.join(" | ") + '"'
63
172
  end
64
173
  end
65
174
 
175
+ # @private
176
+ BUILTIN_DEFAULT_OPTIONS = {
177
+ :stdin => "",
178
+ :stdout => nil,
179
+ :stderr => nil,
180
+ :logger => nil
181
+ }
182
+
183
+ READ = 0 # @private
184
+ WRITE = 1 # @private
185
+
66
186
  class << self
67
- # The global logger or `nil` if none is set (the default). This logger is
68
- # used by {Cheetah.run} unless overridden by the `:logger` option.
187
+ # The default options of the {Cheetah.run} method. Values of options not
188
+ # specified in its `options` parameter are taken from here. If a value is
189
+ # not specified here too, the default value described in the {Cheetah.run}
190
+ # documentation is used.
191
+ #
192
+ # By default, no values are specified here.
193
+ #
194
+ # @example Setting a logger once for execution of multiple commands
195
+ # Cheetah.default_options = { :logger = my_logger }
196
+ # Cheetah.run("./configure")
197
+ # Cheetah.run("make")
198
+ # Cheetah.run("make", "install")
199
+ # Cheetah.default_options = {}
69
200
  #
70
- # @return [Logger, nil] the global logger
71
- attr_accessor :logger
201
+ # @return [Hash] the default options of the {Cheetah.run} method
202
+ attr_accessor :default_options
72
203
 
73
- # Runs an external command with specified arguments, optionally passing it
74
- # an input and capturing its output.
204
+ # Runs external command(s) with specified arguments.
75
205
  #
76
- # If the command execution succeeds, the returned value depends on the value
77
- # of the `:capture` option (see below). If the command can't be executed for
78
- # some reason or returns a non-zero exit status, the method raises an
79
- # {ExecutionFailed} exception with detailed information about the failure.
206
+ # If the execution succeeds, the returned value depends on the value of the
207
+ # `:stdout` and `:stderr` options (see below). If the execution fails, the
208
+ # method raises an {ExecutionFailed} exception with detailed information
209
+ # about the failure. (In the single command case, the execution succeeds if
210
+ # the command can be executed and returns a zero exit status. In the
211
+ # multiple command case, the execution succeeds if the last command can be
212
+ # executed and returns a zero exit status.)
80
213
  #
81
- # The command and its arguments never undergo shell expansion — they are
214
+ # Commands and their arguments never undergo shell expansion — they are
82
215
  # passed directly to the operating system. While this may create some
83
216
  # inconvenience in certain cases, it eliminates a whole class of security
84
217
  # bugs.
85
218
  #
86
- # The command execution can be logged using a logger. It can be set globally
87
- # (using the {Cheetah.logger} attribute) or locally (using the `:logger`
88
- # option). The local setting overrides the global one. If a logger is set,
89
- # the method will log the command, its status, input and both outputs to it.
90
- # The `:info` level will be used for normal messages, the `:error` level for
91
- # messages about errors (non-zero exit status or non-empty error output).
219
+ # The execution can be logged using a logger passed in the `:logger` option.
220
+ # If a logger is set, the method will log the executed command(s), final
221
+ # exit status, passed input and both captured outputs (unless the `:stdin`,
222
+ # `:stdout` or `:stderr` option is set to an `IO`, which prevents logging
223
+ # the corresponding input or output).
224
+ #
225
+ # The actual logging is handled by a separate object called recorder. By
226
+ # default, {DefaultRecorder} instance is used. It uses the `Logger::INFO`
227
+ # level for normal messages and the `Logger::ERROR` level for messages about
228
+ # errors (non-zero exit status or non-empty error output). If you need to
229
+ # customize the recording, you can create your own recorder (implementing
230
+ # the {Recorder} interface) and pass it in the `:recorder` option.
231
+ #
232
+ # Values of options not set using the `options` parameter are taken from
233
+ # {Cheetah.default_options}. If a value is not specified there too, the
234
+ # default value described in the `options` parameter documentation is used.
92
235
  #
93
236
  # @overload run(command, *args, options = {})
237
+ # Runs a command with its arguments specified separately.
238
+ #
94
239
  # @param [String] command the command to execute
95
240
  # @param [Array<String>] args the command arguments
96
241
  # @param [Hash] options the options to execute the command with
97
- # @option options [String] :stdin ('') command's input
98
- # @option options [String] :capture (nil) configures which output(s) to
99
- # capture, the valid values are:
242
+ # @option options [String, IO] :stdin ('') a `String` to use as command's
243
+ # standard input or an `IO` to read it from
244
+ # @option options [nil, :capture, IO] :stdout (nil) specifies command's
245
+ # standard output handling
100
246
  #
101
- # * `nil` no output is captured and returned
102
- # * `:stdout` standard output is captured and returned as a string
103
- # * `:stderr` error output is captured and returned as a string
104
- # * `[:stdout, :stderr]` both outputs are captured and returned as a
105
- # two-element array of strings
247
+ # * if set to `nil`, ignore the output
248
+ # * if set to `:capture`, capture the output and return it as a string
249
+ # (or as the first element of a two-element array of strings if the
250
+ # `:stderr` option is set to `:capture` too)
251
+ # * if set to an `IO`, write the ouptut into it gradually as the command
252
+ # produces it
253
+ # @option options [nil, :capture, IO] :stderr (nil) specifies command's
254
+ # error output handling
255
+ #
256
+ # * if set to `nil`, ignore the output
257
+ # * if set to `:capture`, capture the output and return it as a string
258
+ # (or as the second element of a two-element array of strings if the
259
+ # `:stdout` option is set to `:capture` too)
260
+ # * if set to an `IO`, write the ouptut into it gradually as the command
261
+ # produces it
106
262
  # @option options [Logger, nil] :logger (nil) logger to log the command
107
- # execution; if set, overrides the global logger (set by
108
- # {Cheetah.logger})
263
+ # execution
264
+ # @option options [Recorder, nil] :recorder (DefaultRecorder.new) recorder
265
+ # to handle the command execution logging
266
+ #
267
+ # @example
268
+ # Cheetah.run("tar", "xzf", "foo.tar.gz")
109
269
  #
110
270
  # @overload run(command_and_args, options = {})
111
- # This variant is useful mainly when building the command and its
112
- # arguments programmatically.
271
+ # Runs a command with its arguments specified together. This variant is
272
+ # useful mainly when building the command and its arguments
273
+ # programmatically.
113
274
  #
114
275
  # @param [Array<String>] command_and_args the command to execute (first
115
276
  # element of the array) and its arguments (remaining elements)
116
277
  # @param [Hash] options the options to execute the command with, same as
117
278
  # in the first variant
118
279
  #
119
- # @raise [ExecutionFailed] when the command can't be executed for some
120
- # reason or returns a non-zero exit status
280
+ # @example
281
+ # Cheetah.run(["tar", "xzf", "foo.tar.gz"])
282
+ #
283
+ # @overload run(*commands_and_args, options = {})
284
+ # Runs multiple commands piped togeter. Standard output of each command
285
+ # execpt the last one is connected to the standard input of the next
286
+ # command. Error outputs are aggregated together.
287
+ #
288
+ # @param [Array<Array<String>>] commands_and_args the commands to execute
289
+ # as an array where each item is again an array containing an executed
290
+ # command in the first element and its arguments in the remaining ones
291
+ # @param [Hash] options the options to execute the commands with, same as
292
+ # in the first variant
293
+ #
294
+ # @example
295
+ # processes = Cheetah.run(["ps", "aux"], ["grep", "ruby"], :stdout => :capture)
296
+ #
297
+ # @raise [ExecutionFailed] when the execution fails
121
298
  #
122
299
  # @example Run a command and capture its output
123
- # files = Cheetah.run("ls", "-la", :capture => :stdout)
300
+ # files = Cheetah.run("ls", "-la", :stdout => capture)
301
+ #
302
+ # @example Run a command and capture its output into a stream
303
+ # File.open("files.txt", "w") do |stdout|
304
+ # Cheetah.run("ls", "-la", :stdout => stdout)
305
+ # end
124
306
  #
125
307
  # @example Run a command and handle errors
126
308
  # begin
@@ -130,52 +312,144 @@ module Cheetah
130
312
  # puts "Standard output: #{e.stdout}"
131
313
  # puts "Error ouptut: #{e.stderr}"
132
314
  # end
133
- def run(command, *args)
315
+ def run(*args)
134
316
  options = args.last.is_a?(Hash) ? args.pop : {}
317
+ options = BUILTIN_DEFAULT_OPTIONS.merge(@default_options).merge(options)
318
+
319
+ streamed = compute_streamed(options)
320
+ streams = build_streams(options, streamed)
321
+ commands = build_commands(args)
322
+ recorder = build_recorder(options)
135
323
 
136
- stdin = options[:stdin] || ""
137
- logger = options[:logger] || @logger
324
+ recorder.record_commands(commands)
325
+ recorder.record_stdin(streams[:stdin].string) unless streamed[:stdin]
138
326
 
139
- if command.is_a?(Array)
140
- args = command[1..-1]
141
- command = command.first
327
+ pid, pipes = fork_commands(commands)
328
+ select_loop(streams, pipes)
329
+ pid, status = Process.wait2(pid)
330
+
331
+ begin
332
+ check_errors(commands, status, streams, streamed)
333
+ ensure
334
+ recorder.record_status(status)
335
+ recorder.record_stdout(streams[:stdout].string) unless streamed[:stdout]
336
+ recorder.record_stderr(streams[:stderr].string) unless streamed[:stderr]
142
337
  end
143
338
 
144
- pipe_stdin_read, pipe_stdin_write = IO.pipe
145
- pipe_stdout_read, pipe_stdout_write = IO.pipe
146
- pipe_stderr_read, pipe_stderr_write = IO.pipe
339
+ build_result(streams, options)
340
+ end
341
+
342
+ private
343
+
344
+ # Parts of Cheetah.run
345
+
346
+ def compute_streamed(options)
347
+ # The assumption for :stdout and :stderr is that anything except :capture
348
+ # and nil is an IO-like object. We avoid detecting it directly to allow
349
+ # passing StringIO, mocks, etc.
350
+ {
351
+ :stdin => !options[:stdin].is_a?(String),
352
+ :stdout => ![nil, :capture].include?(options[:stdout]),
353
+ :stderr => ![nil, :capture].include?(options[:stderr])
354
+ }
355
+ end
356
+
357
+ def build_streams(options, streamed)
358
+ {
359
+ :stdin => streamed[:stdin] ? options[:stdin] : StringIO.new(options[:stdin]),
360
+ :stdout => streamed[:stdout] ? options[:stdout] : StringIO.new(""),
361
+ :stderr => streamed[:stderr] ? options[:stderr] : StringIO.new("")
362
+ }
363
+ end
364
+
365
+ def build_commands(args)
366
+ # There are three valid ways how to call Cheetah.run:
367
+ #
368
+ # 1. Single command, e.g. Cheetah.run("ls", "-la")
369
+ #
370
+ # args == ["ls", "-la"]
371
+ #
372
+ # 2. Single command passed as an array, e.g. Cheetah.run(["ls", "-la"])
373
+ #
374
+ # args == [["ls", "-la"]]
375
+ #
376
+ # 3. Piped command, e.g. Cheetah.run(["ps", "aux"], ["grep", "ruby"])
377
+ #
378
+ # args == [["ps", "aux"], ["grep", "ruby"]]
379
+ #
380
+ # The following code ensures that the result consistently (in all three
381
+ # cases) contains an array of arrays specifying commands and their
382
+ # arguments.
383
+ args.all? { |a| a.is_a?(Array) } ? args : [args]
384
+ end
147
385
 
148
- if logger
149
- logger.info "Executing command #{command.inspect} with #{describe_args(args)}."
150
- logger.info "Standard input: " + (stdin.empty? ? "(none)" : stdin)
386
+ def build_recorder(options)
387
+ if options[:recorder]
388
+ options[:recorder]
389
+ else
390
+ options[:logger] ? DefaultRecorder.new(options[:logger]) : NullRecorder.new
151
391
  end
392
+ end
152
393
 
153
- pid = fork do
394
+ def fork_commands_recursive(commands, pipes)
395
+ fork do
154
396
  begin
155
- pipe_stdin_write.close
156
- STDIN.reopen(pipe_stdin_read)
157
- pipe_stdin_read.close
397
+ if commands.size == 1
398
+ pipes[:stdin][WRITE].close
399
+ STDIN.reopen(pipes[:stdin][READ])
400
+ pipes[:stdin][READ].close
401
+ else
402
+ pipe_to_child = IO.pipe
403
+
404
+ fork_commands_recursive(commands[0..-2], {
405
+ :stdin => pipes[:stdin],
406
+ :stdout => pipe_to_child,
407
+ :stderr => pipes[:stderr]
408
+ })
158
409
 
159
- pipe_stdout_read.close
160
- STDOUT.reopen(pipe_stdout_write)
161
- pipe_stdout_write.close
410
+ pipes[:stdin][READ].close
411
+ pipes[:stdin][WRITE].close
412
+
413
+ pipe_to_child[WRITE].close
414
+ STDIN.reopen(pipe_to_child[READ])
415
+ pipe_to_child[READ].close
416
+ end
162
417
 
163
- pipe_stderr_read.close
164
- STDERR.reopen(pipe_stderr_write)
165
- pipe_stderr_write.close
418
+ pipes[:stdout][READ].close
419
+ STDOUT.reopen(pipes[:stdout][WRITE])
420
+ pipes[:stdout][WRITE].close
421
+
422
+ pipes[:stderr][READ].close
423
+ STDERR.reopen(pipes[:stderr][WRITE])
424
+ pipes[:stderr][WRITE].close
166
425
 
167
426
  # All file descriptors from 3 above should be closed here, but since I
168
427
  # don't know about any way how to detect the maximum file descriptor
169
428
  # number portably in Ruby, I didn't implement it. Patches welcome.
170
429
 
430
+ command, *args = commands.last
171
431
  exec([command, command], *args)
172
432
  rescue SystemCallError => e
173
433
  exit!(127)
174
434
  end
175
435
  end
436
+ end
437
+
438
+ def fork_commands(commands)
439
+ pipes = { :stdin => IO.pipe, :stdout => IO.pipe, :stderr => IO.pipe }
176
440
 
177
- [pipe_stdin_read, pipe_stdout_write, pipe_stderr_write].each { |p| p.close }
441
+ pid = fork_commands_recursive(commands, pipes)
178
442
 
443
+ [
444
+ pipes[:stdin][READ],
445
+ pipes[:stdout][WRITE],
446
+ pipes[:stderr][WRITE]
447
+ ].each(&:close)
448
+
449
+ [pid, pipes]
450
+ end
451
+
452
+ def select_loop(streams, pipes)
179
453
  # We write the command's input and read its output using a select loop.
180
454
  # Why? Because otherwise we could end up with a deadlock.
181
455
  #
@@ -186,9 +460,13 @@ module Cheetah
186
460
  # deadlock.
187
461
  #
188
462
  # Similar issues can happen with standard input vs. one of the outputs.
189
- outputs = { pipe_stdout_read => "", pipe_stderr_read => "" }
190
- pipes_readable = [pipe_stdout_read, pipe_stderr_read]
191
- pipes_writable = [pipe_stdin_write]
463
+ stdin_buffer = ""
464
+ outputs = {
465
+ pipes[:stdout][READ] => streams[:stdout],
466
+ pipes[:stderr][READ] => streams[:stderr]
467
+ }
468
+ pipes_readable = [pipes[:stdout][READ], pipes[:stderr][READ]]
469
+ pipes_writable = [pipes[:stdin][WRITE]]
192
470
  loop do
193
471
  pipes_readable.reject!(&:closed?)
194
472
  pipes_writable.reject!(&:closed?)
@@ -211,49 +489,58 @@ module Cheetah
211
489
  end
212
490
 
213
491
  ios_write.each do |pipe|
214
- n = pipe.syswrite(stdin)
215
- stdin = stdin[n..-1]
216
- pipe.close if stdin.empty?
492
+ stdin_buffer = streams[:stdin].read(4096) if stdin_buffer.empty?
493
+ if !stdin_buffer
494
+ pipe.close
495
+ next
496
+ end
497
+
498
+ n = pipe.syswrite(stdin_buffer)
499
+ stdin_buffer = stdin_buffer[n..-1]
217
500
  end
218
501
  end
502
+ end
219
503
 
220
- stdout = outputs[pipe_stdout_read]
221
- stderr = outputs[pipe_stderr_read]
504
+ def check_errors(commands, status, streams, streamed)
505
+ return if status.success?
222
506
 
223
- pid, status = Process.wait2(pid)
224
- begin
225
- if !status.success?
226
- raise ExecutionFailed.new(command, args, status, stdout, stderr,
227
- "Execution of command #{command.inspect} " +
228
- "with #{describe_args(args)} " +
229
- "failed with status #{status.exitstatus}.")
230
- end
231
- ensure
232
- if logger
233
- logger.add status.success? ? Logger::INFO : Logger::ERROR,
234
- "Status: #{status.exitstatus}"
235
- logger.info "Standard output: " + (stdout.empty? ? "(none)" : stdout)
236
- logger.add stderr.empty? ? Logger::INFO : Logger::ERROR,
237
- "Error output: " + (stderr.empty? ? "(none)" : stderr)
238
- end
507
+ stderr_part = if streamed[:stderr]
508
+ " (error output streamed away)"
509
+ elsif streams[:stderr].string.empty?
510
+ " (no error output)"
511
+ else
512
+ lines = streams[:stderr].string.split("\n")
513
+ ": " + lines.first + (lines.size > 1 ? " (...)" : "")
239
514
  end
240
515
 
241
- case options[:capture]
242
- when nil
516
+ raise ExecutionFailed.new(
517
+ commands,
518
+ status,
519
+ streamed[:stdout] ? nil : streams[:stdout].string,
520
+ streamed[:stderr] ? nil : streams[:stderr].string,
521
+ "Execution of #{format_commands(commands)} " +
522
+ "failed with status #{status.exitstatus}#{stderr_part}."
523
+ )
524
+ end
525
+
526
+ def build_result(streams, options)
527
+ case [options[:stdout] == :capture, options[:stderr] == :capture]
528
+ when [false, false]
243
529
  nil
244
- when :stdout
245
- stdout
246
- when :stderr
247
- stderr
248
- when [:stdout, :stderr]
249
- [stdout, stderr]
530
+ when [true, false]
531
+ streams[:stdout].string
532
+ when [false, true]
533
+ streams[:stderr].string
534
+ when [true, true]
535
+ [streams[:stdout].string, streams[:stderr].string]
250
536
  end
251
537
  end
252
538
 
253
- private
254
-
255
- def describe_args(args)
256
- args.empty? ? "no arguments" : "arguments #{args.map(&:inspect).join(", ")}"
539
+ def format_commands(commands)
540
+ '"' + commands.map { |c| Shellwords.join(c) }.join(" | ") + '"'
257
541
  end
258
542
  end
543
+
544
+ self.default_options = {}
259
545
  end
546
+
@@ -0,0 +1,4 @@
1
+ module Cheetah
2
+ # Cheetah version (uses [semantic versioning](http://semver.org/)).
3
+ VERSION = File.read(File.dirname(__FILE__) + "/../../VERSION").strip
4
+ end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cheetah
3
3
  version: !ruby/object:Gem::Version
4
- hash: 21
4
+ hash: 19
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
- - 2
9
- - 1
10
- version: 0.2.1
8
+ - 3
9
+ - 0
10
+ version: 0.3.0
11
11
  platform: ruby
12
12
  authors:
13
13
  - David Majda
@@ -15,13 +15,28 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2012-04-12 00:00:00 +02:00
18
+ date: 2012-06-21 00:00:00 +02:00
19
19
  default_executable:
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency
22
- name: rspec
22
+ name: abstract_method
23
23
  prerelease: false
24
24
  requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ hash: 11
30
+ segments:
31
+ - 1
32
+ - 2
33
+ version: "1.2"
34
+ type: :runtime
35
+ version_requirements: *id001
36
+ - !ruby/object:Gem::Dependency
37
+ name: rspec
38
+ prerelease: false
39
+ requirement: &id002 !ruby/object:Gem::Requirement
25
40
  none: false
26
41
  requirements:
27
42
  - - ">="
@@ -31,11 +46,11 @@ dependencies:
31
46
  - 0
32
47
  version: "0"
33
48
  type: :development
34
- version_requirements: *id001
49
+ version_requirements: *id002
35
50
  - !ruby/object:Gem::Dependency
36
51
  name: redcarpet
37
52
  prerelease: false
38
- requirement: &id002 !ruby/object:Gem::Requirement
53
+ requirement: &id003 !ruby/object:Gem::Requirement
39
54
  none: false
40
55
  requirements:
41
56
  - - ">="
@@ -45,11 +60,11 @@ dependencies:
45
60
  - 0
46
61
  version: "0"
47
62
  type: :development
48
- version_requirements: *id002
63
+ version_requirements: *id003
49
64
  - !ruby/object:Gem::Dependency
50
65
  name: yard
51
66
  prerelease: false
52
- requirement: &id003 !ruby/object:Gem::Requirement
67
+ requirement: &id004 !ruby/object:Gem::Requirement
53
68
  none: false
54
69
  requirements:
55
70
  - - ">="
@@ -59,8 +74,8 @@ dependencies:
59
74
  - 0
60
75
  version: "0"
61
76
  type: :development
62
- version_requirements: *id003
63
- description: Cheetah is a simple library for executing external commands safely and conveniently.
77
+ version_requirements: *id004
78
+ description: Your swiss army knife for executing external commands in Ruby safely and conveniently.
64
79
  email: dmajda@suse.de
65
80
  executables: []
66
81
 
@@ -74,6 +89,7 @@ files:
74
89
  - README.md
75
90
  - VERSION
76
91
  - lib/cheetah.rb
92
+ - lib/cheetah/version.rb
77
93
  has_rdoc: true
78
94
  homepage: https://github.com/openSUSE/cheetah
79
95
  licenses:
@@ -107,6 +123,6 @@ rubyforge_project:
107
123
  rubygems_version: 1.6.2
108
124
  signing_key:
109
125
  specification_version: 3
110
- summary: Simple library for executing external commands safely and conveniently
126
+ summary: Your swiss army knife for executing external commands in Ruby safely and conveniently.
111
127
  test_files: []
112
128