Cheetah

Travis Build Code Climate Coverage Status

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.

Authors