skip to content

Processes & Shell Commands

Running programs with system, backticks, exec, spawn and Open3, reading $? for the exit status, and ENV and Signal.trap. Interviewers probe shell injection via the string form and capturing stderr.

on this pageshow

explore

questions

6

In Ruby, what are the differences between system, backticks (%x), exec and spawn for running an external command?

level: juniorimportance: must knowfreq 72%

answer

  1. wait or not, capture or not
  2. system returns true, false or nil
  3. backticks return stdout as a String
  4. exec replaces the Ruby process
  5. spawn returns a pid at once

basics

~20 s

system runs a command, waits, and returns true, false or nil; backticks and %x run it, wait, and return its standard output as a String; exec replaces the Ruby process; spawn starts it and returns the pid without waiting.

solid answer

~40 s

All four start an external program. `system(...)` waits and returns `true` for exit status zero, `false` for non-zero and `nil` when the command could not be run; the child's output goes straight to your terminal or log. Backticks and `%x(...)` wait too, but return the child's **standard output as a `String`**; stderr is not captured and failure is visible only in `$?`, the `Process::Status` of the last child, which `system` sets too. `exec(...)` **replaces** the current Ruby process with the program, so code after a successful `exec` never runs. `spawn(...)` starts the child and returns its **pid immediately**; the parent must reap it with `Process.wait` or `Process.detach`. `system`, `exec` and `spawn` accept an argument list; backticks accept only one string.

code

ruby · 14 lines
ruby
system("ffprobe", "-version")      # => true, output printed
system("false")                     # => false, $?.exitstatus == 1
system("no_such_program")           # => nil, could not execute

version = `ffmpeg -version`         # => "ffmpeg version ...\n"
$?.success?                         # => true

pid = spawn("ffmpeg", "-i", "in.mov", "out.mp4", err: "ffmpeg.log")
# ... keep working ...
Process.wait(pid)
$?.exitstatus                       # => 0 on success

exec("ffmpeg", "-version")          # this Ruby process becomes ffmpeg
puts "never printed"

go deeper

for a junior

Recall the four return shapes: system gives true, false or nil, backticks give stdout, exec never returns, spawn gives a pid.

for a middle

Explain $? after each call, what nil from system means, the exception: true keyword, and why spawn needs Process.wait or Process.detach.

for a senior

Show judgement: never ignore system's result, capture stderr when diagnosing, prefer the argument-list form, and reap spawned children in long-running services.

for a principal

Set a codebase convention for shelling out, one wrapper with the argument-list form, captured stderr and raised errors, so no call site silently swallows a failed command.

## Four ways to start a program Ruby's `Kernel` module offers four built-in ways to run another program. They differ on three questions: **does Ruby wait** for the child, **what do you get back**, and **does your Ruby process survive**. | Method | Waits? | Returns | Child's stdout | |---|---|---|---| | `system(cmd, ...)` | yes | `true`, `false` or `nil` | inherited: printed where yours goes | | `` `cmd` `` / `%x(cmd)` | yes | stdout as a `String` | captured | | `exec(cmd, ...)` | no return on success | never returns; raises on failure | inherited | | `spawn(cmd, ...)` | no | the child's pid | inherited unless redirected | ## system `system` runs the command, blocks until it exits, and reports the outcome as a boolean-ish value: - `true` when the exit status is zero; - `false` when it is non-zero; - `nil` when the command **could not execute**, for example because the program does not exist. It also assigns `$?`. With `exception: true` it raises instead: `RuntimeError` for a non-zero exit, and a `SystemCallError` such as `Errno::ENOENT` when the program cannot be started. The child writes to the same standard output and error as your process, so `system` is the right tool when you only care whether a step succeeded, such as a build step in a script. ## Backticks and %x `` `ls -l` `` and `%x(ls -l)` are two spellings of one `Kernel` method: they run the command, wait, and **return its standard output as a `String`**, including the trailing newline. Things to know: 1. Standard error is **not** captured; it still goes to your stderr. 2. A non-zero exit does not raise; check `$?.success?` afterwards. 3. They take a **single string** with interpolation, so there is no argument-list form. `%x` exists so the command can contain backticks or quotes without escaping. 4. When the string needs no shell and the program cannot be found, the call raises `Errno::ENOENT`; through a shell, you get a shell error and a status of 127 in `$?` instead. ## exec `exec` does not create a child at all: it **replaces the current Ruby process image** with the new program, keeping the same pid. On success nothing after the `exec` line runs, no `ensure` block of yours, because your program is gone. On failure, for instance a missing executable, it raises `Errno::ENOENT` and your Ruby code continues in the `rescue`. It is used in wrapper scripts that set up the environment and then hand over to the real program. ## spawn `spawn` (also `Process.spawn`) starts the child and **returns its pid at once**, so your Ruby code keeps running in parallel with it. It takes the richest set of options: redirections such as `out: "log.txt"` or `err: [:child, :out]`, `chdir:`, `unsetenv_others:`, `pgroup:` and resource limits. Because Ruby does not wait, the parent must **reap** the child with `Process.wait(pid)` or register disinterest with `Process.detach(pid)`; otherwise the finished child lingers as a zombie until the parent exits. ## Where the child's output goes With `system`, `exec` and `spawn` the child **inherits** your standard streams, so its output interleaves with your own logs. Redirection options change that without any shell: - `system("make", out: File::NULL)` discards standard output; - `spawn(cmd, err: "job.log")` sends standard error to a file; - `spawn(cmd, err: [:child, :out])` merges standard error into standard output. Backticks capture stdout only. Writing `2>&1` inside the string merges stderr, but only by routing the command through the shell, which is why `Open3.capture2e` is the cleaner way to capture both. ## Shared behaviour - `system`, `exec` and `spawn` accept either one command-line string or an **executable plus separate arguments**; the second form never involves a shell. - A leading hash sets environment variables for the child only: `system({"LC_ALL" => "C"}, "sort", "names.txt")`. - `$?` is **thread-local** and holds the status of the most recently exited child in the current thread. - For stdout **and** stderr **and** the status together, reach for the `open3` library rather than any of these four. ## Picking one - Only success or failure matters: `system`. - You need the output of a short command you trust: backticks, then check `$?`. - The script's job is to become another program: `exec`. - Work must continue while the child runs: `spawn`, and reap it later.

  • In Ruby, how do you make system raise instead of returning false or nil?
    Pass `exception: true`: `system("make", "test", exception: true)` raises `RuntimeError` naming the exit status when the command exits non-zero, and a `SystemCallError` such as `Errno::ENOENT` when it cannot be started. That turns a silently ignored failure into an exception your error handling will see.
  • In Ruby, why must a program that calls spawn also call Process.wait or Process.detach?
    `spawn` returns without waiting, so nobody collects the child's exit status. Until the parent reaps it, the finished child stays in the process table as a zombie, and `$?` never learns its result. `Process.wait(pid)` blocks and sets `$?`; `Process.detach(pid)` reaps it in a background thread.

saying these in an interview costs you the question

  • Backticks return true or false like system.
  • system raises an exception when the command exits non-zero.
  • Code after a successful exec runs once the program finishes.
  • spawn waits for the child and returns its exit status.
  • Backticks capture both standard output and standard error.
open as a page

In Ruby, after system or backticks runs a command, how do you check its exit status with $? and Process::Status?

level: middleimportance: must knowfreq 58%

basics

~10 s

$? holds a Process::Status for the most recently exited child in the current thread. Read $?.success? or $?.exitstatus for a normal exit, and $?.signaled? with $?.termsig when a signal killed the process.

open as a page

A Ruby app runs ffmpeg on user-uploaded video files; which calling forms keep a hostile filename from being read by a shell or as an ffmpeg option?

level: seniorimportance: must knowfreq 55%

basics

~20 s

Pass the executable and every argument separately, system("ffmpeg", "-i", path, out) or Open3.capture3 the same way, so no shell runs. Avoid backticks and interpolated strings, and make the path start with a directory, not a dash.

open as a page

In Ruby, how do ENV[] and ENV.fetch differ for reading configuration, and what type are ENV values?

level: middleimportance: should knowfreq 48%

basics

~10 s

ENV["NAME"] returns the value or nil; ENV.fetch("NAME") raises KeyError when it is missing, or returns a default or block result. Values are always frozen Strings, so convert numbers and booleans explicitly.

open as a page

In Ruby, how do Open3.capture3, capture2e and popen3 differ when you need a command's stderr and exit status?

level: middleimportance: should knowfreq 50%

basics

~10 s

Open3.capture3 returns [stdout, stderr, status]; capture2e returns [merged stdout and stderr, status]; popen3 hands you stdin, stdout and stderr streams plus a wait thread, for streaming large or interactive output.

open as a page

In Ruby, how do you use Signal.trap so a video-transcoding worker shuts down gracefully on TERM or INT, and what must the handler avoid?

level: seniorimportance: should knowfreq 35%

basics

~20 s

Without a trap, INT raises Interrupt and TERM raises SignalException in the main thread. Signal.trap("TERM") { @stopping = true } lets the worker finish the current job and exit; the handler must avoid Mutex, Monitor and buffered I/O.

open as a page