A program knows it is piped¶
Level: 101 · you have seen ls print columns on the screen, and one name per line after | wc -l or | less
One line: A program can ask whether its stdout is a terminal, and ls does: it prints columns to a terminal and one name per line into a pipe, which is why ls | wc -l counts files. less asks too, and passes its input straight through like cat when its stdout is not a terminal. In fish, ls is a function that ships with fish, and it asks the same question to add a / after directory names on a terminal only.
Measured¶
Every example in this library writes into a pipe, because the runner reads its output. So the pipe side is the easy one to show:
Verified output of knows_piped_sh.sh, identical on Linux and macOS — regenerated by tools/run_examples.py, never hand-typed.
$ touch alma darkfi fedora; mkdir isos
$ ls
alma
darkfi
fedora
isos
$ ls | less
alma
darkfi
fedora
isos
$ if [ -t 1 ]; then echo "stdout is a terminal"; else echo "stdout is not a terminal"; fi
stdout is not a terminal
Plain ls printed one name per line with no | cat after it, because its stdout already was a pipe. ls | less printed the same four lines and returned. The book's ls -l | less opens a pager because there less writes to your terminal. Here it writes into the runner's pipe, so it copied its input through. (-l prints one entry per line either way, so columns never come into that example.) [ -t 1 ] is the same question asked by a script: true when file descriptor 1 is a terminal.
The terminal side needs a terminal, and a test runner has none. script makes one: it runs a command on a new pseudo-terminal and copies what arrives there to its own stdout. | cat -v then shows the carriage return, ^M, that the terminal puts at the end of every line:
Verified output of knows_piped_tty_sh.sh on Linux — regenerated by tools/run_examples.py, never hand-typed.
$ script -qc 'ls' /dev/null </dev/null | cat -v
alma darkfi fedora isos^M
$ script -qc 'ls | cat' /dev/null </dev/null | cat -v
alma^M
darkfi^M
fedora^M
isos^M
$ script -qc 'ls -1' /dev/null </dev/null | cat -v
alma^M
darkfi^M
fedora^M
isos^M
$ script -qc '[ -t 1 ] && echo terminal || echo not a terminal' /dev/null </dev/null | cat -v
terminal^M
$ ls -C
alma darkfi fedora isos
Verified output of knows_piped_tty_sh.sh on macOS — regenerated by tools/run_examples.py, never hand-typed.
$ script -q /dev/null ls </dev/null | cat -v
^D^H^Halma darkfi fedora isos^M
$ script -q /dev/null sh -c 'ls | cat' </dev/null | cat -v
^D^H^Halma^M
darkfi^M
fedora^M
isos^M
$ script -q /dev/null ls -1 </dev/null | cat -v
^D^H^Halma^M
darkfi^M
fedora^M
isos^M
$ script -q /dev/null sh -c '[ -t 1 ] && echo terminal || echo not a terminal' </dev/null | cat -v
^D^H^Hterminal^M
$ ls -C
alma darkfi fedora isos
lson a terminal prints columns: all four names on one line.ls | caton the same terminal prints one name per line.lsasked about its own stdout, which was the pipe intocat. Thatcatthen wrote to a terminal made no difference.-1and-Coverride the question.ls -1printed one name per line on a terminal, andls -C, the last line, printed columns into a pipe.[ -t 1 ]saidterminalinsidescript, andnot a terminalin the runner above.
Colour follows the same rule. The Encodings library measures it with grep --color=auto, in the page linked below.
On a Mac¶
BSD ls asks the same question and answers it the same way. Three things differ around it, and they are why the terminal key is split:
scripttakes its arguments in a different order. util-linux on Linux:script -qc 'CMD' /dev/null. BSD on macOS:script -q /dev/null CMD ARGS…, with no-c, so a pipeline inside the terminal needssh -c '…'. Each one rejects the other's spelling. The Encodings page has both error messages in a table.- BSD
lsseparates its columns with tabs, and GNUlswith spaces. The last line,ls -Cinto a pipe, shows it on both machines, so a script that splitsls -Coutput on runs of spaces works on only one of them. ^D^H^Hat the start of the macOS lines is not fromls.scriptwas given an empty stdin (< /dev/null, which is also what the runner gives every example). BSDscriptpassed the end of that input on to the terminal as a Ctrl-D, and the terminal echoed it as^Dand two backspaces. util-linuxscriptdid not.
The pipe side, the first key on this page, is shared.
In zsh and fish¶
zsh asks with the same test, and with no startup files its ls is the command itself:
Verified output of knows_piped_zsh.zsh, identical on Linux and macOS — regenerated by tools/run_examples.py, never hand-typed.
$ touch alma darkfi fedora; mkdir isos
$ whence -w ls
ls: command
$ ls | less
alma
darkfi
fedora
isos
$ [[ -t 1 ]] && echo "stdout is a terminal" || echo "stdout is not a terminal"
stdout is not a terminal
whence -w ls says command: there is nothing between zsh and the ls program. If ls is coloured in your own zsh, the colour comes from an alias or a function in your startup files, since zsh -f has neither. [[ -t 1 ]] asks the question in zsh.
fish does put something between them. ls in fish is a function that ships with fish, and the function asks the question itself:
Verified output of knows_piped_fish.fish on Linux — regenerated by tools/run_examples.py, never hand-typed.
$ touch alma darkfi fedora; mkdir isos
$ type -t ls
function
$ ls
alma
darkfi
fedora
isos
$ isatty stdout; and echo "stdout is a terminal"; or echo "stdout is not a terminal"
stdout is not a terminal
$ script -qc 'fish --no-config -c ls' /dev/null </dev/null | cat -v
alma darkfi fedora isos/^M
$ script -qc 'fish --no-config -c "command ls"' /dev/null </dev/null | cat -v
alma darkfi fedora isos^M
Verified output of knows_piped_fish.fish on macOS — regenerated by tools/run_examples.py, never hand-typed.
$ touch alma darkfi fedora; mkdir isos
$ type -t ls
function
$ ls
alma
darkfi
fedora
isos
$ isatty stdout; and echo "stdout is a terminal"; or echo "stdout is not a terminal"
stdout is not a terminal
$ script -q /dev/null fish --no-config -c ls </dev/null | cat -v
^D^H^Halma darkfi fedora isos/^M
$ script -q /dev/null fish --no-config -c 'command ls' </dev/null | cat -v
^D^H^Halma darkfi fedora isos^M
type -t ls says function. Into a pipe it printed the names plain. On a terminal the same ls printed isos/, while command ls (the program, skipping the function) printed isos. The / came from the function, which passes -F to ls only when isatty stdout succeeds, as functions ls shows. isatty stdout is fish's spelling of [ -t 1 ]. So in fish, a person at the prompt sees isos/ and a script reading ls | … gets isos.
This key is split for the same reasons as the bash one: the script spelling, the column separators, and the ^D^H^H. The first four commands printed the same on both machines.
If you are coming from another library¶
- Encodings. A pipe is not a terminal ↗ is the long version of this page:
isattyin the shell, in Python and in Rust; whatgrep --colorputs in the byte stream; thescripttable; and output buffering, which also changes when stdout is a pipe. - Python. Opening a file, who decides when the output leaves ↗ runs one program into a pipe and into a real terminal. stderr overtakes stdout into the pipe, and the terminal adds the same
\ras the^Mabove.
See also¶
- stderr does not go down the pipe — the other descriptor, which a pipe leaves on the terminal
- bash manual, Bash Conditional Expressions ↗ —
-t fd - fish, isatty ↗
- script(1), util-linux ↗ and script(1), FreeBSD ↗