Testing and Branching

Advanced Bash-Scripting Guide: An in-depth exploration of the art of shell scripting
Prev | Chapter 11. Loops and Branches | Next

# 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

  | 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

Prev | Home | Next
Loop Control | Up | Command Substitution