Problem
Sourcing the result of declare -p
for a valid Bash associative array in which keys contain square brackets results in a bad array subscript error.
Testing Procedure
Do:
$ declare -A array
$ key='var[0]'
$ array["$key"]=37
$ echo ${array["$key"]}
37
$ declare -p array > def.sh
$ cat def.sh
declare -A array='(["var[0]"]="37" )'
$ . def.sh
bash: [var[0]]=37: bad array subscript
In the above code, note:
var[0]
declare -p
I am able to save this definition to a file: def.sh
def.sh
an error is emitted.My Environment
Workarounds
If instead of doing declare -p array > def.sh
I do instead:
{
echo 'declare -A array'
for Key in "${!array[@]}"; do
EscapedKey="$(sed 's|"|\\"|g' <<<"$Key")"
echo "array[\"$EscapedKey\"]=${array["$Key"]}"
done
} > def.sh
then sourcing the def.sh file works. Note that in the above example, I'm also escaping quote characters that might be a part of the key. I do understand that what I have above is not exhaustive. Because of these complications, I would prefer a solution that doesn't involve such workarounds, if at all possible.
Question
Is there some shopt
,set -o <option>
, or something else I can do to enable me to persist an associative array whose keys may contain square brackets or other special characters to a file and to later be able to source that file successfully? I am needing a solution that works in my environment above.
This is a bug in bash 4.2
. It's fixed in 4.3
.
I tested this by compiling bash 4.2
, 4.2.53
and 4.3
from http://ftp.gnu.org/gnu/bash/ and replicated the steps above. 4.3
behaves like 4.4
- there is no such issue. In bash 4.3
however, declare
will print
declare -A array='(["var[0]"]="37" )'
just as 4.2
does. 4.4
does not add the quotes around the right-hand side, instead printing this:
declare -A array=(["var[0]"]="37" )
This makes no difference from what the testing showed.
There a possibly related option in complete_fullquote
but it was added in 4.4
so it can't be used as a workaround.
It seems that outside of using a version >=4.3
this needs to be worked around and the one you used is the most straightforward way of doing it.
There is an alternative if you want to avoid the sed
calls though (tested using bash 4.2
):
function array2file {
# local variable for the keys
declare -a keys
# check if the array exists, to protect against injection
# by passing a crafted string
declare -p "$1" >/dev/null || return 1;
printf "declare -A %s\n" "$1"
# create a string with all the keys so we can iterate
# because we can't use eval at for's declaration
# we do it this way to allow for spaces in the keys, since that's valid
eval "keys=(\"\${!$1[@]}\")"
for k in "${keys[@]}"
do
printf "%s[\"${k//\"/\\\\\"}\"]=" "$1"
# the extra quoting here protects against spaces
# within the element's value - injection doesn't work here
# but we still need to make sure there's consistency
eval "printf \"\\\"%s\\\"\n\" \"\${$1[\"${k//\"/\\\"}\"]}\""
done
}
This will properly add quotes around the key and also escape all doublequotes within the key itself. You can place this in a file, which you source. Then use:
array2file array > ./def.sh
where array
is whatever array name you've chosen. By redirecting the output you'll get properly quoted keys and you can define your associative array as you did before, then pass it to this function for storage.
If you change the variable provided to the first printf
inside the for
loop from $1
to ${2:-$1}
and do the same at the printf
at the top, then you can optionally create the definition of a new array with the 2nd argument as its name, allowing renaming of sorts. This will only happen if you provide 2 strings instead of one (quoted of course). The setup allows for this to be done easily, so I've added it here.
This would let you work around cases where interfacing with existing code can be difficult with a predefined function.