system returns true, false or nil¶
Level: 201 · anyone about to shell out from a script and wondering which of the six spellings to use
One line: system returns true for exit 0, false for any other status and nil when the command could not be started; backticks return stdout; spawn returns a pid to Process.wait on; Open3.capture3 returns stdout, stderr and the status together; IO.popen streams — and any of them takes an argv Array that skips the shell, which is the safe form, where Python's subprocess.run is one function with keyword arguments for the same choices.
Ruby has several ways to run a program because they answer different questions. system(cmd) answers "did it succeed?" — true, false, or nil when there was no such command — and leaves the full Process::Status in $?. Backticks and %x() answer "what did it print?": they return the child's stdout as a String and set $? too. spawn starts the child and returns at once with its pid, and Process.wait (or wait2) collects the status later. Open3.capture3 from the standard library answers all three questions at once — stdout, stderr and the status — and capture2e merges the two streams the way a terminal does. IO.popen gives an IO connected to the child, to read as it writes or, in "r+" mode, to write to and read from. Every one of them accepts either one String, which is handed to /bin/sh when it contains shell characters — so * expands and quoting matters — or an argv Array ("echo", "*"), which runs the program directly with exactly those arguments. exec is the odd one out: it replaces the current process and never returns. The program runs each in a temporary directory holding two empty files, so the glob has something to expand to.
Verified output of running_other_programs_rb.rb — regenerated by tools/run_examples.py, never hand-typed.
1. system(ruby, "-e", "exit 0") returns; $?.exitstatus true; 0
2. system(ruby, "-e", "exit 2") returns; $?.exitstatus false; 2
3. system("definitely_not_a_command_xyz"); $?.exitstatus nil; 127
4. `echo hello` returns stdout; $?.success? "hello\n"; true
5. %x(echo hi there) is the same thing "hi there\n"
6. one String goes through /bin/sh: `echo *` expands "a.txt b.txt\n"
7. an argv form skips the shell: capture2("echo", "*") "*\n"
8. spawn returns a pid; after Process.wait, $?.exitstatus Integer; 4
9. Process.wait2 returns [pid, status] pid matches: true; 5
10. Open3.capture3: stdout, stderr, status.exitstatus "out\n", "err\n", 3
11. capture2e merges; a piped stdout is buffered, so "err\nout\n"
12. with $stdout.sync = true in the child "out\nerr\n"
13. IO.popen([argv]) { |io| io.readlines } ["1\n", "2\n"]
14. IO.popen(argv, "r+"): write, close_write, read "ABC\n"
15. capture2 with stdin_data: feeds the child "\"fed\"\n"
16. a leading Hash adds to the child's environment "hi\n"
17. exec replaces the process: nothing after it runs "replaced\n"
18. system(..., exception: true) raises on failure RuntimeError
19. $? is Process.last_status; its class true; Process::Status
Reading the output¶
Rows 1–3 are the three answers of system: true with $?.exitstatus 0, false with 2, and nil for a command that does not exist — with the status set to 127, the shell's own "command not found" number. Rows 4–5: backticks and %x() return stdout including its newline, and $? reports the success. Rows 6–7 are the shell question: the one-String echo * went through /bin/sh and the * became the two file names, while the argv form capture2("echo", "*") passed a literal * to echo. Rows 8–9: spawn returns an Integer pid and does not wait; Process.wait fills $? and Process.wait2 returns the pid and the status as a pair. Row 10 is Open3.capture3, everything at once. Rows 11–12 are a trap worth knowing: with the two streams merged, err arrives before out, because a child whose stdout is a pipe buffers it until exit while stderr is unbuffered — $stdout.sync = true in the child restores the order you meant. Rows 13–14 are IO.popen: a block gets the IO, readlines drains it, and in "r+" mode you write, close_write to send EOF, then read the reply. Row 15 feeds the child's stdin with stdin_data:, and row 16 shows the Hash that any of these accepts first, adding to the child's environment. Row 17: after exec, the puts 'never' never ran. Row 18: system(..., exception: true) turns a failure into a RuntimeError for the callers who would rather rescue than check. Row 19: $? is Process.last_status, a Process::Status — printed by class, because its inspect includes the pid.
Compared with Python¶
subprocess.run(argv, capture_output=True, text=True, input=..., check=True, shell=True, env=...) covers rows 1–2, 4–7 and 10–16 with keyword arguments, and returns a CompletedProcess whose returncode, stdout and stderr are the answers — there is no $?; the object is the status. A missing command is not a None but a FileNotFoundError (row 3). The shell is opt-in through shell=True, and a list of arguments skips it exactly as Ruby's argv form does (rows 6–7). Popen is spawn: it returns an object with a pid, wait() returns the code and stores it in returncode (rows 8–9), and communicate() is the "r+" popen — write, close, read, wait (row 14). stderr=subprocess.STDOUT merges the streams and shows the same buffering trap: err before out until the child passes flush=True (rows 11–12). check=True is exception: true, raising CalledProcessError with the code on it (row 18). os.execvp replaces the process just as exec does (row 17). os.system is the old system: it returns the raw wait status — 1280 for exit 5, the code shifted left eight bits — and os.waitstatus_to_exitcode decodes it (row 19). The Python library's Standard in, standard out, and pipes ↗ measures what a pipe does to a Python program's own streams — the other side of rows 11–12.
Verified output of running_other_programs_py.py — regenerated by tools/run_examples.py, never hand-typed.
1. run([py, '-c', 'exit 0']).returncode; == 0 0; True
2. run([py, '-c', 'exit 2']).returncode; == 0 2; False
3. run(['definitely_not_a_command_xyz']) raises FileNotFoundError
4. run(['echo', 'hello'], capture_output=True).stdout 'hello\n'; True
5. check_output(['echo', 'hi', 'there'], text=True) 'hi there\n'
6. shell=True goes through /bin/sh: 'echo *' expands 'a.txt b.txt\n'
7. a list skips the shell: run(['echo', '*']) '*\n'
8. Popen gives an object with a pid; wait() returns int; 4
9. after wait(), Popen.returncode holds it pid matches: True; 5
10. run(capture_output=True): stdout, stderr, returncode 'out\n', 'err\n', 3
11. stderr=STDOUT merges; a piped stdout is buffered, so 'err\nout\n'
12. with flush=True in the child 'out\nerr\n'
13. run(...).stdout.splitlines(keepends=True) ['1\n', '2\n']
14. Popen(stdin=PIPE, stdout=PIPE).communicate('abc') 'ABC\n'
15. run(input='fed') feeds the child "'fed'\n"
16. env={**os.environ, ...} is the child's whole environment 'hi\n'
17. os.execvp replaces the process: nothing after it runs 'replaced\n'
18. run(check=True) raises on failure CalledProcessError returncode=2
19. os.system returns a wait status; waitstatus_to_exitcode 1280; 5
Try it¶
- Write
run!(*argv)that runs a command withOpen3.capture3, returns its stdout when the status is success, and otherwise raises aCommandFailederror carrying the exit status and the child's stderr. Show both outcomes with a child Ruby started fromRbConfig.ruby. - Use
IO.popenwith an argv Array to read a child's lines as they arrive, printing each upcased, and then do the same withOpen3.popen3, which also hands you the child's stderr and a wait thread whosevalueis the status.
Solution to 1
running_other_programs_kata_rb.rb in full — pasted here by tools/run_examples.py from the file CI runs.
# Kata: run!(*argv) runs a command with Open3.capture3 and returns its stdout,
# or raises CommandFailed carrying the exit status and the child's stderr.
require "rbconfig"
require "open3"
class CommandFailed < StandardError
attr_reader :status, :stderr
def initialize(argv, status, stderr)
@status = status
@stderr = stderr
super("#{File.basename(argv.first)} exited #{status}")
end
end
def run!(*argv)
out, err, status = Open3.capture3(*argv)
raise CommandFailed.new(argv, status.exitstatus, err) unless status.success?
out
end
puts run!(RbConfig.ruby, "-e", "puts 'fine'")
begin
run!(RbConfig.ruby, "-e", "warn 'disk on fire'; exit 3")
rescue CommandFailed => e
puts "#{e.class}: #{e.message} -- status=#{e.status} stderr=#{e.stderr.inspect}"
end
Verified output of running_other_programs_kata_rb.rb — regenerated by tools/run_examples.py, never hand-typed.
See also¶
ARGVholds strings andENVis not a Hash — the child's side: where the arguments, the environment and the exit status come fromgetsreturns nil at EOF, and readsARGV's files first — what the child does with the stdin you feed it- Processes:
forkandwait—fork,spawnandProcess.waitunderneath all of these exit,at_exitandSystemExit— how the child produces the status the parent reads- Pipelines ↗ — the shell's own view of stdout, stderr and exit statuses
- Stderr does not go down the pipe ↗ — why rows 10–12 have two streams to capture
- A pipeline reports its last command ↗ — the shell's
$?, whichsystemreads - Standard in, standard out, and pipes ↗ — Python's streams when it is the child
Pathname#/joins, andDir.globis sorted —Pathnameis an object with/,basename,extname,parent,children,exist?andread;…