Skip to content

Advanced, but easy, control for subprocesses and shell commands in Ruby

License

Notifications You must be signed in to change notification settings

ddd-ruby/command_runner_ng

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

44 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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.

About

Advanced, but easy, control for subprocesses and shell commands in Ruby

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages

  • Ruby 100.0%