Classic Shell Scripting - Arnold Robbins [81]
Reading Lines with read
The read command is one of the most important ways to get information into a shell program:
$ x=abc ; printf "x is now '%s'. Enter new value: " $x ; read x
x is now 'abc'. Enter new value: PDQ
$ echo $x
PDQ
* * *
read
Usage
read [ -r ] variable ...
Purpose
To read information into one or more shell variables.
Major options
-r
Raw read. Don't interpret backslash at end-of-line as meaning line continuation.
Behavior
Lines are read from standard input and split as via shell field splitting (using $IFS). The first word is assigned to the first variable, the second to the second, and so on. If there are more words than variables, all the trailing words are assigned to the last variable. read exits with a failure value upon encountering end-of-file.
If an input line ends with a backslash, read discards the backslash and newline, and continues reading data from the next line. The -r option forces read to treat a final backslash literally.
Caveats
When read is used in a pipeline, many shells execute it in a separate process. In this case, any variables set by read do not retain their values in the parent shell. This is also true for loops in the middle of pipelines.
* * *
read can read values into multiple variables at one time. In this case, characters in $IFS separate the input line into individual words. For example:
printf "Enter name, rank, serial number: "
read name rank serno
A typical use is processing the /etc/passwd file. The standard format is seven colon-separated fields: username, encrypted password, numeric user ID, numeric group ID, full name, home directory, and login shell. For example:
jones:*:32713:899:Adrian W. Jones/OSD211/555-0123:/home/jones:/bin/ksh
You can use a simple loop to process /etc/passwd line by line:
while IFS=: read user pass uid gid fullname homedir shell
do
... Process each user's line
done < /etc/passwd
This loop does not say "while IFS is equal to colon, read . . . " Rather, the assignment to IFS causes read to use a colon as the field separator, without affecting the value of IFS for use in the loop body. It changes the value of IFS only in the environment inherited by read. This was described in Section 6.1.1. The while loop was described in Section 6.4.
read exits with a nonzero exit status when it encounters the end of the input file. This terminates the while loop.
Placing the redirection from /etc/passwd at the end of the loop body looks odd at first. However, it's necessary so that read sees subsequent lines each time around the loop. Had the loop been written this way:
# Incorrect use of redirection:
while IFS=: read user pass uid gid fullname homedir shell < /etc/passwd
do
... Process each user's line
done
it would never terminate! Each time around the loop, the shell would open /etc/passwd anew, and read would read just the first line of the file!
An alternative to the while read ... do ... done < file syntax is to use cat in a pipeline with the loop:
# Easier to read, with tiny efficiency loss in using cat:
cat /etc/passwd |
while IFS=: read user pass uid gid fullname homedir shell
do
... Process each user's line
done
This is a general technique: any command can be used to pipe input into read. This is particularly useful when read is used in a loop. In Section 3.2.7, we presented this simple script for copying a directory tree:
find /home/tolstoy -type d -print | Find all directories
sed 's;/home/tolstoy/;/home/lt/;' | Change name, note use of semicolon delimiter
sed 's/^/mkdir /' | Insert mkdir command
sh -x Execute, with shell tracing
However, it can be done easily, and more naturally from a shell programmer's point of view, with a loop:
find /home/tolstoy -type d -print | Find all directories
sed 's;/home/tolstoy/;/home/lt/;' | Change name, note use of semicolon delimiter
while read newdir Read new directory name
do
mkdir $newdir Make new directory
done
(We note in passing that this