Why this matters
Scripts talk with the outside world in both directions. In one direction they take input: from arguments (last lesson), from a person typing an answer, from a file, or from another program through a pipe. In the other direction, beside their output, they return an exit status that tells the caller (a person, another script, cron, a CI system) whether things worked. A script that always returns 0, even when it failed, will silently break every pipeline and automation built on top of it.
The read builtin
read reads one line from standard input and stores it in a variable:
$ read answer
yes please
$ echo "[$answer]"
[yes please]
The first command waited for you to type a line and press Enter. In a script:
#!/bin/bash
printf 'Your name? '
read -r name
echo "Hello, $name!"
(printf is used for the question because it does not add a newline, so the cursor stays on the same line.) read -p 'Your name? ' -r name does the same in one command: -p prints a prompt, but only when input comes from a terminal.
Underneath: standard input is just file descriptor 0
read does not know or care where its input comes from. It reads from file descriptor 0, which, as you learned in the redirection module, is whatever bash connected it to:
$ printf 'from a pipe\n' | bash greet.sh # stdin is a pipe
$ bash greet.sh < names.txt # stdin is a file
$ bash greet.sh # stdin is the terminal
All three work without changing the script. That is the Unix design paying off: a script that reads standard input can be used interactively and in pipelines.
Several variables at once
Give read several names, and it splits the line into fields using IFS, like word splitting: the first field goes into the first variable, and so on, and the last variable gets the whole rest of the line:
$ read first last <<< "Ada King Lovelace"
$ echo "first=$first last=$last"
first=Ada last=King Lovelace
(<<< is a here-string: it feeds a string as standard input. Module 15 covers it; here it just saves typing.) With a different IFS for this one command, you can split structured lines:
$ IFS=: read -r user pass uid gid rest < /etc/passwd
$ echo "$user has uid $uid"
root has uid 0
< /etc/passwd makes read read the first line of the file. The IFS=: prefix only applies to the read command, so the rest of the script keeps the normal IFS. An underscore _ is a conventional name for a field you do not care about: IFS=: read -r user _ uid _.
-r and IFS=: reading lines exactly
By default read does two kinds of processing that you usually do not want:
- Backslashes are escapes.
a\tbbecomesatb, and a backslash at the end of the line joins the next line on.-r(raw) turns that off. - Leading and trailing whitespace is stripped (because space and tab are in IFS). Setting
IFS=(empty) for the read command turns that off.
$ printf ' C:\\temp\\new \n' > path.txt
$ read p < path.txt; echo "[$p]"
[C:tempnew]
$ read -r p < path.txt; echo "[$p]"
[C:\temp\new]
$ IFS= read -r p < path.txt; echo "[$p]"
[ C:\temp\new ]
So the idiom for "read one line exactly as it is" is:
IFS= read -r line
Use -r always, unless you have a specific reason not to; shell linters warn about every read without it.
When read fails
read returns status 0 when it read a line, and non-zero at end of input (nothing left to read), or if the redirection fails because the file cannot be opened. One subtle case: if the last line of a file has no trailing newline, read still fills the variable but returns 1. You will handle that when reading files line by line with loops in module 10.
$ read -r x < /nonexistent
bash: /nonexistent: No such file or directory
$ echo $?
1
That makes read ... || exit 2 a natural way to stop when input is missing.
The pipeline trap
$ echo hello | read word
$ echo "[$word]"
[]
Every command in a pipeline runs in its own process (the pipes module showed why: they run at the same time). So read ran in a forked subshell and set word there. To get input into a variable of the current shell, use a redirection (< file), a here-string (<<<), or command substitution (word=$(echo hello)).
The exit status of a script
A script returns:
- the number given to
exit N, if it callsexit, at that moment; or - otherwise the status of the last command it ran.
#!/bin/bash
# exists.sh FILE: is the file there?
ls "$1" > /dev/null 2>&1 && echo "yes" || echo "no"
This script always returns 0, because its last command is an echo that succeeds, whatever happened. Callers cannot use it with &&. Compare:
#!/bin/bash
# exists.sh FILE: exit 0 if the file exists, 1 if not
ls "$1" > /dev/null 2>&1 || exit 1
echo "yes"
Now ./exists.sh notes.txt && echo "go on" works as expected.
exit with no number uses the status of the last command. Choose meaningful numbers and stick to the common conventions:
| Status | Use it for |
|---|---|
| 0 | success |
| 1 | a general failure (the work could not be done) |
| 2 | wrong usage: missing or bad arguments |
| 3-125 | your own specific failure kinds, documented in the script's usage text |
Avoid 126 and above: they mean "not executable", "not found" and "killed by a signal", and using them confuses callers.
exit ends the process. In a script run as a program that is the script. If you source a file that calls exit, it ends your interactive shell, which is why sourced files use return (module 11) instead.
Putting it together
#!/bin/bash
# firstword.sh FILE: print the first word of the first line of FILE
read -r word _ < "$1" || exit 1
echo "$word"
$ ./firstword.sh /etc/os-release
PRETTY_NAME="Debian
$ ./firstword.sh /missing; echo "status $?"
./firstword.sh: line 3: /missing: No such file or directory
status 1
Common mistakes
readwithout-r. Backslashes disappear.readwithoutIFS=when whitespace matters: leading and trailing spaces disappear.- Piping into
read. The variable is set in a subshell and lost. - Scripts that always exit 0. Make important failures stop the script with
|| exit 1. exitin a sourced file. It closes your shell.- Expecting
readto read the whole file. It reads one line per call. Loops come in module 10.
Exercises
The code exercises feed input through a pipe and check exit statuses. Try them in the shell with printf ' Ada \\o/ \n' | bash script.sh and bash script.sh nope.txt; echo $?.
Recap
read -r varreads one line from standard input;IFS= read -r linereads it exactly.- Several names split the line on IFS; the last name gets the rest.
IFS=: read -r a b cfor colon-separated data. read var < filereads the first line of a file; in a pipeline,readruns in a subshell.- A script returns the status of
exit Nor of its last command. 0 success, 1 failure, 2 usage.
Going further: run help read and look at -n, -t and -s (read N characters, time out, silent input for passwords). Try read -s -p 'Password: ' pw; echo; echo ${#pw}.