In Ruby, what are the differences between system, backticks (%x), exec and spawn for running an external command?
answer
- wait or not, capture or not
- system returns true, false or nil
- backticks return stdout as a String
- exec replaces the Ruby process
- spawn returns a pid at once
basics
~20 ssystem 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 sAll 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 linessystem("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
Recall the four return shapes: system gives true, false or nil, backticks give stdout, exec never returns, spawn gives a pid.
Explain $? after each call, what nil from system means, the exception: true keyword, and why spawn needs Process.wait or Process.detach.
Show judgement: never ignore system's result, capture stderr when diagnosing, prefer the argument-list form, and reap spawned children in long-running services.
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.