5

Is it possible to check if a variable is contained inside an array using case? I would like to do something like

ARR=( opt1 opt2 opt3 );

case $1 in
    $ARR)
        echo "Option is contained in the array";
    *)
        echo "Option is not contained in the array";
esac
Jeff Schaller
  • 67,283
  • 35
  • 116
  • 255

4 Answers4

7

With ksh93, thanks to this bug, you can do:

IFS='|'
ARR=( opt1 opt2 opt3 )

IFS='|' case $1 in (@("${ARR[*]}")) echo "Option is contained in the array";; (*) echo "Option is not contained in the array";; esac

(I wouldn't rely on it as the bug might get fixed in the future).

With zsh, you could do:

case ${ARR[(Ie)$1]}
  (0)
    echo "Option is not contained in the array";;
  (*)
    echo "Option is contained in the array";;
esac

(though, you'd probably rather want to use if (( $ARR[(Ie)$1] )); then echo is present... here rather than a case construct).

${array[(I)pattern]} returns the index of the last element that matches the pattern in the array, or 0 otherwise. The e flag is for exact match (as opposed to pattern match).

With bash, ksh, yash, zsh, if you're ready to assume that $ARR and $1 don't contain a certain character like @, and that $ARR won't be empty, you can do:

IFS=@
case "@${ARR[*]}@" in
  (*"@$1@"*)
    echo "Option is contained in the array";;
  (*)
    echo "Option is not contained in the array";;
esac

With bash -O extglob, zsh -o kshglob -o globsubst, you could define a helper that builds a pattern based on the elements of the array:

arraypat() {
  awk '
    BEGIN{
      if (ARGC <= 1) print "!(*)"
      else {
        for (i = 1; i < ARGC; i++) {
          gsub(/[][|<>\\?*()]/, "[&]", ARGV[i])
          s = s sep ARGV[i]
          sep = "|"
        }
        print "@(" s ")"
      }
    }' "$@"
}

case $1 in ($(arraypat "${ARR[@]}")) echo "Option is contained in the array";; (*) echo "Option is not contained in the array";; esac

  • I love the bash option and I'd really like you to explain the case with the IFS option. Would it also work without modifying the field separator? – ww12 Dec 08 '22 at 13:46
3

Not really in a compact and easy to use way. Remember that $ARR will expand to only the first element of the array, opt1 in your example.

You could use "${ARR[@]}", but using your data this would give a false positive for the string 1 opt.

With more recent versions of bash, you could consider using an associative array:

declare -A arr
arr=( [opt1]=1 [opt2]=1 [opt3]=1 )

if [[ "${arr[$1]}" -eq 1 ]]; then
   # $1 is a key in arr
else
   # is not
fi
Kusalananda
  • 333,661
2

Why would you want to do it with case? It's meant for string pattern matching, not per-element matching.

Frankly, if you need the "contains" test often and want to make it short because of that, just put the hard part in a function instead of using ugly workarounds:

#!/bin/bash
ARR=( foo bar doo );

contains() {
        typeset _x;
        typeset -n _A="$1"
        for _x in "${_A[@]}" ; do
                [ "$_x" = "$2" ] && return 0
        done
        return 1
}

if contains ARR "$1" ; then
        echo "\"$1\" is contained in ARR"
else
        echo "\"$1\" not contained in ARR"
fi

(That should also work in ksh)

ilkkachu
  • 138,973
  • OP probably wants a builtin for the presumed speed and efficiency. – RonJohn Dec 15 '17 at 13:26
  • 1
    @RonJohn, everything that function does, is built in to Bash. As for speed, it's hard to say, we'd need to test, but for huge data sets, associative arrays would probably be faster (than either the loop, or a case), since key lookups are presumably O(1). But for huge data, you shouldn't use the shell anyway, and for small data, minor performance differences don't matter. – ilkkachu Dec 15 '17 at 14:04
0

For bash users, you can use the non-short-circuiting ;;& operator; it's not case directly supporting arrays in its clauses, but it's reasonably elegant:

ARR=( opt1 opt2 opt3 );

case $1 in ) [[ ${ARR[]} =~ $1 ]]
&& echo "Option is contained in the array"
&& exit 0 ;;&

*)
    echo &quot;Option is not contained in the array&quot;
    ;;

esac

Note ;;& (i.e. non-short circuiting) ensures the $1 is evaluated against subsequent case-clauses; this requires the exit 0 (use return 0 in a function/sourced file) to skip subsequent