1

How to write a function, in_array, that checks if a specified array contains a specified value

The function should take two arguments:

  1. array_name – the name of the array
  2. value – the value to test for

With this test harness:

colors=(red green yellow "royal blue")

test() {
  local answer=no
  if in_array colors "$1"; then
    answer=yes
  fi
  printf "%-13s  %s\n" "$1" "$answer"
}

test red
test green
test "royal blue"
test blue

the following output should be produced:

red            yes
green          yes
royal blue     yes
blue           no

Edit: The essence of this question is that the function must have no hard-coded dependency on the array. I will consider all functions that satisfy this requirement and produce the shown output for the shown array.

Robin A. Meade
  • 1,003
  • 10
  • 12

4 Answers4

2
# bash 4.3+
in_array() {
  local -n a=$1
  # IFS must be set to a character guaranteed not to exist in the 
  # array values; otherwise a false positive could occur.
  # A very safe default has been chosen: non-printable character 0x1F.
  # The IFS character may be specified as an optional 3rd argument.
  local IFS=${3:-$'\x1F'}
  [[ "$IFS${a[*]}$IFS" = *"$IFS$2$IFS"* ]] || return 1
}

The following techniques were used:

  1. name reference (Bash 4.3)
  2. array search technique described here: https://stackoverflow.com/a/58527681
  3. IFS is declared as local so there's no need to restore its value

UPDATE: Here's a variation without the dependency on Bash 4.3's name reference feature. Instead it it uses an obscure (undocumented?) syntax that apparently works as far back as Bash 3. For more information on this syntax see "trick #2" at https://mywiki.wooledge.org/BashFAQ/006

# bash 3+
in_array() {                                                     
  local name="$1[*]"                                                                                                          
  local IFS=${3:-$'\x1F'}                                              
  [[ "$IFS${!name}$IFS" = *"$IFS$2$IFS"* ]] || return 1                
}                                                                      
Robin A. Meade
  • 1,003
  • 10
  • 12
2

Here is one way.

#!/usr/bin/env bash

inarray() {
    local n=$1 h
    shift
    for h; do
      [[ $n = "$h" ]] && return
    done
    return 1
}


colors=(red green yellow "royal blue")

test() {
  local answer=no
  if inarray  "$1" "${colors[@]}"; then
    answer=yes
  fi
  printf "%-13s  %s\n" "$1" "$answer"
}
test red
test green
test "royal blue"
test blue
Jetchisel
  • 3,986
  • 2
  • 10
  • 13
  • 1
    That's a very good answer. Compared to my solution, it doesn't require **name reference** and it's more general because it doesn't involve a forbidden character that the values cannot contain. You changed the function signature and test harness slightly, but not in a detrimental way. I'm therefore accepting your answer. – Robin A. Meade Apr 23 '20 at 06:13
0

Something like this

colors=(red green yellow "royal blue")

in_array () {
    name="$1[@]"
    value="$2"
    for item in "${!name}"; { [[ $value =~ $item ]] && { printf -v answer "yes"; break; } || printf -v answer "no"; }
    echo $answer
}

Testing

$ in_array colors test
no

$ in_array colors green
yes

$ in_array colors royal
no

$ in_array colors 'royal blue'
yes

Update with a better version of this

in_array () {
    name="$1[@]"
    value="$2"
    printf -v re '|%s' "${!name}"
    [[ $value =~ ${re:1} ]] && printf "yes" || printf "no"
}

Or with return

[[ $value =~ ${re:1} ]] && return 0 || return 1
Ivan
  • 3,695
  • 1
  • 10
  • 13
  • Change `[[ $value =~ $item ]]` to `[[ $value = "$item" ]]`. Also, make it more general by returning a 1 or 0 return code. Let the calling function decide what to do with the result. – Robin A. Meade Apr 23 '20 at 08:00
  • Well the idia was to use bash re, but it give false answers on 'royal' with this `[[ ${!name} =~ $value ]]` so i inverted it, but now it's kinda ugly with this for loop, updated with a better version. – Ivan Apr 23 '20 at 08:58
  • Your first approach is nice because it is essentially doing what a **name reference** would do but without requiring bash 4.3. Your second approach of dynamically building an re is clever too. An improvement would be to backslash escape any characters that have special meaning in regex. It's best to post different approaches as separate answers, so they can be voted on and commented on independently. – Robin A. Meade Apr 23 '20 at 20:20
0

This is similar to the accepted answer. It's one less line of code and I've renamed it to better reflect what it does.

#  in_values VAL VAL1 VAL2 ... VALN
#
#  Tests if VAL equals one of the following values
#
in_values() {
  local v=$1
  while shift; do 
    [[ $v == "$1" ]] && return 0
  done
  return 1
}

This approach, while it requires more typing to expand the array, is very versatile because it can work with arrays or listed values or both.

An example of using it:

rgb=(RED GREEN BLUE)
cmyk=(CYAN MAGENTA YELLOW BLACK)
while true; do
  echo -n "Enter the color of the ink you need or 'NONE': " 
  read -i NONE color
  if in_values "$REPLY" "${rgb[@]}" "${cmyk[@]}" NONE; then
    break
  else
    echo "validation error, please try again"
  fi
done
if in_values "$color" "${rgb[@]}"; then
  echo "Order ink cartridge RGB59XL"
elif in_values "$color" "${cmyk[@]}"; then
  echo "Order ink cartridge CMYK64"
fi
Robin A. Meade
  • 1,003
  • 10
  • 12