Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Bash & the POSIX Shell  Module 6 – Redirection, pipes and substitution

Redirection: stdin, stdout, stderr, /dev/null

Point stdin, stdout and stderr at files or /dev/null with <, >, >> and 2>, see why 2>&1 must follow > file, avoid dash's &> trap and log with tee.

  • Beginner
  • 15 minutes
  • Examples run with Bash 5.3.20, dash 0.5.13.5, shellcheck 0.11.0 and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 15 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Read input from a file with <, and send results and errors to separate files or to one with >, >>, 2> and 2>&1
  • Predict where each stream goes for both orders of 2>&1 and > file, with and without a pipe
  • Avoid &> in sh scripts, protect files with set -C and overwrite one on purpose with >|
  • Use /dev/null to discard output and grep -q to test for a match without output
  • Log a command's output with tee and tee -a while it still shows on the screen

Before you start

What you’ll learn · Before you start

What you will learn

  • Read input from a file with <, and send results and errors to separate files or to one with >, >>, 2> and 2>&1
  • Predict where each stream goes for both orders of 2>&1 and > file, with and without a pipe
  • Avoid &> in sh scripts, protect files with set -C and overwrite one on purpose with >|
  • Use /dev/null to discard output and grep -q to test for a match without output
  • Log a command's output with tee and tee -a while it still shows on the screen

Before you start

On this page

./report.sh > out.txt saved two of the report’s three lines in out.txt and still printed the third, a warning, on the screen, because the warning travelled on a different stream. Every command starts with three: standard input: Descriptor 0, the stream a command reads from: the keyboard in a terminal, unless < file or a pipe points it somewhere else. Also called stdin. (stdin), where it reads, by default from the keyboard; standard output: Descriptor 1, where a command writes its results: the terminal, unless > file, >> file or a pipe points it somewhere else. Also called stdout. (stdout), where its results go; and standard error: Descriptor 2, where a command writes its warnings and error messages, kept apart from its results so that they still show when standard output goes to a file or a pipe. Also called stderr. (stderr), where its warnings and errors go. In a terminal, stdout and stderr both go to the screen until a redirection: Notation such as > file, 2> file, < file or 2>&1 that points one of a command's file descriptors at a file, or at the place another descriptor points to, before the command runs. Redirections are applied from left to right. points one of them somewhere else, and null device: /dev/null, a device file that discards everything written to it and gives end of file at once to anything that reads it. Pointing a stream at it throws that output away. is the place to point a stream you want thrown away: whatever is written there disappears.

You will send results and errors to separate files or to one, predict what each order of 2>&1 and > file does when the output goes on into a A one-way channel between two commands, written cmd1 | cmd2: what cmd1 writes on its standard output, cmd2 reads on its standard input., recognise the &> line that dash runs in the background, stop > from overwriting a file, and keep a log with tee while you watch the output.

Every command starts with three descriptors: 0, 1 and 2

A file descriptor: A small number a running program uses to name one of its open files or streams. A program normally starts with 0 (standard input), 1 (standard output) and 2 (standard error) already open., fd for short, is a small number a running program uses to name one of its open files. A program normally starts with three of them open, and POSIX fixes their numbers: 0 is stdin, 1 is stdout and 2 is stderr. A program writes its results to descriptor 1 and its complaints to descriptor 2 without knowing where either goes. The session’s report.sh writes two results with plain echo and one warning with echo … >&2, which points that one echo’s descriptor 1 at whatever descriptor 2 points to:

Results on descriptor 1, a warning on descriptor 2, and the files they went to Python · streams_session.py
from session import run, write

write("report.sh", """#!/usr/bin/env bash
# report.sh: two results on standard output, one warning on standard error
echo "orders read: 3"
echo "warning: order 17 has no price" >&2
echo "total: 4200"
""")
run("""
cat report.sh
chmod +x report.sh
./report.sh
./report.sh > out.txt
cat out.txt
./report.sh 2> err.txt
cat err.txt
!tr a-z A-Z out.txt
tr a-z A-Z < out.txt
./report.sh >> out.txt 2>> err.txt
wc -l out.txt err.txt
./report.sh > out.txt 2> err.txt
wc -l out.txt err.txt
echo "saved" 2 > note.txt; cat note.txt
!cat < missing.txt
!./report.sh > nodir/out.txt
""")

Output

$ cat report.sh
#!/usr/bin/env bash
# report.sh: two results on standard output, one warning on standard error
echo "orders read: 3"
echo "warning: order 17 has no price" >&2
echo "total: 4200"
$ chmod +x report.sh
$ ./report.sh
orders read: 3
warning: order 17 has no price
total: 4200
$ ./report.sh > out.txt
warning: order 17 has no price
$ cat out.txt
orders read: 3
total: 4200
$ ./report.sh 2> err.txt
orders read: 3
total: 4200
$ cat err.txt
warning: order 17 has no price
$ tr a-z A-Z out.txt
usage: tr [-Ccsu] string1 string2
       tr [-Ccu] -d string1
       tr [-Ccu] -s string1
       tr [-Ccu] -ds string1 string2
[exit status 1]
$ tr a-z A-Z < out.txt
ORDERS READ: 3
TOTAL: 4200
$ ./report.sh >> out.txt 2>> err.txt
$ wc -l out.txt err.txt
       4 out.txt
       2 err.txt
       6 total
$ ./report.sh > out.txt 2> err.txt
$ wc -l out.txt err.txt
       2 out.txt
       1 err.txt
       3 total
$ echo "saved" 2 > note.txt; cat note.txt
saved 2
$ cat < missing.txt
bash: missing.txt: No such file or directory
[exit status 1]
$ ./report.sh > nodir/out.txt
bash: nodir/out.txt: No such file or directory
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 streams_session.py

Run with no redirection, the three lines looked alike. > out.txt caught the two results and left the warning on the screen; 2> err.txt caught the warning alone. The operators differ by the descriptor they change and by what they do to the file:

You write Descriptor The file is
< file 0, stdin opened for reading
> file 1, stdout created, or emptied if it exists
>> file 1, stdout created, or added to at its end
2> file 2, stderr created, or emptied if it exists
2>> file 2, stderr created, or added to at its end

Standard input is where a filter such as sort, wc or grep reads when it is given no file name: the keyboard in a terminal, unless < or a pipe points descriptor 0 at something else. tr reads nothing else, so tr a-z A-Z out.txt failed with a usage message and status 1, and tr a-z A-Z < out.txt handed it the file as descriptor 0. One more run with >> and 2>> grew the two files from 2 and 1 lines to 4 and 2; the next run with > and 2> cut them back to 2 and 1, because > empties the file every time it opens it.

The shell carries out every redirection before the command starts. cat < missing.txt failed with a message from bash, not from cat, and ./report.sh > nodir/out.txt printed no warning at all: the folder did not exist, so the script never ran. Both ended with exit status: The number from 0 to 255 every command ends with, kept by the shell in $?. 0 means success; Bash uses 126 for found but not executable and 127 for not found. More in “Your first script: shebang, chmod and running it” 1.

A space between 2 and >

Wrong: echo "saved" 2 > note.txt, which wrote saved 2 into the file: a 2 followed by a space is an ordinary argument, and > redirected descriptor 1 as usual. Right: 2> note.txt, with the number touching the operator.

Redirections run from left to right, and a copy keeps its old target

Sending both streams to one file takes two redirections: > out.txt for descriptor 1, and 2>&1, which makes descriptor 2 a copy of descriptor 1. Written in either order they look alike, and they are not. The session pipes each command into sed 's/^/pipe> /', which marks every line that reaches it, so each line shows which way it went:

Both orders of > out.txt and 2>&1, shown through a pipe Python · order_session.py
from session import run, write

write("report.sh", """#!/usr/bin/env bash
# report.sh: two results on standard output, one warning on standard error
echo "orders read: 3"
echo "warning: order 17 has no price" >&2
echo "total: 4200"
""")
write("check.sh", """#!/usr/bin/env bash
# check.sh: run the report with its results and its warning in out.txt
./report.sh 2>&1 > out.txt
""")
run("""
chmod +x report.sh
./report.sh 2>&1 | sed 's/^/pipe> /'
./report.sh > out.txt 2>&1 | sed 's/^/pipe> /'
cat out.txt
./report.sh 2>&1 > out.txt | sed 's/^/pipe> /'
cat out.txt
!shellcheck check.sh
""")

Output

$ chmod +x report.sh
$ ./report.sh 2>&1 | sed 's/^/pipe> /'
pipe> orders read: 3
pipe> warning: order 17 has no price
pipe> total: 4200
$ ./report.sh > out.txt 2>&1 | sed 's/^/pipe> /'
$ cat out.txt
orders read: 3
warning: order 17 has no price
total: 4200
$ ./report.sh 2>&1 > out.txt | sed 's/^/pipe> /'
pipe> warning: order 17 has no price
$ cat out.txt
orders read: 3
total: 4200
$ shellcheck check.sh

In check.sh line 3:
./report.sh 2>&1 > out.txt
            ^--^ SC2069 (warning): To redirect stdout+stderr, 2>&1 must be last (or use '{ cmd > file; } 2>&1' to clarify).

For more information:
  https://www.shellcheck.net/wiki/SC2069 -- To redirect stdout+stderr, 2>&1 m...
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 order_session.py

The first command is the baseline: 2>&1 alone sent the warning into the pipe with the results, and sed marked all three lines. With > out.txt 2>&1, nothing came through the pipe and out.txt held all three lines. With 2>&1 > out.txt, only the warning came through, as pipe> warning: order 17 has no price, and out.txt held the two results.

Three rules explain it. Bash connects the pipe before it applies any redirection, so descriptor 1 starts at the pipe. The shell then applies the redirections in the order they are written, from left to right, as the Bash manual and POSIX both state. And 2>&1 copies where descriptor 1 points at that moment; it does not tie descriptor 2 to descriptor 1 for later. Step through both orders, one redirection at a time:

Interactive · step by step Where descriptors 1 and 2 point, one redirection at a time Each arrow is where a descriptor points. Step through the first order, > out.txt 2>&1, then the second, 2>&1 > out.txt, both piped into sed.
Where descriptors 1 and 2 point, one redirection at a time, step 8 of 8. Showing fd 1 (stdout) (to out.txt), fd 2 (stderr) (to the pipe), out.txt (got the two results), the pipe to sed (got the warning), the terminal. In focus: the pipe to sed, out.txt.> out.txt2>&1fd 1 (stdout)to out.txtfd 2 (stderr)to the pipeout.txtgot the two resultsthe pipe to sedgot the warningthe terminalyour screen

The drawing shows the whole scene; its 8 steps are listed below.

Every step as text
  1. Before any redirection. Both lines run ./report.sh piped into sed. Bash connects the pipe before it applies a single redirection, so descriptor 1 starts at the pipe, and descriptor 2 keeps the terminal it was given by the shell.
  2. First order, > out.txt. The first order is > out.txt 2>&1. Its first redirection opens out.txt, empty, and points descriptor 1 at it. Descriptor 2 is not touched.
  3. First order, 2>&1. 2>&1 makes descriptor 2 a copy of descriptor 1 as it is at that moment, and at that moment descriptor 1 points to out.txt. Both streams now go to the file.
  4. What the first order did. The session printed nothing through the pipe, and cat out.txt showed all three lines of the report, the warning between the two results.
  5. Second order, same start. The second order is 2>&1 > out.txt. It starts from the same place: descriptor 1 at the pipe, descriptor 2 at the terminal.
  6. Second order, 2>&1 first. Now 2>&1 comes first. Descriptor 1 points to the pipe at this moment, so the copy sends descriptor 2 into the pipe as well.
  7. Second order, > out.txt. > out.txt then moves descriptor 1 to the file. The copy made one step earlier does not follow it: descriptor 2 still points to the pipe.
  8. What the second order did. Only the warning reached sed, which printed it as pipe> warning: order 17 has no price, and out.txt held the two results. The same two redirections in the other order split the streams.

In the first order the copy is made after descriptor 1 has moved to the file, so both streams end there. In the second it is made while descriptor 1 still points to the pipe, and moving descriptor 1 afterwards leaves descriptor 2 behind. Without a pipe, descriptor 1 starts at the terminal, and the second order leaves the warning on the screen. ShellCheck flags that order in a script as SC2069, “2>&1 must be last”. Its wiki adds that the warning stays silent when the output is piped, and there the second order has a real use: cmd 2>&1 > /dev/null | grep word searches the errors alone.

Key idea

Read redirections from left to right: 2>&1 copies where descriptor 1 points at that moment, so > file 2>&1 sends both streams to the file and 2>&1 > file leaves standard error where standard output used to point.

Checkpoint

Check: Descriptors 0, 1 and 2 and the redirection operators · The order of > file and 2>&1

  1. Question 1 of 2 A build script ends with ./backup.sh >> backup.log 2> errors.log, and each run of backup.sh prints one result line and one error line. Starting with neither file present, what do the two files hold after three builds?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: backup.log has three result lines, errors.log only the last run's error

    In the session, a run with >> and 2>> grew out.txt and err.txt from 2 and 1 lines to 4 and 2, and a run with > and 2> cut them back to 2 and 1. Each operator decides for its own descriptor whether the file is emptied or added to.

  2. Question 2 of 2 report.sh prints two results on standard output and one warning on standard error. What does ./report.sh 2>&1 > out.txt | wc -l print?

    Type a number.

    A hint

    Follow descriptor 2: where does descriptor 1 point when 2>&1 runs?

    Show the answer: Question 2 of 2

    Answer: 1 lines

    The pipe is connected first, so descriptor 1 starts at the pipe. 2>&1 copies that, sending descriptor 2 into the pipe, and > out.txt then moves only descriptor 1 to the file. Only the warning reaches wc, as only the warning reached sed in the session.

Bash’s short form for both streams breaks under dash

Bash has a shorter way to send both streams to one file: &> file means > file 2>&1, and &>> file means >> file 2>&1. The Bash manual also accepts >& file for the first, but prefers &>. In the session, &> both.txt caught all three lines, and &>> added three more, six in all:

&> and &>> in Bash 5.3, the same line in dash, and in macOS's Bash 3.2 Python · ampersand_session.py
from session import run, write

write("report.sh", """#!/usr/bin/env bash
# report.sh: two results on standard output, one warning on standard error
echo "orders read: 3"
echo "warning: order 17 has no price" >&2
echo "total: 4200"
""")
write("log.sh", """#!/bin/sh
# log.sh: a POSIX sh script that uses the Bash short form
./report.sh &> report.log
""")
run("""
chmod +x report.sh
./report.sh &> both.txt
cat both.txt
./report.sh &>> both.txt
wc -l both.txt
printf 'kept line\\n' > dash.txt
wc -c dash.txt
dash -c './report.sh &> dash.txt; wait'
wc -c dash.txt
!shellcheck log.sh
/bin/bash -c 'echo "$BASH_VERSION"'
/bin/bash -c './report.sh &> old.txt'; wc -l old.txt
!/bin/bash -c './report.sh &>> old.txt'
""")

Output

$ chmod +x report.sh
$ ./report.sh &> both.txt
$ cat both.txt
orders read: 3
warning: order 17 has no price
total: 4200
$ ./report.sh &>> both.txt
$ wc -l both.txt
       6 both.txt
$ printf 'kept line\n' > dash.txt
$ wc -c dash.txt
      10 dash.txt
$ dash -c './report.sh &> dash.txt; wait'
orders read: 3
warning: order 17 has no price
total: 4200
$ wc -c dash.txt
       0 dash.txt
$ shellcheck log.sh

In log.sh line 3:
./report.sh &> report.log
            ^-----------^ SC3020 (warning): In POSIX sh, &> is undefined.

For more information:
  https://www.shellcheck.net/wiki/SC3020 -- In POSIX sh, &> is undefined.
[exit status 1]
$ /bin/bash -c 'echo "$BASH_VERSION"'
3.2.57(1)-release
$ /bin/bash -c './report.sh &> old.txt'; wc -l old.txt
       3 old.txt
$ /bin/bash -c './report.sh &>> old.txt'
/bin/bash: -c: line 0: syntax error near unexpected token `>'
/bin/bash: -c: line 0: `./report.sh &>> old.txt'
[exit status 2]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 ampersand_session.py

POSIX has no &> operator, and dash, which follows POSIX here, reads the two characters as two operators it knows. ./report.sh & is a command started in the background, and > dash.txt is a command with no name, only a redirection, which POSIX says is carried out and nothing else: the file is created, or emptied. So the report’s three lines went to the terminal, and dash.txt, which held 10 bytes, was left with 0. The session adds ; wait so that dash waits for the background job before the next command, which it otherwise would not do.

ShellCheck catches the line before it ever runs: in a script that starts with #!/bin/sh it reports SC3020, “In POSIX sh, &> is undefined”, and its wiki gives the replacement, > log 2>&1. That long form, and >> log 2>&1 for appending, mean the same in every POSIX shell, so they belong in any script that sh might run.

&>> needs Bash 4.0

Bash’s NEWS lists &>> among the new features of Bash 4.0. The /bin/bash that macOS still ships, 3.2.57, ran &> old.txt correctly and refused &>> old.txt with a syntax error and status 2.

/dev/null throws output away, and a quiet grep needs none

Some output is noise: a progress message, a warning you have already dealt with, the matching lines of a search whose only question is whether anything matched. POSIX defines /dev/null as an empty data source and an endless sink: whatever is written to it is discarded, and reading it gives end of file at once. Point a stream at it to silence it:

Silencing one stream or both, and testing for a match without output Python · null_session.py
from session import run, write

write("report.sh", """#!/usr/bin/env bash
# report.sh: two results on standard output, one warning on standard error
echo "orders read: 3"
echo "warning: order 17 has no price" >&2
echo "total: 4200"
""")
write("orders.log", "order 15 priced\norder 16 priced\norder 17 has no price\n")
run("""
chmod +x report.sh
./report.sh > /dev/null
./report.sh 2> /dev/null
./report.sh > /dev/null 2>&1
!ls missing.txt > /dev/null 2>&1
wc -c < /dev/null
grep 'no price' orders.log > /dev/null && echo "a price is missing"
grep -q 'no price' orders.log && echo "a price is missing"
yes 'order 18 has no price' | grep -q 'no price'; echo "grep -q returned $?"
!grep -q 'no price' missing.log
!grep -qs 'no price' missing.log
""")

Output

$ chmod +x report.sh
$ ./report.sh > /dev/null
warning: order 17 has no price
$ ./report.sh 2> /dev/null
orders read: 3
total: 4200
$ ./report.sh > /dev/null 2>&1
$ ls missing.txt > /dev/null 2>&1
[exit status 1]
$ wc -c < /dev/null
       0
$ grep 'no price' orders.log > /dev/null && echo "a price is missing"
a price is missing
$ grep -q 'no price' orders.log && echo "a price is missing"
a price is missing
$ yes 'order 18 has no price' | grep -q 'no price'; echo "grep -q returned $?"
grep -q returned 0
$ grep -q 'no price' missing.log
grep: missing.log: No such file or directory
[exit status 2]
$ grep -qs 'no price' missing.log
[exit status 2]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 null_session.py

> /dev/null silenced the results and left the warning, 2> /dev/null did the opposite, and > /dev/null 2>&1, in that order, silenced both. The text goes, the status stays: ls missing.txt > /dev/null 2>&1 printed nothing and still ended with status 1, so a script can test a command quietly. As input, /dev/null is empty: wc -c < /dev/null counted 0 bytes, an easy way to give a command nothing to read.

Searching with grep … > /dev/null works, but grep -q says what you mean. POSIX defines -q to write nothing to standard output and to exit with 0 when a line is selected, and notes that it can stop at the first match. That is why yes 'order 18 has no price' | grep -q 'no price' returned with status 0, although yes repeats its line for ever: grep stopped at the first matching line, and the next lesson shows how the pipeline then ends yes.

-q quiets standard output only. With missing.log the error still appeared, on standard error, with status 2, and -s, which POSIX defines to suppress the messages about missing or unreadable files, removed the message but kept the status.

Test the command, not its output

Write if grep -q 'no price' orders.log; then … instead of sending grep’s lines to /dev/null: the intent is plain, nothing is printed, and grep can stop reading at the first match.

Checkpoint

Check: &> and &>> in Bash, dash and Bash 3.2 · /dev/null and grep -q

  1. Question 1 of 2 a.txt holds the 10 bytes of kept line and its newline. In dash you run ./report.sh &> a.txt; wait and then wc -c < a.txt. What number does wc print?

    Type a number.

    Show the answer: Question 1 of 2

    Answer: 0 bytes

    dash reads ./report.sh & as a command started in the background and > a.txt as a command with only a redirection, which empties the file. The report's lines go to the terminal and a.txt is left with 0 bytes, as dash.txt was in the session.

  2. Question 2 of 2 The folder /backups/today does not exist. What does if ls /backups/today > /dev/null 2>&1; then echo ok; else echo missing; fi print?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: missing

    In the session, ls missing.txt > /dev/null 2>&1 printed nothing and still ended with status 1. Silencing a command and testing it go together.

The noclobber option refuses to overwrite a file

> empties a file without asking, so one wrong file name can wipe out a previous run’s results. The noclobber: A shell option, switched on with set -C or set -o noclobber, that makes > refuse to overwrite an existing regular file; >| overrides it for one redirection. option, switched on with set -C (or set -o noclobber) and off with set +C, makes > refuse to open a regular file that already exists:

noclobber refusing >, while >>, >| and new files still work Python · noclobber_session.py
from session import run, write

write("totals.txt", "first run: 4200\n")
run("""
set -C
!echo "second run: 3900" > totals.txt
echo "second run: 3900" >> totals.txt
cat totals.txt
echo "third run: 5100" >| totals.txt
cat totals.txt
echo "draft" > new.txt
echo "noise" > /dev/null
echo "from tee" | tee totals.txt
cat totals.txt
set +C
""")

Output

$ set -C
$ echo "second run: 3900" > totals.txt
bash: totals.txt: cannot overwrite existing file
[exit status 1]
$ echo "second run: 3900" >> totals.txt
$ cat totals.txt
first run: 4200
second run: 3900
$ echo "third run: 5100" >| totals.txt
$ cat totals.txt
third run: 5100
$ echo "draft" > new.txt
$ echo "noise" > /dev/null
$ echo "from tee" | tee totals.txt
from tee
$ cat totals.txt
from tee
$ set +C

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 noclobber_session.py

The refused > printed “cannot overwrite existing file”, ended with status 1 and left totals.txt as it was, while >> still added a line, since appending destroys nothing. >| is the deliberate overwrite: it replaced both lines with the third run’s result. A new file was created without complaint, and the write to /dev/null went through too, since /dev/null is a device, not a regular file. tee totals.txt overwrote the file anyway: noclobber is a rule of the shell’s >, and tee opens its files itself.

POSIX adds a guarantee that makes the option useful beyond caution: the check that the file does not exist and its creation happen as one step, as when a program opens a file with the flags O_CREAT and O_EXCL. Two scripts that run set -C and then : > job.lock at the same moment cannot both succeed, which POSIX says is what makes lock files created this way reliable, with caveats it lists in its rationale.

tee copies one stream to files and to the screen

A long job raises a choice: watch its output or keep a log. tee does both. POSIX defines it to copy its standard input to its standard output and to every file it is given, emptying those files first, and -a makes it append to them instead:

tee and tee -a keeping a log of both streams Python · tee_session.py
from session import run, write

write("report.sh", """#!/usr/bin/env bash
# report.sh: two results on standard output, one warning on standard error
echo "orders read: 3"
echo "warning: order 17 has no price" >&2
echo "total: 4200"
""")
write("count.awk", 'BEGIN { print "line 1 to stdout"; print "line 2 to stderr" > "/dev/stderr"; print "line 3 to stdout" }\n')
run("""
chmod +x report.sh
./report.sh 2> err.txt | tee run.log
cat err.txt
./report.sh 2>&1 | tee run.log
cat run.log
./report.sh 2>&1 | tee -a run.log
wc -l run.log
./report.sh 2>&1 | tee -a run.log > /dev/null
wc -l run.log
awk -f count.awk 2>&1 | tee merged.log
""")

Output

$ chmod +x report.sh
$ ./report.sh 2> err.txt | tee run.log
orders read: 3
total: 4200
$ cat err.txt
warning: order 17 has no price
$ ./report.sh 2>&1 | tee run.log
orders read: 3
warning: order 17 has no price
total: 4200
$ cat run.log
orders read: 3
warning: order 17 has no price
total: 4200
$ ./report.sh 2>&1 | tee -a run.log
orders read: 3
warning: order 17 has no price
total: 4200
$ wc -l run.log
       6 run.log
$ ./report.sh 2>&1 | tee -a run.log > /dev/null
$ wc -l run.log
       9 run.log
$ awk -f count.awk 2>&1 | tee merged.log
line 2 to stderr
line 1 to stdout
line 3 to stdout

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 tee_session.py

2> err.txt | tee run.log showed and logged the two results and kept the warning in err.txt, because only descriptor 1 goes into a pipe. 2>&1 | tee run.log put all three lines on the screen and in the log. A run with tee -a made it 6 lines, and tee -a run.log > /dev/null added three more, 9 in all, without showing them: a quiet way for a script to keep its log. To split a pasted log the same way in a browser, the filter lines tool keeps the lines that contain a word, such as warning, beside the lines it removes.

The last command shows the limit of a merged log. The small awk program prints line 1 and line 3 to standard output and line 2 to standard error, in that order, yet the log begins with line 2. POSIX lets a program keep its standard output in a buffer when that output is not a terminal, and write it later in blocks, here after line 2 had already gone out, while standard error must never be buffered that way. A merged log keeps the order in which bytes were written, not the order of the program’s print statements; the order within each stream is always kept.

Checkpoint

Check: noclobber and >| · tee, tee -a and merged logs

  1. Question 1 of 2 set -C is on, and totals.txt exists. Which of these commands replace its contents with new text?

    Choose every answer that is right.

    Show the answer: Question 1 of 2

    Answer:

    • echo 5100 | tee totals.txt
    • echo 5100 >| totals.txt

    noclobber stops only the shell's > from emptying an existing regular file. >| is the deliberate way past it, and a program such as tee is not affected at all.

  2. Question 2 of 2 run.log is empty. You run ./report.sh 2>&1 | tee run.log twice, then ./report.sh | tee -a run.log once. How many lines does run.log hold?

    Type a number.

    Show the answer: Question 2 of 2

    Answer: 5 lines

    Each plain tee empties run.log first, so after the second run it holds 3 lines. The last run appends, and without 2>&1 only the two results reach tee: 3 + 2 = 5.

Practise: split a report into three logs

Real scripts often need all three logs at once: the results, the warnings, and both together. Build it in your own terminal with the report.sh of the first session:

  1. Write a command, or two, that leaves the two results in out.log, the warning in err.log and all three lines in all.log, each file emptied first.
  2. Check it: wc -l out.log err.log all.log must show 2, 1 and 3, grep -c warning out.log must print 0, and all.log must hold the results in their own order plus the warning.
  3. Run it again and check that the counts stay 2, 1 and 3. Then change it to append, run it twice from empty files, and check for 4, 2 and 6.

One way runs the report once with > out.log 2> err.log and then builds all.log from the two files with cat; it keeps the order within each stream, which is all a merged log can promise. Doing it in a single command is easiest with a third descriptor or with process substitution, from File descriptors and exec redirections and Command and process substitution.

The questions below use new commands and files with the rules of the sessions. Work out where every line goes before you answer:

Practice that adapts to you

Practise this lesson

19 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 19 Which descriptor does a program write its error messages to?

    Choose one answer.

    Show the answer: Question 1 of 19

    Answer: 2, standard error

    report.sh wrote its warning with >&2, to descriptor 2, and 2> err.txt caught exactly that line.

  2. Question 2 of 19 notes.txt holds 4 lines. What does wc -l < notes.txt print after echo a > notes.txt; echo b >> notes.txt?

    Type a number.

    Show the answer: Question 2 of 19

    Answer: 2 lines

    > empties the file and writes a, and >> then adds b at the end: 2 lines. The 4 old lines are gone.

  3. Question 3 of 19 grep error < /var/log/app.log fails with "bash: /var/log/app.log: Permission denied". Which program reported the problem, and did grep run?

    Choose one answer.

    Show the answer: Question 3 of 19

    Answer: bash reported it, and grep never ran

    cat < missing.txt failed with a message that began bash:, and ./report.sh > nodir/out.txt printed no warning, because the script never started. The same holds for every redirection.

  4. Question 4 of 19 In what order does Bash carry out the redirections of one command?

    Choose one answer.

    Show the answer: Question 4 of 19

    Answer: From left to right, as they are written

    Left to right is the whole rule; the rest follows from what each redirection does at its turn.

  5. Question 5 of 19 In a terminal, with no pipe, you run ./report.sh 2>&1 > out.txt. What appears on the screen?

    Choose one answer.

    Show the answer: Question 5 of 19

    Answer: The warning only

    Without a pipe descriptor 1 starts at the terminal, so the copy sends the warning to the screen, while the two results go to out.txt.

  6. Question 6 of 19 You want grep to search only the error messages of ./build.sh and to throw its normal output away. Which line does that?

    Choose one answer.

    Show the answer: Question 6 of 19

    Answer: ./build.sh 2>&1 > /dev/null | grep -i error

    The order that ShellCheck's SC2069 warns about in a plain script is the right tool here, which is why its wiki says the warning stays silent when the output is piped.

  7. Question 7 of 19 In Bash, what does cmd &>> log mean?

    Choose one answer.

    Show the answer: Question 7 of 19

    Answer: cmd >> log 2>&1: append both streams to log

    In the session, &> both.txt wrote three lines and &>> both.txt made it six.

  8. Question 8 of 19 How does dash read the line ./report.sh &> out.txt?

    Choose one answer.

    Show the answer: Question 8 of 19

    Answer: As ./report.sh & and then > out.txt, a command with only a redirection, which empties out.txt

    The session showed the three lines on the terminal and dash.txt cut from 10 bytes to 0.

  9. Question 9 of 19 A script must append both streams of an import command to import.log, and it is run by Bash 5, by dash and by macOS's /bin/bash 3.2. Which line works in all three?

    Choose one answer.

    Show the answer: Question 9 of 19

    Answer: import >> import.log 2>&1

    Spell out >> file 2>&1 in any script that sh may run; ShellCheck's SC3020 points at the short forms.

  10. Question 10 of 19 What does a command read when its standard input is < /dev/null?

    Choose one answer.

    Show the answer: Question 10 of 19

    Answer: Nothing: end of file at once

    /dev/null is an empty source and an endless sink: it gives nothing and keeps nothing.

  11. Question 11 of 19 Lines 3 and 9 of app.log contain the word timeout. grep -q timeout app.log prints nothing. What is $? right after it?

    Type a number.

    Show the answer: Question 11 of 19

    Answer: 0

    -q writes nothing to standard output and exits with 0 when a line is selected, here already at line 3. The empty output is the point; the status carries the answer.

  12. Question 12 of 19 Which line runs ./sync.sh with nothing at all on the screen?

    Choose one answer.

    Show the answer: Question 12 of 19

    Answer: ./sync.sh > /dev/null 2>&1

    The order rule of the lesson applies to /dev/null as to any file: > /dev/null 2>&1, in that order.

  13. Question 13 of 19 A monitor runs grep -qs ERROR /var/log/app.log and reports "no errors found" whenever the status is not 0. One night the log file was deleted. What does the monitor report, and why?

    Choose one answer.

    Show the answer: Question 13 of 19

    Answer: "no errors found", because -s hid the message and status 2 counts as not 0

    Quiet options remove text, never the status. Test 0 (found), 1 (not found) and 2 or more (could not search) separately, or the monitor reports a missing log as a healthy one.

  14. Question 14 of 19 What does set -C change in Bash?

    Choose one answer.

    Show the answer: Question 14 of 19

    Answer: > refuses to overwrite a regular file that already exists

    set -C and set -o noclobber are the same option, and set +C switches it off.

  15. Question 15 of 19 With set -C on, why does echo noise > /dev/null still work?

    Choose one answer.

    Show the answer: Question 15 of 19

    Answer: /dev/null is a device, not a regular file, and noclobber protects only regular files

    That is why scripts that run with noclobber can still silence commands with > /dev/null.

  16. Question 16 of 19 Two copies of a script start at the same moment, and job.lock does not exist yet. Each runs set -C, then : > job.lock, and goes on only if that redirection succeeded. How many copies go on?

    Choose one answer.

    Show the answer: Question 16 of 19

    Answer: Exactly one

    POSIX requires the noclobber check and the creation to happen as with O_CREAT and O_EXCL, and names lock files as what this makes reliable.

  17. Question 17 of 19 What does tee file do?

    Choose one answer.

    Show the answer: Question 17 of 19

    Answer: Copies its standard input to its standard output and to file

    In the session, ./report.sh 2>&1 | tee run.log put the same three lines on the screen and in run.log.

  18. Question 18 of 19 You run ./report.sh | tee run.log. Where does the warning go?

    Choose one answer.

    Show the answer: Question 18 of 19

    Answer: To the screen, but not into run.log

    2> err.txt | tee run.log kept the warning in err.txt and logged only the results; add 2>&1 to log both.

  19. Question 19 of 19 A program prints step 1 and step 3 on standard output and step 2 on standard error, in that order. Run as prog 2>&1 | tee run.log, the log reads step 2, step 1, step 3. Why?

    Choose one answer.

    Show the answer: Question 19 of 19

    Answer: Its standard output is buffered because it is a pipe, and written later; standard error is written at once

    The awk program of the session did the same: line 2 came first in merged.log. Within each stream the order is kept, so check a merged log stream by stream.

Interview questions

Warm-up: name the three standard streams with their numbers, and say what 2>&1 does (fresher to 2–5 years).

Standard input is descriptor 0, where a command reads, by default from the keyboard. Standard output is descriptor 1, for results, and standard error is descriptor 2, for warnings and errors; in a terminal both go to the screen. 2>&1 makes descriptor 2 a copy of descriptor 1 as it points at that moment, so errors go wherever results are going. Because redirections run from left to right, cmd > out.txt 2>&1 puts both streams in out.txt, while cmd 2>&1 > out.txt sends errors to the old standard output, the terminal or a pipe, and only the results to the file. Piped into sed, the second form let the warning alone through the pipe.

The two harder questions are about a scheduled job whose failures never reach its log and a sh script whose log stays empty on another machine. For each, say where every descriptor ends up before you read the first step on its page:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Fresher to mid

    A nightly cron entry runs /opt/jobs/sync.sh 2>&1 >> /var/log/sync.log. The log fills with progress lines, but on the night the remote host was down it holds no error at all, and the error text arrived in the job's cron mail instead. Explain where each stream went, and fix the line.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    setup.sh begins with #!/bin/sh and set -e, then runs ./migrate.sh &> migrate.log and ./seed.sh. Run with bash on a laptop, migrate.log fills. The build server runs ./setup.sh, and its sh is dash: migrate.log stays empty, the migration's output lands in the job console, and seed.sh sometimes fails on missing tables. Explain all three, and fix it.

    Model answer, Pro. First step and follow-ups

Flashcards · 8 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. Which three descriptors does every command start with?

    0 for standard input, where it reads; 1 for standard output, for results; and 2 for standard error, for warnings and errors. In a terminal, 1 and 2 both go to the screen.

  2. How do > file and >> file differ, and when is the file opened?

    > empties the file and writes it, >> appends to it, and 2> and 2>> do the same for standard error. The shell carries out every redirection before the command starts, so a folder that does not exist stops the command before it runs.

  3. Why does cmd > file 2>&1 put both streams in the file while cmd 2>&1 > file does not?

    Redirections run from left to right, and 2>&1 copies where descriptor 1 points at that moment. In the second form, standard error copies the terminal or pipe before standard output moves to the file.

  4. Why should a script that sh may run avoid &> file?

    It is Bash's short form of > file 2>&1. dash reads it as & followed by > file: it runs the command in the background and empties the file. Bash 3.2 also lacks &>>, so portable scripts write the long forms.

  5. What does a redirection to /dev/null hide?

    Only the text: /dev/null discards what is written to it and reads as empty, but the command's exit status is still there for if, && and ||.

  6. How do you test whether a file contains a match without printing anything?

    grep -q prints nothing and reports through its status, 0 for a match, and it can stop at the first match instead of reading the whole file.

  7. What do set -C and >| do?

    set -C, noclobber, makes > refuse to overwrite an existing regular file. >| overrides it for one redirection, while >>, new files and /dev/null still work.

  8. What does tee do, and how do you keep it from emptying its files?

    It copies its standard input to its standard output and to every file it names. It empties those files first unless -a makes it append.

Key takeaways

  • Every command starts with descriptor 0 for standard input, 1 for standard output and 2 for standard error; < reads a file on 0, > and 2> empty a file and write it, >> and 2>> append, and the shell opens each file before the command runs.
  • Redirections are applied from left to right and 2>&1 copies where descriptor 1 points at that moment, so > file 2>&1 sends both streams to the file while 2>&1 > file leaves standard error on the old standard output, the terminal or a pipe.
  • &> file and &>> file are Bash’s short forms of > file 2>&1 and >> file 2>&1; dash runs the command in the background and empties the file instead, and Bash 3.2 lacks &>>, so scripts that sh may run use the long forms.
  • /dev/null discards what is written to it and reads as empty, a redirection to it hides text but never the exit status, and grep -q tests for a match without printing it and can stop at the first one.
  • set -C makes > refuse to overwrite an existing regular file and >| overrides it for one redirection, while >>, new files and /dev/null still work, and tee, which opens its files itself, is not affected.
  • tee copies its standard input to the screen and to files, emptying them unless -a appends, and a merged log keeps the order within each stream but not always the order across the two.

Terms in this lesson

file descriptor
A small number a running program uses to name one of its open files or streams. A program normally starts with 0 (standard input), 1 (standard output) and 2 (standard error) already open.
noclobber
A shell option, switched on with set -C or set -o noclobber, that makes > refuse to overwrite an existing regular file; >| overrides it for one redirection.
null device
/dev/null, a device file that discards everything written to it and gives end of file at once to anything that reads it. Pointing a stream at it throws that output away.
redirection
Notation such as > file, 2> file, < file or 2>&1 that points one of a command's file descriptors at a file, or at the place another descriptor points to, before the command runs. Redirections are applied from left to right.
standard error
Descriptor 2, where a command writes its warnings and error messages, kept apart from its results so that they still show when standard output goes to a file or a pipe. Also called stderr.
standard input
Descriptor 0, the stream a command reads from: the keyboard in a terminal, unless < file or a pipe points it somewhere else. Also called stdin.
standard output
Descriptor 1, where a command writes its results: the terminal, unless > file, >> file or a pipe points it somewhere else. Also called stdout.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.