No commit activity in last 3 years
No release in over 3 years
Helper APIs for advanced interactions with subprocesses and shell commands
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies
 Project Readme

Command Runner NG, for Ruby

Travis CI Status: Travis CI Build Status

Provides advanced, but easy, control for subprocesses and shell commands in Ruby.

Features:

  • Run a command with a set of timeout rules

Usage

Add the following to your Gemfile:

gem 'command_runner_ng'

Examples

The following are equivalent:

require 'command_runner'

CommandRunner.run('sleep 10', timeout: 5) # Spawns a subshell
CommandRunner.run('sleep', '10', timeout: 5) # No subshell. Convenience API to avoid array boxing like next line:
CommandRunner.run(['sleep', '10'], timeout: 5) # No subshell in this one and the rest
CommandRunner.run(['sleep', '10'], timeout: {5 => 'KILL'})
CommandRunner.run(['sleep', '10'], timeout: {5 => Proc.new { |pid| Process.kill('KILL', pid)}})
CommandRunner.run(['sleep', '10'], timeout: {
    5 => 'KILL',
    2 => Proc.new {|pid| puts "Sending SIGKILL to PID #{pid} in 3s"}
})

Inspecting the output - observe that 'world' never gets printed:

require 'command_runner'
result = CommandRunner.run('echo hello; sleep 10; echo world', timeout: 3)
puts result
=> {:out=>"hello\n", :status=>#<Process::Status: pid 13205 SIGKILL (signal 9)>}

If you need to run the same command again and again, or the same command with a variation of sub-commands you can use the create method:

git = CommandRunner.create(['sudo', 'git'], timeout: 10, allowed_sub_commands: [:commit, :pull, :push])
git.run(:pull, 'origin', 'master')
git.run([:pull, 'origin', 'master'])
git.run(:pull, 'origin', 'master', timeout: 2) # override default timeout of 10
git.run(:status) # will raise an error because :status is not in list of allowed commands 

Redirection and Stderr Handling

By default stderr is merged into stdout. You can have it split out by passing split_stderr: true to the runner. Eg:

CommandRunner.run(['ls', '/nosuchdir'], split_stderr: true) # => {out: '', err: 'ls: No such file or directory' ... }

If you just want to silence stderr you can override all standard Kernel.spawn options:

CommandRunner.run(['ls', '/nosuchdir'], options: {err: '/dev/null})

Encoding

If you need to ensure safely encoded output - you can force UTF-8 encoding by passing the :safe symbol. Eg:

require 'command_runner'
result = CommandRunner.run('echo hellò', encoding: :safe) # => {out: 'hellò\n'}

By default we don't apply any encoding changed to the output.

Debugging and Logging

If you need insight to what commands you're running you can pass CommandRunner an object responding to :puts such as $stdout, $stderr, the writing end of an IO.pipe, or a file opened for writing. CommandRunnerNG will log start, stop, and timeouts. Eg:

require 'command_runner'

CommandRunner.run('ls /tmp', debug_log: $stdout)
# Outputs:
# CommandRunnerNG spawn: args=["ls /tmp"], timeout=, options: {:err=>[:child, :out]}, PID: 10973
# CommandRunnerNG exit: PID: 10973, code: 0

my_log = File.open('log.txt, 'a')
CommandRunner.run('ls /tmp', debug_log: my_log) # Log appended to a file

Or you can enable debug logging for all command line invocations like this:

CommandRunner.set_debug_log!($stderr)

Why?

We have used too many subtly broken approaches to handling child processes in many different projects. In particular wrt timeouts, but also other issues. There are numerous examples scattered around StackExchange and pastebins that are also subtly broken. This project tries to collect the pieces and provide a simple Bug Free (TM) API for doing the common tasks that are not straight forward on the bare Ruby Process API.