I am trying to use a variable consisting of different strings separated with a | as a case statement test. For example:
string=""foo"|"bar""
read choice
case $choice in
$string)
echo "You chose $choice";;
*)
echo "Bad choice!";;
esac
I want to be able to type foo or bar and execute the first part of the case statement. However, both foo and bar take me to the second:
$ foo.sh foo Bad choice! $ foo.sh bar Bad choice!
Using "$string" instead of $string makes no difference. Neither does using string="foo|bar".
I know I can do it this way:
case $choice in
"foo"|"bar")
echo "You chose $choice";;
*)
echo "Bad choice!";;
esac
I can think of various workarounds but I would like to know if it’s possible to use a variable as a case condition in bash. Is it possible and, if so, how?
Answers:
Thank you for visiting the Q&A section on Magenaut. Please note that all the answers may not help you solve the issue immediately. So please treat them as advisements. If you found the post helpful (or not), leave a comment & I’ll get back to you as soon as possible.
Method 1
The bash manual states:
case word in [ [(] pattern [ | pattern ] … ) list ;; ] … esac
Each pattern examined is expanded using tilde expansion, parameter and variable expansion, arithmetic substitution, command substitution, and process substitution.
No «Pathname expansion»
Thus: a pattern is NOT expanded with «Pathname expansion».
Therefore: a pattern could NOT contain “|” inside. Only: two patterns could be joined with the “|”.
This works:
s1="foo"; s2="bar" # or even s1="*foo*"; s2="*bar*"
read choice
case $choice in
$s1|$s2 ) echo "Two val choice $choice"; ;; # not "$s1"|"$s2"
* ) echo "A Bad choice! $choice"; ;;
esac
Using « Extended Globbing »
However, word is matched with pattern using « Pathname Expansion » rules.
And « Extended Globbing » here, here and, here allows the use of alternating (“|”) patterns.
This also work:
shopt -s extglob
string='@(foo|bar)'
read choice
case $choice in
$string ) printf 'String choice %-20s' "$choice"; ;;&
$s1|$s2 ) printf 'Two val choice %-20s' "$choice"; ;;
*) printf 'A Bad choice! %-20s' "$choice"; ;;
esac
echo
String content
The next test script shows that the pattern that match all lines that contain either foo or bar anywhere is '*$(foo|bar)*' or the two variables $s1=*foo* and $s2=*bar*
Testing script:
shopt -s extglob # comment out this line to test unset extglob.
shopt -p extglob
s1="*foo*"; s2="*bar*"
string="*foo*"
string="*foo*|*bar*"
string='@(*foo*|*bar)'
string='*@(foo|bar)*'
printf "%sn" "$string"
while IFS= read -r choice; do
case $choice in
"$s1"|"$s2" ) printf 'A first choice %-20s' "$choice"; ;;&
$string ) printf 'String choice %-20s' "$choice"; ;;&
$s1|$s2 ) printf 'Two val choice %-20s' "$choice"; ;;
*) printf 'A Bad choice! %-20s' "$choice"; ;;
esac
echo
done <<-_several_strings_
f
b
foo
bar
*foo*
*foo*|*bar*
"foo"
"foo"
afooline
onebarvalue
now foo with spaces
_several_strings_
Method 2
You can use the extglob option:
shopt -s extglob string='@(foo|bar)'
Method 3
You need two variables for case because the or | pipe is parsed before the patterns are expanded.
v1=foo v2=bar
case foo in ("$v1"|"$v2") echo foo; esac
foo
Shell patterns in variables are handled differently when quoted or unquoted as well:
q=?
case a in
("$q") echo question mark;;
($q) echo not a question mark
esac
not a question mark
Method 4
Here is a POSIX solution using eval :
#!/bin/sh
string="foo|bar"
read choice
eval "
case $choice in
$string)
echo "You chose $choice";;
*)
echo "Bad choice!";;
esac
"
Explanation : the eval command makes the shell first expand the arguments so we are left with
case $choice in
foo|bar)
echo "You chose $choice";;
*)
echo "Bad choice!";;
esac
and then it interprets again the command and executes it.
Method 5
One can use AWK’s syntax
if($field ~ /regex/)
as well as
if($i ~ var)
to compare variable to input (var and star-argument list $*)
parse_arg_exists() {
[ $# -eq 1 ] && return
[ $# -lt 2 ] && printf "%sn" "Usage: ${FUNCNAME[*]} <match_case> list-or-$*"
"Prints the argument index that's matched in the regex-case (~ patn|patn2)" && exit 1
export arg_case=$1
shift
echo "[email protected]" | awk 'BEGIN{FS=" "; ORS=" "; split(ENVIRON["arg_case"], a, "|")} {
n=-1
for(i in a) {
for(f=1; f<=NF; f++) {
if($f ~ a[i]) n=f
}
}
}
END {
if(n >= 0) print "arg index " n "n"
}'
unset arg_case
}
string="--dot|-d"
printf "testing %sn" "$string"
args="--dot -b -c"; printf "%sn" "$args"
parse_arg_exists "$string" "$args"
args="-b -o"; printf "%sn" "$args"
parse_arg_exists "$string" "$args"
args="-b -d -a"; printf "%sn" "$args"
parse_arg_exists "$string" "$args"
Prints out:
testing --dot|-d --dot -b -c arg index 1 -b -o -b -d -a arg index 2
Method 6
If You want a dash-compatible work-around, You could write:
string="foo|bar"
read choice
awk 'BEGIN{
n=split("'$string'",p,"|");
for(i=1;i<=n;i++)
if(system("
case "'$choice'" in "p[i]")
echo "You chose '$choice'";
exit 1;;
esac"))
exit;
print "Bad choice"
}'
Explanation:
awkis used to splitstringand test each part separately. Ifchoicematches the currently tested partp[i], theawk-command will be ended withexitin line 11.- For the very test, the shell’s
caseis used (within asystem-call), as asked by @terdon. This keeps the possibility to modify the intended test-stringfor example tofoo*|barin order to match also forfoooo(“search pattern”), as the shell’scaseallows. - If instead you would prefer regular expressions, you could omit the
system-call and useawk‘s~ormatchinstead.
All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0