user137369
user137369

Reputation: 5724

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?

Upvotes: 101

Views: 165563

Answers (22)

ghoti
ghoti

Reputation: 46896

For the archaeologists out there, here's one more take on the idea of this being an associative array lookup. In this answer I showed a way to rotate an array for the purpose of finding unique values, but a similar solution works here too. It's perhaps not as concise a solution as the one Steve provided, but it may be useful nonetheless.

myArray=('red' 'orange' 'green')
declare -A reverse
for i in "${!myArray[@]}"; do reverse[${myArray[$i]}]="$i"; done

declare -p reverse
echo "${reverse['green']}"

which of course returns,

declare -A reverse=([orange]="1" [red]="0" [green]="2" )
2

One gotcha is of course that this uses an associtive array, so it requires bash 4+. And works with sparse and associative arrays. :)

Or if you prefer, a function:

#!/usr/bin/env bash

indexof() {
  local -n a="$1"
  local lookup="$2"
  local -A b
  local i
  for i in "${!a[@]}"; do b[${a[$i]}]="$i"; done
  echo "${b[$2]}"
}

declare -A myArray
myArray=(
 ["pomegranate"]="red"
 ["kumquat"]="orange"
 ["basil"]="green"
)

echo $(indexof myArray "green")

which returns what you would expect. Or you could have a function that populates a lookup array:

#!/usr/bin/env bash

revarray() {
  local -n a="$1" # source array
  local -n b="$2" # target
  local i
  for i in "${!a[@]}"; do b[${a[$i]}]="$i"; done
}

#myArray=('red' 'orange' 'green' blue yellos)
declare -A myArray
myArray=(
 ["pomegranate"]="red"
 ["kumquat"]="orange"
 ["basil"]="green"
)

declare -A reverse
revarray myArray reverse

echo "${reverse[green]}"

Of course this has no input checking or error management, but give it goof things to eat and it should be happy.

I'd love to hear any feedback.

Upvotes: 0

Nikhil Gupta
Nikhil Gupta

Reputation: 291

Simple solution:

my_array=(red orange green)
# ^ and $ delimits the beginning and end of the exact match.
echo ${my_array[*]} | tr ' ' '\n' | awk '/^green$/ {print NR-1}'

Upvotes: -1

Dourado
Dourado

Reputation: 174

It works for me in zsh

declare -a myArray=('red' 'orange' 'green')

value='green'

echo ${myArray[(I)$value]}

Upvotes: 0

Kaleb Coberly
Kaleb Coberly

Reputation: 460

If you're going to repeatedly look up values in the array, you could build a reverse index out of an associative array.

my_array=(red orange green)
declare -A my_ass_arr

for i in ${!my_array[@]}; do my_ass_arr[${my_array[$i]}]=$i; done

this_val="green"
echo ${my_ass_arr[$this_val]}

This way you only loop through the array once. I'm not sure what bash does under the hood with associative arrays as far as indexing and searching, but it might be faster than a brute force search every time.

Upvotes: 2

mgutt
mgutt

Reputation: 6177

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.

Upvotes: 0

Jbar
Jbar

Reputation: 44

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

Upvotes: -1

milahu
milahu

Reputation: 3609

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

Upvotes: -1

AIe
AIe

Reputation: 21

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

Upvotes: 1

pointo1d
pointo1d

Reputation: 1

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

Upvotes: 0

kp123
kp123

Reputation: 1340

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[@]}

Upvotes: 0

cognativeorc
cognativeorc

Reputation: 1

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

Upvotes: 0

Carl Smith
Carl Smith

Reputation: 728

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.

Upvotes: 3

srbs
srbs

Reputation: 634

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

Upvotes: 6

cmcginty
cmcginty

Reputation: 117176

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

Upvotes: 10

chepner
chepner

Reputation: 532508

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

Upvotes: 15

Jan Matejka
Jan Matejka

Reputation: 1968

In zsh you can do

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

see zshparam(1) subsection Subscript Flags

Upvotes: 2

Manish Sharma
Manish Sharma

Reputation: 131

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'

Upvotes: 3

sbts
sbts

Reputation: 101

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

Upvotes: 2

user3680055
user3680055

Reputation: 53

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!

Upvotes: 2

Steve Walsh
Steve Walsh

Reputation: 6655

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

Upvotes: 122

PiotrO
PiotrO

Reputation: 1338

There is also one tricky way:

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

And you get 2 Here are references

Upvotes: 24

Olaf Dietsche
Olaf Dietsche

Reputation: 74118

You must declare your array before use with

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

Upvotes: 38

Related Questions