Doug R. Doug R. -4 years ago 208
Bash Question

Unable to Retrieve Key/Value Pairs From Bash Associative Array

TL;DR



I'm unable to retrieve the list of keys from an associative array in a bash
script. However, when I attempt to write a simplified MCVE, everything
works as expected, and I can't tell what I'm doing differently between the two.

Long Version



I'm writing a bash script which needs to read in a set of preferences from
an file and stores them in an associative array. Note that I'm using XML and
would prefer to use a Perl module to read the file, but the script will
be run on a large number of systems, most of which don't have the proper
Perl modules installed, and on almost none of which I have the necessary
access to install them. Anyway, parsing the XML file is not the issue here.

The issue is that when I run the following script against the XML file,
the settings I'm reading in from the XML file appear to be saving to the
associative array. In other words, the following code works as expected:

while read line
do
# Trimming code that sets key-val.
$config[$key]=$val
echo "${key}=${config[$key]}"
done


But later in the script when I attempt to retrive the config values, nothing
appears to be there. It appears that the config array is getting cleared
somehow, but I'm not clear where or how.

xml_config.sh



Here's the non-working script.

#! /bin/bash

# Configuration Array.
declare -A config

CONFIG_XML=./config.xml

# Extract key-value pairs.
echo "Extracting and setting initial key-value pairs:"
grep "^.*<setting.*id=.*$" $CONFIG_XML |
while read line
do
key=$(echo "${line}" | perl -pe "s|^.*id.*?=.*?[\'\"](.*?)[\'\"].*$|\1|")
val=$(echo "${line}" | perl -pe "s|^.*value.*?=.*?[\'\"](.*?)[\'\"].*$|\1|")
config[$key]="${val}"

# This line prints key=value pairs as expected.
echo "${key}=${config[$key]}"
done

# This loop does not print key=value pairs as expected.
echo -e "\n\nExtracting key-value pairs from associative array and printing:"
for key in "${!config[@]}"
do
echo "${key}=${config[$key]}"
done

echo -e "\n\nAlso doesn't work:\nkey1=${config[key1]}"


config.xml



This is the contents of the config.xml file that goes along with the script:

<?xml version="1.0" encoding="UTF-8"?>
<config>
<setting id="key1" value="val1" />
<setting id="key2" value="val2" />
<setting id="key3" value="val3" />
<setting id="key4" value="val4" />
<setting id="key5" value="val5" />
</config>


Simplified MCVE



Here are the contents of my simplified MCVE, which I expected to
fail, but which works as expected:

declare -A aa
for i in {0..10}
do
key="KEY${i}"
val="VAL${i}"
aa[$key]=$val
done

for key in "${!aa[@]}"
do
# Prints key-value pairs as expected.
echo "${key}=${aa[$key]}"
done

Answer Source

The while loop must execute in the current shell in order for the changes to the array to take effect. One solution is to use a process substitution instead of a pipe.

while read line
do
    key=$(echo "${line}" | perl -pe "s|^.*id.*?=.*?[\'\"](.*?)[\'\"].*$|\1|")
    val=$(echo "${line}" | perl -pe "s|^.*value.*?=.*?[\'\"](.*?)[\'\"].*$|\1|")
    config[$key]="${val}"

    # This line prints key=value pairs as expected.
    echo "${key}=${config[$key]}"
done < <(grep "^.*<setting.*id=.*$" $CONFIG_X)

If you are using bash 4.2 or later, you can use the lastpipe option instead.

shopt -s lastpipe

grep "^.*<setting.*id=.*$" $CONFIG_XML | while read line; do
    ...
done
Recommended from our users: Dynamic Network Monitoring from WhatsUp Gold from IPSwitch. Free Download