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 '%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.
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