Feeding stdin¶
Level: 201 · for anyone with a terminal
One line: Your program reads file descriptor 0 and has no idea what put the bytes there — a keyboard, a pipe, a redirect, a here-document or another program — so the shell is where you make the bytes on purpose, look at exactly what they are, and end them when you mean to; every command below was run on this machine, and the encodings library has the page that measures the ones that differ on Linux.
Nothing on this page is Rust. It is the minute before Reading a line from standard input runs, and the way to answer "what did my program actually receive?" without adding a dbg! to it. Every fence is from one session: macOS 26.6.2, bash 3.2.57, 2026-09-13.
Five ways bytes reach fd 0¶
| Spelling | What arrives | Worth knowing |
|---|---|---|
printf 'x\n' \| prog |
whatever the left side wrote | a pipe: no size, no seek, may arrive in pieces |
prog < file |
the file's bytes | a file on fd 0: read sees it whole, and it can be rewound |
prog <<< 'abc' |
abc and a newline |
a here-string adds \n — 4 bytes, not 3 |
prog <<'END' … END |
the lines between | a here-doc; quote the END or $HOME inside it expands |
prog <(cmd) |
a path like /dev/fd/63 |
process substitution: an argument, not stdin, but it is a pipe |
$ printf 'abc' | wc -c | tr -d ' '
3
$ wc -c <<< 'abc' | tr -d ' '
4
$ cat <<'END'
$HOME is not expanded in a quoted here-doc
END
$HOME is not expanded in a quoted here-doc
$ cat <<END
$HOME is expanded in a bare one
END
/Users/amasa is expanded in a bare one
The extra byte matters exactly once: when the program is counting. printf writes what you typed and nothing more, which is why it is the tool for building a test file by hand ↗; echo adds a newline, and echo -n is not the same command in every shell.
Making bytes on purpose¶
$ printf 'a\0b\n' | xxd
00000000: 6100 620a a.b.
$ printf '\x48\x69\n' | xxd -p
48690a
$ printf '48690a' | xxd -r -p | cat -vet
Hi$
$ printf '%s\n' a b c
a
b
c
$ printf '%d\n' 0x41 "'A"
65
65
\0,\xHHand\NNNname a byte and ask nothing of the locale.\uHHHHnames a code point and gives three different answers on three configurations ↗ — one of them a euro sign.printfreuses its format for every remaining argument, so'%s\n' a b cis three lines. That is the loop most people write infor."'A"— a quote then a character — isprintf's way of asking for a character's code, and%dof0x41is the same number the other way round.xxd -r -pis the only one of the dump tools that runs backwards, which is the whole reason to install it ↗ on a machine that lacks it.- A lot of one byte:
head -c 64 /dev/zero, and a lot of one line:yes | head -3.seq 3for numbers.
Seeing what you are about to feed¶
The program cannot tell you what it read, but the shell can show you what it is about to send. Put the inspection before the pipe:
$ printf 'a\tb\n' | cat -vet
a^Ib$
$ printf 'one\r\ntwo\r\n' | tr -d '\r' | cat -vet
one$
two$
$ printf 'a\nb' | wc -l | tr -d ' '
1
$ printf 'a\nb' | grep -c ''
2
$ printf 'hi\n' | file -
/dev/stdin: ASCII text
$ printf 'hi\n' | file --mime-type -
/dev/stdin: text/plain
cat -vetdraws the tab as^I, a carriage return as^M, and ends every line with$, so a trailing space and a missing final newline both become visible. It is the portable spelling;cat -Adoes not exist on macOS ↗.wc -lcounts newlines;grep -c ''counts lines. On a file whose last line has no newline they differ by one ↗, and a Rustlines()loop agrees withgrep.file -reads stdin and, having no name, prints/dev/stdin. Ask for--mime-type: the English wording changes between versions and the MIME form does not ↗.tee /dev/stderrin the middle of a pipeline shows you the bytes going past without changing them —printf 'x\n' | tee /dev/stderr | wc -lprintsxon the terminal and1on stdout.
The first few bytes, and the first few lines¶
$ head -c 4 /etc/hosts | xxd
00000000: 2323 0a23 ##.#
$ xxd -l 4 /etc/hosts
00000000: 2323 0a23 ##.#
$ hexdump -n 4 -C /etc/hosts
00000000 23 23 0a 23 |##.#|
00000004
$ od -N 4 -An -tx1 /etc/hosts
23 23 0a 23
head -c cuts bytes and works on a pipe; -l, -n and -N are the same bound spelled three ways by three tools. Lines are head -3 or sed -n '1,3p', and both stop reading, which on a pipe means the producer sees a broken pipe — the event Broken pipe is about, from the other side. The od line's padding is not the same on Linux ↗, which is why the encodings library never records it raw.
Transforming on the way in¶
$ printf 'first\nsecond\nthird\n' | sed -n '1,2p'
first
second
$ printf 'hello\n' | sed 's/l/L/'
heLlo
$ printf 'hello\n' | sed 's/l/L/g'
heLLo
$ printf 'k=v\n' | cut -d= -f2
v
$ printf 'aXbXc' | tr X '\n'
a
b
c
$ printf '3\n1\n2\n' | sort -n | tr '\n' ' '
1 2 3
$ seq 3 | paste -sd+ - | bc
6
$ printf 'a b\n' | xargs -n1 echo
a
b
sed 's/l/L/'changes the first match on each line;gchanges all of them.sed -n 'N,Mp'isheadandtailwith a range.trmaps bytes andsedmatches patterns, which is the whole difference between them on anything outside ASCII —tr -d 'é'damagesnaïveandsed 's/é//'does not ↗.cut -ccounts characters on one platform and bytes on the other ↗.- Two of the outputs above have no trailing newline —
printf 'aXbXc'had none andtrpreserved that;tr '\n' ' 'removed the last one — so the next$prompt lands on the same line. That is not a display glitch; it is the trailing-newline question again. xargssplits on spaces as well as newlines and treats quotes as syntax ↗:find -print0 | xargs -0is the pairing for filenames, not a refinement.grepin a UTF-8 locale may drop a line it cannot decode and exit 0 ↗ on macOS; search a file of unknown encoding underLC_ALL=C.
Ending the input¶
$ cat </dev/null | wc -c | tr -d ' '
0
$ printf 'x\n' | sh -c 'test -t 0 && echo terminal || echo not a terminal'
not a terminal
$ sh -c 'test -t 0 && echo terminal || echo not a terminal' </dev/null
not a terminal
- `