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.
- data/CHANGELOG +18 -0
- data/README.md +97 -17
- data/VERSION +1 -1
- data/lib/cheetah.rb +401 -114
- data/lib/cheetah/version.rb +4 -0
- 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
|
-
|
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", :
|
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
|
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
|
-
|
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
|
-
|
62
|
-
files = Cheetah.run("ls", "-la", :
|
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
|
-
|
65
|
-
results, errors = Cheetah.run("grep", "-r", "User", ".", :
|
98
|
+
```ruby
|
99
|
+
results, errors = Cheetah.run("grep", "-r", "User", ".", :stdout => :capture, :stderr => :capture)
|
66
100
|
```
|
67
101
|
|
68
|
-
If the
|
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
|
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
|
-
|
88
|
-
|
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.
|
156
|
+
Cheetah.default_options = {}
|
93
157
|
```
|
94
158
|
|
95
|
-
|
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.
|
1
|
+
0.3.0
|
data/lib/cheetah.rb
CHANGED
@@ -1,20 +1,33 @@
|
|
1
|
-
|
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", :
|
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
|
34
|
-
|
35
|
-
|
36
|
-
|
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]
|
51
|
-
#
|
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(
|
68
|
+
def initialize(commands, status, stdout, stderr, message = nil)
|
57
69
|
super(message)
|
58
|
-
@
|
59
|
-
@
|
60
|
-
@
|
61
|
-
@
|
62
|
-
|
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
|
68
|
-
#
|
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 [
|
71
|
-
attr_accessor :
|
201
|
+
# @return [Hash] the default options of the {Cheetah.run} method
|
202
|
+
attr_accessor :default_options
|
72
203
|
|
73
|
-
# Runs
|
74
|
-
# an input and capturing its output.
|
204
|
+
# Runs external command(s) with specified arguments.
|
75
205
|
#
|
76
|
-
# If the
|
77
|
-
#
|
78
|
-
#
|
79
|
-
#
|
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
|
-
#
|
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
|
87
|
-
#
|
88
|
-
#
|
89
|
-
#
|
90
|
-
#
|
91
|
-
#
|
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
|
98
|
-
#
|
99
|
-
#
|
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
|
-
#
|
102
|
-
#
|
103
|
-
#
|
104
|
-
#
|
105
|
-
#
|
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
|
108
|
-
#
|
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
|
-
#
|
112
|
-
# arguments
|
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
|
-
#
|
120
|
-
#
|
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", :
|
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(
|
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
|
-
|
137
|
-
|
324
|
+
recorder.record_commands(commands)
|
325
|
+
recorder.record_stdin(streams[:stdin].string) unless streamed[:stdin]
|
138
326
|
|
139
|
-
|
140
|
-
|
141
|
-
|
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
|
-
|
145
|
-
|
146
|
-
|
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
|
-
|
149
|
-
|
150
|
-
|
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
|
-
|
394
|
+
def fork_commands_recursive(commands, pipes)
|
395
|
+
fork do
|
154
396
|
begin
|
155
|
-
|
156
|
-
|
157
|
-
|
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
|
-
|
160
|
-
|
161
|
-
|
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
|
-
|
164
|
-
|
165
|
-
|
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
|
-
|
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
|
-
|
190
|
-
|
191
|
-
|
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
|
-
|
215
|
-
|
216
|
-
|
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
|
-
|
221
|
-
|
504
|
+
def check_errors(commands, status, streams, streamed)
|
505
|
+
return if status.success?
|
222
506
|
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
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
|
-
|
242
|
-
|
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
|
245
|
-
stdout
|
246
|
-
when
|
247
|
-
stderr
|
248
|
-
when [
|
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
|
-
|
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
|
+
|
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:
|
4
|
+
hash: 19
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
|
-
-
|
9
|
-
-
|
10
|
-
version: 0.
|
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-
|
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:
|
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: *
|
49
|
+
version_requirements: *id002
|
35
50
|
- !ruby/object:Gem::Dependency
|
36
51
|
name: redcarpet
|
37
52
|
prerelease: false
|
38
|
-
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: *
|
63
|
+
version_requirements: *id003
|
49
64
|
- !ruby/object:Gem::Dependency
|
50
65
|
name: yard
|
51
66
|
prerelease: false
|
52
|
-
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: *
|
63
|
-
description:
|
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:
|
126
|
+
summary: Your swiss army knife for executing external commands in Ruby safely and conveniently.
|
111
127
|
test_files: []
|
112
128
|
|