16

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?

  • 2
    I can't bring myself to suggest it as a real answer, but since no one else has mentioned it, you could wrap the case statement in an eval, escaping $choice, the parens, asterisk, semicolons, and newlines. Ugly, but it "works". – Jeff Schaller Oct 7 '15 at 18:50
  • @JeffSchaller - it's not a bad idea a lot of times, and is maybe just the ticket in this case. i considered recommending it, too, but the read bit stopped me. in my opinion user input validation, which is what this appears to be, case patterns should not be at the top of the evaluation list, and should rather be pruned down to the * default pattern such that the only results that reach there are guaranteed acceptable. still, because the issue is parse/expansion order, then a second evaluation could be what's called for. – mikeserv Oct 8 '15 at 0:57
10

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 "%s\n" "$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_
9

You can use the extglob option:

shopt -s extglob
string='@(foo|bar)'
  • Interesting; what makes @(foo|bar) special compared to foo|bar? Both are valid patterns that work the same when typed literally. – chepner Oct 6 '15 at 13:53
  • 4
    Ah, never mind. | isn't part of the pattern in foo|bar, it's part of the syntax of the case statement to allow multiple patterns in one clause. | is part of the extended pattern, though. – chepner Oct 6 '15 at 13:57
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
-1

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: awk is used to split "string" and test each part separately. If "choice" matches the currently tested part p[i], the awk-command will be ended with "exit" in line 11. For the very test, the shell's "case" is used (within a 'system'-call), as asked by terdon. This keeps the possibility to modify the intended test-"string" for example to "foo*|bar" in order to match also for "foooo" ("search pattern"), as the shell's "case" allows. If instead You would prefer regular expressions, You could omit the "system"-call and use awk's "~" or "match" instead.

  • Dear downvoter, I could better learn how to avoid useless solution candidates if You would tell me the reason for Your downvote. – Gerald Schade Jun 17 at 5:31

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.