Is there a way to have sh/bash/zsh "case" use a variable for a test value?

2

I'd like to have something like the following:

#!/bin/sh
# ... other stuff ...
# some relatively static possibilities (srsp):
srsp='this|or|that|the|other'
# more other stuff
case $something in
    $srsp) # <- problem is here
        do_something # or maybe nothing
        ;;
    this|or|that|the|other);; # this would work, but loses the benefit of a variable
    *)
        # anything not in my list is an error:
        echo "Sorry, I don't recognize $something as one of $srsp" >&2
        exit 1;;
esac

do_something | egrep "blah($srsp)thing" # or whatever

The problem is, having $srsp there only matches the whole string (if $something was exactly the string "this|or|that|or|some|other|stuff", it would match and call do_something), rather than any of this or or or that... and so forth: the values I actually want to match.

If I put that literal string in the case statement (my "this would work" line), it matches what I want it to match, but I'm trying to keep this DRY, and I need the same set of strings in the regular expression I use later. (Side note: I understand that what's possible in a case match and what's possible in a regular expression can differ dramatically, but in my particular situation, they're compatible with both. It really is just letters in the individual components, no wildcards, just the |, which exists as a special case in both systems.)

So, is there a way to do this? Especially without wrapping the whole case statement inside an eval or something? I'd like to still keep it in a case statement, because I have other stuff going on. (I'm sure I could implement a workaround by re-structuring and using egrep as my match test (if echo $something | egrep "^($srsp)$" > /dev/null, or some such). This question is about trying to find a way to do it without having to resort to that. Or definitively knowing that it can't be done would also be a valid answer.)

(Or should I switch to common-lisp? ;) )

For my needs, I'd be happy with sh or bash for sure, and possibly zsh, though if there's a way to do this in a maximally-portable way (i.e. sh), that would make for a better answer, IMHO.

lindes

Posted 2015-02-27T03:06:25.693

Reputation: 370

Answers

2

This really isn't about case per se: you need a mechanism to check if a word is present in a list. With bash you can do this:

srsp=(this that or the other)
in_srsp() {
    # this joins the array into a string with spaces (default value of IFS)
    # then tests if the space-delimited word is a match for that string.
    [[ " ${srsp[*]} " == *" $1 "* ]]
}
something=foo
if in_srsp $something; then echo y; else echo n; fi   # =>  n
something=other
if in_srsp $something; then echo y; else echo n; fi   # =>  y

More portably just turn the case around

srsp="this|that|or|the|other"
something=other
case "|$srsp|" in
    *"|$something|"*) echo in;;
    *) echo not;;
esac

glenn jackman

Posted 2015-02-27T03:06:25.693

Reputation: 18 546

Well, the first option here doesn't really work for me, because it loses the compatibility with egrep (and similar), which was a big part of the point. Your latter suggestion does at least give me a match, though, and that works in this case. (It would in principal be nice to not have to invert the case, but for my purposes, it'll do.) Thanks! – lindes – 2015-03-03T21:39:24.503

1

You can use eval, of course, but it is rather unwieldy. Perhaps encapsulate it into a function so you can avoid too many backslashes outside of the actually complex eval.

in_list () {
    local var
    var=$1
    local list
    list=$2
    eval case \$var in "$list"\) return 0\;\; \*) return 1\;\; esac
}

in_list "foo" "foo|bar|baz" && echo Success
in_list "nonesvch" "foo|bar|baz" || echo More so

If you know you can restrict the character set in the list, maybe run a filtering case before the eval for security, something like

in_list () {
    local var
    var=$1
    local list
    list=$2

    case $var in *[!-a-z0-9_]*) return 1;; esac

    # Additionally, allow wildcards and whitespace in list
    case $list in *[!?*| a-z0-9_-]*)
        echo "$0: Invalid characters in $list" >&2; return 2;;
    esac

    eval case \$var in "$list"\) return 0\;\; \*) return 1\;\; esac
}

tripleee

Posted 2015-02-27T03:06:25.693

Reputation: 2 480

The difficulty I have with this is that while $list (as used here, $srsp in the original question) is safe to eval, because it's coded in the script, $var is actually getting data from the command line, and thus could contain things that would break syntax here. It's possible this could be fixed with appropriate quoting? But as you have it, this strikes me as insecure and thus problematic. Still, I do like the idea of making a function out of this, so thank you for that... I might play with this more. – lindes – 2015-03-03T21:42:50.830

1I updated the answer with a couple of prefiltering cases. Hope this helps. – tripleee – 2015-03-04T05:00:49.797

With that addition, this strikes me as a workable option (though I'm still going with http://superuser.com/a/883191/57367 for my particular situation). Thanks!

– lindes – 2015-03-05T19:16:24.987