20

I’m looking for an “in” operator that works something like this:

if [ "$1" in ("cat","dog","mouse") ]; then
    echo "dollar 1 is either a cat or a dog or a mouse"
fi

It's obviously a much shorter statement compared to, say, using several "or" tests.

Jeff Schaller
  • 67,283
  • 35
  • 116
  • 255
mrjayviper
  • 2,051

5 Answers5

38

You can use case ... esac

$ cat in.sh 
#!/bin/bash

case "$1" in 
  "cat"|"dog"|"mouse")
    echo "dollar 1 is either a cat or a dog or a mouse"
  ;;
  *)
    echo "none of the above"
  ;;
esac

Ex.

$ ./in.sh dog
dollar 1 is either a cat or a dog or a mouse
$ ./in.sh hamster
none of the above

With ksh, bash -O extglob or zsh -o kshglob, you could also use an extended glob pattern:

if [[ "$1" = @(cat|dog|mouse) ]]; then
  echo "dollar 1 is either a cat or a dog or a mouse"
else
  echo "none of the above"
fi

With bash, ksh93 or zsh, you could also use a regular expression comparison:

if [[ "$1" =~ ^(cat|dog|mouse)$ ]]; then
  echo "dollar 1 is either a cat or a dog or a mouse"
else
  echo "none of the above"
fi
steeldriver
  • 81,074
  • any reason for the double brackets? Thanks again! – mrjayviper Jun 02 '18 at 02:19
  • 2
    @mrjayviper the double brackets are an extended test construct - AFAIK the regex operator =~ isn't valid inside the POSIX single-bracket test – steeldriver Jun 02 '18 at 02:29
  • 1
    @steeldriver Only [ is POSIX, but [[ is extended feature of bash, ksh ( apparently it's originated from there, and zsh. case example is most POSIX of all, though – Sergiy Kolodyazhnyy Jun 02 '18 at 02:55
  • 2
    @StéphaneChazelas Since at least bash 4.1-alpha there is no need to set extglog explicitly. From bash changes: s. Force extglob on temporarily when parsing the pattern argument to the == and != operators to the [[ command, for compatibility. –  Jun 02 '18 at 10:22
  • @steeldriver The $1 in case "$1" in does not need to be quoted, no word splitting nor pathname expansion are performed in that token. –  Jun 02 '18 at 10:58
  • @Cyrus thanks for the regex catch - StéphaneChazelas thanks for the improvements, the extended glob in particular – steeldriver Jun 02 '18 at 13:02
11

There is not an "in" test in bash, but there is a regex test (not in bourne):

if [[ $1 =~ ^(cat|dog|mouse)$ ]]; then
    echo "dollar 1 is either a cat or a dog or a mouse"
fi

And usually written using a variable (less problems with quoting):

regex='^(cat|dog|mouse)$'

if [[ $1 =~ $regex ]]; then
    echo "dollar 1 is either a cat or a dog or a mouse"
fi

For an older Bourne shell you need to use a case match:

case $1 in
    cat|dog|mouse)   echo "dollar 1 is either a cat or a dog or a mouse";;
esac
8

Using a case is fine and well if you have a fixed set of pets you want to match against. But it won't work if you need to build the pattern in runtime, as case doesn't interpret alternation from within expanded parameters.

This will match only the literal string cat|dog|mouse:

patt='cat|dog|mouse'
case $1 in 
        $patt) echo "$1 matches the case" ;; 
esac

You can, however, use a variable with the regular expression match. As long as the variable isn't quoted, any regex operators within it have their special meanings.

patt='cat|dog|mouse'
if [[ "$1" =~ ^($patt)$ ]]; then
        echo "$1 matches the pattern"
fi

You could also use associative arrays. Checking if a key exists in one is the closest thing to an in operator that Bash gives. Though the syntax is a bit ugly:

declare -A arr
arr[cat]=1
arr[dog]=1
arr[mouse]=1

if [ "${arr[$1]+x}" ]; then
        echo "$1 is in the array"
fi

(${arr[$1]+x} expands to x if arr[$1] is set, empty otherwise.)

ilkkachu
  • 138,973
6

You could use a case statement in an if test, but the code would look a bit hairy:

if case "$1" in (cat|dog|mouse) true ;; (*) false; esac; then
    printf '"%s" is one of cat, dog or mouse\n' "$1"
else
    printf '"%s" is unknown\n' "$1"
fi

or slightly shorter,

if ! case "$1" in (cat|dog|mouse) false; esac; then
    printf '"%s" is one of cat, dog or mouse\n' "$1"
else
    printf '"%s" is unknown\n' "$1"
fi

This is using an case clause just to do the pattern matching for the if clause. It introduces an unnecessary true/false test.

It's better to just use case:

case "$1" in
    cat|dog|mouse)
        printf '"%s" is one of cat, dog or mouse\n' "$1"
        ;;
    *)
        printf '"%s" is unknown\n' "$1"
esac

Don't do this:

is_one_of () {
    eval "case $1 in ($2) return 0; esac"
    return 1
}

if is_one_of "$1" 'cat|dog|mouse'; then
    printf '"%s" is one of cat, dog or mouse\n' "$1"
else
    printf '"%s" is unknown\n' "$1"
fi

or this:

is_one_of () (
    word=$1
    shift
    IFS='|'
    eval "case $word in ($*) return 0; esac"
    return 1
)

if is_one_of "$1" cat dog mouse; then
    printf '"%s" is one of cat, dog or mouse\n' "$1"
else
    printf '"%s" is unknown\n' "$1"
fi

... because you're just adding more dangerous cruft, just to be able to use an if statement in your code in place of a perfectly reasonable case statement.

Kusalananda
  • 333,661
  • Wouldn't that be better to split case into function call and evaluate exit status inside the if statement? – Sergiy Kolodyazhnyy Jun 02 '18 at 07:04
  • @SergiyKolodyazhnyy And let the pattern be an argument to the function? It would have to do an eval of the case statement in that case, and it would be even more prone to errors. – Kusalananda Jun 02 '18 at 07:16
  • In this case there's simple pattern, each could be passed as positional parameter to function and do "$1"|"$2"|"$3". Also https://unix.stackexchange.com/a/234415/85039 But yeah, it's a bit hairy. – Sergiy Kolodyazhnyy Jun 02 '18 at 07:23
4

grep approach.

if echo $1 | grep -qE "^(cat|dog|mouse)$"; then 
    echo "dollar 1 is either a cat or a dog or a mouse"
fi
  • -q to avoid any output to screen (quicker to type than >/dev/null).
  • -E for extended regular expressions (cat|dog|mouse) aspects needs this.
  • ^(cat|dog|mouse)$ matches any lines starting (^) with cat, dog or mouse ((cat|dog|mouse)) followed by end of line ($)
steve
  • 21,892