DirectNET

Data Center Management Solutions including UPS Systems, Data Center Cooling, KVM over IP & IP Power Strips, Server Racks and Server Rack accessories; KVM Switches and KVM Extenders; Rackmount Monitors and Rackmount Keyboards.


NAVIGATION
Home
Store
INSIDE MAC
Television Shows
Broadcast Shows
Daily News Shows
Special Shows
EVENTS
DAILY TIPS
Design
Mac OS X
Mac OS X UNIX
COMMUNITY
Surveys
NEWS
Current
Press
Archive
FEATURES
Editorial
Dr. Mac
Reviews
Reader Reports
RESOURCES
FAQ
Documentation
Learning Center
MAN pages
Glossary
Tutorials
Tips
Links

OUR PARTNERS

OS X | UNIX

back

Unix

Mac OS X Unix Tutorial

Part 9 - Shell Scripting 2 (page 2 of 2)

if-elif-elif-else-fi

The 'if' statement was discussed in Part 8 (Shell Scripting 1). In addition to the simple 'if-fi' and 'if-else-fi' variants, one is able to specify any number of alternatives. For example:

$ cat ifelif 
#!/bin/sh

read -p "Type a letter: " l
if [ "$l" = "a" ]; then echo "case a" elif [ "$l" = "b" ]; then echo "case b" elif [ "$l" = "c" ]; then echo "case c" else echo "default case" fi

Here there are four alternatives; thought there is no (sensible) upper limit. The first alternative is selected when the first 'if' condition is TRUE. If the first condition is FALSE, the second 'elif' condition (short for else if) is tried, and so on. If no conditions are TRUE the 'else' part is executed.

Running this script we get:

$ ./ifelif
Type a letter: c
case c

$ ./ifelif
Type a letter: a
case a

$ ./ifelif
Type a letter: z
default case

$ ./ifelif
Type a letter: b
case b

...which I guess is self-explanatory

More Conditions

So far, the conditions I have given to 'if' and 'elif' have been simple equality tests on strings. One can test strings, numeric values, and files.

Here are some other conditions you can use. I am assuming the existence of shell variables num1 and num2. It helps if these contain strings that can be interpreted as integer numbers such as "123", not "abc".

if [ $1 != $2 ]; then
TRUE if the parameter strings are not the same

if [ $num1 -eq $num2 ]; then
TRUE if the variables are numerically equal

if [ $num1 -le $num2 ]; then
TRUE if num1 is less than or equal to num2

if [ -e filename ]; then
TRUE if the specified file exists (the filename may include a pathname too)

if [ -d filename ]; then
TRUE if the files exists and is a directory

There are lots more conditions. They can be viewed with:

$ man test

Read this fully. It shows all manner of tests, particularly on files, and is invaluable when writing scripts.

See the sidebar for an explanation of 'test condition' versus '[ condition ]'.


 
Tell Me More...

[ and 'test'

Have a look in /bin and you find an odd entry that is simply '['.

You will also see a command called 'test'

Now try this:

$ test 1 = 2
$ echo $?
1

$ test 1 = 1
$ echo $?
0

The test command evaluates the expression given to it, and returns TRUE (0) or FALSE (1).

To obtain the return value of the last command executed use the shell special variable $?. This variable can also be checked in shell scripts after calling an external command.

Now look at this simple shell script:

$ cat bkt
#!/bin/sh

if test "$1" = "" ; then echo "Empty" else echo $1 fi
$ ./bkt Empty
$ ./bkt Hello Hello

The script executes the command 'test "$1" = ""' (after the shell has expanded $1) and the 'if' statement is tests the result of this (TRUE/FALSE).

This is exactly equivalent to the more usual:

if [ "$1" = "" ]; then

except that the command '[' is executed instead of test. '[' additionally looks for a matching ']' and discards it. 'test' and '[' are otherwise equivalent.


'Case' versus 'if'

The example I gave above using if-elif-elif-else-fi would have been better done using a 'case-esac' control construct. The two constructs are very similar, and the case equivalent looks like this:

% cat case
#!/bin/sh

read -p "Type a letter: " l
case "$l" in "a") echo "case a" ;; "b") echo "case b" ;; "c") echo "case c" ;; *) echo "None of A, B, C" echo "This is the default case." ;; esac

Executing the script gives:

$ ./case
Type a letter: b
case b

$ ./case
Type a letter: c
case c

$ ./case
Type a letter: f
None of A, B, C
This is the default case.

$ ./case
Type a letter: a
case a

'Case' works like this:

case "$l" in

compares the value of variable 'l' (in this example) against each of the cases listed below it - "a", "b", and "c" (in this example).

The alternatives are tried in order, and for the first that matches the statements between the closing bracket and ';;' are executed. You may place any statement, and any number of statements, between ')' and ';;'. When ';;' is reached, execution of the case statement completes and continues from the statement following 'esac'. ('esac' is case backwards, in the same vein as the if-fi pair.)

The special alternative '*)' is a catch-all and behaves just like the 'else' part of an 'if' statement.

Note that:

"a")
    echo "case a"
    ;;

can be written as:

"a") echo "case a" ;;

The former is preferred if the case contains more than one statement like:

*)
    echo "None of A, B, C"
    echo "This is the default case."
    ;;

When to use 'if' and when to use 'case'

The example I have presented is better coded as a 'case' statement: it is simpler to follow that way. However, 'case' is limited in that each alternative must compare the variable given in:

case $var in

for equality to the variable or constant given in each alternative:

"value")
  statement
  statement
  ;;

An 'if' statement is more powerful as each condition ('if' and 'elif') is independent. The following example looks quite complex, but in actuality is quite easy to follow through. It makes use of more 'test' conditions, and '$#' as representing the number of parameters passed to the script. In particular, I have used a nested condition - an 'if' statement within another 'if' statement. (In fact any condition can be nested within any another condition.)

% cat ifelif2
#!/bin/sh

# Check if the wrong number of parameters has been given, and if so issue # a usage message. We require 1 or 2 parameters, and choose to check # for an error by testing for greater than two or equal to 0. if [ "$#" -gt "2" ]; then echo "Usage: $0 filename [filetype]" echo "Too many parameters" elif [ "$#" = "0" ]; then echo "Usage: $0 filename [filetype]" echo "Too few parameters"
# At this point, we have the correct number of parameters (1 or 2). # If two parameters were given we test if the file specified by the # first parameter is of the type specified by the second parameter. elif [ "$#" = "2" ]; then if [ -$2 $1 ]; then echo "Yes, $1 is of type $2" else echo "No, $1 isn't of type $2" fi
# If just one parameter has been given, we simply report whether the file specified # by the parameter is of type file, directory, or something else. elif [ -f $1 ]; then echo "$1 is a file" elif [ -d $1 ]; then echo "$1 is a directory" else echo "$1 is something other than a file or directory" fi

Executing this:

$ ./ifelif2
Usage: ./ifelif2 filename [filetype]
Too few parameters

$ ./ifelif2 file -d junk
Usage: ./ifelif2 filename [filetype]
Too many parameters

$ touch a-file
$ mkdir a-dir

$ ./ifelif2 a-file f
Yes, a-file is of type f

$ ./ifelif2 a-dir d
Yes, a-dir is of type d

$ ./ifelif2 a-dir f
No, a-dir isn't of type f

$ ./ifelif2 a-file d
No, a-file isn't of type d

$ ./ifelif2 a-dir
a-dir is a directory

$ ./ifelif2 a-file
a-file is a file

$ ./ifelif2 asdasdasd
asdasdasd is something other than a file or directory

$ ./ifelif2 a-file q
./ifelif2: [: -q: unary operator expected
No, a-file isn't of type q

The last two executions show that the script could do with some improvements.


Loops

A loop is a control construct that goes round and round, either forever, or more usually until a condition is met. The Bourne shell provides three kinds of loop:

for - loop n times to process a list of n values
while - loop continually while a condition is true
until - loop continually until a condition is true

'while' and 'until' are similar, but reverse the termination condition.

For Loops

Here is a very simple 'for' loop:

$ cat for1
#!/bin/sh

count=0 for var in one two three a b c do echo "The value of var is now $var" count=$(expr $count + 1) echo "This is iteration number $count" done

Executing script 'for1' gives:

$ ./for1
The value of var is now one
This is iteration number 1
The value of var is now two
This is iteration number 2
The value of var is now three
This is iteration number 3
The value of var is now a
This is iteration number 4
The value of var is now b
This is iteration number 5
The value of var is now c
This is iteration number 6

The loop was executed six times, once for each value listed in:

for var in one two three a b c

Each iteration, the variable specified in the 'for' loop (in this case 'var') takes on the next value in the list (in this case 'one two three a b c'). When the list is exhausted the 'for' loop completes.

A 'for' loop is more useful when the list is generated dynamically, for example when it is the output of a program or expanded by the shell. This next example prints the type of each file in the current directory.

$ cat for2
#!/bin/sh
for var in * 
do
  if [ -d "$var" ]; then
    echo "$var - directory"
  elif [ -f "$var" ]; then
    echo "$var - file"
  else
    echo "$var - dunno"
  fi
done

The list is "*", which of course the shell expands to all files in the current directory.

Alternatively,

for var in $*

will generate a list containing all the parameters passed to the script, and:

for var in $(command)

will generate a list containing the output generated by 'command'.

Note that the list is space-separated.

While Loops

Here is a simple while loop:

$ cat while1
#!/bin/sh

read -p "Give a filename: " fn while [ "$fn" != "" ] do if [ -e $fn ]; then file $fn else echo "File $fn does not exist" fi
read -p "Give a filename: " fn done
echo "Bye."

The loop comprises the statements between 'do' and 'done'. They are executed while the condition given in:

while [ "$fn" != "" ]

remains TRUE. A 'while' condition is formed exactly as for an 'if' or 'elif' condition. In this example, the loop executes while the input filename is not empty. Note that two 'read' statements are required, one before the loop and one within the loop. Running the script gives:

$ ./while1
Give a filename: xzy
File xzy does not exist
Give a filename: while1
while1: Bourne shell script text
Give a filename:
Bye.

The last request for a filename is responded to with a press of the return key, resulting in an empty string being assigned to fn, and thus the 'while' condition being FLASE. At this point the loop completes.

Note the use of command 'file', which attempts (with a little /etc/magic) to identify the type of the file passed to it.

Until Loops

An 'until' loop simply uses the keyword 'until' instead of 'while', and as one might guess from the linguistic sense of the construct, the condition for exiting the loop is reversed.

Here is the above example written as an 'until' loop:

$ cat until1
#!/bin/sh

read -p "Give a filename: " fn until [ "$fn" = "" ] do if [ -e $fn ]; then file $fn else echo "File $fn does not exist" fi
read -p "Give a filename: " fn done
echo "Bye."

Executing it:

$ ./until1
Give a filename: hello
File hello does not exist
Give a filename: until1
until1: Bourne shell script text
Give a filename: while1
while1: Bourne shell script text
Give a filename:
Bye.
 
Tell Me More...

Adding Up

Note the statement:

count=$(expr $count + 1)

in the script 'for1'

The command 'expr' evaluates the expression given to it, in this case $count (which will be expanded by the shell before being passed to 'eval') plus 1. The output from the command is assigned back to 'count' (overwriting its previous value) using the $(command) technique to execute 'command' and place its output into a variable. See Shell Scripting 1.

$ man eval

will reveal the power of 'eval'.

Some 'for' list tricks

Here are some variants of the 'for' statement from the script 'for2':

1) filenames with spaces

Suppose we have a file called:

"file with space"

The 'for' loop:

for var in *

will correctly produce:

file with space - file

whereas the supposedly equivalent:

for var in $(ls)

will return:

file - dunno
with - dunno
space - dunno

This is because the shell expands "*" itself and understands which spaces are part of the filename, and which separate elements of the 'for' list. Using '$(ls)', the necessary information is lost because the shell sees only the output from the 'ls' command.

2) parameters with spaces

The special parameter '$@' is similar to '$*', but is useful when input parameters might contain spaces. This is most easily illustrated by an example.

The script:

$ cat for3
#!/bin/sh

echo "Using \$*" for var in $* do echo "$var" done
echo echo "Using \"\$*\"" for var in "$*" do echo "$var" done
echo echo "Using \$@" for var in $@ do echo "$var" done
echo echo "Using \"\$@\"" for var in "$@" do echo "$var" done

will behave as below.

Note that the third parameter:

"three four"

contains a space and is quoted to indicate that it is supposed to be a single parameter.

Note also the heavy use of escaping in the 'echo' commands.

$ ./for3 one two "three four"

Using $*
one
two
three
four

Using "$*"
one two three four

Using $@
one
two
three
four

Using "$@"
one
two
three four

"$@" is often the correct expression to use.


Next Part

In the next episode, I will continue the shell scripting tutorial, covering more complex conditions, nesting, more on loops, shell functions, and other such goodies.

Meanwhile, check out the Daily Unix Tips.

Week 54 gives comparisons between sh and tcsh.

Week 56 gives some more scripting tips.

Until then, enjoy :-)



Discuss this article in the Learning Center forum




previous

Part 9 - Shell Scripting 2 (Page 2 of 2)

end

Copyright © 2000-2010 Inside Mac Media, Inc. All rights reserved.
Apple assumes no responsibility with regard to the selection, performance, or use of the products or services. All understandings, agreements, or warranties, if any, take place directly between the vendors and prospective users.
Apple, the Apple logo, Mac, PowerMac G4, PowerMac G5, Xserve, Xserve RAID, PowerBook, iBook, Airport, AirPort Extreme, iMac, eMac, iLife, iMovie, iCal, iPhoto, iTunes, QuickTime, FireWire, iPod, iSight, AppleWorks, Macintosh, Jaguar, Panther, Mac OS, Mac OS X and Mac OS X Server are trademarks of Apple Computer, Inc.