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.echoprinted/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
hellowent intotrand another intosed, tee's own copy went to/dev/null, and the two commands' output reachedsortthrough the pipeline. - Why
sort?trandsedrun at the same time, and nothing decides which prints first.sortgives 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.txtexists and is empty. tee > /dev/null | trgavetrnothing 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, ateebuilt in. It is on by default, andsetopt nomultiosbrings back bash's behaviour. - A pipe counts as one of the outputs.
echo hello > copy.txt | tr a-z A-Zboth saved the file and fedtr. - So
tee > /dev/null | tris a different command in zsh. It passedHELLOon, because stdout went to/dev/nulland into the pipe. The bash linetee >(…) >(…) > /dev/null | sortwould therefore push tee's own copy intosortas well. - The braces fix it. Inside
{ … },teehas a single redirection, so zsh makes no copy, andsortreads 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
psubis the input half.(cmd | psub)turns a command's output into a file name, sodiffcan 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.teegot no file argument, its stdout went intoupper.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, andtee > /dev/null | trpasses nothing on. - What fish does instead. Keep one copy in a file with
teeand 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,lookandtee↗ showsteepassing every byte through unchanged, which is what makes it safe to fan a stream out: each command receives exactly the same bytes.
See also¶
- tee saves and passes on — the one-file version
- Descriptors 0, 1 and 2 —
/dev/fd, where>(…)gets its name - Input from a file, a string, a here-document — zsh's MULTIOS for input
- bash manual, Process Substitution ↗
- zsh manual, Redirection: Multios ↗
- zsh manual, Process Substitution ↗ — the note about not waiting
- fish, psub ↗