Chapter 10. Reading and Printing Data

In this chapter you'll learn how to read data from the terminal or from a file using the read command and how to print formatted data to standard output using the printf command.

The read Command

The general format of the read command is

read variables

When this command is executed, the shell reads a line from standard input and assigns the first word read to the first variable listed in variables, the second word read to the second variable, and so on. If there are more words on the line than there are variables listed, the excess words get assigned to the last variable. So for example, the command

read x y

reads a line from standard input, storing the first word read in the variable x, and the remainder of the line in the variable y. It follows from this that the command

read text

reads and stores an entire line into the shell variable text.

A Program to Copy Files

Let's put the read command to work. We'll write a simplified version of the cp command that will be a bit more user friendly than the standard Unix one. We'll call it mycp, and we'll have it take two arguments: the source file and the destination file. If the destination file already exists, we'll tell the user and then ask him (or her) if he wants to proceed with the copy. If the answer is “yes,” we'll go ahead with it; otherwise, we won't.

$ cat mycp
#
# Copy a file
#

if [ "$#" -ne 2 ]
then
        echo "Usage: mycp from to"
        exit 1
fi

from="$1"
to="$2"

#
# See if the destination file already exists
#

if [ -e "$to" ]
then
        echo "$to already exists; overwrite (yes/no)?"
        read answer

        if [ "$answer" != yes ]
        then
                echo "Copy not performed"
                exit 0
        fi
fi

#
# Either destination doesn't exist or "yes" was typed
#

cp $from $to       # proceed with the copy
$

And now for the test:

$ ls                       What files are around?
addresses
intro
lotsaspaces
mycp
names
nu
numbers
phonebook
stat
$ mycp                     No arguments
Usage: mycp from to
$ mycp names names2        Make a copy of names
$ ls -l names*             Did it work?
-rw-r--r--   1 steve   steve      43 Jul  20 11:12 names
-rw-r--r--   1 steve   steve      43 Jul  21 14:16 names2
$ mycp names numbers       Try to overwrite an existing file
numbers already exists; overwrite (yes/no)?
no
Copy not performed
$

To complete the test cases, try answering yes and ensuring that the program proceeds with the copy.

There are a few things worthy of mention with the mycp program. First, if the file already exists, the echo command that prompts for the yes/no response is executed. The read command that follows causes the shell to wait for you to type something in. Note that the shell does not prompt you when it's waiting for you to enter data; it's up to you to add your own prompt message to the program.

The data that is typed is stored in the variable answer and is then tested against the characters “yes” to determine whether the copy is to proceed. The quotes around answer in the test

[ "$answer" != yes]

are necessary in case the user just presses the Enter key without typing any data. In that case, the shell would store a null value in answer, and test would issue an error message if the quotes were omitted.

Special echo Escape Characters

A slight annoyance with mycp is that after the echo command is executed to alert the user that the file already exists, the response that is typed by the user appears on the next line. This happens because the echo command always automatically displays a terminating newline character after the last argument.

This can be suppressed if the last two characters given to echo are the special escape characters c. This tells echo to leave the cursor right where it is after displaying the last argument and not to go to the next line. So if you changed the echo command in mycp to read like this:

echo "$to already exists; overwrite (yes/no)? c"

the user's input would be typed right after the message on the same line. Bear in mind that the c is interpreted by echo and not by the shell, meaning that it must be quoted so that the backslash makes it to echo.

echo interprets other special characters. These must each be preceded by a backslash. They're summarized in Table 10.1.

Table 10.1. echo Escape Characters

Character

Prints



Backspace

c

The line without a terminating newline

f

Formfeed

Newline

Carriage return

Tab character

\

Backslash character

nnn

The character whose ASCII value is nnn, where nnn is a one- to three-digit octal number

An Improved Version of mycp

Suppose that you have a program called prog1 in your current directory and you want to copy it into your bin directory directly below. Take another look at the mycp program and determine what happens if you type in

mycp prog1 bin

The -e test on bin will succeed (because –e tests for existence of a file), and mycp will display the “already exists” message and wait for a yes/no answer.

If the second argument is a directory, mycp should check to see whether the from file exists inside this directory. The next version of mycp performs this check. It also has the modified echo command that includes the c to suppress the terminating newline.

$ cat mycp
#
# Copy a file -- version 2
#

if [ "$#" -ne 2 ]
then
        echo "Usage: mycp from to"
        exit 1
fi

from="$1"
to="$2"

#
# See if destination file is a directory
#

if [ -d "$to" ]
then
        to="$to/$(basename $from)"
fi

#
# See if the destination file already exists
#

if [ -e "$to" ]
then
        echo "$to already exists; overwrite (yes/no)? c"
        read answer

        if [ "$answer" != yes ]
        then
                echo "Copy not performed"
                exit 0
        fi
fi
#
# Either destination doesn't exist or ''yes'' was typed
#

cp $from $to       # proceed with the copy
$

If the destination file is a directory, the program changes the variable to to more precisely identify the file inside the directory as $to/$(basename $from). This ensures that the following test on the existence of the ordinary file $to will be done on the file in the directory, not on the directory itself as the previous version of mycp did. The basename command gives the base filename of its argument (for example, basename /usr/bin/troff gives troff; basename troff gives troff). This ensures that the copy is made to the correct place. (For example, if mycp /tmp/data bin is typed, where bin is a directory, you want to copy /tmp/data into bin/data and not into bin/tmp/data.)

Here's some sample output. Note the effect of the c escape characters.

$ ls                  Check out current directory
bin
prog1
$ ls bin              Look inside bin
lu
nu
prog1
$ mycp prog1 prog2    Simple case
$ mycp prog1 bin      Copy into directory
bin/prog1 already exists; overwrite (yes/no)? yes
$

A Final Version of mycp

The last modification to mycp makes the program virtually equivalent to the standard Unix cp command by allowing a variable number of arguments. Recall that any number of files can precede the name of a directory, as in

cp prog1 prog2 greetings bin

To modify mycp to accept any number of files, you can use this approach:

  1. Get each argument but the last from the command line and store it in the shell variable filelist.

  2. Store the last argument in the variable to.

  3. If $to is not a directory, there must be exactly two arguments.

  4. For each file in $filelist, check whether the file already exists. If it does, ask the user whether the file should be overwritten. If the answer is “yes,” or if the file doesn't already exist, add the file to the variable copylist.

  5. If copylist is nonnull, copy the files in it to $to.

If this algorithm seems a bit fuzzy, perhaps the program, followed by a detailed explanation, will help clear things up. Note the modified command usage message.

$ cat mycp
#
# Copy a file -- final version
#

numargs=$#                  # save this for later use
filelist=
copylist=

#
# Process the arguments, storing all but the last in filelist
#

while [ "$#" -gt 1 ]
do
        filelist="$filelist $1"
        shift
done

to="$1"

#
# If less than two args, or if more than two args and last arg
# is not a directory, then issue an error message
#

if [ "$numargs" -lt 2  -o  "$numargs" -gt 2  -a  ! -d "$to" ]
then
    echo "Usage: mycp file1 file2"
    echo "       mycp file(s) dir"
    exit 1
fi

#
# Sequence through each file in filelist
#

for from in $filelist
do
    #
    # See if destination file is a directory
    #

    if [ -d "$to" ]
    then
           tofile="$to/$(basename $from)"
    else
           tofile="$to"
    fi

    #
    # Add file to copylist if file doesn't already exist
    # or if user says it's okay to overwrite
    #

    if [ -e "$tofile" ]
    then
           echo "$tofile already exists; overwrite (yes/no)? c"
           read answer

           if [ "$answer" = yes ]
           then
                  copylist="$copylist $from"
           fi
    else
           copylist="$copylist $from"
    fi
done

#
# Now do the copy -- first make sure there's something to copy
#
if [ -n "$copylist" ]
then
        cp $copylist $to        # proceed with the copy
fi
$

Let's look at some sample output before getting into the explanation.

$ ls                                  See what's around
bin
lu
names
prog1
prog2
$ ls bin                             And what's in bin?
lu
nu
prog1
$ mycp                               No arguments
Usage: mycp file1 file2
       mycp file(s) dir
$ mycp names prog1 prog2             Last arg isn't a directory
Usage: mycp file1 file2
       mycp file(s) dir
$ mycp names prog1 prog2 lu bin      Legitimate use
bin/prog1 already exists; overwrite (yes/no)? yes
bin/lu already exists; overwrite (yes/no)? no
$ ls -l bin                          See what happened
total 5
-rw-r--r--   1 steve   steve   543 Jul 19 14:10 lu
-rw-r--r--   1 steve   steve   949 Jul 21 17:11 names
-rw-r--r--   1 steve   steve    38 Jul 19 09:55 nu
-rw-r--r--   1 steve   steve   498 Jul 21 17:11 prog1
-rw-r--r--   1 steve   steve   498 Jul 21 17:11 prog2
$

In the last case, prog1 was overwritten and lu wasn't, as per the user's request.

When the program starts execution, it saves the number of arguments in the variable numargs. This is done because it's changed later in the program by the shift command.

Next a loop is entered that is executed as long as the number of arguments is greater than one. The purpose of this loop is to get the last argument on the line. While doing this, the loop stashes away the first argument into the shell variable filelist, which contains a list of all the files to be copied. The statement

filelist="$filelist $1"

says to take the previous value of filelist, add on a space followed by the value of $1, and then store the result back into filelist. Then the shift command is executed to “move” all the arguments over by one. Eventually, $# will be equal to one, and the loop will be exited. At that point, filelist will contain a space-delimited list of all the files to be copied, and $1 will contain the last argument, which is the destination file (or directory). To see how this works, consider execution of the while loop when the command is executed as

mycp names prog1 prog2 lu bin

Figure 10.1 depicts the changing values of the variables through each iteration of the loop. The first line shows the state of the variables before the loop is entered.

Processing command-line arguments.

Figure 10.1. Processing command-line arguments.

After the loop is exited, the last argument contained in $1 is stored in the variable to. Next, a test is made to ensure that at least two arguments were typed on the command line and if more than two were typed, that the last argument is a directory. If either condition isn't satisfied, usage information is displayed to the user, and the program exits with a status of 1.

Following this, a for loop is entered for the purpose of individually examining each file in the list to see whether it already exists. If it does, the user is prompted as before. If the user wants to overwrite the file, or if the file doesn't already exist, the file is added to the shell variable copylist. The technique used here is the same used to accumulate the arguments inside filelist.

When the for loop is exited, copylist contains a list of all the files to be copied. This list can be null if each of the destination files exists and the user types “no” for each one. So a test is made to ensure copylist is nonnull, and if it is, the copy is performed.

Take some time to review the logic of the final version of mycp; it does a good job at illustrating many of the features you've learned so far in this book. Some exercises at the end of this chapter will help test your understanding of this program.

A Menu-Driven Phone Program

One nice thing about the read command is that it enables you to write menu-driven shell programs. As an example, we'll return to our phone book programs add, lu, and rem and gather their execution together under one program, which we'll call rolo (for rolodex program). rolo will display a list of choices to the user and then execute the appropriate program depending on the selection. It will also prompt for the proper arguments to the program. Here, then, is the program:

$ cat rolo
#
# rolo - rolodex program to look up, add, and
#         remove people from the phone book
#

#
# Display menu
#

echo '
      Would you like to:

            1. Look someone up
            2. Add someone to the phone book
            3. Remove someone from the phone book

      Please select one of the above (1-3): c'

#
# Read and process selection
#

read choice
echo
case "$choice"
in
       1) echo "Enter name to look up: c"
          read name
          lu "$name";;
       2) echo "Enter name to be added: c"
          read name
          echo "Enter number: c"
          read number
          add "$name" "$number";;
       3) echo "Enter name to be removed: c"
          read name
          rem "$name";;
       *) echo "Bad choice";;
esac
$

A single echo command is used to display the menu at the terminal, taking advantage of the fact that the quotes preserve the embedded newline characters. Then the read command is executed to get the selection from the user and store it in the variable choice.

A case statement is next entered to determine what choice was made. If choice 1 was selected, the user wants to look up someone in the phone book. In that case, the user is asked to enter the name to be looked up, and the lu program is called, passing it the name typed in by the user as the argument. Note that the double quotes around name in

lu "$name"

are necessary to ensure that two or more words typed in by the user are handed over to lu as a single argument.

A similar sequence occurs if the user selects menu items 2 or 3.

The programs lu, rem, and add are from earlier chapters (lu is from page 137 rem from page 167, and add from page 138).

Here are some sample runs of rolo:

$ rolo

      Would you like to:

            1.  Look someone up
            2.  Add someone to the phone book
            3.  Remove someone from the phone book

Please select one of the above (1-3): 2
Enter name to be added: El Coyote
Enter number: 212-555-3232
$ rolo                       Try it again

      Would you like to:

            1. Look someone up
            2. Add someone to the phone book
            3. Remove someone from the phone book

      Please select one of the above (1-3): 1

Enter name to look up: Coyote
El Coyote        212-555-3232
$ rolo                      Once again

      Would you like to:

            1. Look someone up
            2. Add someone to the phone book
            3. Remove someone from the phone book

      Please select one of the above (1-3): 4
Bad choice
$

When an invalid choice is entered, the program simply displays Bad choice and then terminates. A friendlier approach would be to reprompt the user until a proper choice is made. This can be done by enclosing the entire program inside an until loop that will be executed until a valid selection is made. To determine when a valid choice has been made, we can test a variable in the until that won't be assigned a value in the program until either 1, 2, or 3 is selected by the user.

Another change to make to rolo involves the way it will be used. Because the most common operation performed will be one of lookup, there will probably be a tendency on the part of the user to avoid typing rolo, then making selection 1, and then typing the name to be found when instead he or she can still type in

lu name

directly. Given all this, it might be a good idea to allow rolo to take command-line arguments. If any arguments are typed, rolo can assume that a lookup is being requested and just call lu directly. So if the user wants to perform a quick lookup, he or she can type rolo followed by the name. On the other hand, if the user wants to see the menu, typing just rolo causes the program to display its menu and prompt for a choice.

The preceding two changes (looping until a valid choice is selected and doing a quick lookup) were added to version 2 of rolo that is shown next.

$ cat rolo
#
# rolo - rolodex program to look up, add, and
#    remove people from the phone book -- version 2
#

#
# If arguments are supplied, then do a lookup
#

if [ "$#" -ne 0 ]
then
        lu "$@"
        exit
fi

validchoice=""         # set it null

#
# Loop until a valid selection is made
#

until [ -n "$validchoice" ]
do
        #
        # Display menu
        #

        echo '

        Would you like to:

            1. Look someone up
            2. Add someone to the phone book
            3. Remove someone from the phone book

        Please select one of the above (1-3): c'

        #
        # Read and process selection
        #
        read choice
        echo

        case "$choice"
        in
            1) echo "Enter name to look up: c"
                  read name
                  lu "$name"
                  validchoice=TRUE;;
            2) echo "Enter name to be added: c"
                  read name
                  echo "Enter number: c"
                  read number
                  add "$name" "$number"
                  validchoice=TRUE;;
            3) echo "Enter name to be removed: c"
                  read name
                  rem "$name"
                  validchoice=TRUE;;
            *) echo "Bad choice";;
      esac
done
$

If $# is nonzero, lu is called directly with the arguments typed on the command line. Then the program exits. Otherwise, the until loop is executed until the variable validchoice is nonnull. The only way it can ever become nonnull is if the command

validchoice=TRUE

is executed inside the case on selection of either 1, 2, or 3. Otherwise, the program continues to loop until one of these three choices is made.

$ rolo Bill                         Quick lookup
Billy Bach     201-555-7618
$ rolo                              Let's have the menu this time
      Would you like to:

          1. Look someone up
          2. Add someone to the phone book
          3. Remove someone from the phone book

      Please select one of the above (1-3): 4
Bad choice


      Would you like to:

           1. Look someone up
           2. Add someone to the phone book
           3. Remove someone from the phone book

      Please select one of the above (1-3): 0
Bad choice


      Would you like to:

           1. Look someone up
           2. Add someone to the phone book
           3. Remove someone from the phone book

      Please select one of the above (1-3): 1

Enter name to look up: Tony
Tony Iannino    973-555-1295
$

The $$ Variable and Temporary Files

If two or more people on your system use the rolo program at the same time, a potential problem may occur. Look at the rem program and see whether you can spot it. The problem occurs with the temporary file /tmp/phonebook that is used to create a new version of the phone book file.

grep -v "$name" phonebook > /tmp/phonebook
mv /tmp/phonebook phonebook

If more than one person uses rolo to remove an entry at the same time, there's a chance that the phone book file can get messed up because the same temporary file will be used by all rolo users.[1] Naturally, the chances of this happening (that is, the preceding two commands being executed at the same time by more than one user) are rather small, but, nevertheless there still is that chance. Anyway, it brings up an important point when dealing with temporary files in general.

When writing shell programs to be run by more than one person, make your temporary files unique. One way is to create the temporary file in the user's home directory, for example. Another way is to choose a temporary filename that will be unique for that particular process. To do this, you can use the special $$ shell variable, which contains the process id number (PID) of the current process:

$ echo $$
4668
$ ps
  PID  TTY TIME COMMAND
  4668 co  0:09 sh
  6470 co  0:03 ps
$

As you can see, $$ is equal to the process id number of your login shell. Because each process on the Unix system is given a unique process id number, using the value of $$ in the name of a file minimizes the possibility of another process using the same file. So you can replace the two lines from rem with these

grep -v "$name" phonebook > /tmp/phonebook$$
mv /tmp/phonebook$$ phonebook

to circumvent any potential problems. Each person running rolo will run it as a different process, so the temporary file used in each case will be different.

The Exit Status from read

read always returns an exit status of zero unless an end of file condition is detected on the input. If the data is coming from the terminal, this means that Ctrl+d has been typed. If the data is coming from a file, it means that there's no more data to read from the file.

Knowing about the exit status returned by read makes it easy to write a loop that will read any number of lines of data from a file or from the terminal. The next program, called addi, reads in lines containing pairs of integers. Each pair of numbers is summed, and the result written to standard output.

$ cat addi
#
# add pairs of integers on standard input
#

while read n1 n2
do
        echo $((n1 + n2))
done
$

The while loop is executed as long as the read command returns an exit status of zero; that is, as long as there's still data to be read. Inside the loop, the two values read from the line (presumably integers—no error checking is done here) are summed and the result written to standard output by echo.

$ addi
10 25
35
-5 12
7
123 3
126
Ctrl+d
$

It goes without saying that standard input for addi can be redirected, as can standard output:

$ cat data
1234 7960
593 -595
395 304
3234 999
-394 -493
$ addi < data > sums
$ cat sums
9194
-2
699
4233
-887
$

The following program, called number, is a simplified version of the standard Unix nl command: It takes one or more files given as arguments and displays them preceded by line numbers. If no arguments are supplied, it uses standard input instead.

$ cat number
#
# Number lines from files given as argument or from
# standard input if none supplied
#

lineno=1

cat $* |
while read line
do
        echo "$lineno: $line"
        lineno=$((lineno + 1))
done
$

The variable lineno—the line number count—is initially set to 1. Then the arguments typed to number are given to cat to be collectively written to standard output. If no arguments are supplied, $* will be null, and cat will be passed no arguments. This will cause it to read from standard input.

The output from cat is piped into the while loop. For each line read by read, the line is echoed at the terminal, preceded by the value of lineno, whose value is then incremented by one.

$ number phonebook
1: Alice Chebba     973-555-2015
2: Barbara Swingle  201-555-9257
3: Billy Bach       201-555-7618
4: El Coyote        212-555-3232
5: Liz Stachiw      212-555-2298
6: Susan Goldberg   201-555-7776
7: Teri Zak         201-555-6000
8: Tony Iannino     973-555-1295
$ who | number                  Try from standard input
1: root     console  Jul 25 07:55
2: pat      tty03    Jul 25 09:26
3: steve    tty04    Jul 25 10:58
4: george   tty13    Jul 25 08:05
$

Note that number won't work too well for lines that contain backslashes or leading whitespace characters. The following example illustrates this point.

$ number
            Here are some backslashes:  *
1: Here are some backslashes: *
$

Leading whitespace characters are removed from any line that's read. The backslash characters are also interpreted by the shell when it reads the line. You can use the –r option to read to prevent it from interpreting the backslash character. If we change the

while read line

in number to

while read –r line

the output will look better:

$ number
            Here are some backslashes:  *
1: Here are some backslashes:  *
$

In Chapter 12, “More on Parameters,” you'll learn how to preserve the leading whitespace characters and also how to have some control over the parsing of the input data.

The printf Command

Although echo is adequate for displaying simple messages, sometimes you'll want to print formatted output: for example, lining up columns of data. Unix systems provide the printf command. Those of you familiar with the C programming language will notice many similarities.

The general format of the printf command is

printf "format" arg1 arg2 ...

where format is a string that describes how the remaining arguments are to be displayed. (Note that the format string is a single argument, so it's a good idea to get into the habit of enclosing it in quotes because it often contains whitespace.) Characters in the format string that are not preceded by a percent sign (%) are written to standard output. One or more characters preceded by a percent sign are called conversion specifications and tell printf how the corresponding argument should be displayed. So, for each percent sign in the format string there should be a corresponding argument, except for the special conversion specification %%, which causes a single percent sign to be displayed.

Here's a simple example of printf:

$ printf "This is a number: %d
" 10
This is a number: 10
$

printf doesn't add a newline character to its output like echo; however, printf understands the same escape characters that echo does (refer to Table 10.1 earlier in this chapter), so adding to the end of the format string causes the prompt to appear on the next line.

Although this is a simple case that could easily be handled by echo, it helps to illustrate how the conversion specification (%d) is interpreted by printf: When the format string is scanned by printf, it outputs each character in the string without modification until it sees the percent sign; then it reads the d and recognizes that the %d should be replaced by the next argument, which must be an integer number. After that argument (10) is sent to standard output, printf sees the and outputs a newline.

Table 10.2 summarizes the different conversion specification characters.

Table 10.2. printf Conversion Specification Characters

Character

Use for Printing

d

Integers

u

Unsigned integers

o

Octal integers

x

Hexadecimal integers, using a-f

X

Hexadecimal integers, using A-F

c

Single characters

s

Literal strings

b

Strings containing backslash escape characters

%

Percent signs

The first five conversion specification characters are all used for displaying integers. %d displays signed integers, and %u displays unsigned integers; %u can also be used to display the positive representation of a negative number (note that the result is machine dependent). By default, integers displayed as octal or hexadecimal numbers do not have a leading 0 or 0x, but we'll show you how to enable this later in this section.

Strings are printed using %s or %b. %s is used to print strings literally, without any processing of backslash escape characters; %b is used to force interpretation of the backslash escape characters in the string argument.

Here are a few printf examples:

$ printf "The octal value for %d is %o
" 20 20
The octal value for 20 is 24
$ printf "The hexadecimal value for %d is %x
" 30 30
The hexadecimal value for 30 is 1e
$ printf "The unsigned value for %d is %u
" –1000 –1000
The unsigned value for -1000 is 4294966296
$ printf "This string contains a backslash escape: %s
" "test
string"
This string contains a backslash escape: test
string
$ printf "This string contains an interpreted escape: %b
" "test
string"
This string contains an interpreted escape: test string
$ printf "A string: %s and a character: %c
" hello A
A string: hello and a character: A
$

In the last printf, %c is used to display a single character. If the corresponding argument is longer than one character, only the first is displayed:

$ printf "Just the first character: %c
" abc
a
$

The general format of a conversion specification is

%[flags][width][.precision]type

The type is the conversion specification character from Table 10.2. As you can see, only the percent sign and type are required; the other parameters are called modifiers and are optional. Valid flags are -, +, #, and the space character. left justifies the value being printed; this will make more sense when we discuss the width modifier. + causes printf to precede integers with a + or sign (by default, only negative integers are printed with a sign). # causes printf to precede octal integers with 0 and hexadecimal integers with 0x or 0X for %#x or %#X, respectively. The space character causes printf to precede positive integers with a space and negative integers with a -.

$ printf "%+d
%+d
%+d
" 10 –10 20
+10
–10
+20
$ printf "% d
% d
% d
" 10 –10 20
 10
-10
 20
$ printf "%#o %#x
" 100 200
0144 0xc8
$

As you can see, using + or space as the flag lines up columns of positive and negative numbers nicely.

The width modifier is a positive number that specifies the minimum field width for printing an argument. The argument is right justified within this field unless the flag is used:

$ printf "%20s%20s
" string1 string2
             string1             string2
$ printf "%-20s%-20s
" string1 string2
string1             string2
$ printf "%5d%5d%5d
" 1 10 100
    1   10  100
$ printf "%5d%5d%5d
" -1 -10 -100
   -1  -10 –100
$ printf "%-5d%-5d%-5d
" 1 10 100
1    10   100
$

The width modifier can be useful for lining up columns of text or numbers (note that signs for numbers and leading 0, 0x, and 0X characters are counted as part of the argument's width). The width specifies a minimum size for the field; if the width of an argument exceeds width, it is not truncated.

The .precision modifier is a positive number that specifies a minimum number of digits to be displayed for %d, %u, %o, %x, and %X. This results in zero padding on the left of the value:

$ printf "%.5d %.4X
" 10 27
00010 001B
$

For strings, the .precision modifier specifies the maximum number of characters to be printed from the string; if the string is longer than precision characters, it is truncated on the right:

$ printf "%.5s
" abcdefg
abcde
$

A width can be combined with .precision to specify both a field width and zero padding (for numbers) or truncation (for strings):

$ printf ":%#10.5x:%5.4x:%5.4d
" 1 10 100
:   0x00001: 000a: 0100
$ printf ":%9.5s:
" abcdefg
:    abcde:
$ printf ":%-9.5s:
" abcdefg
:abcde    :
$

Finally, if a * is used in place of a number for width or precision, the argument preceding the value to be printed must be a number and will be used as the width or precision, respectively. If a * is used in place of both, two integer arguments must precede the value being printed and are used for the width and precision:

$ printf "%*s%*.*s
" 12 "test one" 10 2 "test two"
    test one        te
$ printf "%12s%10.2s
" "test one" "test two"
    test one        te
$

As you can see, the two printfs in this example produce the same results. In the first printf, 12 is used as the width for the first string, 10 as the width for the second string, and 2 as the precision for the second string. In the second printf, these numbers are specified as part of the conversion specification.

Table 10.3 summarizes the various conversion specification modifiers.

Table 10.3. printf Conversion Specification Modifiers

Modifier

Meaning

flags

 

-

Left justify value.

+

Precede integer with + or -.

(space)

Precede positive integer with space character.

#

Precede octal integer with 0, hexadecimal integer with 0x or 0X.

width

Minimum width of field; * means use next argument as width.

precision

Minimum number of digits to display for integers; maximum number of characters to display for strings; * means use next argument as precision.

Here's a simple example that uses printf to align two columns of numbers from a file:

$ cat align
#
# Align two columns of numbers
# (works for numbers up to 12 digits long, including sign)

cat $* |
while read number1 number2
do
      printf "%12d %12d
" $number1 $number2
done
$ cat data
1234 7960
593 -595
395 304
3234 999
-394 -493
$ align data
        1234         7960
         593         -595
         395          304
        3234          999
        -394         -493
$

In Chapters 12, 14, and 15 you'll see more uses for printf. But first try your hand at the following exercises.

Exercises

1:

What happens to mycp if one or more of the files to be copied doesn't exist? Can you make any suggestions to better handle the situation?

2:

What happens to mycp if one of the filenames contains a character that has a special meaning to the shell such as ; or |?

3:

Write a program called mymv that does with the mv command what mycp does with the cp command. How many changes did you have to make to mycp to produce this new program?

4:

Modify mycp to prompt for arguments if none are supplied. A typical execution of the modified version should look like this:

$ mycp
Source file name? voucher
Destination file name? voucher.sv
$

Make sure that the program allows one or both of the files to be specified with filename substitution characters.

5:

Add a -n option to mycp that suppresses the normal check for the existence of the destination files.

6:

Modify mycp to use sed instead of the while loop to process the arguments typed on the command line.

7:

Modify the rem program used by rolo so that if multiple entries are found, the program will prompt the user for the entry to be removed.

Here's a sample session:

$ rolo
   ...
      Please select one of the above (1-3): 3

Enter name to be removed: Susan

More than one match; please select the one to remove:
Susan Goldberg    Remove (y/n)? n
Susan Topple      Remove (y/n)? y
$

8:

Modify rolo so that the menu is redisplayed after each selection is made and processed. To allow the user to get out of this, add another selection to the menu to exit from the program.

9:

What happens to the rolo program if just an Enter is given as the name for the add, look up, or remove options?

10:

Modify lu to use printf to print the name and phone number so that they line up in columns for names up to 40 characters in length (Hint: use cut –f and the fact that the fields in the phonebook are separated by tabs).



[1] Actually, it depends on the users' default file creation mask (known as umask). If one person has created /tmp/phonebook and it's not writable by anyone else, the next person who comes along and tries to create it will get an error message from the shell. The net result is that the first user's file will get properly updated, and the second user's won't; neither file will get corrupted.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.133.159.224