Get the index of a value in a Bash array

I have something in bash like

myArray=('red' 'orange' 'green')

And I would like to do something like

echo ${myArray['green']}

Which in this case would output 2. Is this achievable?

129026 次浏览

No. You can only index a simple array with an integer in bash. Associative arrays (introduced in bash 4) can be indexed by strings. They don't, however, provided for the type of reverse lookup you are asking for, without a specially constructed associative array.

$ declare -A myArray
$ myArray=([red]=0 [orange]=1 [green]=2)
$ echo ${myArray[green]}
2

This will do it:

#!/bin/bash


my_array=(red orange green)
value='green'


for i in "${!my_array[@]}"; do
if [[ "${my_array[$i]}" = "${value}" ]]; then
echo "${i}";
fi
done

Obviously, if you turn this into a function (e.g. get_index() ) - you can make it generic

You must declare your array before use with

declare -A myArray
myArray=([red]=1 [orange]=2 [green]=3)
echo ${myArray['orange']}

There is also one tricky way:

echo ${myArray[@]/green//} | cut -d/ -f1 | wc -w | tr -d ' '

And you get 2 Here are references

I like that solution:

let "n=(`echo ${myArray[@]} | tr -s " " "\n" | grep -n "green" | cut -d":" -f 1`)-1"

The variable n will contain the result!

This is just another way to initialize an associative array as chepner showed. Don't forget that you need to explicitly declare or typset an associative array with -A attribute.

i=0; declare -A myArray=( [red]=$((i++)) [orange]=$((i++)) [green]=$((i++)) )
echo ${myArray[green]}
2

This removes the need to hard code values and makes it unlikely you will end up with duplicates.

If you have lots of values to add it may help to put them on separate lines.

i=0; declare -A myArray;
myArray+=( [red]=$((i++)) )
myArray+=( [orange]=$((i++)) )
myArray+=( [green]=$((i++)) )
echo ${myArray[green]}
2

Say you want an array of numbers and lowercase letters (eg: for a menu selection) you can also do something like this.

declare -a mKeys_1=( \{\{0..9},{a..z}} );
i=0; declare -A mKeys_1_Lookup; eval mKeys_1_Lookup[\{\{0..9},{a..z}}]="$((i++))";

If you then run

echo "${mKeys_1[15]}"
f
echo "${mKeys_1_Lookup[f]}"
15

This might just work for arrays,

my_array=(red orange green)
echo "$(printf "%s\n" "${my_array[@]}")" | grep -n '^orange$' | sed 's/:orange//'

Output:

2

If you want to find header index in a tsv file,

head -n 1 tsv_filename | sed 's/\t/\n/g' | grep -n '^header_name$' | sed 's/:header_name//g'

In zsh you can do

xs=( foo bar qux )
echo ${xs[(ie)bar]}

see zshparam(1) subsection Subscript Flags

A little more concise and works in Bash 3.x:

my_array=(red orange green)
value='green'


for i in "${!my_array[@]}"; do
[[ "${my_array[$i]}" = "${value}" ]] && break
done


echo $i

Another tricky one-liner:

index=$((-1 + 10#0$(IFS=$'\n' echo "${my_array[*]}" | grep --line-number --fixed-strings -- "$value" | cut -f1 -d:)))

features:

  • supports elements with spaces
  • returns -1 when not found

caveats:

  • requires value to be non-empty
  • difficult to read

Explanations by breaking it down in execution order:

IFS=$'\n' echo "${my_array[*]}"

set array expansion separator (IFS) to a new line char & expand the array

grep --line-number --fixed-strings -- "$value"

grep for a match:

  • show line numbers (--line-number or -n)
  • use a fixed string (--fixed-strings or -F; disables regex)
  • allow for elements starting with a - (--)

    cut -f1 -d:

extract only the line number (format is <line_num>:<matched line>)

$((-1 + 10#0$(...)))

subtract 1 since line numbers are 1-indexed and arrays are 0-indexed

  • if $(...) does not match:

    • nothing is returned & the default of 0 is used (10#0)
  • if $(...) matches:
    • a line number exists & is prefixed with 10#0; i.e. 10#02, 10#09, 10#014, etc
    • the 10# prefix forces base-10/decimal numbers instead of octal


Using awk instead of grep, cut & bash arithmetic:

IFS=$'\n'; awk "\$0 == \"${value//\"/\\\"}\" {print NR-1}" <<< "${my_array[*]}"

features:

  • supports elements with spaces
  • supports empty elements
  • less commands opened in a subshell

caveats:

  • returns when not found

Explanations by breaking it down in execution order:

IFS=$'\n' [...] <<< "${my_array[*]}"

set array expansion separator (IFS) to a new line char & expand the array

awk "\$0 == \"${value//\"/\\\"}\" {print NR-1}"

match the entire line & print the 0-indexed line number

  • ${value//\"/\\\"} replaces double quotes in $value with escaped versions
  • since we need variable substitution, this segment has more escaping than wanted

Simple solution:

my_array=(red orange green)
echo ${my_array[*]} | tr ' ' '\n' | awk '/green/ {print NR-1}'

This outputs the 0-based array index of the query (here "orange").

echo $(( $(printf "%s\n" "${myArray[@]}" | sed -n '/^orange$/{=;q}') - 1 ))

If the query does not occur in the array then the above outputs -1.

If the query occurs multiple times in the array then the above outputs the index of the query's first occurrence.

Since this solution invokes sed, I doubt that it can compete with some of the pure bash solutions in this thread in efficiency.

This shows some methods for returning an index of an array member. The array uses non-applicable values for the first and last index, to provide an index starting at 1, and to provide limits.

The while loop is an interesting method for iteration, with cutoff, with the purpose of generating an index for an array value, the body of the loop contains only a colon for null operation. The important part is the iteration of i until a match, or past the possible matches.

The function indexof() will translate a text value to an index. If a value is unmatched the function returns an error code that can be used in a test to perform error handling. An input value unmatched to the array will exceed the range limits (-gt, -lt) tests.

There is a test (main code) that loops good/bad values, the first 3 lines are commented out, but try some variations to see interesting results (lines 1,3 or 2,3 or 4). I included some code that considers error conditions, because it can be useful.

The last line of code invokes function indexof with a known good value "green" which will echo the index value.

indexof(){
local s i;


#   0    1   2     3    4
s=( @@@ red green blue @o@ )


while [ ${s[i++]} != $1 ] && [ $i -lt ${#s[@]} ]; do :; done


[ $i -gt 1 ] && [ $i -lt ${#s[@]} ] || return


let i--


echo $i
};# end function indexof


# --- main code ---
echo -e \\033c
echo 'Testing good and bad variables:'
for x in @@@ red pot green blue frog bob @o@;
do
#v=$(indexof $x) || break
#v=$(indexof $x) || continue
#echo $v
v=$(indexof $x) && echo -e "$x:\t ok" || echo -e "$x:\t unmatched"
done


echo -e '\nShow the index of array member green:'
indexof green
myArray=('red' 'orange' 'green')
echo ${myArray[@]}
arrayElementToBeRemoved='orange'
echo "removing element: $arrayElementToBeRemoved"
# Find index of the array element (to be kept or preserved)
let "index=(`echo ${myArray[@]} | tr -s " " "\n" | grep -n "$arrayElementToBeRemoved" | cut -d":" -f 1`)-1"
unset "myArray[$index]"
echo ${myArray[@]}

I wanted something similar myself and avoiding a loop, came up with ...

myArray=('red' 'orange' 'green')
declare -p myArray | sed -n "s,.*\[\([^]]*\)\]=\"green\".*,\1,p"

... which leaves stdout unsullied should the element not be found...

$ myArray=('red' 'orange' 'green')
$ declare -p myArray | sed -n "s,.*\[\([^]]*\)\]=\"green\".*,\1,p"
2


$ declare -p myArray | sed -n "s,.*\[\([^]]*\)\]=\"gren\".*,\1,p"
$

After which I googled, found this question and thought I'd share ;)

This one outputs the 1based NEUROMANCER index of the character "Molly" ;)

get_index() {


declare -n dummy_array="$1"
# alternative: split read -ra array <<< "${dummy_array[@]}"
local array=( "${dummy_array[@]}" )
# alternative: local value; value="$( for dummy_value; do true; done; echo "$dummy_value" )"
local value=$2
local length="${#array[@]}"
local i=0
  

while (( i < length ))
do
if [ "${array[$i]}" = "$value" ]
then echo $(( i + 1 )); return 0
fi; (( i++ ))
done
  

echo "$2 not found beneath $1"
exit 1


}


NEUROMANCER=(Case Molly Riviera)
get_index NEUROMANCER Molly
get_index NEUROMANCER 'John Doe'

If you then run:

$ bash script.sh
2
John Doe not found beneath NEUROMANCER


function array_indexof() {
[ $# -lt 2 ] && return 1
local a=("$@")
local v="${a[-1]}"
unset a[-1]
local i
for i in ${!a[@]}; do
if [ "${a[$i]}" = "$v" ]; then
echo $i
return 0 # stop after first match
fi
done
return 1
}


a=(a b c d)
i=$(array_indexof "${a[@]}" d)
echo $i # 3

Purest bash function:

_indexof() {
for ((;$#;)) ; do
case "$1" in
--) shift ; break ;;
-*) printf "Usage: %s [--] VALUE ARRAY...\n" "$FUNCNAME" >&2 ; return 2 ;;
*) break ;;
esac
shift
done
local asize value=$1
shift
asize=$#
((asize)) || { printf "Usage: %s [--] VALUE ARRAY...\n" "$FUNCNAME" >&2 ; return 2 ;}
while (($#)) ; do
[[ "$1" != "${value}" ]] || break
shift
done
(($#)) || return 1
echo $((asize-$#))
}
  • ✓ work with any inputs
  • ✓ work even with "set -e"
  • ✓ integrate helping error message
  • ✓ return non-zero on error (1 if not found, 2 if non-proper call)
  • ✓ output first index if found

Example:

set "Peace & Love" "ПТН Х̆ЛО" "Cupidity" "Vanity" "$(printf "Ideology\nFear")" "Bayraktar"
_indexof "Vanity" "$@"

Return 0, output "3".

This solution is similar to the answer of @pointo1d but easier to read:

# myArray=('red' 'orange' 'green')
# declare -p myArray | grep -oP '[0-9]+(?=]="green")'
2

(?=string) is called a positive lookahead which allows matching something (in our case a number) followed by something else which won't be added to the result.