Standard Input, Output, and Error

Every program you run on Linux — whether it’s ls, a shell script, or a compiled binary — is handed three open data streams automatically, before it even starts running. These are standard input (stdin), standard output (stdout), and standard error (stderr): the channels a process uses to read input and to report normal results versus problems. Once you see that these aren’t magic but simply three numbered file descriptors the kernel wires up for every process, redirection and pipes stop feeling like memorized tricks and start feeling like plumbing you control.

Overview: How stdin, stdout, and stderr work

When the Linux kernel creates a new process, it gives that process a small table of open file descriptors — non-negative integers the kernel uses internally to track open files, pipes, devices, and sockets. By long-standing Unix convention, every process starts life with three of these already open:

File Descriptor Name Default Destination Purpose
0 stdin Keyboard / terminal Input the program reads
1 stdout Terminal screen Normal output and results
2 stderr Terminal screen Error messages and diagnostics

In an interactive terminal session, all three file descriptors point at the same underlying device — your terminal (something like /dev/pts/3 for a terminal emulator). You can see this for any running process by listing the symlinks under /proc/<pid>/fd/. For your own shell, /proc/self/fd works:

ls -l /proc/self/fd

Output:

lrwx------ 1 user user 64 Aug  4 10:20 0 -> /dev/pts/3
lrwx------ 1 user user 64 Aug  4 10:20 1 -> /dev/pts/3
lrwx------ 1 user user 64 Aug  4 10:20 2 -> /dev/pts/3

Notice that fd 0, 1, and 2 are three separate symlinks that merely happen to point at the same device. Nothing forces them to stay pointed together — a program has no idea, and doesn’t care, whether fd 1 and fd 2 point at your terminal, a regular file, or a pipe feeding another process. That independence is exactly what makes redirection possible: the shell quietly changes where a file descriptor points before it hands control to the program, and the program keeps calling the same write()s to fd 1 or fd 2, unaware anything changed.

This is also why stderr exists as its own stream: it lets error messages reach you (or a dedicated error log) even when a program’s normal output has been redirected into a file or piped into another command. If errors shared fd 1 with regular output, every pipeline would silently mix diagnostic noise into the data it’s processing — a script that does grep "ERROR" app.log | mail -s report ops@example.com would end up mailing warnings about missing files right alongside the actual log data.

Redirection happens before the program runs

When you write command > file.log, the shell — not command itself — parses that redirection. Before it execs command, the shell opens file.log and uses the dup2() system call to make file descriptor 1 point at that open file instead of the terminal. Only then does it replace the process image with command. The program never sees the > token at all; it just writes to fd 1 as always, and the kernel delivers those bytes wherever fd 1 currently points. This is why redirection works identically for every program on the system, from ls to a Python script — it’s a shell-level, kernel-enforced mechanism, not something each program has to implement.

Syntax

The general form is a command followed by one or more redirection operators, each naming a file descriptor (defaulting to stdout if omitted) and a target:

command > file
command >> file
command < file
command 2> file
command 2>> file
command > file 2>&1
command &> file
command <<< "text"
Operator Meaning
> Redirect stdout to a file, overwriting it
>> Redirect stdout to a file, appending to the end
< Redirect stdin to read from a file instead of the keyboard
2> Redirect stderr to a file, overwriting it
2>> Redirect stderr to a file, appending to the end
2>&1 Duplicate stderr onto wherever stdout currently points
&> / &>> Bash shortcut for sending both stdout and stderr to a file (overwrite / append)
| Connect one command’s stdout directly to the next command’s stdin
<<< Here-string: feed a single string to a command’s stdin
<< Here-document: feed multiple lines of literal text to stdin

Examples

Example 1: Redirecting stdout to a file

echo "Deployment started at $(date)" > /var/log/app.log
cat /var/log/app.log

Output:

Deployment started at Tue Aug  4 10:15:32 UTC 2026

The > operator opens /var/log/app.log for writing (creating it if it doesn’t exist, truncating it if it does) and points echo‘s fd 1 at it. echo never printed to the terminal at all — everything it wrote went straight into the file.

Example 2: Separating stdout and stderr into different files

ls -l /var/log/app.log /var/log/does-not-exist.log > found.log 2> missing.log
cat found.log
cat missing.log

Output:

-rw-r--r-- 1 root root 42 Aug  4 10:15 /var/log/app.log
ls: cannot access '/var/log/does-not-exist.log': No such file or directory

ls writes the successful listing line to stdout (fd 1), which lands in found.log, and writes its error line to stderr (fd 2), which lands in missing.log. The two streams never touch each other, even though a single command produced both.

Example 3: Feeding a value to stdin without a file

read -r name <<< "Ada Lovelace"
echo "Hello, $name"

Output:

Hello, Ada Lovelace

The <<< here-string operator writes its text into a temporary buffer and connects that buffer to the command’s stdin, exactly as if you’d typed it and pressed Enter. read never touches the keyboard here — it just reads fd 0 like it always does.

Example 4: Combining stderr into stdout and piping to another command

grep "ERROR" /var/log/app.log 2>&1 | tee /var/log/error-report.log

Output:

Aug  4 10:12:03 app[1421]: ERROR failed to connect to database
Aug  4 10:14:51 app[1421]: ERROR request timed out after 30s

2>&1 merges stderr into stdout before the pipe is set up, so if grep itself failed (say, the log file didn’t exist), that error would flow into the pipe too instead of only appearing on your terminal. tee then does two things at once: it prints whatever arrives on its stdin to the terminal and writes a copy to error-report.log.

How it works step by step

Take a realistic case: running a backup script and capturing everything it prints, success or failure, into one log file.

backup.sh > /var/log/backup.log 2>&1
  1. The shell reads the whole line and identifies two redirections before it does anything else: > /var/log/backup.log and 2>&1.
  2. It processes them left to right. First, > /var/log/backup.log opens (or creates/truncates) that file and points fd 1 at it.
  3. Next, 2>&1 duplicates whatever fd 1 currently points to — which is now /var/log/backup.log — onto fd 2. Both descriptors now refer to the same open file.
  4. The shell forks a child process and calls exec() to load backup.sh into it. The child inherits the already-redirected file descriptor table.
  5. As backup.sh runs, every line it writes to stdout and every line it writes to stderr lands in /var/log/backup.log, in the order each write() actually happened.
  6. When the script exits, the shell captures its exit status in $?, which you can check immediately afterward to see whether the backup actually succeeded.

Order matters here specifically because 2>&1 means "point fd 2 at whatever fd 1 points at right now," not "keep fd 2 permanently tied to fd 1." That’s the source of one of the most common redirection bugs, covered next.

Common Mistakes

Mistake 1: Putting 2>&1 before the stdout redirect

backup.sh 2>&1 > /var/log/backup.log

This looks equivalent to the earlier example but isn’t. Here, 2>&1 runs first, while fd 1 still points at the terminal — so fd 2 is duplicated onto the terminal. Then > /var/log/backup.log repoints fd 1 at the file, but fd 2 is untouched and keeps pointing at the terminal. Result: stdout goes to the file, but stderr still prints on screen instead of being captured.

backup.sh > /var/log/backup.log 2>&1

Redirect stdout to its final destination first, then duplicate stderr onto it — always in that order.

Mistake 2: Using > when you meant >>

for i in 1 2 3; do
  echo "Run $i completed" > /var/log/app.log
done

Because > truncates the file every time it runs, this loop overwrites /var/log/app.log on each iteration — after it finishes, the file contains only Run 3 completed, not all three lines. It’s an easy mistake because > and >> differ by a single character but behave completely differently.

for i in 1 2 3; do
  echo "Run $i completed" >> /var/log/app.log
done

Use >> whenever you’re accumulating log entries across multiple writes or multiple script runs.

Mistake 3: Assuming a pipe carries stderr too

backup.sh | grep -i error

A plain | only connects the left command’s stdout to the right command’s stdin — stderr is left alone and still prints straight to your terminal. If backup.sh writes its error messages to stderr (which well-behaved scripts do), grep never sees them, so this pipeline can report "no errors found" while errors were printed right above it.

backup.sh 2>&1 | grep -i error

Merge stderr into stdout before the pipe if you want a search or filter to see both streams.

Best Practices

  • Redirect stdout and stderr separately while debugging, so you never lose an error message inside a file full of normal output.
  • When combining streams, always write the stdout redirect first, then 2>&1 — reversing the order silently breaks it.
  • Use >> for logs you want to accumulate over time; reserve > for files you intend to recreate fresh on each run.
  • Use tee when you need to watch output live on the terminal and save it to a file at the same time.
  • Don’t casually silence stderr with 2>/dev/null — you’ll also hide the diagnostics you’d need if something goes wrong.
  • Quote variables that hold redirection targets or piped data, e.g. echo "$message" >> "$logfile", so paths or values containing spaces don’t break the command.
  • Check $? immediately after a command whose success matters — redirection changes where output goes, not whether the command succeeded.

Practice Exercises

  1. Write two commands that run ping -c 3 example.com, sending successful output to ~/ping-success.log and any error output to ~/ping-errors.log, using two separate redirection operators on one line.
  2. You have a script deploy.sh that you run several times a day. Modify how you invoke it so that every run’s combined stdout and stderr is appended (not overwritten) to /var/log/deploy.log, preserving the history of all previous runs.
  3. Using a here-string, feed the text staging into a variable named environment with read, then print Deploying to staging using that variable — without creating any file or typing at a prompt.

Summary

  • Every process starts with three open file descriptors: 0 (stdin), 1 (stdout), and 2 (stderr), all connected to the terminal by default.
  • The shell rewires these file descriptors with dup2() before a program runs — the program itself has no idea redirection happened.
  • stdout and stderr are independent streams on purpose, so error messages can still reach you even when normal output is redirected or piped elsewhere.
  • > overwrites a file; >> appends to it — mixing them up is one of the most common shell scripting bugs.
  • 2>&1 must come after the stdout redirect it’s meant to follow, or stderr won’t end up where you expect.
  • A plain pipe | only carries stdout; add 2>&1 before the pipe if you need stderr included too.