cheetah 0.1.0 → 0.2.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 (5) hide show
  1. data/CHANGELOG +12 -0
  2. data/README.md +91 -2
  3. data/VERSION +1 -1
  4. data/lib/cheetah.rb +195 -158
  5. metadata +21 -7
data/CHANGELOG CHANGED
@@ -1,3 +1,15 @@
1
+ 0.2.0 (2012-04-05)
2
+ ------------------
3
+
4
+ * Logger can be set globally.
5
+ * Use :info and :error levels for logging instead of :debug.
6
+ * Added API documentation.
7
+ * Added proper README.md.
8
+ * Updated gem description.
9
+ * Rewrote tests into RSpec.
10
+ * Improved performance for commands with big outputs.
11
+ * Internal code improvements.
12
+
1
13
  0.1.0 (2012-03-23)
2
14
  ------------------
3
15
 
data/README.md CHANGED
@@ -1,6 +1,95 @@
1
1
  Cheetah
2
2
  =======
3
3
 
4
- Cheetah is a simple library for executing external commands safely and conveniently. It is meant as a safe replacement of `backticks`, Kernel#system and similar methods, which are often used in unsecure way (they allow shell expansion of commands).
4
+ Cheetah is a simple library for executing external commands safely and conveniently.
5
5
 
6
- Proper documentation is coming soon.
6
+ Examples
7
+ --------
8
+
9
+ ```ruby
10
+ # Run a command and capture its output
11
+ files = Cheetah.run("ls", "-la", :capture => :stdout)
12
+
13
+ # Run a command and handle errors
14
+ begin
15
+ Cheetah.run("rm", "/etc/passwd")
16
+ rescue Cheetah::ExecutionFailed => e
17
+ puts e.message
18
+ puts "Standard output: #{e.stdout}"
19
+ puts "Error ouptut: #{e.stderr}"
20
+ end
21
+ ```
22
+
23
+ Features
24
+ --------
25
+
26
+ * Easy passing of command input
27
+ * Easy capturing of command output (standard, error, or both)
28
+ * 100% secure (shell expansion is impossible by design)
29
+ * Raises exceptions on errors (no more manual status code checks)
30
+ * Optional logging for easy debugging
31
+
32
+ Non-features
33
+ ------------
34
+
35
+ * Handling of commands producing big outputs
36
+ * Handling of interactive commands
37
+
38
+ Installation
39
+ ------------
40
+
41
+ $ gem install cheetah
42
+
43
+ Usage
44
+ -----
45
+
46
+ First, require the library:
47
+
48
+ ```ruby
49
+ require "cheetah"
50
+ ```
51
+
52
+ You can now use the `Cheetah.run` method to run commands, pass them an input and capture their output:
53
+
54
+ ```ruby
55
+ # Run a command with arguments
56
+ Cheetah.run("tar", "xzf", "foo.tar.gz")
57
+
58
+ # Pass an input
59
+ Cheetah.run("python", :stdin => source_code)
60
+
61
+ # Capture standard output
62
+ files = Cheetah.run("ls", "-la", :capture => :stdout)
63
+
64
+ # Capture both standard and error output
65
+ results, errors = Cheetah.run("grep", "-r", "User", ".", :capture => [:stdout, :stderr))
66
+ ```
67
+
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:
69
+
70
+ ```ruby
71
+ # Run a command and handle errors
72
+ begin
73
+ Cheetah.run("rm", "/etc/passwd")
74
+ rescue Cheetah::ExecutionFailed => e
75
+ puts e.message
76
+ puts "Standard output: #{e.stdout}"
77
+ puts "Error ouptut: #{e.stderr}"
78
+ end
79
+ ```
80
+
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):
82
+
83
+ ```ruby
84
+ # Log the execution
85
+ Cheetah.run("ls -l", :logger => logger)
86
+
87
+ # Or, if you're tired of passing the :logger option all the time...
88
+ Cheetah.logger = my_logger
89
+ Cheetah.run("./configure")
90
+ Cheetah.run("make")
91
+ Cheetah.run("make", "install")
92
+ Cheetah.logger = nil
93
+ ```
94
+
95
+ For more information, see the [API documentation](http://rubydoc.info/github/openSUSE/cheetah/frames).
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.0
1
+ 0.2.0
@@ -1,11 +1,58 @@
1
- # Contains methods for executing external commands safely and conveniently.
1
+ # A simple library for executing external commands safely and conveniently.
2
+ #
3
+ # ## Features
4
+ #
5
+ # * Easy passing of command input
6
+ # * Easy capturing of command output (standard, error, or both)
7
+ # * 100% secure (shell expansion is impossible by design)
8
+ # * Raises exceptions on errors (no more manual status code checks)
9
+ # * Optional logging for easy debugging
10
+ #
11
+ # ## Non-features
12
+ #
13
+ # * Handling of commands producing big outputs
14
+ # * Handling of interactive commands
15
+ #
16
+ # @example Run a command and capture its output
17
+ # files = Cheetah.run("ls", "-la", :capture => :stdout)
18
+ #
19
+ # @example Run a command and handle errors
20
+ # begin
21
+ # Cheetah.run("rm", "/etc/passwd")
22
+ # rescue Cheetah::ExecutionFailed => e
23
+ # puts e.message
24
+ # puts "Standard output: #{e.stdout}"
25
+ # puts "Error ouptut: #{e.stderr}"
26
+ # end
2
27
  module Cheetah
28
+ # Cheetah version (uses [semantic versioning](http://semver.org/)).
3
29
  VERSION = File.read(File.dirname(__FILE__) + "/../VERSION").strip
4
30
 
5
31
  # Exception raised when a command execution fails.
6
32
  class ExecutionFailed < StandardError
7
- attr_reader :command, :args, :status, :stdout, :stderr
33
+ # @return [String] the executed command
34
+ attr_reader :command
8
35
 
36
+ # @return [Array<String>] the executed command arguments
37
+ attr_reader :args
38
+
39
+ # @return [Process::Status] the executed command exit status
40
+ attr_reader :status
41
+
42
+ # @return [String] the output the executed command wrote to stdout
43
+ attr_reader :stdout
44
+
45
+ # @return [String] the output the executed command wrote to stderr
46
+ attr_reader :stderr
47
+
48
+ # Initializes a new {ExecutionFailed} instance.
49
+ #
50
+ # @param [String] command the executed command
51
+ # @param [Array<String>] args the executed command arguments
52
+ # @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
55
+ # @param [String, nil] message the exception message
9
56
  def initialize(command, args, status, stdout, stderr, message = nil)
10
57
  super(message)
11
58
  @command = command
@@ -16,142 +63,135 @@ module Cheetah
16
63
  end
17
64
  end
18
65
 
19
- # Runs an external command, optionally capturing its output. Meant as a safe
20
- # replacement of `backticks`, Kernel#system and similar methods, which are
21
- # often used in unsecure way. (They allow shell expansion of commands, which
22
- # often means their arguments need proper escaping. The problem is that people
23
- # forget to do it or do it badly, causing serious security issues.)
24
- #
25
- # Examples:
26
- #
27
- # # Run a command, grab its output and handle failures.
28
- # files = nil
29
- # begin
30
- # files = Cheetah.run("ls", "-la", :capture => :stdout)
31
- # rescue Cheetah::ExecutionFailed => e
32
- # puts "Command #{e.command} failed with status #{e.status}."
33
- # end
34
- #
35
- # # Log the executed command, it's status, input and both outputs into
36
- # # user-supplied logger.
37
- # Cheetah.run("qemu-kvm", "foo.raw", :logger => my_logger)
38
- #
39
- # The first parameter specifies the command to run, the remaining parameters
40
- # specify its arguments. It is also possible to specify both the command and
41
- # arguments in the first parameter using an array. If the last parameter is a
42
- # hash, it specifies options.
43
- #
44
- # For security reasons, the command never goes through shell expansion even if
45
- # only one parameter is specified (i.e. the method does do not adhere to the
46
- # convention used by other Ruby methods for launching external commands, e.g.
47
- # Kernel#system).
48
- #
49
- # If the command execution succeeds, the returned value depends on the
50
- # value of the :capture option (see below). If it fails (the command is not
51
- # executed for some reason or returns a non-zero exit status), the method
52
- # raises a ExecutionFailed exception with detailed information about the
53
- # failure.
54
- #
55
- # Options:
56
- #
57
- # :capture - configures which output(s) the method captures and returns, the
58
- # valid values are:
59
- #
60
- # - nil - no output is captured and returned
61
- # (the default)
62
- # - :stdout - standard output is captured and
63
- # returned as a string
64
- # - :stderr - error output is captured and returned
65
- # as a string
66
- # - [:stdout, :stderr] - both outputs are captured and returned
67
- # as a two-element array of strings
68
- #
69
- # :stdin - if specified, it is a string sent to command's standard input
70
- #
71
- # :logger - if specified, the method will log the command, its status, input
72
- # and both outputs to passed logger at the "debug" level
73
- #
74
- def self.run(command, *args)
75
- options = args.last.is_a?(Hash) ? args.pop : {}
76
-
77
- capture = options[:capture]
78
- stdin = options[:stdin] || ""
79
- logger = options[:logger]
80
-
81
- if command.is_a?(Array)
82
- args = command[1..-1]
83
- command = command.first
84
- end
66
+ 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.
69
+ #
70
+ # @return [Logger, nil] the global logger
71
+ attr_accessor :logger
85
72
 
86
- pass_stdin = !stdin.empty?
87
- pipe_stdin_read, pipe_stdin_write = pass_stdin ? IO.pipe : [nil, nil]
73
+ # Runs an external command with specified arguments, optionally passing it
74
+ # an input and capturing its output.
75
+ #
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.
80
+ #
81
+ # The command and its arguments never undergo shell expansion — they are
82
+ # passed directly to the operating system. While this may create some
83
+ # inconvenience in certain cases, it eliminates a whole class of security
84
+ # bugs.
85
+ #
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).
92
+ #
93
+ # @overload run(command, *args, options = {})
94
+ # @param [String] command the command to execute
95
+ # @param [Array<String>] args the command arguments
96
+ # @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:
100
+ #
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
106
+ # @option options [Logger, nil] :logger (nil) logger to log the command
107
+ # execution; if set, overrides the global logger (set by
108
+ # {Cheetah.logger})
109
+ #
110
+ # @overload run(command_and_args, options = {})
111
+ # This variant is useful mainly when building the command and its
112
+ # arguments programmatically.
113
+ #
114
+ # @param [Array<String>] command_and_args the command to execute (first
115
+ # element of the array) and its arguments (remaining elements)
116
+ # @param [Hash] options the options to execute the command with, same as
117
+ # in the first variant
118
+ #
119
+ # @raise [ExecutionFailed] when the command can't be executed for some
120
+ # reason or returns a non-zero exit status
121
+ #
122
+ # @example Run a command and capture its output
123
+ # files = Cheetah.run("ls", "-la", :capture => :stdout)
124
+ #
125
+ # @example Run a command and handle errors
126
+ # begin
127
+ # Cheetah.run("rm", "/etc/passwd")
128
+ # rescue Cheetah::ExecutionFailed => e
129
+ # puts e.message
130
+ # puts "Standard output: #{e.stdout}"
131
+ # puts "Error ouptut: #{e.stderr}"
132
+ # end
133
+ def run(command, *args)
134
+ options = args.last.is_a?(Hash) ? args.pop : {}
88
135
 
89
- capture_stdout = [:stdout, [:stdout, :stderr]].include?(capture) || logger
90
- pipe_stdout_read, pipe_stdout_write = capture_stdout ? IO.pipe : [nil, nil]
136
+ stdin = options[:stdin] || ""
137
+ logger = options[:logger] || @logger
91
138
 
92
- capture_stderr = [:stderr, [:stdout, :stderr]].include?(capture) || logger
93
- pipe_stderr_read, pipe_stderr_write = capture_stderr ? IO.pipe : [nil, nil]
139
+ if command.is_a?(Array)
140
+ args = command[1..-1]
141
+ command = command.first
142
+ end
94
143
 
95
- if logger
96
- logger.debug "Executing command #{command.inspect} with #{describe_args(args)}."
97
- logger.debug "Standard input: " + (stdin.empty? ? "(none)" : stdin)
98
- end
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
99
147
 
100
- pid = fork do
101
- begin
102
- if pass_stdin
148
+ if logger
149
+ logger.info "Executing command #{command.inspect} with #{describe_args(args)}."
150
+ logger.info "Standard input: " + (stdin.empty? ? "(none)" : stdin)
151
+ end
152
+
153
+ pid = fork do
154
+ begin
103
155
  pipe_stdin_write.close
104
156
  STDIN.reopen(pipe_stdin_read)
105
157
  pipe_stdin_read.close
106
- else
107
- STDIN.reopen("/dev/null", "r")
108
- end
109
158
 
110
- if capture_stdout
111
159
  pipe_stdout_read.close
112
160
  STDOUT.reopen(pipe_stdout_write)
113
161
  pipe_stdout_write.close
114
- else
115
- STDOUT.reopen("/dev/null", "w")
116
- end
117
162
 
118
- if capture_stderr
119
163
  pipe_stderr_read.close
120
164
  STDERR.reopen(pipe_stderr_write)
121
165
  pipe_stderr_write.close
122
- else
123
- STDERR.reopen("/dev/null", "w")
124
- end
125
166
 
126
- # All file descriptors from 3 above should be closed here, but since I
127
- # don't know about any way how to detect the maximum file descriptor
128
- # number portably in Ruby, I didn't implement it. Patches welcome.
167
+ # All file descriptors from 3 above should be closed here, but since I
168
+ # don't know about any way how to detect the maximum file descriptor
169
+ # number portably in Ruby, I didn't implement it. Patches welcome.
129
170
 
130
- exec([command, command], *args)
131
- rescue SystemCallError => e
132
- exit!(127)
171
+ exec([command, command], *args)
172
+ rescue SystemCallError => e
173
+ exit!(127)
174
+ end
133
175
  end
134
- end
135
-
136
- [pipe_stdin_read, pipe_stdout_write, pipe_stderr_write].each { |p| p.close if p }
137
176
 
138
- # We write the command's input and read its output using a select loop. Why?
139
- # Because otherwise we could end up with a deadlock.
140
- #
141
- # Imagine if we first read the whole standard output and then the whole
142
- # error output, but the executed command would write lot of data but only to
143
- # the error output. Sooner or later it would fill the buffer and block,
144
- # while we would be blocked on reading the standard output -- classic
145
- # deadlock.
146
- #
147
- # Similar issues can happen with standard input vs. one of the outputs.
148
- if pass_stdin || capture_stdout || capture_stderr
149
- stdout = ""
150
- stderr = ""
177
+ [pipe_stdin_read, pipe_stdout_write, pipe_stderr_write].each { |p| p.close }
151
178
 
179
+ # We write the command's input and read its output using a select loop.
180
+ # Why? Because otherwise we could end up with a deadlock.
181
+ #
182
+ # Imagine if we first read the whole standard output and then the whole
183
+ # error output, but the executed command would write lot of data but only
184
+ # to the error output. Sooner or later it would fill the buffer and block,
185
+ # while we would be blocked on reading the standard output -- classic
186
+ # deadlock.
187
+ #
188
+ # 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]
152
192
  loop do
153
- pipes_readable = [pipe_stdout_read, pipe_stderr_read].compact.select { |p| !p.closed? }
154
- pipes_writable = [pipe_stdin_write].compact.select { |p| !p.closed? }
193
+ pipes_readable.reject!(&:closed?)
194
+ pipes_writable.reject!(&:closed?)
155
195
 
156
196
  break if pipes_readable.empty? && pipes_writable.empty?
157
197
 
@@ -162,61 +202,58 @@ module Cheetah
162
202
  raise IOError, "Error when communicating with executed program."
163
203
  end
164
204
 
165
- if ios_read.include?(pipe_stdout_read)
205
+ ios_read.each do |pipe|
166
206
  begin
167
- stdout += pipe_stdout_read.readpartial(4096)
207
+ outputs[pipe] << pipe.readpartial(4096)
168
208
  rescue EOFError
169
- pipe_stdout_read.close
209
+ pipe.close
170
210
  end
171
211
  end
172
212
 
173
- if ios_read.include?(pipe_stderr_read)
174
- begin
175
- stderr += pipe_stderr_read.readpartial(4096)
176
- rescue EOFError
177
- pipe_stderr_read.close
178
- end
179
- end
180
-
181
- if ios_write.include?(pipe_stdin_write)
182
- n = pipe_stdin_write.syswrite(stdin)
213
+ ios_write.each do |pipe|
214
+ n = pipe.syswrite(stdin)
183
215
  stdin = stdin[n..-1]
184
- pipe_stdin_write.close if stdin.empty?
216
+ pipe.close if stdin.empty?
185
217
  end
186
218
  end
187
- end
188
219
 
189
- pid, status = Process.wait2(pid)
190
- begin
191
- if !status.success?
192
- raise ExecutionFailed.new(command, args, status,
193
- capture_stdout ? stdout : nil,
194
- capture_stderr ? stderr : nil,
195
- "Execution of command #{command.inspect} " +
196
- "with #{describe_args(args)} " +
197
- "failed with status #{status.exitstatus}.")
220
+ stdout = outputs[pipe_stdout_read]
221
+ stderr = outputs[pipe_stderr_read]
222
+
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.log status.success? ? Logger::INFO : Logger::ERROR,
234
+ "Status: #{status.exitstatus}"
235
+ logger.info "Standard output: " + (stdout.empty? ? "(none)" : stdout)
236
+ logger.log stderr.empty? ? Logger::INFO : Logger::ERROR,
237
+ "Error output: " + (stderr.empty? ? "(none)" : stderr)
238
+ end
198
239
  end
199
- ensure
200
- if logger
201
- logger.debug "Status: #{status.exitstatus}"
202
- logger.debug "Standard output: " + (stdout.empty? ? "(none)" : stdout)
203
- logger.debug "Error output: " + (stderr.empty? ? "(none)" : stderr)
240
+
241
+ case options[:capture]
242
+ when nil
243
+ nil
244
+ when :stdout
245
+ stdout
246
+ when :stderr
247
+ stderr
248
+ when [:stdout, :stderr]
249
+ [stdout, stderr]
204
250
  end
205
251
  end
206
252
 
207
- case capture
208
- when nil
209
- nil
210
- when :stdout
211
- stdout
212
- when :stderr
213
- stderr
214
- when [:stdout, :stderr]
215
- [stdout, stderr]
216
- end
217
- end
253
+ private
218
254
 
219
- def self.describe_args(args)
220
- args.empty? ? "no arguments" : "arguments #{args.map(&:inspect).join(", ")}"
255
+ def describe_args(args)
256
+ args.empty? ? "no arguments" : "arguments #{args.map(&:inspect).join(", ")}"
257
+ end
221
258
  end
222
259
  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: 27
4
+ hash: 23
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
- - 1
8
+ - 2
9
9
  - 0
10
- version: 0.1.0
10
+ version: 0.2.0
11
11
  platform: ruby
12
12
  authors:
13
13
  - David Majda
@@ -15,11 +15,11 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2012-03-23 00:00:00 +01:00
18
+ date: 2012-04-05 00:00:00 +02:00
19
19
  default_executable:
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency
22
- name: shoulda-context
22
+ name: rspec
23
23
  prerelease: false
24
24
  requirement: &id001 !ruby/object:Gem::Requirement
25
25
  none: false
@@ -33,7 +33,7 @@ dependencies:
33
33
  type: :development
34
34
  version_requirements: *id001
35
35
  - !ruby/object:Gem::Dependency
36
- name: mocha
36
+ name: redcarpet
37
37
  prerelease: false
38
38
  requirement: &id002 !ruby/object:Gem::Requirement
39
39
  none: false
@@ -46,7 +46,21 @@ dependencies:
46
46
  version: "0"
47
47
  type: :development
48
48
  version_requirements: *id002
49
- description: Cheetah is a simple library for executing external commands safely and conveniently. It is meant as a safe replacement of `backticks`, Kernel#system and similar methods, which are often used in unsecure way (they allow shell expansion of commands).
49
+ - !ruby/object:Gem::Dependency
50
+ name: yard
51
+ prerelease: false
52
+ requirement: &id003 !ruby/object:Gem::Requirement
53
+ none: false
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ hash: 3
58
+ segments:
59
+ - 0
60
+ version: "0"
61
+ type: :development
62
+ version_requirements: *id003
63
+ description: Cheetah is a simple library for executing external commands safely and conveniently.
50
64
  email: dmajda@suse.de
51
65
  executables: []
52
66