cheetah 0.2.1 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
|