Shell Commands
Command Output
Programming
Scripting
Code Debugging

Running shell command and capturing the output

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Running a shell command from code is easy; doing it safely and predictably takes more care. The main design questions are whether you really need a shell, how you want to capture standard output and standard error, and how you will avoid command-injection bugs when arguments come from outside your program.

Prefer Process APIs Over Raw Shell Strings

If all you need is to run a program and read its output, use your language's process API directly instead of feeding a single string into a shell. That avoids quoting problems and reduces injection risk.

In Python, subprocess.run is the usual answer:

python
1import subprocess
2
3result = subprocess.run(
4    ["echo", "hello"],
5    capture_output=True,
6    text=True,
7    check=True,
8)
9
10print(result.stdout.strip())

This launches echo directly, captures the output as text, and raises an exception if the command fails.

Capture stdout and stderr Separately

Most process APIs expose both output streams. Keep them separate unless you have a strong reason to merge them.

python
1import subprocess
2
3result = subprocess.run(
4    ["python3", "-c", "import sys; print('out'); print('err', file=sys.stderr)"],
5    capture_output=True,
6    text=True,
7)
8
9print("stdout:", result.stdout.strip())
10print("stderr:", result.stderr.strip())
11print("code:", result.returncode)

This separation matters when a command writes normal data to standard output but writes diagnostics to standard error.

Use the Shell Only When You Need Shell Features

A shell is useful for pipelines, wildcard expansion, redirection, and compound expressions. If you need those features, then use the shell consciously.

python
1import subprocess
2
3result = subprocess.run(
4    "printf 'a\nb\n' | wc -l",
5    shell=True,
6    capture_output=True,
7    text=True,
8    check=True,
9)
10
11print(result.stdout.strip())

That works, but shell=True should be the exception, not the default. If untrusted input is interpolated into that string, the risk becomes immediate.

Timeouts and Long-Running Commands

Real programs also need control over hanging processes. Use a timeout so a stalled child process does not block your program forever.

python
1import subprocess
2
3try:
4    subprocess.run(["sleep", "10"], timeout=1, check=True)
5except subprocess.TimeoutExpired:
6    print("command timed out")

This is one of the easiest reliability wins when wrapping external commands.

A Reusable Helper

If you run commands often, wrap the behavior you want in one helper function.

python
1import subprocess
2
3
4def run_command(args):
5    completed = subprocess.run(
6        args,
7        capture_output=True,
8        text=True,
9        check=False,
10    )
11    return {
12        "stdout": completed.stdout,
13        "stderr": completed.stderr,
14        "returncode": completed.returncode,
15    }
16
17
18print(run_command(["uname", "-s"]))

A helper keeps output capture, error handling, and timeouts consistent across the codebase.

If the command can produce a lot of output, think about whether you want to buffer everything in memory. subprocess.run is convenient for short output, but long-running tools are sometimes better handled with Popen so you can stream lines incrementally instead of waiting for the process to finish.

Common Pitfalls

The biggest mistake is building shell strings from user input. If you do that with shell=True, you are inviting command injection.

Another mistake is reading only standard output and ignoring standard error and the return code. Many failures become invisible that way.

A third mistake is using shell execution for tasks that should use a native library. If you need to list files, parse JSON, or move data within the same program, a library call is usually safer and faster than spawning a shell.

Summary

  • Use your language's process API to run commands and capture output safely.
  • Prefer argument lists over raw shell strings whenever possible.
  • Capture stdout, stderr, and the exit code explicitly.
  • Use shell=True only when you need actual shell features such as pipes or redirection.
  • Add timeouts and avoid interpolating untrusted input into commands.

Course illustration
Course illustration

All Rights Reserved.