Is there an "in" operator in bash/bourne?

12,544

Solution 1

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

Solution 2

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

Solution 3

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.)

Solution 4

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.

Solution 5

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 ($)
Share:
12,544

Related videos on Youtube

mrjayviper
Author by

mrjayviper

Updated on September 18, 2022

Comments

  • mrjayviper
    mrjayviper over 1 year

    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.

  • mrjayviper
    mrjayviper almost 6 years
    any reason for the double brackets? Thanks again!
  • smw
    smw almost 6 years
    @mrjayviper the double brackets are an extended test construct - AFAIK the regex operator =~ isn't valid inside the POSIX single-bracket test
  • Sergiy Kolodyazhnyy
    Sergiy Kolodyazhnyy almost 6 years
    @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
    Sergiy Kolodyazhnyy almost 6 years
    Wouldn't that be better to split case into function call and evaluate exit status inside the if statement?
  • Kusalananda
    Kusalananda almost 6 years
    @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.
  • done
    done almost 6 years
    @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.
  • done
    done almost 6 years
    @steeldriver The $1 in case "$1" in does not need to be quoted, no word splitting nor pathname expansion are performed in that token.
  • smw
    smw almost 6 years
    @Cyrus thanks for the regex catch - StéphaneChazelas thanks for the improvements, the extended glob in particular