Short answer: command > file 2>&1 sends both standard output (stdout) and standard error (stderr) to file, replacing what was in it; command >> file 2>&1 appends instead. In bash, command &> file and command &>> file are shorter ways to write the same thing. The order matters: 2>&1 > file leaves the errors on your screen. To watch the output and save it at the same time, pipe it into tee: command 2>&1 | tee -a file.
./backup.sh > backup.log 2>&1 # both streams, overwrite the file
./backup.sh >> backup.log 2>&1 # both streams, append
./backup.sh &> backup.log # bash shorthand for the first line
./backup.sh 2>&1 | tee -a backup.log # on screen and in the fileThe shell examples on this page were run in bash 5.2.15 on Debian 12, and the /bin/sh cases in dash 0.5.12, which is /bin/sh on Debian and the shell cron uses there. Outputs are copied from those runs. The sudo, crontab and systemd parts rest on the bash, crontab and systemd documentation.
Redirection cheat sheet
| Goal | Command |
|---|---|
| stdout to a file, overwrite | cmd > out.log |
| stdout to a file, append | cmd >> out.log |
| stderr only | cmd 2> err.log |
| Both to one file | cmd > all.log 2>&1 |
| Both to one file, append | cmd >> all.log 2>&1 |
| Both, bash shorthand | cmd &> all.log |
| Both, append, bash shorthand | cmd &>> all.log |
| Each to its own file | cmd > out.log 2> err.log |
| Discard everything | cmd > /dev/null 2>&1 |
| Discard errors only | cmd 2> /dev/null |
| Screen and file | cmd 2>&1 | tee all.log |
| Screen and file, append | cmd 2>&1 | tee -a all.log |
| Only stderr into a pipe | cmd 2>&1 > /dev/null | grep -i error |
| Write your own message to stderr | echo "failed" >&2 |
| Everything a script prints from here on | exec >> /var/log/job.log 2>&1 |
stdout, stderr and the numbers 1 and 2
Every process starts with three open streams, known by number: 0 is standard input, 1 is standard output (the normal results) and 2 is standard error (error and diagnostic messages). Both 1 and 2 go to your terminal by default, which is why they look like one stream until you redirect one of them. A bare > means 1>.
$ ls /etc/hostname /nonexistent > out.txt
ls: cannot access '/nonexistent': No such file or directory
$ cat out.txt
/etc/hostnameThe listing went into out.txt, but the error was written to stream 2 and still reached the screen. 2> err.txt does the opposite, and the two together split the streams into separate files:
$ ls /etc/hostname /nonexistent > out.txt 2> err.txt
$ cat out.txt
/etc/hostname
$ cat err.txt
ls: cannot access '/nonexistent': No such file or directoryWhy 2>&1 goes after the file name
$ ls /etc/hostname /nonexistent > all.txt 2>&1
$ cat all.txt
ls: cannot access '/nonexistent': No such file or directory
/etc/hostname
$ ls /etc/hostname /nonexistent 2>&1 > wrong.txt
ls: cannot access '/nonexistent': No such file or directory
$ cat wrong.txt
/etc/hostnameRedirections are processed from left to right, and 2>&1 means “point stream 2 wherever stream 1 points right now”. It is a copy taken at that moment, not a lasting link. In the first command, stream 1 already points at the file when stream 2 copies it. In the second, stream 2 copies the terminal, and only then does stream 1 move to the file. The bash manual uses this same pair of commands to explain it.
Two near misses fail without any error message. 2>1, without the &, sends stderr into a new file literally named 1 (checked: the file appeared, holding the error). And 2>&1 with no file redirection anywhere on the line changes nothing you can see, because both streams already go to the terminal.
&> and &>>: the bash shorthand
In bash, &> file is exactly > file 2>&1, and &>> file is >> file 2>&1. >& file also works, but the bash manual prefers the &> form. For pipes, |& is short for 2>&1 |.
Throwing output away: /dev/null
cmd > /dev/null 2>&1 # silence everything
cmd > /dev/null # hide normal output, still see errors
find / -name nginx.conf 2> /dev/null # hide the "Permission denied" noise/dev/null accepts every write and keeps nothing. Hiding errors is handy for find run as a normal user, but do it on purpose: a scheduled job that sends both streams to /dev/null can fail every night for months without anyone seeing a single message. If a job should be quiet, send stdout to /dev/null and keep stderr in a log.
See it and save it: tee and tee -a
$ ls /etc/hostname /nonexistent | tee t1.log
ls: cannot access '/nonexistent': No such file or directory
/etc/hostname
$ cat t1.log
/etc/hostnametee copies its input to the screen and to a file, but its input is only what came through the pipe, and a pipe carries stdout. The error went straight to the terminal and never reached t1.log. Merge the streams before the pipe:
cmd 2>&1 | tee deploy.log # overwrite deploy.log
cmd 2>&1 | tee -a deploy.log # append to it
cmd |& tee -a deploy.log # bash shorthand for the line aboveAfter a pipe into tee, $? is tee’s exit code
$ ls /nonexistent 2>&1 | tee out.log > /dev/null
$ echo "rc=$? PIPESTATUS=${PIPESTATUS[*]}"
rc=0 PIPESTATUS=2 0
$ set -o pipefail
$ ls /nonexistent 2>&1 | tee out.log > /dev/null
$ echo "rc=$?"
rc=2The pipeline reports the exit code of its last command, and tee succeeded. A script that checks $? after | tee never sees the failure unless it sets set -o pipefail (bash), which makes the pipeline fail when any part of it fails, or reads PIPESTATUS, which holds every command’s code in order.
Writing to a root-owned file: sudo tee
sudo echo "vm.swappiness=10" >> /etc/sysctl.conf # Permission denied
echo "vm.swappiness=10" | sudo tee -a /etc/sysctl.conf > /dev/null # worksThe redirection belongs to your own shell, which opens the file as your user before sudo even starts; sudo only lifts echo, which never needed it. With tee, the program that opens the file is the one running under sudo. The trailing > /dev/null stops tee from printing the line back to you.
Only stderr into a pipe
$ ls /etc/hostname /nonexistent 2>&1 > /dev/null | grep -c 'cannot access'
1This uses the “wrong” order on purpose: stream 2 copies the pipe first, then stream 1 goes to /dev/null, so only the errors travel down the pipe and grep counts exactly one line. It is the standard way to filter or count error messages while throwing the normal output away; use > out.log instead of /dev/null to keep it. Searching that error stream is what the grep command guide covers.
Redirecting a whole script with exec
#!/bin/sh
exec >> /var/log/backup.log 2>&1 # every line below goes to the log
echo "start $(date -u +%FT%TZ)"
tar -czf /backup/www.tar.gz /var/www
echo "done"exec with redirections and no command changes the streams of the running script itself, from that line on, so you do not have to add >> log 2>&1 to every command. This form works in sh and bash. In bash you can keep the output on screen as well:
#!/bin/bash
exec > >(tee -a /var/log/backup.log) 2>&1Both forms were checked on Debian 12. The second uses process substitution, >(...), which dash does not have, so that script must start with #!/bin/bash.
Redirecting output in cron
# both streams, appended: the line to copy
15 3 * * * /usr/local/bin/backup.sh >> /home/deploy/backup.log 2>&1
# one log file per day: % has to be escaped in a crontab
15 3 * * * /usr/local/bin/backup.sh >> /home/deploy/backup-$(date +\%F).log 2>&1
# keep only the errors
*/5 * * * * /usr/local/bin/check.sh > /dev/null 2>> /home/deploy/check-errors.logWithout a redirect, cron mails a job’s output to the crontab’s owner, and on a server without a mail transfer agent that output is simply thrown away. Three cron details trip people up: the line runs under /bin/sh, so &> fails as shown above; an unescaped % ends the command (the crontab guide explains why); and the log’s directory must be writable by the crontab’s user, which /var/log is not for a normal account. If a job seems to do nothing even with a log, cron job not running goes through the other causes in order.
Redirecting output in a systemd service
A service has no terminal. By default systemd connects its stdout to the journal and its stderr to wherever stdout goes, so both show up in journalctl -u NAME without any redirection. To write a file instead:
[Service]
ExecStart=/usr/local/bin/myapp
StandardOutput=append:/var/log/myapp.log
StandardError=append:/var/log/myapp-error.log- append: adds to the file, like
>>. - truncate: empties it each time the command starts, like
>. - file: is the trap. It writes from the start of the file without emptying it, so a shorter run leaves the end of the previous one behind.
- Leave
StandardError=out and errors followStandardOutput=into the same file.
All of this is from the systemd.exec man page for systemd 252, the version in Debian 12. The same page warns about one shell habit: when output goes to the journal, stdout and stderr are sockets, so echo "msg" > /dev/stderr fails inside a script. Write echo "msg" >&2 instead, which works everywhere.
Common redirection errors
| Message or symptom | Cause | Fix |
|---|---|---|
bash: $f: ambiguous redirect | The variable after > is empty, or holds a name with a space and is not quoted | Quote it: > "$f", and make sure it is set |
bash: ./logs/x.log: No such file or directory | The directory does not exist; > creates files, not directories | mkdir -p ./logs first |
bash: out.txt: cannot overwrite existing file | noclobber is on (set -o noclobber or set -C) | >| out.txt overwrites anyway; >> appends |
| Permission denied with sudo in front | Your shell opens the file before sudo runs | | sudo tee file, or | sudo tee -a file |
| A file named 1 appears | 2>1 was typed instead of 2>&1 | Add the & |
| The file is empty after sort file > file | > empties the file before sort reads it (checked: 0 bytes) | sort -o file file, or write to a temporary file and mv it |
| The log stays empty while the program runs | Programs buffer output when it does not go to a terminal | python3 -u for Python, stdbuf -oL for C programs |
| &> file leaves an empty file in cron or a #!/bin/sh script | sh reads it as & plus > file | > file 2>&1 |
Frequently asked
What does 2>&1 mean?
It points file descriptor 2 (standard error) at whatever file descriptor 1 (standard output) points to at that moment. Written after a file redirect, as in command > file 2>&1, it sends errors into the same file. Written before it, as in command 2>&1 > file, the errors still go to the terminal.
How do I redirect stdout and stderr to the same file and append?
command >> file 2>&1 works in every shell, including the /bin/sh that cron uses. In bash, command &>> file is a shorter way to write the same thing.
How do I show output on screen and save it to a file at the same time?
Pipe it into tee: command 2>&1 | tee file overwrites the file and command 2>&1 | tee -a file appends to it. The 2>&1 before the pipe matters, because a pipe only carries standard output. After the pipe, $? is tee's exit code; set -o pipefail to get the command's.
Why does &> leave an empty log in my cron job?
Cron runs commands with /bin/sh, which is dash on Debian and Ubuntu. dash reads command &> file as command & (run it in the background) followed by > file, which only creates or empties the file. Write command > file 2>&1 instead, or set SHELL=/bin/bash at the top of the crontab.