Get the index of a value in a Bash array
ArraysBashIndexingArrays Problem Overview
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?
Arrays Solutions
Solution 1 - Arrays
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
Solution 2 - Arrays
You must declare your array before use with
declare -A myArray
myArray=([red]=1 [orange]=2 [green]=3)
echo ${myArray['orange']}
Solution 3 - Arrays
There is also one tricky way:
echo ${myArray[@]/green//} | cut -d/ -f1 | wc -w | tr -d ' '
And you get 2 Here are references
Solution 4 - Arrays
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
Solution 5 - Arrays
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
Solution 6 - Arrays
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
)
- nothing is returned & the default of
-
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
- a line number exists & is prefixed with
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
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
Solution 7 - Arrays
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'
Solution 8 - Arrays
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!
Solution 9 - Arrays
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
Solution 10 - Arrays
In zsh you can do
xs=( foo bar qux )
echo ${xs[(ie)bar]}
see zshparam(1) subsection Subscript Flags
Solution 11 - Arrays
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.
Solution 12 - Arrays
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
Solution 13 - Arrays
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[@]}
Solution 14 - Arrays
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 ;)
Solution 15 - Arrays
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
Solution 16 - Arrays
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
Solution 17 - Arrays
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".
Solution 18 - Arrays
Simple solution:
my_array=(red orange green)
echo ${my_array[*]} | tr ' ' '\n' | awk '/green/ {print NR-1}'