sections in this module City College of San Francisco - CS160B
Unix/Linux Shell Scripting
Module: Functions
module list

read

read reads one line from standard input. If the read command has variables attached, the line's contents are distributed between the variables. The first word is placed in the first variable, the second is placed in the second variable, etc, and the last variable will have the rest of the input line. Thus, if the line

Today is Tuesday

is read using the command

read a b

the contents of the variables a and b are

$ echo "$a"
Today
$ echo "$b"
is Tuesday
$

You can, of course, read the entire line into a single variable. Using the same input line with 

read line

results in 

$ echo "$line"
Today is Tuesday
$

The line is read in, and any leading and/or trailing whitespace (spaces and tabs) is removed before it is placed in the variable(s).

Special characters

The only special character that read understands is a backslash. It can be used to escape (quote) any character, but the only characters that are affected are whitespace and a newline. If you issue the command

read line

with the input

This input is \
divided into several \
lines

results in

$ echo "$line"
This input is divided into several lines
$

(Note the spaces before the backslash (\). This is so that a space appears between the words when the physical lines are joined together.)

The backslash can also be used to control how text is distributed between the variables. In the input line

Greg Boyd Teacher

the command

read name job

results in

$ echo "$name"
Greg
$ echo "$job"
Boyd Teacher
$

If you use a backslash to escape the space between Greg and Boyd, however, the input

Greg\ Boyd Teacher

with the same command

read name job

results in 

$ echo "$name"
Greg Boyd
$ echo "$job"
Teacher
$

Avoid pipes

You might be tempted to try something cute like this:

date | read wday mon day time other

and expect read to extract the pieces of the output of date, distributing them between the variables. Indeed it does, but the pipe forces read to run in a separate process from your shell script, so the variables read sets are not yours!

$ date
Tue Mar 16 17:29:34 PDT 2010
$ date | read wday mon day rest
$ echo $wday

$

Because of the pipe, the read command runs in a separate process. The variables wday, mon, day and rest are set for that process, but not for the process running your shell script! Your shell script does not have the variables wday, mon, day and rest. You can see this by grepping for the variable in the output of set:

$ set | grep wday
$

We will see this same situation later when we cover loops. The only solution is to avoid using the pipe. A simple solution is to save the data you want in a file, then use read to read from the file:

$ date > $$date
$ read wday mon day rest < $$date
$ echo $wday
Tue
$ rm $$date

but this is more clumsy than using the set -- command with $(date)

Note: the issue with pipes starting a child shell even to run a shell built-in command like read occurs in the bash shell. Other shells may be less restrictive. Using redirection from a file works always, however.

Prompt and read

One important use of read is to write interactive shell scripts - shell scripts that ask the user a question and retrieve some information from them. For example, the program chfn is used to change the finger information of the user. This information is kept in the outdated GECOS field of /etc/passwd and contains subfields for the user's real name, phone number, office number, etc. After retrieving the current information from /etc/passwd, chfn asks the user to update each field. Running chfn looks like the following:

-bash$ chfn
Default values are printed inside of '[]'.
To accept the default, type <return>.
To have a blank entry, type the word 'none'.
 
Name [Greg Boyd]:
Location (Ex: 42U-J4) []:L462
Office Phone (Ex: 1632) []:use email
Home Phone (Ex: 9875432) []:none
-bash$

After outputting some instructions, chfn asks a series of four questions. Each question consists of a prompt, which contains the default response, and a user response. The prompt and user response occur on the same line. If chfn was a shell script and the current value of the user's Name was kept in the variable $Name, you could code the first question like this:

echo -n "Name [$Name]:"
read ans

This is the basis of writing an interactive shell script, called prompt and read. The user is asked a question, then the response is retrieved using read

In the example above, when we retrieve the user's name, the question is output to standard output. Often, shell scripts want to send their results to standard output as well. This is not a problem unless a user wants to save the shell script's output. Let's look at a simple example of this.

The shell script uinfo below simply asks the user for a userid, then outputs information about that user:

#!/bin/bash
echo -n "Enter the username:"
read user
if line=$(grep "^$user:" /etc/passwd); then
home=$(echo "$line" | cut -d: -f6)
shell=$(echo "$line" | cut -d: -f7)
echo "Home directory: $home"
echo "Shell: $shell"
else
echo "user '$user' not found" >&2
exit 1
fi

When we run uinfo, everything is fine:

-bash$ uinfo
Enter the username:gboyd
Home directory: /users/gboyd
Shell: /usr/bin/bash
-bash$

However, if we decide we want to save the output of uinfo in a file, we have a problem:

-bash$ uinfo > uinfo.out
(the cursor sits here, uinfo is running)

The problem is due to using standard output for both the prompt and the shell script's output. If we just continue as if we could see the prompts, uinfo works fine:

-bash$ uinfo > uinfo.out
gboyd
-bash$ cat uinfo.out
Enter the username:Home directory: /users/gboyd
Shell: /usr/bin/bash
-bash$

but this is not practical. To make matters worse, our output file has been polluted with the prompt! Instead, we need a way to get our prompt to appear on the screen, while reserving standard output for actual output.

For this reason, it is customary to use standard error when outputting prompts. Let's rewrite our program with this modification:

#!/bin/bash
echo -n "Enter the username:" >&2
read user
if line=$(grep "^$user:" /etc/passwd); then

home=$(echo "$line" | cut -d: -f6)
shell=$(echo "$line" | cut -d: -f7)
echo "Home directory: $home"
echo "Shell: $shell"
else
echo "user '$user' not found" >&2
exit 1
fi

Now the user can run our program, redirect its output to a file and still interact with it:

-bash$ uinfo > uinfo.out
Enter the username:gboyd
-bash$ cat uinfo.out
Home directory: /users/gboyd
Shell: /usr/bin/bash
-bash$ 

read -p

The read command has an option to output a prompt prior to reading the input line. This frees you from writing a separate echo command. The prompt is even output to standard error for you, and assumes that the prompt and input should occur on the same line:

using echo and readusing read -p
echo -n "Enter the filename:" >&2
read filename
read -p "Enter filename:" filename

IFS (Internal Field Separators)

IFS is a variable that contains delimiters. When IFS is used to break text into tokens, each character in IFS is treated as a delimiter, and each is equivalent. The default contents of IFS is a space, a tab, and a newline.

IFS is used when a line is read from standard input using the read command. The text read is distributed between the variables attached to the read command using the delimiters in IFS. Normally this means that whitespace is used to delimit the tokens on the input line:

$ cat tmp
hello there
$
$ read a b < tmp
$ echo "$b"
there
$

You can control this process by manipulating the delimiters in IFS. Suppose we are reading a line from a CSV file, where fields are delimited by commas. An example of a line might be

$ cat tmp
Greg Boyd,66 First St.,San Francisco,CA,94103,4155551212,Unix Instructor
$

If we set IFS to only contain the comma, we can chop this record apart easily. (As we shall see, IFS affects other things besides the read command, so it is customary to restore the default value after you are finished.)

$ OIFS="$IFS"
$ IFS=,
$ read name addr city state zip phone job < tmp
$ echo "$job"
Unix Instructor
$ IFS="$OIFS"
$

This is a bit cumbersome, but, in some cases, it is very useful.

IFS is also used when substitutions are performed in every Unix command. Returning to our command execution for a moment, the shell processes commands in many steps. Briefly,

  1. the shell breaks the command into words using a fixed set of delimiters.
  2. the shell expands the words, performing variable, command, and arithmetic substitutions. the result of the substitutions is then re-tokenized using the delimiters in $IFS
  3. shell directives are processed, and the command is executed

Using the same example as above, start by setting IFS to a comma:

$ echo "$line"
Greg Boyd,66 First St.,San Francisco,CA,94103,4155551212,Unix Instructor
$ OIFS="$IFS"
$ IFS=,

Now you can use the contents of the variable line to set the positional parameters using the command set --

$ set -- $line
$ echo $#
7
$ echo "$7"
Unix Instructor
$

Notes:

$ line="hello   there"
$ set -- $line
$ echo $#
2
$ IFS=:
$ line="hello:::there"
$ set -- $line
$ echo $#
4
$

Preview question: 

Prev This page was made entirely with free software on linux:  
Kompozer
and Openoffice.org    
Next

Copyright 2009 Greg Boyd - All Rights Reserved.

Document made with Kompozer