Cheetah
Your swiss army knife for executing external commands in Ruby safely and conveniently.
Examples
# Run a command and capture its output
files = Cheetah.run("ls", "-la", stdout: :capture)
# Run a command and capture its output into a stream
File.open("files.txt", "w") do |stdout|
Cheetah.run("ls", "-la", stdout: stdout)
end
# Run a command and handle errors
begin
Cheetah.run("rm", "/etc/passwd")
rescue Cheetah::ExecutionFailed => e
puts e.message
puts "Standard output: #{e.stdout}"
puts "Error output: #{e.stderr}"
end
Features
- Easy passing of command input
- Easy capturing of command output (standard, error, or both)
- Piping commands together
- 100% secure (shell expansion is impossible by design)
- Raises exceptions on errors (no more manual status code checks) but allows to specify which non-zero codes are not an error
- Thread-safety
- Allows overriding environment variables
- Optional logging for easy debugging
- Running on changed root ( requires chroot permission )
Non-features
- Handling of interactive commands
Installation
$ gem install cheetah
Usage
First, require the library:
require "cheetah"
You can now use the Cheetah.run
method to run commands.
Running Commands
To run a command, just specify it together with its arguments:
Cheetah.run("tar", "xzf", "foo.tar.gz")
Cheetah converts each argument to a string using `#to_s`.
Passing Input
Using the :stdin
option you can pass a string to command's standard input:
Cheetah.run("python", stdin: source_code)
If the input is big you may want to avoid passing it in one huge string. In that
case, pass an IO
as a value of the :stdin
option. The command will read its
input from it gradually.
File.open("huge_program.py") do |stdin|
Cheetah.run("python", stdin: stdin)
end
Capturing Output
To capture command's standard output, set the :stdout
option to :capture
.
You will receive the output as a return value of the call:
files = Cheetah.run("ls", "-la", stdout: :capture)
The same technique works with the error output — just use the :stderr
option.
If you specify capturing of both outputs, the return value will be a two-element
array:
results, errors = Cheetah.run("grep", "-r", "User", ".", stdout: => :capture, stderr: => :capture)
If the output is big you may want to avoid capturing it into a huge string. In
that case, pass an IO
as a value of the :stdout
or :stderr
option. The
command will write its output into it gradually.
File.open("files.txt", "w") do |stdout|
Cheetah.run("ls", "-la", stdout: stdout)
end
Piping Commands
You can pipe multiple commands together and execute them as one. Just specify the commands together with their arguments as arrays:
processes = Cheetah.run(["ps", "aux"], ["grep", "ruby"], stdout: :capture)
Error Handling
If the command can't be executed for some reason or returns an unexpected non-zero exit status, Cheetah raises an exception with detailed information about the failure:
# Run a command and handle errors
begin
Cheetah.run("rm", "/etc/passwd")
rescue Cheetah::ExecutionFailed => e
puts e.message
puts "Standard output: #{e.stdout}"
puts "Error output: #{e.stderr}"
puts "Exit status: #{e.status.exitstatus}"
end
Logging
For debugging purposes, you can use a logger. Cheetah will log the command, its status, input and both outputs to it:
Cheetah.run("ls -l", logger: logger)
Overwriting env
If the command needs adapted environment variables, use the :env option. Passed hash is used to update existing env (for details see ENV.update). Nil value means unset variable. Environment is restored to its original state after running the command.
Cheetah.run("env", env: { "LC_ALL" => "C" })
Expecting Non-zero Exit Status
If command is expected to return valid a non-zero exit status like grep
command
which return 1
if given regexp is not found, then option :allowed_exitstatus
can be used:
# Run a command, handle exitstatus and handle errors
begin
exitstatus = Cheetah.run("grep", "userA", "/etc/passwd", allowed_exitstatus: 1)
if exitstates == 0
puts "found"
else
puts "not found"
end
rescue Cheetah::ExecutionFailed => e
puts e.message
puts "Standard output: #{e.stdout}"
puts "Error output: #{e.stderr}"
puts "Exit status: #{e.status.exitstatus}"
end
Exit status is returned as last element of result. If it is only captured thing,
then it is return without array.
Supported input for allowed_exitstatus
are anything supporting include, fixnum
or nil for no allowed existatus.
# allowed inputs
allowed_exitstatus: 1
allowed_exitstatus: 1..5
allowed_exitstatus: [1, 2]
allowed_exitstatus: object_with_include_method
allowed_exitstatus: nil
Setting Defaults
To avoid repetition, you can set global default value of any option passed too
Cheetah.run
:
# If you're tired of passing the :logger option all the time...
Cheetah.default_options = { :logger => my_logger }
Cheetah.run("./configure")
Cheetah.run("make")
Cheetah.run("make", "install")
Cheetah.default_options = {}
Changing Working Directory
If diferent working directory is needed for running program, then suggested
usage is to enclose call into Dir.chdir
method.
Dir.chdir("/workspace") do
Cheetah.run("make")
end
Changing System Root
If a command needs to be executed in different system root then the :chroot
option can be used:
Cheetah.run("/usr/bin/inspect", chroot: "/mnt/target_system")
More Information
For more information, see the API documentation.
Compatibility
Cheetah should run well on any Unix system with Ruby 2.0.0, 2.1 and 2.2. Non-Unix systems and different Ruby implementations/versions may work too but they were not tested.