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.
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:
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:
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:
The drawing shows the whole scene; its 8 steps are listed below.
Every step as text
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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
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:
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:
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
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:
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:
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
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:
- 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.
- Check it:
wc -l out.log err.log all.logmust show 2, 1 and 3,grep -c warning out.logmust print 0, and all.log must hold the results in their own order plus the warning. - 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.
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.
-
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.
-
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.
Flashcards · 8 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
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.
-
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.
-
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.
-
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.
-
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 ||.
-
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.
-
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.
-
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,>and2>empty a file and write it,>>and2>>append, and the shell opens each file before the command runs. - Redirections are applied from left to right and
2>&1copies where descriptor 1 points at that moment, so> file 2>&1sends both streams to the file while2>&1 > fileleaves standard error on the old standard output, the terminal or a pipe. &> fileand&>> fileare Bash’s short forms of> file 2>&1and>> 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 -qtests for a match without printing it and can stop at the first one. set -Cmakes>refuse to overwrite an existing regular file and>|overrides it for one redirection, while>>, new files and /dev/null still work, andtee, which opens its files itself, is not affected.teecopies its standard input to the screen and to files, emptying them unless-aappends, 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
- Bash Reference Manual: Redirections (Chet Ramey (GNU Bash maintainer))
- Bash Reference Manual: Redirecting Standard Output and Standard Error (Chet Ramey (GNU Bash maintainer))
- Bash Reference Manual: Duplicating File Descriptors (Chet Ramey (GNU Bash maintainer))
- Bash Reference Manual: Pipelines (Chet Ramey (GNU Bash maintainer))
- Bash Reference Manual: The Set Builtin (Chet Ramey (GNU Bash maintainer))
- Bash NEWS: the new features of each release (Chet Ramey (GNU Bash maintainer))
- POSIX.1-2024: Shell Command Language (2.7 Redirection) (The Open Group and IEEE)
- POSIX.1-2024: Shell Command Language (2.7.2 Redirecting Output) (The Open Group and IEEE)
- POSIX.1-2024: Shell Command Language (2.7.6 Duplicating an Output File Descriptor) (The Open Group and IEEE)
- POSIX.1-2024: Shell Command Language (2.9.1.3 Commands with no Command Name) (The Open Group and IEEE)
- POSIX.1-2024: Shell Command Language (2.9.3.1 Asynchronous AND-OR Lists) (The Open Group and IEEE)
- POSIX.1-2024: set (special built-in, -C) (The Open Group and IEEE)
- POSIX.1-2024: unistd.h (STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO) (The Open Group and IEEE)
- POSIX.1-2024: Directory Structure and Devices (/dev/null) (The Open Group and IEEE)
- POSIX.1-2024: Standard I/O Streams (buffering) (The Open Group and IEEE)
- POSIX.1-2024: grep (-q and -s) (The Open Group and IEEE)
- POSIX.1-2024: tee (The Open Group and IEEE)
- tee(1): Linux manual page (Linux man-pages project)
- stdin(3): standard I/O streams, Linux manual page (Linux man-pages project)
- ShellCheck wiki: SC2069 (ShellCheck project)
- ShellCheck wiki: SC3020 (ShellCheck project)
Report a problem with this lesson
Kept only in this browser. Your Learn progress