opscape

Shell builtins · bash builtin

read

Read one line from standard input and split it into shell variables.

read [-ers] [-a ARRAY] [-d DELIM] [-n NCHARS] [-N NCHARS] [-p PROMPT] [-t TIMEOUT] [-u FD] [NAME...]

14 of 14 examples run in Debian 13.7 on 2026-10-31 · manual: bash(1), SHELL BUILTIN COMMANDS

read takes one line from standard input, splits it into words using the characters in IFS (space, tab and newline by default), and assigns the words to the names you give it in order. The last name gets everything that is left, separators included. With no name, the whole line lands in REPLY. It returns 0 when it read a line and non-zero at end of input, which is what makes while read ...; do ...; done < file stop.

Two defaults surprise almost everyone. Backslashes are treated as escape characters and removed unless you pass -r, and leading and trailing whitespace is trimmed unless IFS is empty for that command. So the line-faithful form is IFS= read -r line, and it is what you should type by habit.

read is a builtin because it has to set variables in the current shell. That is also its biggest trap: cmd | read var runs read in a pipeline subshell in bash, and the variable is gone on the next line. Feed it with <, a here-string <<< or process substitution instead. For reading a whole file into an array, mapfile is shorter and faster.

Examples

Read a line into a variable

read name < data/hosts.txt
echo "first line: $name"
output
first line: web01 10.0.0.11

read consumes exactly one line, so only web01 10.0.0.11 is read. With a single name, the whole line goes into it, internal spaces included.

Split a line into several variables

read host ip <<< "web01 10.0.0.11 primary rack-3"
echo "host=$host"
echo "ip=$ip"
output
host=web01
ip=10.0.0.11 primary rack-3

Words are assigned left to right and the last variable gets the rest of the line. That is why ip holds 10.0.0.11 primary rack-3. Add a throwaway name such as _ at the end if you want only the first fields.

Process a file line by line

while read -r host ip; do
  echo "$host is at $ip"
done < data/hosts.txt
output
web01 is at 10.0.0.11
web02 is at 10.0.0.12
db01 is at 10.0.0.21
cache01 is at 10.0.0.31

The redirect after done feeds the whole loop. Each read takes the next line and the loop ends when read hits end of file and returns non-zero.

Split on a different separator with IFS

tail -n +2 data/users.csv | head -n 3 | while IFS=, read -r id name email dept joined; do
  printf '%-14s %s\n' "$name" "$dept"
done
output
Alice Moreau   engineering
Bob Okafor     sales
Chen Wei       engineering

IFS=, set in front of read applies to that one command only. Names with spaces such as Alice Moreau stay whole because only commas separate fields now. This is fine for simple CSV; quoted fields with embedded commas need a real parser.

Keep backslashes with -r

printf '%s\n' 'C:\new\tools' > path.txt
read p < path.txt; echo "without -r: $p"
read -r p < path.txt; echo "with -r:    $p"
output
without -r: C:newtools
with -r:    C:\new\tools

Without -r, a backslash escapes the next character and is removed, so \n becomes n and \t becomes t. With -r the line arrives untouched. There is almost never a reason to leave -r out.

Preserve leading spaces with IFS=

printf '    indented line   \n' > in.txt
read -r line < in.txt;     echo "[$line]"
IFS= read -r line < in.txt; echo "[$line]"
output
[indented line]
[    indented line   ]

With the default IFS, leading and trailing spaces are stripped. An empty IFS turns splitting off, so the line is kept byte for byte. IFS= read -r line is the idiom for reading lines exactly.

Do not lose a last line that has no newline

printf 'one\ntwo\nthree' > list.txt
while IFS= read -r l; do echo "got $l"; done < list.txt
echo ---
while IFS= read -r l || [ -n "$l" ]; do echo "got $l"; done < list.txt
output
got one
got two
---
got one
got two
got three

read stores three but returns non-zero because it hit end of file before a newline, so the plain loop drops it. || [ -n "$l" ] runs the body once more when something was read. Files written by some editors and by printf without \n end like this.

Read words into an array with -a

read -r -a parts <<< "deploy v2.4 to production"
echo "${#parts[@]} words, last is ${parts[-1]}"
echo "second: ${parts[1]}"
output
4 words, last is production
second: v2.4

-a puts each word into consecutive array elements starting at index 0, so a line with any number of fields can be handled. ${parts[-1]} is the last element.

Piping into read loses the variable

count=0
echo "a b c" | read -r first rest
echo "first=[$first]"
grep -c . todo.txt | read -r count
echo "count=$count"
output
first=[]
count=0

Each part of a pipeline runs in a subshell in bash, so read sets its variables in a process that exits right away. first is empty and count keeps its old 0. The fixes are in the next example.

Read command output into variables in the current shell

read -r count < <(grep -c . todo.txt)
echo "count=$count"
read -r first rest <<< "a b c"
echo "first=$first rest=$rest"
output
count=5
first=a rest=b c

Process substitution < <(cmd) and the here-string <<< both feed read without a pipeline, so it runs in the current shell and the variables survive. shopt -s lastpipe is another option in scripts.

Read NUL-separated names with -d ''

find photos -name '*.png' -print0 | sort -z |
while IFS= read -r -d '' f; do
  echo "file: $f"
done
output
file: photos/IMG_2041.png
file: photos/beach.png
file: photos/forest.png

-d '' makes NUL the line terminator, matching find -print0. Since NUL cannot occur in a file name, this is the only fully safe way to loop over arbitrary names, including ones with newlines.

Prompt for input with -p

read -r -p "Username: " user
echo "hello, $user"
output
hello, alice

-p prints a prompt (without a newline) on stderr, but only when input comes from a terminal. Here stdin is fed from a pipe, so the prompt is not shown at all; the answer is still read.

Give up after a timeout with -t

Setup run before this example
mkfifo slow
{ sleep 3; echo late; } > slow &
if read -r -t 1 answer < slow; then echo "got $answer"; else echo "timed out, status $?"; fi
output
timed out, status 142

-t 1 waits at most one second. On timeout read returns a status greater than 128 (142 in bash) rather than 1, so a script can tell a timeout from end of file.

Read a fixed number of characters with -n

read -r -n 3 code <<< "ERR42 disk full"
echo "code=$code"
output
code=ERR

-n 3 returns after three characters without waiting for a newline. In a terminal this is how "press y or n" prompts react to a single keypress (read -n 1).

Common pitfalls

  • Leaving out -r: any backslash in the input is silently eaten. Write read -r every time.

  • cmd | while read ...; do count=$((count+1)); done updates count in a subshell; after the loop it is unchanged. Use done < <(cmd) instead.

  • Reading from stdin inside a while read loop: commands in the body such as ssh or another read swallow the rest of the file. Give them < /dev/null (or ssh -n), or read the loop from another descriptor with read -u 3 ... 3< file.

  • Without IFS=, leading and trailing whitespace is trimmed, which breaks indentation-sensitive files such as YAML or Makefiles.

  • The last line of a file without a trailing newline is read but read returns 1, so the loop skips it. Add || [ -n "$line" ].

GNU vs BSD and macOS

The examples above ran the GNU/Linux version. These are the differences that matter on macOS and the BSDs (written from their documentation, not run here).

  • -a, -t, -n, -N, -s, -u and the <<< here-string are bash extensions that dash (/bin/sh on Debian and Ubuntu) does not have; in a #!/bin/sh script stick to read -r. zsh's read uses -A for arrays and read 'var?prompt' for prompts.

Options worth knowing

A selection, described in our own words. For the full list, see bash(1), SHELL BUILTIN COMMANDS.

OptionWhat it does
-rRaw mode: backslashes are kept as ordinary characters.
-a ARRAYAssign the words to elements of ARRAY, starting at index 0.
-d DELIMStop at the first character of DELIM instead of newline; an empty string means NUL.
-p PROMPTPrint PROMPT on stderr before reading, only when input is a terminal.
-t SECONDSFail with a status above 128 if no full line arrives in time; fractions are allowed.
-n NCHARSReturn after NCHARS characters, or earlier at a delimiter.
-N NCHARSRead exactly NCHARS characters, ignoring delimiters and IFS splitting.
-sSilent: do not echo typed characters (for passwords).
-u FDRead from file descriptor FD instead of standard input.
-eUse readline for input when interactive, with line editing.
-i TEXTWith -e, prefill the input line with TEXT.

Related