Skip to content

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
Measured 2026-09-13 — the here-string's extra byte, and the here-doc's quote
$ 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

Measured 2026-09-13 — printf names bytes; xxd -r -p turns hex back into them
$ 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, \xHH and \NNN name a byte and ask nothing of the locale. \uHHHH names a code point and gives three different answers on three configurations ↗ — one of them a euro sign.
  • printf reuses its format for every remaining argument, so '%s\n' a b c is three lines. That is the loop most people write in for.
  • "'A" — a quote then a character — is printf's way of asking for a character's code, and %d of 0x41 is the same number the other way round.
  • xxd -r -p is 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 3 for 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:

Measured 2026-09-13 — the same bytes, five views
$ 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 -vet draws 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 -A does not exist on macOS ↗.
  • wc -l counts newlines; grep -c '' counts lines. On a file whose last line has no newline they differ by one ↗, and a Rust lines() loop agrees with grep.
  • 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/stderr in the middle of a pipeline shows you the bytes going past without changing them — printf 'x\n' | tee /dev/stderr | wc -l prints x on the terminal and 1 on stdout.

The first few bytes, and the first few lines

Measured 2026-09-13 — four spellings of the first four bytes of /etc/hosts, and od is the one that pads
$ 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

Measured 2026-09-13 — the classic filters, one line each
$ 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

Ending the input

Measured 2026-09-13 — three ways the input is over, and one question about who is on the other end
$ 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
  • `