7

I have the following code.

read -p "Enter a word: " word

case $word in
  [aeiou]* | [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." ;;
  [aeiou]* && [AEIOU]* && *[0-9])
          echo "The word begins with vowel and ends with a digit." ;;
   ????)
          echo "You entered a four letter word." ;;
   *)
          echo "I don't know what you've entered," ;;
esac

When I run this:

Enter a word: apple123
case2.sh: line 10: syntax error near unexpected token `&&'
case2.sh: line 10: `  [aeiou]* && [AEIOU]* && *[0-9])'

It looks like case statement doesn't support AND operator, and also I believe the && operator in my above case statement logically incorrect.

I understand that we can use if...else to check if the input starts with a vowel and digit. But I am curious if case has any builtin function like AND operator.

4
  • Use | instead of &&. Have in mind that the conditions are mutually exclusive: it is OR not AND. Commented Oct 28, 2019 at 17:48
  • @guillermochamorro My end goal is if the user enters input that starts with vowel and ends with a number, it needs to print "The word ends with vowel AND ends with a digit.". But if I use OR (|), it will be escaped and I guess the case statement will choose option [aeiou]* | [AEIOU]* instead. Commented Oct 28, 2019 at 17:55
  • 2
    @smc I suppose [aeiouAEIOU]*[0-9] would cover that case, but you'd need to have that as the first case because otherwise it would be covered by the current first one. Besides, your logic for begins/ends is wrong in a few places. Commented Oct 28, 2019 at 18:04
  • @DonHolgo Ah, thanks for the hint. It worked, I also modified my question where my logic was wrong. Please feel free to make this comment as answer :). Commented Oct 28, 2019 at 18:13

3 Answers 3

13

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 to 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 '%s\n' "$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.
4
  • 1
    I was preparing a similar answer using ;;& but i was late! +1 for the nice answer :-) You can also fake a kind of and using something like [0-9]*);;&*[0-9]) echo "word begins and ends with number";; Commented Oct 28, 2019 at 18:15
  • Thanks @jeff-schaller for the explanation, also I have corrected my question after George pointed out about the incorrect placement of "words begins/ends with" part. Commented Oct 28, 2019 at 18:51
  • 1
    Note that zsh had ;| (also supported by mksh) before (2007) bash added ;;& (2009) for the same thing (;& itself comes from ksh88e, added to zsh in 1997 and bash in 2009). Commented Oct 29, 2019 at 16:25
  • 1
    For booleans (0 or 1) A and B, the usual solution is case $A$B in 11) .... . Simpler. Commented Oct 30, 2019 at 17:00
7

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+¹ or 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.


¹ Beware that ksh88 and older versions of ksh93 had that misfeature by which if a string didn't match a pattern, case would fall back to do a literal match. For instance, the @(x|y) pattern matches on both x and y but also on @(x|y) there!

1
  • For booleans (0 or 1) A and B, the usual solution is case $A$B in 11) .... Simpler. Commented Oct 30, 2019 at 16:59
3

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
0

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.