11

I am trying to match a pattern with a case-statement where the pattern is stored inside a variable. Here is a minimal example:

PATTERN="foo|bar|baz|bla"

case "foo" in
    ${PATTERN})
        printf "matched\n"
        ;;
    *)
        printf "no match\n"
        ;;
esac

Unfortunately the "|" seems to be escaped (interestingly "*" or "?" are not). How do I get this to work, i.e. to match "foo"? I need to store the pattern in a variable because it is constructed dynamically. This needs to work on a POSIX compatible shell.

Jonathan Leffler
  • 730,956
  • 141
  • 904
  • 1,278
  • 1
    Thanks for all the suggestions but I cannot rely on bash or GNU tools this needs to work on a *POSIX shell environment*. I wanted to use a case construct to avoid a `if printf "foo\n" | grep -E -q "${PATTERN}" 2>/dev/null; then ... else ... fi` for performance reasons (subshell + fork) as I a shell pattern would do and I iterate over this code several hundered times. I was just surprised that this did not work. –  Oct 13 '09 at 19:44

7 Answers7

6

It is possible to match a sub-string in a string without spawning a sub-process (such as grep) using the only POSIX compliant methods of sh(1p), as defined in Section 2.6.2, Parameter Expansion.

Here is a convenience function:

# Note:
# Unlike a regular expression, the separator *must* enclose the pattern;
# and it could be a multi chars.

isin() {
    PATTERN=${2:?a pattern is required}
    SEP=${3:-|}
    [ -z "${PATTERN##*${SEP}${1}${SEP}*}" ]
}

Examples:

for needle in foo bar; do
    isin "$needle" "|hello|world|foo|" && echo "found: $needle"
done

# using ";" as separator
for needle in foo bar; do
    isin "$needle" ";hello;world;foo;" \; && echo "found: $needle"
done

# using the string "RS" as separator
for needle in foo bar; do
    isin "$needle" "RShelloRSworldRSfooRS" RS && echo "found: $needle"
done

You can mix this solution with the case statement if you want both of the worlds:

PATTERN="|foo bar|baz|bla|"

case "$needle" in
    xyz) echo "matched in a static part" ;;
    *)
        if [ -z "${PATTERN##*|${needle}|*}" ]; then
            echo "$needle matched $PATTERN"
        else
            echo "not found"
        fi
esac

Note

Sometimes it is good to remember you could do your whole script in awk(1p), which is also POSIX, but I believe this is for another answer.

pevik
  • 4,523
  • 3
  • 33
  • 44
bufh
  • 3,153
  • 31
  • 36
3

This should work:

PATTERN="foo|bar|baz|bla"

shopt -s extglob

case "foo" in
    @($(echo $PATTERN)))
        printf "matched\n"
        ;;
    *)
        printf "no match\n"
        ;;
esac
dimba
  • 26,717
  • 34
  • 141
  • 196
  • 2
    You don't need the `$(echo)` just do `@($PATTERN)` – Dennis Williamson Oct 13 '09 at 19:20
  • 4
    Is that a posixism or is that a bashism? I looked in the ash man page and didn't see this structure, but I also didn't read every letter. – chris Nov 06 '09 at 17:41
  • 3
    `shopt` is clearly not [POSIX](http://man7.org/linux/man-pages/man1/sh.1p.html); see also [GreyCat's page on the subject](http://mywiki.wooledge.org/Bashism). – bufh May 11 '17 at 09:11
1

Your pattern is in fact a list of patterns, and the separator | must be given literally. Your only option seems to be eval. However, try to avoid that if you can.

Philipp
  • 48,066
  • 12
  • 84
  • 109
  • I agree -- you can use eval but it is the path to madness. – chris Nov 06 '09 at 16:23
  • The whole `case` should be in the `eval` statement (that is why it's madness); there is an example given here: http://stackoverflow.com/a/38153401/248390 – bufh May 11 '17 at 09:15
1

"You can't get there from here"

I love using case for pattern matching but in this situation you're running past the edge of what bourne shell is good for.

there are two hacks to solve this problem:

at the expense of a fork, you could use egrep

pattern="this|that|those"

if
  echo "foo" | egrep "$pattern"  > /dev/null 2>&1
then
  echo "found"
else
  echo "not found"
fi

You can also do this with only built-ins using a loop. Depending on the situation, this may make your code run a billion times slower, so be sure you understand what's going on with the code.

pattern="this|that|those"

IFS="|" temp_pattern="$pattern"
echo=echo

for value in $temp_pattern
do
  case foo 
  in
    "$list") echo "matched" ; echo=: ; break ;;
  esac
done
$echo not matched

This is clearly a horror show, an example of how shell scripts can quickly spin out of control if you try to make do anything even a little bit off the map..

chris
  • 141
  • 4
0

Some versions of expr (e.g. GNU) allow pattern matching with alternation.

PATTERN="foo\|bar\|baz"
VALUE="bar"
expr "$VALUE" : "$PATTERN" && echo match || echo no match

Otherwise, I'd use a tool like awk:

awk -v value="foo" -v pattern="$PATTERN" '
    BEGIN {
        if (value ~ pattern) {
            exit 0
        } else {
            exit 1
        }
    }'

or more tersely:

awk -v v="foo" -v p="$PATTERN" 'BEGIN {exit !(v~p)}'

You can use it like this:

PATTERN="foo|bar|baz"
VALUE=oops
matches() { awk -v v="$1" -v p="$2" 'BEGIN {exit !(v~p)}'; }
if matches "$VALUE" "$PATTERN"; then
    echo match
else
    echo no match
fi
glenn jackman
  • 238,783
  • 38
  • 220
  • 352
0

You can use regex matching in Bash:

PATTERN="foo|bar|baz|bla"

if [[ "foo" =~ $PATTERN ]]
then
    printf "matched\n"
elif . . .
    . . .
elif . . .
    . . .
else
    printf "no match\n"
fi
Dennis Williamson
  • 346,391
  • 90
  • 374
  • 439
-1

This obviates the need for escaping or anything else:

PATTERN="foo bar baz bla"

case "foo" in
    ${PATTERN// /|})
        printf "matched\n"
        ;;
    *)
        printf "no match\n"
        ;;
esac
ata
  • 2,045
  • 1
  • 14
  • 19