Bash how to substitute string variables for array name [duplicate] - arrays

Let's say we declared two associative arrays:
#!/bin/bash
declare -A first
declare -A second
first=([ele]=value [elem]=valuee [element]=valueee)
second=([ele]=foo [elem]=fooo [element]=foooo)
# echo ${$1[$2]}
I want to echo the given hashmap and element from script inputs. For example, if I run sh.sh second elem, the script should echo fooo.

An inelegant but bullet-proof solution would be to white-list $1 with the allowed values:
#!/bin/bash
# ...
[[ $2 ]] || exit 1
unset result
case $1 in
first) [[ ${first["$2"]+X} ]] && result=${first["$2"]} ;;
second) [[ ${second["$2"]+X} ]] && result=${second["$2"]} ;;
*) exit 1 ;;
esac
[[ ${result+X} ]] && printf '%s\n' "$result"
notes:
[[ $2 ]] || exit 1 because bash doesn't allow empty keys
[[ ${var+X} ]] checks that the variable var is defined; with this expansion you can also check that an index or key is defined in an array.

A couple ideas come to mind:
Variable indirection expansion
Per this answer:
arr="$1[$2]" # build array reference from input fields
echo "${!arr}" # indirect reference via the ! character
For the sample call sh.sh second elem this generates:
fooo
Nameref (declare -n) (requires bash 4.3+)
declare -n arr="$1"
echo "${arr[$2]}"
For the sample call sh.sh second elem this generates:
fooo

Related

bash access associative array creates unbound variable [duplicate]

Using:
set -o nounset
Having an indexed array like:
myArray=( "red" "black" "blue" )
What is the shortest way to check if element 1 is set?
I sometimes use the following:
test "${#myArray[#]}" -gt "1" && echo "1 exists" || echo "1 doesn't exist"
I would like to know if there's a preferred one.
How to deal with non-consecutive indexes?
myArray=()
myArray[12]="red"
myArray[51]="black"
myArray[129]="blue"
How to quick check that 51 is already set for example?
How to deal with associative arrays?
declare -A myArray
myArray["key1"]="red"
myArray["key2"]="black"
myArray["key3"]="blue"
How to quick check that key2 is already used for example?
To check if the element is set (applies to both indexed and associative array)
[ "${array[key]+abc}" ] && echo "exists"
Basically what ${array[key]+abc} does is
if array[key] is set, return abc
if array[key] is not set, return nothing
References:
See Parameter Expansion in Bash manual and the little note
if the colon is omitted, the operator tests only for existence [of parameter]
This answer is actually adapted from the answers for this SO question: How to tell if a string is not defined in a bash shell script?
A wrapper function:
exists(){
if [ "$2" != in ]; then
echo "Incorrect usage."
echo "Correct usage: exists {key} in {array}"
return
fi
eval '[ ${'$3'[$1]+muahaha} ]'
}
For example
if ! exists key in array; then echo "No such array element"; fi
From man bash, conditional expressions:
-v varname
True if the shell variable varname is set (has been assigned a value).
example:
declare -A foo
foo[bar]="this is bar"
foo[baz]=""
if [[ -v "foo[bar]" ]] ; then
echo "foo[bar] is set"
fi
if [[ -v "foo[baz]" ]] ; then
echo "foo[baz] is set"
fi
if [[ -v "foo[quux]" ]] ; then
echo "foo[quux] is set"
fi
This will show that both foo[bar] and foo[baz] are set (even though the latter is set to an empty value) and foo[quux] is not.
New answer
From version 4.2 of bash (and newer), there is a new -v option to built-in test command.
From version 4.3, this test could address element of arrays.
array=([12]="red" [51]="black" [129]="blue")
for i in 10 12 30 {50..52} {128..131};do
if [ -v 'array[i]' ];then
echo "Variable 'array[$i]' is defined"
else
echo "Variable 'array[$i]' not exist"
fi
done
Variable 'array[10]' not exist
Variable 'array[12]' is defined
Variable 'array[30]' not exist
Variable 'array[50]' not exist
Variable 'array[51]' is defined
Variable 'array[52]' not exist
Variable 'array[128]' not exist
Variable 'array[129]' is defined
Variable 'array[130]' not exist
Variable 'array[131]' not exist
Note: regarding ssc's comment, I've single quoted 'array[i]' in -v test, in order to satisfy shellcheck's error SC2208. This seem not really required here, because there is no glob character in array[i], anyway...
This work with associative arrays in same way:
declare -A aArray=([foo]="bar" [bar]="baz" [baz]=$'Hello world\041')
for i in alpha bar baz dummy foo test;do
if [ -v 'aArray[$i]' ];then
echo "Variable 'aArray[$i]' is defined"
else
echo "Variable 'aArray[$i]' not exist"
fi
done
Variable 'aArray[alpha]' not exist
Variable 'aArray[bar]' is defined
Variable 'aArray[baz]' is defined
Variable 'aArray[dummy]' not exist
Variable 'aArray[foo]' is defined
Variable 'aArray[test]' not exist
With a little difference:In regular arrays, variable between brackets ([i]) is integer, so dollar symbol ($) is not required, but for associative array, as key is a word, $ is required ([$i])!
Old answer for bash prior to V4.2
Unfortunately, bash give no way to make difference betwen empty and undefined variable.
But there is some ways:
$ array=()
$ array[12]="red"
$ array[51]="black"
$ array[129]="blue"
$ echo ${array[#]}
red black blue
$ echo ${!array[#]}
12 51 129
$ echo "${#array[#]}"
3
$ printf "%s\n" ${!array[#]}|grep -q ^51$ && echo 51 exist
51 exist
$ printf "%s\n" ${!array[#]}|grep -q ^52$ && echo 52 exist
(give no answer)
And for associative array, you could use the same:
$ unset array
$ declare -A array
$ array["key1"]="red"
$ array["key2"]="black"
$ array["key3"]="blue"
$ echo ${array[#]}
blue black red
$ echo ${!array[#]}
key3 key2 key1
$ echo ${#array[#]}
3
$ set | grep ^array=
array=([key3]="blue" [key2]="black" [key1]="red" )
$ printf "%s\n" ${!array[#]}|grep -q ^key2$ && echo key2 exist || echo key2 not exist
key2 exist
$ printf "%s\n" ${!array[#]}|grep -q ^key5$ && echo key5 exist || echo key5 not exist
key5 not exist
You could do the job without the need of externals tools (no printf|grep as pure bash), and why not, build checkIfExist() as a new bash function:
$ checkIfExist() {
eval 'local keys=${!'$1'[#]}';
eval "case '$2' in
${keys// /|}) return 0 ;;
* ) return 1 ;;
esac";
}
$ checkIfExist array key2 && echo exist || echo don\'t
exist
$ checkIfExist array key5 && echo exist || echo don\'t
don't
or even create a new getIfExist bash function that return the desired value and exit with false result-code if desired value not exist:
$ getIfExist() {
eval 'local keys=${!'$1'[#]}';
eval "case '$2' in
${keys// /|}) echo \${$1[$2]};return 0 ;;
* ) return 1 ;;
esac";
}
$ getIfExist array key1
red
$ echo $?
0
$ # now with an empty defined value
$ array["key4"]=""
$ getIfExist array key4
$ echo $?
0
$ getIfExist array key5
$ echo $?
1
What about a -n test and the :- operator?
For example, this script:
#!/usr/bin/env bash
set -e
set -u
declare -A sample
sample["ABC"]=2
sample["DEF"]=3
if [[ -n "${sample['ABC']:-}" ]]; then
echo "ABC is set"
fi
if [[ -n "${sample['DEF']:-}" ]]; then
echo "DEF is set"
fi
if [[ -n "${sample['GHI']:-}" ]]; then
echo "GHI is set"
fi
Prints:
ABC is set
DEF is set
tested in bash 4.3.39(1)-release
declare -A fmap
fmap['foo']="boo"
key='foo'
# should echo foo is set to 'boo'
if [[ -z "${fmap[${key}]}" ]]; then echo "$key is unset in fmap"; else echo "${key} is set to '${fmap[${key}]}'"; fi
key='blah'
# should echo blah is unset in fmap
if [[ -z "${fmap[${key}]}" ]]; then echo "$key is unset in fmap"; else echo "${key} is set to '${fmap[${key}]}'"; fi
Reiterating this from Thamme:
[[ ${array[key]+Y} ]] && echo Y || echo N
This tests if the variable/array element exists, including if it is set to a null value. This works with a wider range of bash versions than -v and doesn't appear sensitive to things like set -u. If you see a "bad array subscript" using this method please post an example.
This is the easiest way I found for scripts.
<search> is the string you want to find, ASSOC_ARRAY the name of the variable holding your associative array.
Dependign on what you want to achieve:
key exists:
if grep -qe "<search>" <(echo "${!ASSOC_ARRAY[#]}"); then echo key is present; fi
key exists not:
if ! grep -qe "<search>" <(echo "${!ASSOC_ARRAY[#]}"); then echo key not present; fi
value exists:
if grep -qe "<search>" <(echo "${ASSOC_ARRAY[#]}"); then echo value is present; fi
value exists not:
if ! grep -qe "<search>" <(echo "${ASSOC_ARRAY[#]}"); then echo value not present; fi
I wrote a function to check if a key exists in an array in Bash:
# Check if array key exists
# Usage: array_key_exists $array_name $key
# Returns: 0 = key exists, 1 = key does NOT exist
function array_key_exists() {
local _array_name="$1"
local _key="$2"
local _cmd='echo ${!'$_array_name'[#]}'
local _array_keys=($(eval $_cmd))
local _key_exists=$(echo " ${_array_keys[#]} " | grep " $_key " &>/dev/null; echo $?)
[[ "$_key_exists" = "0" ]] && return 0 || return 1
}
Example
declare -A my_array
my_array['foo']="bar"
if [[ "$(array_key_exists 'my_array' 'foo'; echo $?)" = "0" ]]; then
echo "OK"
else
echo "ERROR"
fi
Tested with GNU bash, version 4.1.5(1)-release (i486-pc-linux-gnu)
For all time people, once and for all.
There's a "clean code" long way, and there is a shorter, more concise, bash centered way.
$1 = The index or key you are looking for.
$2 = The array / map passed in by reference.
function hasKey ()
{
local -r needle="${1:?}"
local -nr haystack=${2:?}
for key in "${!haystack[#]}"; do
if [[ $key == $needle ]] ;
return 0
fi
done
return 1
}
A linear search can be replaced by a binary search, which would perform better with larger data sets. Simply count and sort the keys first, then do a classic binary halving of of the haystack as you get closer and closer to the answer.
Now, for the purist out there that is like "No, I want the more performant version because I may have to deal with large arrays in bash," lets look at a more bash centered solution, but one that maintains clean code and the flexibility to deal with arrays or maps.
function hasKey ()
{
local -r needle="${1:?}"
local -nr haystack=${2:?}
[ -n ${haystack["$needle"]+found} ]
}
The line [ -n ${haystack["$needle"]+found} ]uses the ${parameter+word} form of bash variable expansion, not the ${parameter:+word} form, which attempts to test the value of a key, too, which is not the matter at hand.
Usage
local -A person=(firstname Anthony lastname Rutledge)
if hasMapKey "firstname" person; then
# Do something
fi
When not performing substring expansion, using the form described
below (e.g., ‘:-’), Bash tests for a parameter that is unset or null.
Omitting the colon results in a test only for a parameter that is
unset. Put another way, if the colon is included, the operator tests
for both parameter’s existence and that its value is not null; if the
colon is omitted, the operator tests only for existence.
${parameter:-word}
If parameter is unset or null, the expansion of word is substituted. Otherwise, the value of parameter is substituted.
${parameter:=word}
If parameter is unset or null, the expansion of word is assigned to parameter. The value of parameter is then substituted. Positional
parameters and special parameters may not be assigned to in this way.
${parameter:?word}
If parameter is null or unset, the expansion of word (or a message to that effect if word is not present) is written to the standard
error and the shell, if it is not interactive, exits. Otherwise, the
value of parameter is substituted. ${parameter:+word}
If parameter is null or unset, nothing is substituted, otherwise the expansion of word is substituted.
https://www.gnu.org/savannah-checkouts/gnu/bash/manual/bash.html#Shell-Parameter-Expansion
If $needle does not exist expand to nothing, otherwise expand to the non-zero length string, "found". This will make the -n test succeed if the $needle in fact does exist (as I say "found"), and fail otherwise.
Both in the case of arrays and hash maps I find the easiest and more straightforward solution is to use the matching operator =~.
For arrays:
myArray=("red" "black" "blue")
if [[ " ${myArray[#]} " =~ " blue " ]]; then
echo "blue exists in myArray"
else
echo "blue does not exist in myArray"
fi
NOTE: The spaces around the array guarantee the first and last element can match. The spaces around the value guarantee an exact match.
For hash maps, it's actually the same solution since printing a hash map as a string gives you a list of its values.
declare -A myMap
myMap=(
["key1"]="red"
["key2"]="black"
["key3"]="blue"
)
if [[ " ${myMap[#]} " =~ " blue " ]]; then
echo "blue exists in myMap"
else
echo "blue does not exist in myMap"
fi
But what if you would like to check whether a key exists in a hash map? In the case you can use the ! operator which gives you the list of keys in a hash map.
if [[ " ${!myMap[#]} " =~ " key3 " ]]; then
echo "key3 exists in myMap"
else
echo "key3 does not exist in myMap"
fi
I get bad array subscript error when the key I'm checking is not set. So, I wrote a function that loops over the keys:
#!/usr/bin/env bash
declare -A helpList
function get_help(){
target="$1"
for key in "${!helpList[#]}";do
if [[ "$key" == "$target" ]];then
echo "${helpList["$target"]}"
return;
fi
done
}
targetValue="$(get_help command_name)"
if [[ -z "$targetvalue" ]];then
echo "command_name is not set"
fi
It echos the value when it is found & echos nothing when not found. All the other solutions I tried gave me that error.

bad array subscript in if statement

I have a script where I'm creating a couple of a couple of command line arguments to pass in (file and output). The script appears to be working as I can successfully pass files into the script and parse them with the "-f" flag, but I noticed I'm getting the following error (on this line in the code below: if [[ -n ${variables[$argument_label]} ]]):
line 29: variables: bad array subscript
# declaring a couple of associative arrays
declare -A arguments=();
declare -A variables=();
# declaring an index integer
declare -i index=1;
variables["-o"]="output";
variables["--output"]="output";
variables["-f"]="file";
variables["--file"]="file";
# $# here represents all arguments passed in
for i in "$#"
do
arguments[$index]=$i;
prev_index="$(expr $index - 1)";
# this if block does something akin to "where $i contains ="
# "%=*" here strips out everything from the = to the end of the argument leaving only the label
if [[ $i == *"="* ]]
then argument_label=${i%=*}
else argument_label=${arguments[$prev_index]}
fi
# this if block only evaluates to true if the argument label exists in the variables array
if [[ -n ${variables[$argument_label]} ]]
then
# dynamically creating variables names using declare
# "#$argument_label=" here strips out the label leaving only the value
if [[ $i == *"="* ]]
then declare ${variables[$argument_label]}=${i#$argument_label=}
else declare ${variables[$argument_label]}=${arguments[$index]}
fi
fi
index=index+1;
done;
Any ideas or insight is greatly appreciated.
Maybe try adding quotes?
if [[ -n "${variables[$argument_label]}" ]]

associative array name substitution and copy bash

I have a bash script which checks if the input date($1) falls in a range/ranges of dates. User inputs a date and (a or b, which is $2).
#!/usr/bin/env bash
today=$(date +"%Y%M%d")
declare -A dict=$2_range
a_range=( ["20140602"]="20151222" ["20170201"]="$today" )
b_range=( ["20140602"]="20150130" )
for key in ${!dict[#]}; do
if [[ $1 -le ${dict[$key]} ]] && [[ $1 -ge $key ]]; then
echo $1 falls in the range of $2
fi
done
I do not know how to copy the associative array to the dict variable.
Sample usage
$ ./script.sh 20170707 a
20170707 falls in the range of a
You don't need to copy anything at all; you just need an alias.
#!/usr/bin/env bash
today=$(date +"%Y%M%d")
# you need declare -A **before** data is given.
# previously, these were sparse indexed arrays, not associative arrays at all.
declare -A a_range=( ["20140602"]="20151222" ["20170201"]="$today" )
declare -A b_range=( ["20140602"]="20150130" )
# declare -n makes dict a reference to (not a copy of) your named range.
declare -n dict="$2_range"
for key in "${!dict[#]}"; do
if (( $1 <= ${dict[$key]} )) && (( $1 >= key )); then
echo "$1 falls in the range of $2"
fi
done
declare -n is the bash (4.3+) version of the ksh93 feature nameref; see http://wiki.bash-hackers.org/commands/builtin/declare#nameref

Checking if variable starts with another variable in bash

Similar to this >> In bash, how can I check if a string begins with some value?, but not duplicated.
I have two arrays, and for each string inside the first one, I wanna check whether the strings in the second one start with the string from the first one or not.
array1=("test1","test2","test3");
array2=("test1 etc","test1 nanana","test2 zzz","test3 abracadabra");
for i in "${!array1[#]}"; do
for j in "${!array2[#]}"; do
if [[ "${array1[i]}" == "${array2[j]}*" ]]; then
echo "array1[$i] and arry2[$j] initial matches!";
fi;
done;
done
I tried many conditions inside if, such as:
if [[ "${array1[i]}" == "${array2[j]*}" ]]
if [[ "${array1[i]}" == "${array2[j]}*" ]]
if [[ "${array1[i]}" = "${array2[j]*}" ]]
if [[ "${array1[i]}" = "${array2[j]}*" ]]
Also without quotes, braces, and so on, all with no success.
There are some errors in your code, first of all, array declaration in bash: if you don't put spaces you have only one element. Remember to always print the variables before trying anything else on them.
From the bash docs:
ARRAY=(value1 value2 ... valueN)
Each value is then in the form of [indexnumber=]string. The index
number is optional. If it is supplied, that index is assigned to it;
otherwise the index of the element assigned is the number of the last
index that was assigned, plus one. This format is accepted by declare
as well. If no index numbers are supplied, indexing starts at zero.
To cycle array elements:
for element in "${array[#]}"
do
echo "$element"
done
Here is a code snippet:
array1=(test1 test2 test3);
array2=(test1 etc "test1 nanana" test2zzz test3 abracadabra);
for word1 in "${array1[#]}"; do
for word2 in "${array2[#]}"; do
echo "w1=$word1, w2=$word2"
if [[ ${word2} == ${word1}* ]]; then
echo "$word1 and $word2 initial matches!";
fi;
done;
done
After the comment by the OP I realised that He was trying to use indices, to do that you have to use the "$" also for the indices "i" and "j".
Here is a working solution:
for i in "${!array1[#]}"; do
for j in "${!array2[#]}"; do
echo "${array1[$i]} ${array2[$j]}"
if [[ ${array2[$j]} == ${array1[$i]}* ]]; then
echo "$array1[$i] and $array2[$j] initial matches!";
fi;
done;
done

Check if an element is present in a Bash array [duplicate]

This question already has answers here:
Check if a Bash array contains a value
(41 answers)
Closed 2 years ago.
I was wondering if there is an efficient way to check if an element is present within an array in Bash? I am looking for something similar to what I can do in Python, like:
arr = ['a','b','c','d']
if 'd' in arr:
do your thing
else:
do something
I've seen solutions using associative array for bash for Bash 4+, but I am wondering if there is another solution out there.
Please understand that I know the trivial solution is to iterate in the array, but I don't want that.
You could do:
if [[ " ${arr[*]} " == *" d "* ]]; then
echo "arr contains d"
fi
This will give false positives for example if you look for "a b" -- that substring is in the joined string but not as an array element. This dilemma will occur for whatever delimiter you choose.
The safest way is to loop over the array until you find the element:
array_contains () {
local seeking=$1; shift
local in=1
for element; do
if [[ $element == "$seeking" ]]; then
in=0
break
fi
done
return $in
}
arr=(a b c "d e" f g)
array_contains "a b" "${arr[#]}" && echo yes || echo no # no
array_contains "d e" "${arr[#]}" && echo yes || echo no # yes
Here's a "cleaner" version where you just pass the array name, not all its elements
array_contains2 () {
local array="$1[#]"
local seeking=$2
local in=1
for element in "${!array}"; do
if [[ $element == "$seeking" ]]; then
in=0
break
fi
done
return $in
}
array_contains2 arr "a b" && echo yes || echo no # no
array_contains2 arr "d e" && echo yes || echo no # yes
For associative arrays, there's a very tidy way to test if the array contains a given key: The -v operator
$ declare -A arr=( [foo]=bar [baz]=qux )
$ [[ -v arr[foo] ]] && echo yes || echo no
yes
$ [[ -v arr[bar] ]] && echo yes || echo no
no
See 6.4 Bash Conditional Expressions in the manual.
Obvious caveats aside, if your array was actually like the one above, you could do
if [[ ${arr[*]} =~ d ]]
then
do your thing
else
do something
fi
Initialize array arr and add elements
set variable to search for SEARCH_STRING
check if your array contains element
arr=()
arr+=('a')
arr+=('b')
arr+=('c')
SEARCH_STRING='b'
if [[ " ${arr[*]} " == *"$SEARCH_STRING"* ]];
then
echo "YES, your arr contains $SEARCH_STRING"
else
echo "NO, your arr does not contain $SEARCH_STRING"
fi
If array elements don't contain spaces, another (perhaps more readable) solution would be:
if echo ${arr[#]} | grep -q -w "d"; then
echo "is in array"
else
echo "is not in array"
fi
array=("word" "two words") # let's look for "two words"
using grep and printf:
(printf '%s\n' "${array[#]}" | grep -x -q "two words") && <run_your_if_found_command_here>
using for:
(for e in "${array[#]}"; do [[ "$e" == "two words" ]] && exit 0; done; exit 1) && <run_your_if_found_command_here>
For not_found results add || <run_your_if_notfound_command_here>
As bash does not have a built-in value in array operator and the =~ operator or the [[ "${array[#]" == *"${item}"* ]] notation keep confusing me, I usually combine grep with a here-string:
colors=('black' 'blue' 'light green')
if grep -q 'black' <<< "${colors[#]}"
then
echo 'match'
fi
Beware however that this suffers from the same false positives issue as many of the other answers that occurs when the item to search for is fully contained, but is not equal to another item:
if grep -q 'green' <<< "${colors[#]}"
then
echo 'should not match, but does'
fi
If that is an issue for your use case, you probably won't get around looping over the array:
for color in "${colors[#]}"
do
if [ "${color}" = 'green' ]
then
echo "should not match and won't"
break
fi
done
for color in "${colors[#]}"
do
if [ "${color}" = 'light green' ]
then
echo 'match'
break
fi
done
Here's another way that might be faster, in terms of compute time, than iterating. Not sure. The idea is to convert the array to a string, truncate it, and get the size of the new array.
For example, to find the index of 'd':
arr=(a b c d)
temp=`echo ${arr[#]}`
temp=( ${temp%%d*} )
index=${#temp[#]}
You could turn this into a function like:
get-index() {
Item=$1
Array="$2[#]"
ArgArray=( ${!Array} )
NewArray=( ${!Array%%${Item}*} )
Index=${#NewArray[#]}
[[ ${#ArgArray[#]} == ${#NewArray[#]} ]] && echo -1 || echo $Index
}
You could then call:
get-index d arr
and it would echo back 3, which would be assignable with:
index=`get-index d arr`
FWIW, here's what I used:
expr "${arr[*]}" : ".*\<$item\>"
This works where there are no delimiters in any of the array items or in the search target. I didn't need to solve the general case for my applicaiton.

Resources