cheetah 0.1.0 → 0.2.0

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