Skip to content

tee into several commands

Level: 201 · you want one stream to feed two commands without running it twice

One line: bash and zsh can hand tee a file name that leads into a command, >(cmd), so one stream feeds several commands at once. The copies run side by side, in no fixed order. zsh can also copy output by itself: echo hi > a > b fills both files, where bash fills only b. fish has neither: its psub covers only the input half, and >(…) typed into fish silently means something else.

Measured

Verified output of tee_fanout_sh.sh, identical on Linux and macOS — regenerated by tools/run_examples.py, never hand-typed.

$ echo >(true) <(true)
/dev/fd/63 /dev/fd/62

$ echo hello | tee >(tr a-z A-Z) >(sed "s/^/copy: /") > /dev/null | sort
HELLO
copy: hello

$ seq 1 5 | tee >(wc -l | sed "s/^ */lines: /") >(tail -n 1 | sed "s/^/last: /") > /dev/null | sort
last: 5
lines: 5

$ diff <(printf "a\nb\n") <(printf "a\nc\n")
2c2
< b
---
> c

$ echo hello > a.txt > b.txt; echo "a.txt: [$(cat a.txt)]"; echo "b.txt: [$(cat b.txt)]"
a.txt: []
b.txt: [hello]

$ echo hello | tee > /dev/null | tr a-z A-Z
  • >(cmd) is a file name. echo printed /dev/fd/63: bash started the command, connected a pipe to its stdin, and put a name for that pipe where >(…) was. <(…) is the same in the other direction.
  • tee writes to it like any file. One hello went into tr and another into sed, tee's own copy went to /dev/null, and the two commands' output reached sort through the pipeline.
  • Why sort? tr and sed run at the same time, and nothing decides which prints first. sort gives their lines one order, so the page reads the same on every run. The second example is a practical use of the pattern: count the lines and keep the last one, in one pass over the input.
  • diff <(…) <(…) is the input half: two commands' output compared as if they were two files.
  • Two > on one command: bash opened both files, which emptied both, then wrote only to the last. a.txt exists and is empty.
  • tee > /dev/null | tr gave tr nothing in bash: tee's stdout went to /dev/null, not into the pipe. Keep this one in mind for zsh.

If the command inside >(…) writes a file instead of printing, do not read that file on the next line. zsh's manual warns that the shell does not wait for a >(…) handed to an external command, so the next command cannot count on the output being complete. The examples here read the output through a pipe, which is complete only when every writer has closed it.

On a Mac

Nothing changes. The key is shared: bash 3.2 has process substitution and names it /dev/fd/63 too, even though /dev/fd is a directory on a Mac and a link into /proc on Linux (Descriptors 0, 1 and 2).

In zsh and fish

zsh has >(cmd) and <(cmd), and an option that changes several lines of the bash example:

Verified output of tee_fanout_zsh.zsh, identical on Linux and macOS — regenerated by tools/run_examples.py, never hand-typed.

$ echo hello > a.txt > b.txt; echo "a.txt: [$(cat a.txt)]"; echo "b.txt: [$(cat b.txt)]"
a.txt: [hello]
b.txt: [hello]

$ echo hello > copy.txt | tr a-z A-Z; echo "copy.txt: [$(cat copy.txt)]"
HELLO
copy.txt: [hello]

$ echo hello | tee > /dev/null | tr a-z A-Z
HELLO

$ setopt nomultios; echo hello > c.txt > d.txt; echo "c.txt: [$(cat c.txt)]"; echo "d.txt: [$(cat d.txt)]"; setopt multios
c.txt: []
d.txt: [hello]

$ { echo hello | tee >(tr a-z A-Z) >(sed "s/^/copy: /") > /dev/null } | sort
HELLO
copy: hello

$ diff <(printf "a\nb\n") <(printf "a\nc\n")
2c2
< b
---
> c
  • MULTIOS. With two > on one command, zsh wrote to both files: the shell itself runs a copying process, a tee built in. It is on by default, and setopt nomultios brings back bash's behaviour.
  • A pipe counts as one of the outputs. echo hello > copy.txt | tr a-z A-Z both saved the file and fed tr.
  • So tee > /dev/null | tr is a different command in zsh. It passed HELLO on, because stdout went to /dev/null and into the pipe. The bash line tee >(…) >(…) > /dev/null | sort would therefore push tee's own copy into sort as well.
  • The braces fix it. Inside { … }, tee has a single redirection, so zsh makes no copy, and sort reads the whole group through the pipe.

fish has no >(cmd):

Verified output of tee_fanout_fish.fish, identical on Linux and macOS — regenerated by tools/run_examples.py, never hand-typed.

$ diff (printf "a\nb\n" | psub) (printf "a\nc\n" | psub)
2c2
< b
---
> c

$ echo hello | tee >(echo upper.txt); echo "upper.txt: [$(cat upper.txt)]"
upper.txt: [hello]

$ echo hello > a.txt > b.txt; echo "a.txt: [$(cat a.txt)]"; echo "b.txt: [$(cat b.txt)]"
a.txt: []
b.txt: [hello]

$ echo hello | tee > /dev/null | tr a-z A-Z

$ echo hello | tee copy.txt | tr a-z A-Z; sed "s/^/copy: /" copy.txt
HELLO
copy: hello

$ set -l line (echo hello); echo $line | tr a-z A-Z; echo $line | sed "s/^/copy: /"
HELLO
copy: hello
  • psub is the input half. (cmd | psub) turns a command's output into a file name, so diff can compare two commands. fish has nothing for the output direction.
  • >(…) is not an error. fish read > followed by a command substitution, (echo upper.txt), whose output names the file. tee got no file argument, its stdout went into upper.txt, and nothing appeared on the screen. The bash idiom typed into fish silently writes a file named after whatever the inner command prints.
  • No MULTIOS. Two > keep only the last, as in bash, and tee > /dev/null | tr passes nothing on.
  • What fish does instead. Keep one copy in a file with tee and run the second command on that file, or, for small output, keep it in a variable and pipe it into each command in turn.
bash zsh fish
a command as an output file >(cmd) >(cmd) none; tee to a file
a command as an input file <(cmd) <(cmd) (cmd \| psub)
cmd > a > b only b both only b
tee > /dev/null \| next next gets nothing next gets a copy next gets nothing

If you are coming from another library

  • Encodings. split, paste, look and tee shows tee passing every byte through unchanged, which is what makes it safe to fan a stream out: each command receives exactly the same bytes.

See also