Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Bash: How to persist and restore associative arrays with keys that contain square brackets or other special characters

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:

  • I am able to specify a key that contains square brackets: var[0]
  • The key is quoted for setters and getters
  • I am able to do an assignment using this key
  • I am able to get the value from the associative array using this key
  • Using declare -p I am able to save this definition to a file: def.sh
  • When sourcing the file def.sh an error is emitted.

My Environment

  • The version of Bash I'm using is 4.2.46(1)-release (x86_64-redhat-linux-gnu).
  • I am on a CentOS 7.3.1611 (Core) server

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.

like image 930
Steve Amerige Avatar asked Aug 13 '17 21:08

Steve Amerige


1 Answers

It's a bug

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.

A workaround

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.

Extra credit

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.

like image 115
mechalynx Avatar answered Sep 22 '22 00:09

mechalynx