tee: writing to a file you are not allowed to write to
The pipe that forks
Everyone meets this one eventually. You need to drop a line into a file under /etc, you remember to prefix with sudo, and the shell tells you no anyway:
$ sudo echo "net.ipv4.ip_forward=1" > /etc/demo.conf
bash: /etc/demo.conf: Permission deniedYou used sudo. You still got Permission denied. The reason is worth understanding, because it explains a whole family of shell surprises.
The redirection happens before sudo
> is not part of the command. It’s the shell’s own syntax, and the shell handles it first: it opens the target file, then forks and executes what’s left. So the sequence is:
- your shell, running as you, tries to open
/etc/demo.conffor writing, - it fails, because you are not root,
sudois never reached.
sudo would have elevated echo, which never needed the privilege. The thing that needed it was your shell. That’s why sudo command > file fails while sudo command alone works.
tee, the T-junction
tee is named after the plumbing fitting. It reads standard input, writes it to standard output and to the files you name. Put it on the far side of the pipe and it becomes the process doing the writing, so sudo applies to the right thing:
$ echo "net.ipv4.ip_forward=1" | sudo tee /etc/demo.conf
net.ipv4.ip_forward=1
$ cat /etc/demo.conf
net.ipv4.ip_forward=1Note that it echoed the line back: tee passes its input through, so it chains. Most of the time you don’t want that on your terminal, so send stdout to the bin:
echo "second line" | sudo tee -a /etc/demo.conf > /dev/null-a appends instead of truncating, the same distinction as >> versus >. Forgetting it is how people overwrite a config file they meant to extend.
More than one output
The sudo trick is the famous use, but tee accepts several files at once, handy for keeping a log of something you are also watching:
# wc -l is only here to prove the line came out the other side
$ echo bonjour | tee /tmp/a /tmp/b | wc -l
1
$ cat /tmp/a /tmp/b
bonjour
bonjourOne input, two files on disk, and the stream still reaching wc at the end. This is the shape I use most: run a build, watch it scroll, and keep the full output for later.
make 2>&1 | tee build.log2>&1 matters there. tee only ever sees standard output, so without it your errors go straight to the terminal and never reach the log, exactly when you need them.
Where it stops
Two limits worth keeping in mind.
tee writes as it reads, so a crash halfway through leaves a half-written file. If you are transforming a file in place, that is the wrong tool, and it produces the classic empty-file disaster that deserves its own post.
The other is that tee -a on a shared file is only atomic for small writes. Two processes appending long lines at once can interleave them. For real concurrent logging, let syslog or your init system handle it.
Small caveat on exit codes, since it bites in CI: in a pipeline, the shell reports the status of the last command, so make | tee build.log returns tee’s success even when make failed. Add set -o pipefail in bash, or read ${PIPESTATUS[0]}, otherwise a red build looks green.
← → jump to the previous / next post