cheetah 0.2.1 → 0.3.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 (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