Module: ProcessExecuter

Defined in:
lib/process_executer.rb,
lib/process_executer/status.rb,
lib/process_executer/command.rb,
lib/process_executer/options.rb,
lib/process_executer/version.rb,
lib/process_executer/command/errors.rb,
lib/process_executer/command/result.rb,
lib/process_executer/command/runner.rb,
lib/process_executer/monitored_pipe.rb

Overview

rubocop:disable Layout/LineLength

Defined Under Namespace

Modules: Command Classes: MonitoredPipe, Options, Status

Constant Summary collapse

VERSION =

The current Gem version

API:

  • public

'1.3.0'

Class Method Summary collapse

Class Method Details

.run(*command, logger: Logger.new(nil), **options_hash) ⇒ ProcessExecuter::Command::Result

Execute the given command as a subprocess, blocking until it finishes

Returns a result object which includes the process's status and output.

Supports the same features as Process.spawn. In addition, it:

  1. Blocks until the command exits
  2. Captures stdout and stderr to a buffer or file
  3. Optionally kills the command if it exceeds a timeout

This method takes two forms:

  1. The command is executed via a shell when the command is given as a single string:

    ProcessExecuter.run([env, ] command_line, options = {}) -> ProcessExecuter::Command::Result

  2. The command is executed directly (bypassing the shell) when the command and it arguments are given as an array of strings:

    ProcessExecuter.run([env, ] exe_path, *args, options = {}) -> ProcessExecuter::Command::Result

Optional argument env is a hash that affects ENV for the new process; see Execution Environment.

Argument options is a hash of options for the new process. See the options listed below.

Examples:

Run a command given as a single string (uses shell)

# The command must be properly shell escaped when passed as a single string.
command = 'echo "stdout: `pwd`"" && echo "stderr: $HOME" 1>&2'
result = ProcessExecuter.run(command)
result.success? #=> true
result.stdout.string #=> "stdout: /Users/james/projects/main-branch/process_executer\n"
result.stderr.string #=> "stderr: /Users/james\n"

Run a command given as an array of strings (does not use shell)

# The command and its args must be provided as separate strings in the array.
# Shell expansions and redirections are not supported.
command = ['git', 'clone', 'https://github.com/main-branch/process_executer']
result = ProcessExecuter.run(*command)
result.success? #=> true
result.stdout.string #=> ""
result.stderr.string #=> "Cloning into 'process_executer'...\n"

Run a command with a timeout

command = ['sleep', '1']
result = ProcessExecuter.run(*command, timeout: 0.01)
#=> raises ProcessExecuter::Command::TimeoutError which contains the command result

Run a command which fails

command = ['exit 1']
result = ProcessExecuter.run(*command)
#=> raises ProcessExecuter::Command::FailedError which contains the command result

Run a command which exits due to an unhandled signal

command = ['kill -9 $$']
result = ProcessExecuter.run(*command)
#=> raises ProcessExecuter::Command::SignaledError which contains the command result

Return a result instead of raising an error when raise_errors is false

# By setting `raise_errors` to `false`, exceptions will not be raised even
# if the command fails.
command = ['echo "Some error" 1>&2 && exit 1']
result = ProcessExecuter.run(*command, raise_errors: false)
# An error is not raised
result.success? #=> false
result.exitstatus #=> 1
result.stdout.string #=> ""
result.stderr.string #=> "Some error\n"

Set environment variables

env = { 'FOO' => 'foo', 'BAR' => 'bar' }
command = 'echo "$FOO$BAR"'
result = ProcessExecuter.run(env, *command)
result.stdout.string #=> "foobar\n"

Set environment variables when using a command array

env = { 'GIT_DIR' => '/path/to/.git' }
command = ['git', 'status']
result = ProcessExecuter.run(env, *command)
result.stdout.string #=> "On branch main\nYour branch is ..."

Unset environment variables

env = { 'GIT_DIR' => nil } # setting to nil unsets the variable in the environment
command = ['git', 'status']
result = ProcessExecuter.run(env, *command)
result.stdout.string #=> "On branch main\nYour branch is ..."

Reset existing environment variables and add new ones

env = { 'PATH' => '/bin' }
result = ProcessExecuter.run(env, 'echo "Home: $HOME" && echo "Path: $PATH"', unsetenv_others: true)
result.stdout.string #=> "Home: \n/Path: /bin\n"

Run command in a different directory

command = ['pwd']
result = ProcessExecuter.run(*command, chdir: '/tmp')
result.stdout.string #=> "/tmp\n"

Capture stdout and stderr into a single buffer

command = ['echo "stdout" && echo "stderr" 1>&2']
result = ProcessExecuter.run(*command, merge: true)
result.stdout.string #=> "stdout\nstderr\n"
result.stdout.object_id == result.stderr.object_id #=> true

Capture to an explicit buffer

out = StringIO.new
err = StringIO.new
command = ['echo "stdout" && echo "stderr" 1>&2']
result = ProcessExecuter.run(*command, out: out, err: err)
out.string #=> "stdout\n"
err.string #=> "stderr\n"
result.stdout.object_id == out.object_id #=> true
result.stderr.object_id == err.object_id #=> true

Capture to a file

# Same technique can be used for stderr
out = File.open('stdout.txt', 'w')
command = ['echo "stdout" && echo "stderr" 1>&2']
result = ProcessExecuter.run(*command, out: out, err: err)
out.close
File.read('stdout.txt') #=> "stdout\n"
# stderr is still captured to a StringIO buffer internally
result.stderr.string #=> "stderr\n"

Capture to multiple writers (e.g. files, buffers, STDOUT, etc.)

# Same technique can be used for stderr
out_buffer = StringIO.new
out_file = File.open('stdout.txt', 'w')
command = ['echo "stdout" && echo "stderr" 1>&2']
result = ProcessExecuter.run(*command, out: [out_buffer, out_file])
# You must manage closing resources you create yourself
out_file.close
out_buffer.string #=> "stdout\n"
File.read('stdout.txt') #=> "stdout\n"

Options Hash (**options_hash):

  • :timeout (Numeric)

    The maximum seconds to wait for the command to complete

    If timeout is zero or nil, the command will not time out. If the command times out, it is killed via a SIGKILL signal and ProcessExecuter::Command::TimeoutError is raised.

    If the command does not exit when receiving the SIGKILL signal, this method may hang indefinitely.

  • :out (#write) — default: nil

    The object to write stdout to

  • :err (#write) — default: nil

    The object to write stderr to

  • :merge (Boolean) — default: false

    If true, stdout and stderr are written to the same capture buffer

  • :raise_errors (Boolean) — default: true

    Raise an exception if the command fails

  • :unsetenv_others (Boolean) — default: false

    If true, unset all environment variables before applying the new ones

  • :pgroup (true, Integer, nil) — default: nil

    true or 0: new process group; non-zero: join the group, nil: existing group

  • :new_pgroup (Boolean) — default: nil

    Create a new process group (Windows only)

  • :rlimit_resource_name (Integer) — default: nil

    Set resource limits (see Process.setrlimit)

  • :umask (Integer) — default: nil

    Set the umask (see File.umask)

  • :close_others (Boolean) — default: false

    If true, close non-standard file descriptors

  • :chdir (String) — default: nil

    The directory to run the command in

Raises:

  • if the command returned a non-zero exit status

  • if the command exited because of an unhandled signal

  • if the command timed out

  • if an exception was raised while collecting subprocess output

Parameters:

  • The command to run

    If the first element of command is a Hash, it is added to the ENV of the new process. See Execution Environment for more details. The env hash is then removed from the command array.

    If the first and only (remaining) command element is a string, it is passed to a subshell if it begins with a shell reserved word, contains special built-ins, or includes shell metacharacters.

    Care must be taken to properly escape shell metacharacters in the command string.

    Otherwise, the command is run bypassing the shell. When bypassing the shell, shell expansions and redirections are not supported.

  • (defaults to: Logger.new(nil))

    The logger to use

  • Additional options

Returns:

  • A result object containing the process status and captured output

API:

  • public



256
257
258
# File 'lib/process_executer.rb', line 256

def self.run(*command, logger: Logger.new(nil), **options_hash)
  ProcessExecuter::Command::Runner.new(logger).call(*command, **options_hash)
end

.spawn(*command, **options_hash) ⇒ Process::Status

Execute the given command as a subprocess and return the exit status

This is a convenience method that calls Process.spawn and blocks until the command terminates.

The command will be sent the SIGKILL signal if it does not terminate within the specified timeout.

Examples:

status = ProcessExecuter.spawn('echo hello')
status.exited? # => true
status.success? # => true
status.timeout? # => false

with a timeout

status = ProcessExecuter.spawn('sleep 10', timeout: 0.01)
status.exited? # => false
status.success? # => nil
status.signaled? # => true
status.termsig # => 9
status.timeout? # => true

capturing stdout to a string

stdout = StringIO.new
status = ProcessExecuter.spawn('echo hello', out: stdout)
stdout.string # => "hello"

See Also:

Parameters:

  • The command to execute

  • The options to use when executing the command

Returns:

  • the exit status of the process

API:

  • public



67
68
69
70
71
# File 'lib/process_executer.rb', line 67

def self.spawn(*command, **options_hash)
  options = ProcessExecuter::Options.new(**options_hash)
  pid = Process.spawn(*command, **options.spawn_options)
  wait_for_process(pid, options)
end