Skip to content

Bash File Descriptors and I/O Redirection

In Bash, process input and output are handled using numbers called file descriptors. By default, standard input is 0, standard output is 1, and standard error is 2.

Understanding redirection lets you separate normal logs from error logs, collect output from multiple commands, and combine screen output with file logging using tee.

2>&1 requires particular care. It is not a persistent association that means “always send standard error to the same place as standard output”; it duplicates, at that point, the destination currently referenced by standard output onto standard error. As a result, the order of redirections changes the outcome.

Step 1: Separate stdout and stderr into different files

Section titled “Step 1: Separate stdout and stderr into different files”

First, save standard output and standard error produced by the same operation into separate files.

> changes the destination of file descriptor 1, while 2> changes the destination of file descriptor 2.

Terminal window
sudo bash -c '
set -euo pipefail
rm -f /tmp/bash-fd-stdout.log /tmp/bash-fd-stderr.log
emit() {
printf "%s\n" "STDOUT_LINE"
printf "%s\n" "STDERR_LINE" >&2
}
emit \
> /tmp/bash-fd-stdout.log \
2> /tmp/bash-fd-stderr.log
grep -qx "STDOUT_LINE" /tmp/bash-fd-stdout.log
grep -qx "STDERR_LINE" /tmp/bash-fd-stderr.log
! grep -q "STDERR_LINE" /tmp/bash-fd-stdout.log
! grep -q "STDOUT_LINE" /tmp/bash-fd-stderr.log
printf "%s\n" "stdout-stderr-separated-ok"
'

With this arrangement, normal output is saved independently to /tmp/bash-fd-stdout.log, while error output is saved to /tmp/bash-fd-stderr.log.

This is the basic pattern when you want to manage an application’s normal and error logs separately.

2>&1 duplicates onto file descriptor 2 the destination that file descriptor 1 points to at the moment the expression is evaluated.

Therefore, the result differs depending on whether it appears before or after the standard-output redirection.

The following example compares the two orders.

Terminal window
sudo bash -c '
set -euo pipefail
rm -f \
/tmp/bash-order-combined.log \
/tmp/bash-order-stdout.log \
/tmp/bash-order-stderr.log
emit_a() {
printf "%s\n" "OUT_A"
printf "%s\n" "ERR_A" >&2
}
emit_b() {
printf "%s\n" "OUT_B"
printf "%s\n" "ERR_B" >&2
}
emit_a > /tmp/bash-order-combined.log 2>&1
emit_b 2>&1 > /tmp/bash-order-stdout.log \
| tee /tmp/bash-order-stderr.log > /dev/null
grep -qx "OUT_A" /tmp/bash-order-combined.log
grep -qx "ERR_A" /tmp/bash-order-combined.log
grep -qx "OUT_B" /tmp/bash-order-stdout.log
! grep -q "ERR_B" /tmp/bash-order-stdout.log
grep -qx "ERR_B" /tmp/bash-order-stderr.log
! grep -q "OUT_B" /tmp/bash-order-stderr.log
printf "%s\n" "redirection-order-ok"
'

In the first case, standard output is directed to a file first, and then standard error is duplicated to the same destination as standard output. Both therefore go into the same file.

In the second case, standard error is first duplicated to the original standard output, and only then is standard output changed to a file. Standard error does not follow the later change to standard output.

It is easier to reason about redirection if you remember that it is evaluated from left to right.

When you want to apply the same redirection to several operations, grouping them with braces makes the structure clearer than specifying the redirection on each command individually.

The next example collects multiple standard-output lines into one file and multiple standard-error lines into another.

Terminal window
sudo bash -c '
set -euo pipefail
rm -f /tmp/bash-group-stdout.log /tmp/bash-group-stderr.log
{
printf "%s\n" "GROUP_OUT_1"
printf "%s\n" "GROUP_ERR_1" >&2
printf "%s\n" "GROUP_OUT_2"
printf "%s\n" "GROUP_ERR_2" >&2
} > /tmp/bash-group-stdout.log 2> /tmp/bash-group-stderr.log
grep -qx "GROUP_OUT_1" /tmp/bash-group-stdout.log
grep -qx "GROUP_OUT_2" /tmp/bash-group-stdout.log
grep -qx "GROUP_ERR_1" /tmp/bash-group-stderr.log
grep -qx "GROUP_ERR_2" /tmp/bash-group-stderr.log
! grep -q "GROUP_ERR" /tmp/bash-group-stdout.log
! grep -q "GROUP_OUT" /tmp/bash-group-stderr.log
printf "%s\n" "grouped-output-routed-ok"
'

A brace group runs in the current shell, and the trailing redirections apply to the entire group.

This is useful when you want to collect several checks or a sequence of batch operations in one log.

You can also open a number other than the standard 0, 1, and 2 and use it as a dedicated output destination.

The following example assigns file descriptor 3 to a log file and closes it after writing.

Terminal window
sudo bash -c '
set -euo pipefail
rm -f /tmp/bash-fd3.log
exec 3> /tmp/bash-fd3.log
printf "%s\n" "FD3_LINE" >&3
exec 3>&-
grep -qx "FD3_LINE" /tmp/bash-fd3.log
printf "%s\n" "custom-fd-ok"
'

exec 3> adds file descriptor 3 to the current shell, and >&3 writes to that descriptor.

When processing is complete, close it with exec 3>&-. In long-running scripts, closing file descriptors that are no longer needed makes the I/O paths easier to manage.

Step 5: Compare tee with shell redirection

Section titled “Step 5: Compare tee with shell redirection”

Shell operators such as > and 2> change the destination of a file descriptor itself. By contrast, tee is a command that writes the data it receives from a pipe both to standard output and to a file.

Only standard output normally flows into a pipe, so standard error is not sent to tee automatically.

The following example compares direct redirection, sending only standard output through tee, and merging standard output and standard error before sending them through tee.

Terminal window
sudo bash -c '
set -euo pipefail
rm -f \
/tmp/bash-direct-stdout.log \
/tmp/bash-direct-stderr.log \
/tmp/bash-tee-stdout.log \
/tmp/bash-tee-stderr.log \
/tmp/bash-tee-merged.log
emit() {
printf "%s\n" "TEE_OUT"
printf "%s\n" "TEE_ERR" >&2
}
emit \
> /tmp/bash-direct-stdout.log \
2> /tmp/bash-direct-stderr.log
emit \
2> /tmp/bash-tee-stderr.log \
| tee /tmp/bash-tee-stdout.log > /dev/null
emit 2>&1 \
| tee /tmp/bash-tee-merged.log > /dev/null
cmp /tmp/bash-direct-stdout.log /tmp/bash-tee-stdout.log
cmp /tmp/bash-direct-stderr.log /tmp/bash-tee-stderr.log
grep -qx "TEE_OUT" /tmp/bash-tee-stdout.log
! grep -q "TEE_ERR" /tmp/bash-tee-stdout.log
grep -qx "TEE_ERR" /tmp/bash-tee-stderr.log
! grep -q "TEE_OUT" /tmp/bash-tee-stderr.log
grep -qx "TEE_OUT" /tmp/bash-tee-merged.log
grep -qx "TEE_ERR" /tmp/bash-tee-merged.log
printf "%s\n" "tee-behavior-ok"
'

When only standard output is sent through the pipe, tee saves only standard output. By directing standard error to a separate file, you can manage normal and error logs independently.

If you first merge standard error into standard output with 2>&1 and then send the stream through a pipe, tee receives both as a single stream. Because the original stdout and stderr can no longer be distinguished after the merge, this approach is unsuitable when you need to separate them completely afterward.

If stdout and stderr need to be processed separately later, decide each destination before merging them.

If both should be stored in the same log, decide the standard-output destination first and then duplicate standard error to it.

When multiple commands should use the same logging policy, group them; when you want to keep screen output while also saving it to a file, use tee.

The key is not to treat tee and redirection as the same mechanism. Redirection changes where file descriptors point, while tee duplicates the stream it receives. Understanding this distinction helps prevent mixed logs and lost error output.

Category: Linux