11.4. Testing and Branching

The case and select constructs are technically not loops, since they do not iterate the execution of a code block. Like loops, however, they direct program flow according to conditions at the top or bottom of the block.

Controlling program flow in a code block

case (in) / esac

The case construct is the shell scripting analog to switch in C/C++. It permits branching to one of a number of code blocks, depending on condition tests. It serves as a kind of shorthand for multiple if/then/else statements and is an appropriate tool for creating menus.

case "$variable" in

 "$condition1" )
 command...
 ;;

 "$condition2" )
 command...
 ;;


esac

Note

  • Quoting the variables is not mandatory, since word splitting does not take place.

  • Each test line ends with a right paren ). [1]

  • Each condition block ends with a double semicolon ;;.

  • If a condition tests true, then the associated commands execute and the case block terminates.

  • The entire case block ends with an esac (case spelled backwards).


Example 11-25. Using case

   1 #!/bin/bash
   2 # Testing ranges of characters.
   3 
   4 echo; echo "Hit a key, then hit return."
   5 read Keypress
   6 
   7 case "$Keypress" in
   8   [[:lower:]]   ) echo "Lowercase letter";;
   9   [[:upper:]]   ) echo "Uppercase letter";;
  10   [0-9]         ) echo "Digit";;
  11   *             ) echo "Punctuation, whitespace, or other";;
  12 esac      #  Allows ranges of characters in [square brackets],
  13           #+ or POSIX ranges in [[double square brackets.
  14 
  15 #  In the first version of this example,
  16 #+ the tests for lowercase and uppercase characters were
  17 #+ [a-z] and [A-Z].
  18 #  This no longer works in certain locales and/or Linux distros.
  19 #  POSIX is more portable.
  20 #  Thanks to Frank Wang for pointing this out.
  21 
  22 #  Exercise:
  23 #  --------
  24 #  As the script stands, it accepts a single keystroke, then terminates.
  25 #  Change the script so it accepts repeated input,
  26 #+ reports on each keystroke, and terminates only when "X" is hit.
  27 #  Hint: enclose everything in a "while" loop.
  28 
  29 exit 0


Example 11-26. Creating menus using case

   1 #!/bin/bash
   2 
   3 # Crude address database
   4 
   5 clear # Clear the screen.
   6 
   7 echo "          Contact List"
   8 echo "          ------- ----"
   9 echo "Choose one of the following persons:" 
  10 echo
  11 echo "[E]vans, Roland"
  12 echo "[J]ones, Mildred"
  13 echo "[S]mith, Julie"
  14 echo "[Z]ane, Morris"
  15 echo
  16 
  17 read person
  18 
  19 case "$person" in
  20 # Note variable is quoted.
  21 
  22   "E" | "e" )
  23   # Accept upper or lowercase input.
  24   echo
  25   echo "Roland Evans"
  26   echo "4321 Flash Dr."
  27   echo "Hardscrabble, CO 80753"
  28   echo "(303) 734-9874"
  29   echo "(303) 734-9892 fax"
  30   echo "revans@zzy.net"
  31   echo "Business partner & old friend"
  32   ;;
  33 # Note double semicolon to terminate each option.
  34 
  35   "J" | "j" )
  36   echo
  37   echo "Mildred Jones"
  38   echo "249 E. 7th St., Apt. 19"
  39   echo "New York, NY 10009"
  40   echo "(212) 533-2814"
  41   echo "(212) 533-9972 fax"
  42   echo "milliej@loisaida.com"
  43   echo "Ex-girlfriend"
  44   echo "Birthday: Feb. 11"
  45   ;;
  46 
  47 # Add info for Smith & Zane later.
  48 
  49           * )
  50    # Default option.	  
  51    # Empty input (hitting RETURN) fits here, too.
  52    echo
  53    echo "Not yet in database."
  54   ;;
  55 
  56 esac
  57 
  58 echo
  59 
  60 #  Exercise:
  61 #  --------
  62 #  Change the script so it accepts multiple inputs,
  63 #+ instead of terminating after displaying just one address.
  64 
  65 exit 0

An exceptionally clever use of case involves testing for command-line parameters.
   1 #! /bin/bash
   2 
   3 case "$1" in
   4   "") echo "Usage: ${0##*/} <filename>"; exit $E_PARAM;;
   5                       # No command-line parameters,
   6                       # or first parameter empty.
   7 # Note that ${0##*/} is ${var##pattern} param substitution.
   8                       # Net result is $0.
   9 
  10   -*) FILENAME=./$1;;   #  If filename passed as argument ($1)
  11                       #+ starts with a dash,
  12                       #+ replace it with ./$1
  13                       #+ so further commands don't interpret it
  14                       #+ as an option.
  15 
  16   * ) FILENAME=$1;;     # Otherwise, $1.
  17 esac

Here is a more straightforward example of command-line parameter handling:
   1 #! /bin/bash
   2 
   3 
   4 while [ $# -gt 0 ]; do    # Until you run out of parameters . . .
   5   case "$1" in
   6     -d|--debug)
   7               # "-d" or "--debug" parameter?
   8               DEBUG=1
   9               ;;
  10     -c|--conf)
  11               CONFFILE="$2"
  12               shift
  13               if [ ! -f $CONFFILE ]; then
  14                 echo "Error: Supplied file doesn't exist!"
  15                 exit $E_CONFFILE     # File not found error.
  16               fi
  17               ;;
  18   esac
  19   shift       # Check next set of parameters.
  20 done
  21 
  22 #  From Stefano Falsetto's "Log2Rot" script,
  23 #+ part of his "rottlog" package.
  24 #  Used with permission.


Example 11-27. Using command substitution to generate the case variable

   1 #!/bin/bash
   2 # case-cmd.sh: Using command substitution to generate a "case" variable.
   3 
   4 case $( arch ) in   # $( arch ) returns machine architecture.
   5                     # Equivalent to 'uname -m' ...
   6   i386 ) echo "80386-based machine";;
   7   i486 ) echo "80486-based machine";;
   8   i586 ) echo "Pentium-based machine";;
   9   i686 ) echo "Pentium2+-based machine";;
  10   *    ) echo "Other type of machine";;
  11 esac
  12 
  13 exit 0

A case construct can filter strings for globbing patterns.


Example 11-28. Simple string matching

   1 #!/bin/bash
   2 # match-string.sh: Simple string matching
   3 #                  using a 'case' construct.
   4 
   5 match_string ()
   6 { # Exact string match.
   7   MATCH=0
   8   E_NOMATCH=90
   9   PARAMS=2     # Function requires 2 arguments.
  10   E_BAD_PARAMS=91
  11 
  12   [ $# -eq $PARAMS ] || return $E_BAD_PARAMS
  13 
  14   case "$1" in
  15   "$2") return $MATCH;;
  16   *   ) return $E_NOMATCH;;
  17   esac
  18 
  19 }  
  20 
  21 
  22 a=one
  23 b=two
  24 c=three
  25 d=two
  26 
  27 
  28 match_string $a     # wrong number of parameters
  29 echo $?             # 91
  30 
  31 match_string $a $b  # no match
  32 echo $?             # 90
  33 
  34 match_string $b $d  # match
  35 echo $?             # 0
  36 
  37 
  38 exit 0		    


Example 11-29. Checking for alphabetic input

   1 #!/bin/bash
   2 # isalpha.sh: Using a "case" structure to filter a string.
   3 
   4 SUCCESS=0
   5 FAILURE=1   #  Was FAILURE=-1,
   6             #+ but Bash no longer allows negative return value.
   7 
   8 isalpha ()  # Tests whether *first character* of input string is alphabetic.
   9 {
  10 if [ -z "$1" ]                # No argument passed?
  11 then
  12   return $FAILURE
  13 fi
  14 
  15 case "$1" in
  16   [a-zA-Z]*) return $SUCCESS;;  # Begins with a letter?
  17   *        ) return $FAILURE;;
  18 esac
  19 }             # Compare this with "isalpha ()" function in C.
  20 
  21 
  22 isalpha2 ()   # Tests whether *entire string* is alphabetic.
  23 {
  24   [ $# -eq 1 ] || return $FAILURE
  25 
  26   case $1 in
  27   *[!a-zA-Z]*|"") return $FAILURE;;
  28                *) return $SUCCESS;;
  29   esac
  30 }
  31 
  32 isdigit ()    # Tests whether *entire string* is numerical.
  33 {             # In other words, tests for integer variable.
  34   [ $# -eq 1 ] || return $FAILURE
  35 
  36   case $1 in
  37     *[!0-9]*|"") return $FAILURE;;
  38               *) return $SUCCESS;;
  39   esac
  40 }
  41 
  42 
  43 
  44 check_var ()  # Front-end to isalpha ().
  45 {
  46 if isalpha "$@"
  47 then
  48   echo "\"$*\" begins with an alpha character."
  49   if isalpha2 "$@"
  50   then        # No point in testing if first char is non-alpha.
  51     echo "\"$*\" contains only alpha characters."
  52   else
  53     echo "\"$*\" contains at least one non-alpha character."
  54   fi  
  55 else
  56   echo "\"$*\" begins with a non-alpha character."
  57               # Also "non-alpha" if no argument passed.
  58 fi
  59 
  60 echo
  61 
  62 }
  63 
  64 digit_check ()  # Front-end to isdigit ().
  65 {
  66 if isdigit "$@"
  67 then
  68   echo "\"$*\" contains only digits [0 - 9]."
  69 else
  70   echo "\"$*\" has at least one non-digit character."
  71 fi
  72 
  73 echo
  74 
  75 }
  76 
  77 a=23skidoo
  78 b=H3llo
  79 c=-What?
  80 d=What?
  81 e=$(echo $b)   # Command substitution.
  82 f=AbcDef
  83 g=27234
  84 h=27a34
  85 i=27.34
  86 
  87 check_var $a
  88 check_var $b
  89 check_var $c
  90 check_var $d
  91 check_var $e
  92 check_var $f
  93 check_var     # No argument passed, so what happens?
  94 #
  95 digit_check $g
  96 digit_check $h
  97 digit_check $i
  98 
  99 
 100 exit 0        # Script improved by S.C.
 101 
 102 # Exercise:
 103 # --------
 104 #  Write an 'isfloat ()' function that tests for floating point numbers.
 105 #  Hint: The function duplicates 'isdigit ()',
 106 #+ but adds a test for a mandatory decimal point.

select

The select construct, adopted from the Korn Shell, is yet another tool for building menus.

select variable [in list]
do
 command...
 break
done

This prompts the user to enter one of the choices presented in the variable list. Note that select uses the $PS3 prompt (#? ) by default, but this may be changed.


Example 11-30. Creating menus using select

   1 #!/bin/bash
   2 
   3 PS3='Choose your favorite vegetable: ' # Sets the prompt string.
   4                                        # Otherwise it defaults to #? .
   5 
   6 echo
   7 
   8 select vegetable in "beans" "carrots" "potatoes" "onions" "rutabagas"
   9 do
  10   echo
  11   echo "Your favorite veggie is $vegetable."
  12   echo "Yuck!"
  13   echo
  14   break  # What happens if there is no 'break' here?
  15 done
  16 
  17 exit
  18 
  19 # Exercise:
  20 # --------
  21 #  Fix this script to accept user input not specified in
  22 #+ the "select" statement.
  23 #  For example, if the user inputs "peas,"
  24 #+ the script would respond "Sorry. That is not on the menu."

If in list is omitted, then select uses the list of command line arguments ($@) passed to the script or the function containing the select construct.

Compare this to the behavior of a

for variable [in list]

construct with the in list omitted.


Example 11-31. Creating menus using select in a function

   1 #!/bin/bash
   2 
   3 PS3='Choose your favorite vegetable: '
   4 
   5 echo
   6 
   7 choice_of()
   8 {
   9 select vegetable
  10 # [in list] omitted, so 'select' uses arguments passed to function.
  11 do
  12   echo
  13   echo "Your favorite veggie is $vegetable."
  14   echo "Yuck!"
  15   echo
  16   break
  17 done
  18 }
  19 
  20 choice_of beans rice carrots radishes rutabaga spinach
  21 #         $1    $2   $3      $4       $5       $6
  22 #         passed to choice_of() function
  23 
  24 exit 0

See also Example 37-3.

Notes

[1]

Pattern-match lines may also start with a ( left paren to give the layout a more structured appearance.

   1 case $( arch ) in   # $( arch ) returns machine architecture.
   2   ( i386 ) echo "80386-based machine";;
   3 # ^      ^
   4   ( i486 ) echo "80486-based machine";;
   5   ( i586 ) echo "Pentium-based machine";;
   6   ( i686 ) echo "Pentium2+-based machine";;
   7   (    * ) echo "Other type of machine";;
   8 esac