Reputation: 5724
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
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
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
Reputation: 174
It works for me in zsh
declare -a myArray=('red' 'orange' 'green')
value='green'
echo ${myArray[(I)$value]}
Upvotes: 0
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
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
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-$#))
}
Example:
set "Peace & Love" "ПТН Х̆ЛО" "Cupidity" "Vanity" "$(printf "Ideology\nFear")" "Bayraktar"
_indexof "Vanity" "$@"
Return 0, output "3".
Upvotes: -1
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
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
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
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
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
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
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:
-1
when not foundcaveats:
value
to be non-emptyExplanations 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:
--line-number
or -n
)--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:
0
is used (10#0
)$(...)
matches:
10#0
; i.e. 10#02
, 10#09
, 10#014
, etc10#
prefix forces base-10/decimal numbers instead of octalUsing awk
instead of grep
, cut
& bash arithmetic:
IFS=$'\n'; awk "\$0 == \"${value//\"/\\\"}\" {print NR-1}" <<< "${my_array[*]}"
features:
caveats:
when not foundExplanations 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 versionsUpvotes: 6
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
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
Reputation: 1968
In zsh you can do
xs=( foo bar qux )
echo ${xs[(ie)bar]}
see zshparam(1) subsection Subscript Flags
Upvotes: 2
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
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
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
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
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
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