Guides

How to redirect stdout and stderr to a file in Bash

Redirect stdout and stderr to a file in Bash: > and >>, 2>, why 2>&1 goes last, &>, /dev/null, tee -a, and the versions for cron and systemd services.

Published ~8 min read
On this page

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.

bash
./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 file

The 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

GoalCommand
stdout to a file, overwritecmd > out.log
stdout to a file, appendcmd >> out.log
stderr onlycmd 2> err.log
Both to one filecmd > all.log 2>&1
Both to one file, appendcmd >> all.log 2>&1
Both, bash shorthandcmd &> all.log
Both, append, bash shorthandcmd &>> all.log
Each to its own filecmd > out.log 2> err.log
Discard everythingcmd > /dev/null 2>&1
Discard errors onlycmd 2> /dev/null
Screen and filecmd 2>&1 | tee all.log
Screen and file, appendcmd 2>&1 | tee -a all.log
Only stderr into a pipecmd 2>&1 > /dev/null | grep -i error
Write your own message to stderrecho "failed" >&2
Everything a script prints from here onexec >> /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>.

bash
$ ls /etc/hostname /nonexistent > out.txt
ls: cannot access '/nonexistent': No such file or directory
$ cat out.txt
/etc/hostname

The 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:

bash
$ 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 directory

Why 2>&1 goes after the file name

bash
$ 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/hostname

Redirections 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

bash
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

bash
$ ls /etc/hostname /nonexistent | tee t1.log
ls: cannot access '/nonexistent': No such file or directory
/etc/hostname
$ cat t1.log
/etc/hostname

tee 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:

bash
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 above

After a pipe into tee, $? is tee’s exit code

bash
$ 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=2

The 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

bash
sudo echo "vm.swappiness=10" >> /etc/sysctl.conf                     # Permission denied
echo "vm.swappiness=10" | sudo tee -a /etc/sysctl.conf > /dev/null   # works

The 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

bash
$ ls /etc/hostname /nonexistent 2>&1 > /dev/null | grep -c 'cannot access'
1

This 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

/usr/local/bin/backup.sh
#!/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:

bash script
#!/bin/bash
exec > >(tee -a /var/log/backup.log) 2>&1

Both 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

crontab -e
# 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.log

Without 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:

/etc/systemd/system/myapp.service (excerpt)
[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 follow StandardOutput= 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 symptomCauseFix
bash: $f: ambiguous redirectThe variable after > is empty, or holds a name with a space and is not quotedQuote it: > "$f", and make sure it is set
bash: ./logs/x.log: No such file or directoryThe directory does not exist; > creates files, not directoriesmkdir -p ./logs first
bash: out.txt: cannot overwrite existing filenoclobber is on (set -o noclobber or set -C)>| out.txt overwrites anyway; >> appends
Permission denied with sudo in frontYour shell opens the file before sudo runs| sudo tee file, or | sudo tee -a file
A file named 1 appears2>1 was typed instead of 2>&1Add 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 runsPrograms buffer output when it does not go to a terminalpython3 -u for Python, stdbuf -oL for C programs
&> file leaves an empty file in cron or a #!/bin/sh scriptsh 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.

Related articles

All guides