Skip to content
Select theme

How to run an external command and read its output

Short answer: Command::new("git").args(["status"]).output() runs the command, waits for it to exit, and returns everything it produced in one struct: the exit status, the bytes the child wrote to stdout, and the bytes it wrote to stderr. A nonzero exit is not an Err — it arrives in the status field. Err is reserved for the case where the command could not be started at all.

Everything below uses commands that exist on any macOS or Linux machine: echo and /bin/sh. The sh -c '...' form is a way to run a small shell snippet as the child, so the examples can fail, exit with a chosen code, or get killed without installing anything.

status: exit status: 0
stdout: hello from a child process
stderr: 0 bytes

Command::output() is the whole API. It spawns the child, connects pipes to all three standard streams, waits for the child to exit, and hands back a std::process::Output with three public fields:

  • status — an ExitStatus, which prints as exit status: 0 here
  • stdout — a Vec<u8> of everything the child wrote to stdout
  • stderr — the same for stderr

The blank line after stdout: is not noise. echo writes its argument followed by a newline, so the captured stdout ends in \n — and println! adds another one. A child’s output usually ends in a newline; trim it with .trim_end() when comparing or printing it as one line.

The child’s output never reaches your terminal

Section titled “The child’s output never reaches your terminal”

Without .output(), a child inherits your stdout and its output appears as if your own program printed it. With .output(), the child is redirected: everything it writes goes into the buffers, and nothing appears until your code prints it.

this never reaches the terminal

The line appears exactly once, and only because the last line prints it. If you capture the output and forget to print it, it is simply gone. This is the most common reason captured output “disappears”: the program ran the command, kept the bytes, and never wrote them anywhere.

output() returns Ok as long as the child started — even if it exits with an error code. The failure lives in status:

success: false
code: Some(3)
stdout: to stdout
stderr: to stderr

The shell wrote one line to each stream and exited with code 3, and all three facts come back separately: status.success() is the quick true/false check, status.code() is the Option<i32> number, and the two streams arrive already separated. Note the same trailing-newline effect on both captured lines — each echo left a \n, and println! added another.

If the child exits because a signal killed it — the Unix case, not portable to Windows — code() returns None and the Display of the status says which signal:

status: signal: 15 (SIGTERM)
code: None

kill -TERM $$ sends SIGTERM to the shell’s own process, so the child dies by signal rather than exiting normally. That is why code() is None: there is no exit code, there is a signal. Code that checks only code() == Some(0) treats a signal-killed child the same as a successful one — check success() instead, which is false for both a nonzero exit and a signal death.

The stdout and stderr fields are Vec<u8>, because command output has no guarantee of being UTF-8. String::from_utf8_lossy turns the bytes into something printable, replacing invalid sequences with the replacement character:

4 bytes: [99, 97, 102, 255]
lossy: caf�

The 0xff byte survives intact in the Vec<u8> — it is not valid UTF-8, so the lossy conversion turns it into �. If you need the strict version, String::from_utf8(output.stdout) returns a Result instead of inventing replacement characters.

When you do not want the output: .status()

Section titled “When you do not want the output: .status()”

Capturing is not always what you want. An interactive child — an editor, a pager — needs its output to go straight to the terminal, not into a buffer. .status() runs the same command with inherited streams and returns only the ExitStatus:

straight to the terminal
success: false

The child’s line appears even though the program never prints it — with .status() there is no redirection at all. The rule of thumb: .status() when the user should see the child, .output() when the program needs the bytes.

  • It does not stream. output() waits for the child to exit and returns everything at once. Reading a child’s output while it is still running means piping streams yourself — that is its own article in this chapter.
  • It does not find the command. A name that is not on PATH never starts, and that comes back as Err — the next article covers what that error looks like and how to check first.