Bash: File Descriptors and I/O Redirection — Understanding stdin, stdout, and stderr

Learn how Bash handles input, output, and errors using file descriptors. Explore stdin, stdout, stderr, redirection operators, pipes, and practical techniques for logging command output without losing important error messages.

Bash: File Descriptors and I/O Redirection — Understanding stdin, stdout, and stderr

File descriptors and redirection are among the most important Bash concepts to understand if you want to write reliable shell scripts. They can also be surprisingly confusing: a single misplaced > or 2>&1 can change where your output goes, hide an error message, or even overwrite a file you wanted to keep.

The key to understanding them is to stop thinking of a command as something that simply prints text on the screen. Every command has input and output channels, and Bash lets you connect those channels to files, other commands, or even nowhere at all.

In this article, we'll explore how Bash handles input and output, what file descriptors are, and how to redirect data wherever we need it. We'll start with the basics, then move on to combining redirections, handling errors, working with pipelines, and using file descriptors in scripts.

The examples are designed to be tried directly in your terminal. Once you understand the underlying rules, even complicated redirection expressions become much easier to read.

1. What Is a File Descriptor?

A file descriptor (FD) is a small integer that a process uses to refer to an open file or another input/output resource.

Despite the name, a file descriptor doesn't necessarily refer to a regular file on disk. It can refer to a terminal, a pipe, a socket, or another resource through which a process reads or writes data.

When you run a command in Bash, it normally starts with three standard file descriptors:

File descriptor

Name

Abbreviation

Typical destination

0

Standard input

stdin

Keyboard or input from another command

1

Standard output

stdout

Terminal screen

2

Standard error

stderr

Terminal screen

These three descriptors are the foundation of shell redirection.

How the three standard streams work

File descriptor 0 — stdin

The command reads input from here.

File descriptor 1 — stdout

The command sends normal output here.

File descriptor 2 — stderr

The command sends diagnostic messages and errors here.

Why are standard output and standard error separate?

Imagine that a command processes hundreds of files. You want to save the successful results to a file while still seeing any errors on your terminal. Separate file descriptors let you do exactly that.

For example:

find /etc -type f -print > files.txt

The list of files goes into files.txt. If find encounters an error, such as a permission problem, the diagnostic message normally goes to standard error and remains visible in your terminal.

You can redirect those two streams independently, which is what makes file descriptors so useful.

2. Redirecting Standard Output

The simplest redirection is >.

command > output.txt

This tells Bash to open output.txt for writing and connect the command's standard output to that file.

For example:

printf 'Hello, world!\n' > greeting.txt

Nothing appears on the terminal because the output is written to greeting.txt.

You can inspect the result with:

cat greeting.txt

Output:

Hello, world!

Overwriting a file

When you use > with an existing regular file, Bash normally truncates the file before the command runs.

For example:

printf 'First version\n' > greeting.txt
printf 'Second version\n' > greeting.txt

The file contains only:

Second version

The first line has been replaced.

Be careful with >: it can destroy existing file contents even if the command subsequently fails or produces no output.

Appending to a file

If you want to add output without replacing the existing contents, use >>:

printf 'First line\n' > output.txt
printf 'Second line\n' >> output.txt

Now the file contains:

First line
Second line

The difference is simple:

  • > creates or truncates the destination file.
  • >> creates the file if necessary and appends output to it.

Appending is useful for log files, reports, and scripts that collect results over time.

Preventing accidental overwrites with noclobber

Bash has an option called noclobber, which prevents ordinary output redirection from overwriting an existing regular file.

Enable it with:

set -o noclobber

Now this command fails if output.txt already exists:

printf 'New data\n' > output.txt

You can still append with >>.

If you deliberately want to overwrite a file despite noclobber, use >|:

printf 'Replacement data\n' >| output.txt

This forces the output redirection.

To disable noclobber again:

set +o noclobber

This option is a useful safeguard, although it doesn't replace careful handling of important files.

3. Redirecting Standard Input

So far, we've redirected output. But commands can also read input from a file instead of the keyboard.

The < operator redirects standard input:

command < input.txt

For example, suppose names.txt contains a list of names. You can pass its contents to a command that reads from standard input:

sort < names.txt

The output is sorted and printed to the terminal. The original file is not modified.

You can also use input redirection with commands that process standard input line by line:

while IFS= read -r line; do
    printf 'Name: %s\n' "$line"
done < names.txt

Here, Bash reads the file through the loop's standard input.

A subtle but important point: input redirection is not the same as passing a filename as an argument.

Compare:

sort < names.txt

with:

sort names.txt

Both sort the file's contents in this example, but they work differently. In the first command, Bash opens the file and connects it to standard input. In the second, sort receives the filename as an argument and opens the file itself.

That distinction matters because some commands read from standard input when no filename is supplied, while others require a filename argument.

4. Redirecting Standard Error

Standard error is file descriptor 2. To redirect it, place 2> immediately before the destination.

command 2> errors.txt

For example:

ls /directory/that/does/not/exist 2> errors.txt

The error message is written to errors.txt instead of being displayed on the terminal.

The command's standard output remains unchanged.

You can also append errors to a file:

command 2>> errors.txt

This is particularly useful for scripts that should preserve diagnostic information across multiple runs.

Redirecting output and errors to different files

Suppose you want to save normal output and error messages separately:

find /etc -type f -print > files.txt 2> errors.txt

The results are:

  • files.txt contains the paths printed by find.
  • errors.txt contains diagnostic messages written to standard error.

This is often a good approach for long-running jobs because you can inspect the results and investigate failures independently.

One detail worth remembering: redirection doesn't change whether a command succeeds or fails. A command can still return a nonzero exit status even when its error messages have been redirected to a file.

5. Redirecting Both Standard Output and Standard Error

Often, you want to capture everything a command writes to standard output and standard error.

Bash provides several ways to do this.

Method 1: Redirect standard output, then duplicate it for standard error

command > output.log 2>&1

This is one of the most important redirection patterns in Bash.

Let's break it down:

  1. > output.log connects standard output, descriptor 1, to output.log.
  2. 2>&1 makes standard error, descriptor 2, refer to the same destination as standard output currently uses.

Both streams therefore go to output.log.

For example:

find /etc -type f -print > find.log 2>&1

Both the list of files and any diagnostic messages are written to the same log.

The order is essential. We'll explore why in the next section.

Method 2: Use &>

Bash also supports a shorter form:

command &> output.log

This redirects both standard output and standard error to the same file, overwriting it if it already exists.

To append both streams instead, use:

command &>> output.log

These forms are concise and easy to read in Bash scripts.

Portability note: &> and &>> are Bash features and aren't supported identically by every shell. If your script must run in other POSIX-compatible shells, prefer portable redirection forms such as > output.log 2>&1.

Method 3: Use >& file

Bash also accepts:

command >& output.log

When the word after >& is a filename rather than a file descriptor, Bash treats this as a redirection of both standard output and standard error to that file.

Although this syntax is valid in Bash, &> output.log is generally easier to recognize as the Bash-specific form for redirecting both streams.

For scripts intended to be portable, use > output.log 2>&1.

6. Why the Order of Redirections Matters

This is where file descriptors become especially interesting.

Consider these two commands:

command > output.log 2>&1
command 2>&1 > output.log

They may look almost identical, but they do different things.

To understand why, remember this rule:

Bash processes redirections from left to right.

Each redirection changes where a file descriptor points. A later redirection uses the configuration that exists at that moment; it doesn't automatically follow changes made to another descriptor afterward.

First command: > output.log 2>&1

command > output.log 2>&1

Bash performs the following steps:

  1. Redirect standard output to output.log.
  2. Make standard error point to the same destination as standard output now uses.

Both streams go to the file.

Second command: 2>&1 > output.log

command 2>&1 > output.log

This time, Bash performs the steps in the opposite order:

  1. Make standard error point to the destination standard output currently uses — typically the terminal.
  2. Redirect standard output to output.log.

The result is:

  • Standard output goes to output.log.
  • Standard error remains connected to the original destination, usually the terminal.

The second command does not mean “make standard error follow standard output forever.” The 2>&1 operation duplicates the destination currently associated with descriptor 1; it doesn't create a permanent link between the descriptors.

This principle is fundamental to understanding Bash redirection. Whenever you see multiple redirections on one command, read them from left to right.

7. Closing a File Descriptor

Sometimes you want a command to have no access to a particular stream.

You can close standard output with:

command 1>&-

Or close standard error with:

command 2>&-

The >&- syntax closes the specified output descriptor.

For example:

find /etc -type f -print 1>&-

This closes standard output for the command. The command may encounter a write error when it attempts to print results, so closing a descriptor isn't necessarily the same as discarding output safely.

If your goal is simply to discard normal output, redirect it to /dev/null instead:

command > /dev/null

To discard standard error:

command 2> /dev/null

To discard both:

command > /dev/null 2>&1

/dev/null is a special device that discards data written to it. It is usually the right choice when you don't need a command's output.

8. Using Additional File Descriptors

Descriptors 0, 1, and 2 are standard, but they're not the only descriptors a process can use.

You can open additional descriptors for your own purposes. This can be useful when a script needs to keep a log file open, preserve the terminal for messages, or read from multiple sources.

For example, the following command opens output.log for writing on descriptor 3:

command 3> output.log

However, simply opening descriptor 3 doesn't redirect standard output to it. To use it, the command or script must explicitly read from or write to that descriptor.

In Bash, you can write to descriptor 3 using:

printf 'This goes to descriptor 3\n' >&3

For a complete example:

{
    printf 'Normal output\n'
    printf 'Log message\n' >&3
} 3> output.log

The result is:

  • Normal output appears on standard output, normally the terminal.
  • Log message is written to output.log through descriptor 3.

This is one way to separate a script's ordinary output from a dedicated log stream.

Duplicating a descriptor

The general form for duplicating an output descriptor is:

n>&m

This makes descriptor n refer to the same destination as descriptor m currently uses.

For example:

command 3>&1

This makes descriptor 3 point to the same destination as standard output at that moment.

For input descriptors, use <&:

command 3<&0

This makes descriptor 3 refer to the same input source as standard input.

These operations duplicate an existing descriptor; they don't open a new file by themselves.

Bash also supports moving a descriptor, using syntax such as 3>&1-. This duplicates descriptor 1 onto descriptor 3 and then closes descriptor 1. Such forms are more advanced and should be used only when you specifically need to manage descriptor ownership.

9. Redirecting Output Through a Pipeline

A pipe (|) connects the standard output of one command to the standard input of another.

For example:

printf '%s\n' apple orange banana | sort

The output of printf becomes the input to sort, which prints the sorted result:

apple
banana
orange

Pipelines are powerful because they allow small commands to be combined into more complex operations.

But there is an important detail: a normal pipeline connects standard output, not standard error.

Consider:

find /etc -type f 2> errors.log | sort

Here:

  • Standard output from find is sent to sort.
  • Standard error from find is written to errors.log.
  • Standard output from sort goes to the terminal unless redirected elsewhere.

Sending both output streams into a pipeline

If you want standard output and standard error to travel through the same pipeline, you can write:

command 2>&1 | another_command

Bash redirects standard error to the current standard output destination before connecting the pipeline. Both streams then go into another_command.

Bash also provides the shorthand operator |&:

command |& another_command

This sends both standard output and standard error through the pipe. In Bash, the implicit standard-error redirection is performed after any explicit redirections on the command.

For example:

find /etc -type f |& tee output.log

Both streams are sent through tee, which writes them to the terminal and to output.log.

Be aware that combining the streams means normal output and error messages can become interleaved. If you need to preserve the distinction between them, redirect them separately instead.

10. Using tee to Save Output and Still See It

Normally, redirecting output to a file means you no longer see it on the terminal.

But sometimes you want both: save the output for later and watch it as the command runs.

That's what tee does.

command | tee output.log

tee reads from standard input and writes the data both to standard output and to the specified file.

For example:

printf 'Starting deployment\n' | tee deployment.log

The message appears on the terminal and is written to deployment.log.

To append instead of overwriting the log, use tee -a:

printf 'Deployment finished\n' | tee -a deployment.log

Capturing errors with tee

A normal pipeline sends only standard output through the pipe. To capture errors too, redirect standard error to standard output:

command 2>&1 | tee output.log

Or use Bash's |& shorthand:

command |& tee output.log

Both let you see the combined output while saving it to a file.

One additional consideration: by default, the exit status of a pipeline is usually the exit status of its last command. If you want Bash to report a failure from command rather than just the status of tee, consider enabling pipefail:

set -o pipefail
command 2>&1 | tee output.log

With pipefail enabled, the pipeline returns a nonzero status if a command in the pipeline fails, subject to Bash's pipeline status rules.

11. Redirecting Input and Output Together

You can combine input and output redirections in the same command.

For example:

sort < unsorted.txt > sorted.txt

Bash redirects standard input from unsorted.txt and standard output to sorted.txt.

The command reads the original data and writes the sorted result to a separate file.

You can also redirect standard error independently:

sort < unsorted.txt > sorted.txt 2> errors.log

This is a useful pattern for scripts that process input files and need to keep diagnostic messages separate from the results.

The general rule remains the same: each redirection configures a particular file descriptor, and Bash processes the redirections from left to right.

12. Redirection for an Entire Block of Commands

Redirections don't have to be attached to individual commands. Bash lets you apply them to a group of commands enclosed in braces or parentheses.

For example:

{
    printf 'Starting task\n'
    date
    printf 'Task finished\n'
} > task.log 2>&1

All three commands run with standard output and standard error directed to task.log.

This is convenient when a script performs several related operations and you want to capture all their output in one place.

You can also redirect a block's input:

{
    read -r first
    read -r second
    printf 'First: %s\nSecond: %s\n' "$first" "$second"
} < input.txt

The commands in the block share the redirected standard input.

Braces run the commands in the current shell context, whereas parentheses run them in a subshell. That distinction matters when the commands change shell variables or other shell state, although the basic redirection syntax works with both forms.

13. Keeping Redirections Open with exec

So far, most examples have applied redirection to a single command or group of commands. Bash's exec builtin allows you to change the current shell's file descriptors so that later commands inherit the new configuration.

For example:

exec >> script.log 2>&1

After this command, subsequent standard output and standard error from the current shell and its commands will normally be appended to script.log.

This can be useful for scripts that need to send all subsequent output to a log file without repeating the redirections on every command.

Use it carefully: the redirection persists in the current shell. If you run it interactively, your terminal may stop showing ordinary output from that shell.

You can also use exec to open a dedicated descriptor:

exec 3>> script.log
printf 'This is a log entry\n' >&3

Descriptor 3 remains open for subsequent commands in the current shell until you close it:

exec 3>&-

This technique is useful for separating log messages from standard output and standard error in larger scripts.

14. A Practical Logging Pattern

Let's combine several concepts into a small example.

Suppose a script needs to:

  • Write normal output to a log file.
  • Write errors to the same log file.
  • Continue displaying progress on the terminal.

You could use tee for the combined output:

#!/usr/bin/env bash

set -o pipefail

run_task() {
    printf 'Starting task...\n'
    find /etc -type f
    printf 'Task finished.\n'
}

run_task 2>&1 | tee task.log
status=$?

if (( status != 0 )); then
    printf 'Task failed with status %s\n' "$status" >&2
    exit "$status"
fi

In this example, run_task sends both output streams into the pipeline. tee displays the output and writes it to task.log. The pipefail option allows the pipeline to report a failure from run_task, even if tee succeeds.

Notice that the script captures $? immediately after the pipeline. Any intervening command would change that value.

For production scripts, you would also want to consider what happens if the log file cannot be opened or if the filesystem runs out of space. Redirection is a powerful mechanism, but it doesn't guarantee that logging will succeed.

15. Common Redirection Mistakes

A few common mistakes account for much of the confusion around file descriptors.

Mistake 1: Reversing the order of 2>&1

These commands are not equivalent:

command > output.log 2>&1
command 2>&1 > output.log

The first sends both streams to the file. The second sends standard output to the file but leaves standard error connected to the original standard output destination.

Always read redirections from left to right.

Mistake 2: Thinking > redirects errors too

This command:

command > output.log

redirects standard output only.

To redirect standard error as well, use:

command > output.log 2>&1

Mistake 3: Using the wrong syntax

To redirect standard error to a file, write:

command 2> errors.log

The 2 is the file descriptor number and must appear before the redirection operator. 2&>file is not the correct syntax for this operation.

Mistake 4: Accidentally truncating a file

This:

command > output.log

overwrites the file's contents.

If you want to append, use:

command >> output.log

Mistake 5: Assuming jobs or a pipeline changes file descriptors permanently

A redirection normally applies to the command or command group where it appears. A pipeline connects streams between commands. Neither operation automatically changes the persistent descriptors of your interactive shell.

To change the current shell's descriptors for subsequent commands, use exec deliberately.

Conclusion

File descriptors are the mechanism Bash uses to connect commands to their input and output. Once you understand the three standard descriptors — stdin (0), stdout (1), and stderr (2) — the rest of redirection becomes much easier to reason about.

The most important patterns to remember are:

command > output.log
command >> output.log
command 2> errors.log
command > output.log 2>&1
command 2>&1 | tee output.log

And above all, remember that redirections are processed from left to right. Understanding that single rule will help you predict where data goes, diagnose confusing behavior, and write more reliable Bash scripts.

File descriptors may look like a small detail, but they're one of the foundations of effective shell programming. Once they click, you can connect commands, handle errors, build logging systems, and control data flow with far more confidence.

Read next