AND operator in case statement

You are correct in that the standard definition of case does not allow for a AND operator in the pattern. You’re also correct that trying to say “starts with a lower-case vowel AND starts with an upper-case vowel” would not match anything. Note also that you have your patterns & explanations reversed for the begins/ends with a digit tests — using a pattern of [0-9]* would match words that begin with a digit, not end with a digit.

One approach this would be to combine your tests into the same pattern, most-restrictive first:

case $word in
  ([AaEeIiOoUu]??[0-9]) echo it is four characters long and begins with a vowel and ends with a digit;;
  ([AaEeIiOoUu]*[0-9])  echo it is not four characters long begins with a vowel and ends with a digit;;
# ...
esac

Another (lengthy!) approach would be to nest your case statements, building up appropriate responses each time. Does it begin with a vowel, yes or no? Now, does it end in a digit, yes or no? This would get unwieldy quickly, and annoying to maintain.

Another approach would be to use a sequence of case statements that builds up a string (or array) of applicable statements; you could even add * catch-all patterns to each if you wanted to provide “negative” feedback (“word does not begin with a vowel”, etc).

result=""
case $word in
  [AaEeIiOoUu]*)
          result="The word begins with a vowel." ;;
esac

case $word in
  [0-9]*)
          result="${result} The word begins with a digit." ;;
esac

case $word in
  *[0-9])
          result="${result} The word ends with a digit." ;;
esac

case $word in
   ????)
          result="${result} You entered four characters." ;;
esac

printf '%sn' "$result"

For examples:

$ ./go.sh
Enter a word: aieee
The word begins with a vowel.
$ ./go.sh
Enter a word: jeff42
 The word ends with a digit.
$ ./go.sh
Enter a word: aiee
The word begins with a vowel. You entered four characters.
$ ./go.sh
Enter a word: 9arm
 The word begins with a digit. You entered four characters.
$ ./go.sh
Enter a word: arm9
The word begins with a vowel. The word ends with a digit. You entered four characters.

Alternatively, bash extended the syntax for the case statement to allow for multiple patterns to be selected, if you end the pattern(s) with ;;&:

shopt -s nocasematch
case $word in
  [aeiou]*)
          echo "The word begins with a vowel." ;;&
  [0-9]*)
          echo "The word begins with a digit." ;;&
  *[0-9])
          echo "The word ends with a digit." ;;&
   ????)
          echo "You entered four characters." ;;
esac

Note that I removed the * catch-all pattern, since that would match anything & everything, when falling through the patterns this way. Bash also has a shell option called nocasematch, which I set above, that enables case-insensitive matching of the patterns. That helps reduce redundancy — I removed the | [AEIOU]* part of the pattern.

For examples:

$ ./go.sh
Enter a word: aieee
The word begins with a vowel.
$ ./go.sh
Enter a word: jeff42
The word ends with a digit.
$ ./go.sh
Enter a word: aiee
The word begins with a vowel.
You entered four characters.
$ ./go.sh
Enter a word: 9arm
The word begins with a digit.
You entered four characters.
$ ./go.sh
Enter a word: arm9
The word begins with a vowel.
The word ends with a digit.
You entered four characters.

For completeness, while case has a | OR operator, it doesn’t have an AND operator but if using shells with extended glob operators (ksh, zsh, bash), you can implement the AND in the pattern syntax:

  • ksh93‘s @(x&y&z) operator:

      case $string in
        ( @({12}(?)&~(i:[aeiou]*)&*[0123456789]) )
          echo is 12 characters long AND starts with a vowel AND ends in a decimal
      esac
    
  • zsh (using ~ (AND-NOT) combined with ^ (NOT)): x~^y~^z

      set -o extendedglob
      case $string in
        ( ?(#c12)~^(#i)[aeiou]*~^*[0-9] )
          echo is 12 characters long AND starts with a vowel AND ends in a decimal
      esac
    
  • ksh88, bash, using double negation with OR (!(!(x)|!(y)|!(z)))

      shopt -s extglob # bash only
      case $string in
        ( !(!(????????????)|!([aAeEıiIİoOuU]*)|!(*[0123456789])) )
          echo is 12 characters long AND starts with a vowel AND ends in a decimal
      esac
    

In any case, remember that except in zsh where ranges are always based on codepoint values, ranges like [0-9] cannot be used reliably outside of the POSIX/C locale (hence the [0123456789] instead above).

ksh93 and zsh’s case insensitive matching operators (~(i) and (#i)) honour the locale for case sensitive comparison. For instance, in a Turkish locale on a GNU system, (#i)[aeiou] will match on İ, but not I (because uppercase i is İ there). To get a consistent outcome regardless of the locale, you may want to hard code all possible values instead like in the ksh88/bash approach.

The usual portable solution to implement an AND in case statements is to concatenate the boolean values:

case $A$B in
    11) echo "Both conditions are true";;
    1*) echo "Condition A is true";;
    *1) echo "Condition B is true";;
    00) echo "Both conditions are false";;
     *) echo "There is an unexpected error";;
esac

For your use case:

printf "Enter a word: "; read word

A=0 B=0 C=0

case $word in    ( [aeiouAEIOU]* ) A=1;; esac
case $word in    ( *[0-9]        ) B=1;; esac
case $word in    ( ????          ) C=1;; esac

case $A$B$C in
  111)     echo "Four letters that start with a vowel and end with a digit" ;;
  11*)     echo "The word begins with a vowel AND ends with a digit."       ;;
  1* )     echo "The word begins with a vowel."                             ;;
  *1?)     echo "The word ends with a digit."                               ;;
   *1)     echo "The word is four letters long"                             ;;
    *)     echo "I don't understand what you've entered,"                   ;;
esac

Using a portable case for each boolean option. You can use ;;& in bash, or ;| in zsh. Sadly ksh doesn’t have such option for case.

An alternative to set the booleans (in some shells: ksh, bash, zsh at least) is:

[[ $word ==   [aeiouAEIOU]* ]] && A=1 || A=0
[[ $word ==   *[0-9]        ]] && B=1 || B=0
[[ $word ==   ????          ]] && C=1 || c=0

Leave a Comment