in_array() in Bash: Check if an Array Contains a Value
PHP's in_array($needle, $haystack) tells you whether an array contains a value. Bash has no such test built in. People often share a one-liner that matches against "${arr[*]}", but it gives wrong answers as soon as an element contains a space. A ten-line loop does the job correctly. For large lists, an associative array does it much faster.
I use this mostly for validating input: is this action one of start stop restart, is this hostname in the list of servers I am allowed to change.
The function
# in_array() -- check whether a value is in a list
# Usage: in_array needle item...
# in_array "$value" "${array[@]}"
in_array() {
[ $# -ge 1 ] || return 2
local needle="$1" item
shift
for item in "$@"; do
[[ $item == "$needle" ]] && return 0
done
return 1
}It returns 0 if the value is found, 1 if not, and 2 if it was called with no arguments at all. This way, a caller can tell "not found" from "used wrongly". It prints nothing. You use it directly in an if.
How it works
Bash cannot pass an array to a function as a single argument, so the array is expanded into separate arguments with "${array[@]}". The quotes are essential: they keep each element as exactly one word, including any spaces. Inside the function, the first argument is saved as the needle, shift drops it, and for item in "$@" goes through the rest.
The comparison is [[ $item == "$needle" ]]. On the right-hand side of ==, an unquoted word is a glob pattern. Quoting "$needle" makes it a literal string. Without those quotes, in_array "a*" ... would match apple. The left side does not need quotes inside [[ ]], because no word splitting happens there.
Usage
fruits=("apple" "banana split" "cherry")
if in_array "cherry" "${fruits[@]}"; then
echo "we have cherries"
fi
actions=(start stop restart status)
in_array "reload" "${actions[@]}" || echo "Unknown action: reload"we have cherries
Unknown action: reloadAnd the cases that matter, run with set -u on Bash 4.4:
$ in_array "banana split" "${fruits[@]}" && echo yes || echo no
yes
$ in_array "banana" "${fruits[@]}" && echo yes || echo no
no
$ in_array "a*" "${fruits[@]}" && echo yes || echo no
no
$ in_array "" "${fruits[@]}" && echo yes || echo no
no
$ in_array "" "a" "" && echo yes || echo no
yes
$ in_array; echo "rc=$?"
rc=2A partial word is not a match, a glob is not a match, and an empty string is only found when the array really contains an empty element. That is the PHP behaviour with strict comparison.
Why the "${arr[*]}" trick gives false positives
This is the version you will find in many scripts:
[[ " ${arr[*]} " =~ " $value " ]] && echo foundIt joins the array into one string separated by spaces, pads it with a space at each end, and looks for the value surrounded by spaces. It works for arrays of single words. It fails as soon as an element contains the separator:
arr=("apple" "banana split" "cherry")
[[ " ${arr[*]} " =~ " banana " ]] && echo "found banana (wrong)"
[[ " ${arr[*]} " =~ " split cherry " ]] && echo "found 'split cherry' (wrong)"
[[ " ${arr[*]} " == *" banana "* ]] && echo "glob version also wrong"found banana (wrong)
found 'split cherry' (wrong)
glob version also wrongThe joined string is " apple banana split cherry ", and the information about where one element ends and the next begins is gone. banana is found although no element is banana, and split cherry is found by matching across two elements. Changing IFS to a different separator just moves the problem to values that contain that character.
There is a second problem if the regex is kept in a variable, which is the usual way to avoid quoting problems with =~. Then it really is a regular expression, and a dot matches any character:
$ arr=("xzy"); re=" x.y "
$ [[ " ${arr[*]} " =~ $re ]] && echo "x.y matched xzy"
x.y matched xzyFor hostnames, IP addresses and version strings, that is a real bug, not a theoretical one. Use the loop.
Many lookups: an associative array as a set
The loop is linear: every call goes through the whole list. If you check thousands of values against a long list, build an associative array once and look keys up directly. This needs Bash 4.0 or later, so it does not work with the default Bash 3.2 on macOS:
declare -A allowed=()
for u in alice bob "carol smith"; do
allowed[$u]=1
done
for u in bob carol "carol smith"; do
if [[ ${allowed[$u]+set} ]]; then
echo "[$u] allowed"
else
echo "[$u] denied"
fi
done[bob] allowed
[carol] denied
[carol smith] allowed${allowed[$u]+set} expands to set if the key exists and to nothing if it does not. So the test works whatever value you stored, and it is safe under set -u. You will also see [[ -v allowed[$u] ]] (Bash 4.2+). It is easy to read, but in my tests on 4.4 it gave the wrong answer for a key containing ], while the +set form handled it. Bash 4.4 also rejects an empty key completely, with bad array subscript. So check for an empty value before using it as a key.
To see the difference, I timed 1,000 lookups in a 2,000-element list on this machine. The in_array loop took 6.3 seconds. The associative array took 0.004 seconds. With ten elements you will not notice, so use whichever reads better.
Getting the index: array_search()
PHP's array_search() returns the key instead of true. With a nameref (Bash 4.3+) the function can walk the real indexes, which matters for sparse arrays:
# array_search() -- print the index of the first match
# Usage: array_search needle arrayname (Bash 4.3+)
array_search() {
[ $# -eq 2 ] || return 2
local -n _as_arr="$2"
local i
for i in "${!_as_arr[@]}"; do
if [[ ${_as_arr[$i]} == "$1" ]]; then
printf '%s\n' "$i"
return 0
fi
done
return 1
}$ array_search "cherry" fruits
2
$ array_search "kiwi" fruits; echo "rc=$?"
rc=1
$ sparse=([3]=a [10]=b); array_search b sparse
10Note that this one takes the array name, not its expanded values.
Pitfalls
- Forgetting the quotes in
"${array[@]}"splits elements on spaces and expands globs. This gives you exactly the false positives the function is meant to avoid. - Empty arrays and set -u. On Bash 4.4 and later,
"${empty[@]}"underset -uis fine. Older versions (including 3.2 and 4.0-4.3) stop with an "unbound variable" error.${empty[@]+"${empty[@]}"}works on all of them. - Case. The comparison is case-sensitive. Lower-case both sides first with strtolower() if you need otherwise.
See Arrays in the Bash manual and in_array() on php.net.
Related functions
- explode() and implode(): turn a string into an array and back
- strstr(): substring search in a plain string
- yesno(): another input-checking helper
- Parsing options with getopts: where validating values is common