Storing Bash associative arrays - arrays

I want to store (and retrieve, of course) Bash's associative arrays and am looking for a simple way to do that.
I know that it is possible to do it using a look over all keys:
for key in "${!arr[#]}"
do
echo "$key ${arr[$key]}"
done
Retrieving it could also be done in a loop:
declare -A arr
while read key value
do
arr[$key]=$value
done < store
But I also see that set will print a version of the array in this style:
arr=([key1]="value1" [key2]="value2" )
(Unfortunately along with all other shell variables.)
Is there a simpler way for storing and retrieving an associative array than my proposed loop?

To save to a file:
declare -p arr > saved.sh
(You can also use typeset instead of declare if you prefer.)
To load from the file:
source saved.sh

Related

Bash: Iterate over variable names

I'm writing a bash script to analyse some files. In a first iteration I create associative arrays with word counts for each category that is analysed. These categories are not known in advance so the names of these associative arrays are variable but all with the same prefix count_$category. The associative arrays have a word as key and its occurrence count in that category as value.
After all files are analysed, I have to summarise the results for each category. I can iterate over the variable names using ${count_*} but how can I access the associative arrays behind those variable names? For each associative array (each count_* variable) I should iterate over the words and their counts.
I have already tried with indirect access like this but it doesn't work:
for categorycount in ${count_*} # categorycount now holds the name of the associative array variable for each category
do
array=${!categorycount}
for word in ${!array[#]}
do
echo "$word occurred ${array[$word]} times"
done
done
The modern (bash 4.3+) approach uses "namevars", a facility borrowed from ksh:
for _count_var in "${!count_#}"; do
declare -n count=$_count_var # make count an alias for $_count_var
for key in "${!count[#]}"; do # iterate over keys, via same
echo "$key occurred ${count[$key]} times" # extract value, likewise
done
unset -n count # clear that alias
done
declare -n count=$count_var allows "${count[foo]}" to be used to look up item foo in the associative array named count_var; similarly, count[foo]=bar will assign to that same item. unset -n count then removes this mapping.
Prior to bash 4.3:
for _count_var in "${!count_#}"; do
printf -v cmd '_count_keys=( "${!%q[#]}" )' "$_count_var" && eval "$cmd"
for key in "${_count_keys[#]}"; do
var="$_count_var[$key]"
echo "$key occurred ${!var} times"
done
done
Note the use of %q, rather than substituting a variable name directly into a string, to generate the command to eval. Even though in this case we're probably safe (because the set of possible variable names is restricted), following this practice reduces the amount of context that needs to be considered to determine whether an indirect expansion is secure.
In both cases, note that internal variables (_count_var, _count_keys, etc) use names that don't match the count_* pattern.

bash script for substituition array does not give expected output.previous value being overwritten

declare -a args=()
args["p4"]=3
args["ifile"]=4
echo ${args["p4"]}
I am getting output
4
Why previous array entry is being overwritten?
Just so there's an answer here:
Using declare -a specifies that you are declaring an indexed array. Any non-numeric indices will be interpreted as a zero. Instead, use declare -A which initializes an associative array. Note however, that only a few shells support associative arrays.
From the man page:
Options which set attributes:
-a to make NAMEs indexed arrays (if supported)
-A to make NAMEs associative arrays (if supported)

bash4 read file into associative array

I am able to read file into a regular array with a single statement:
local -a ary
readarray -t ary < $fileName
Not happening is reading a file into assoc. array.
I have control over file creation and so would like to do as simply as possible w/o loops if possible at all.
So file content can be following to be read in as:
keyname=valueInfo
But I am willing to replace = with another string if cuts down on code, especially in a single line code as above.
And ...
So would it be possible to read such a file into an assoc array using something like an until or from - i.e. read into an assoc array until it hits a word, or would I have to do this as part of loop?
This will allow me to keep a lot of similar values in same file, but read into separate arrays.
I looked at mapfile as well, but does same as readarray.
Finally ...
I am creating an options list - to select from - as below:
local -a arr=("${!1}")
select option in ${arr[*]}; do
echo ${option}
break
done
Works fine - however the list shown is not sorted. I would like to have it sorted if possible at all.
Hope it is ok to put all 3 questions into 1 as the questions are similar - all on arrays.
Thank you.
First thing, associative arrays are declared with -A not -a:
local -A ary
And if you want to declare a variable on global scope, use declare outside of a function:
declare -A ary
Or use -g if BASH_VERSION >= 4.2.
If your lines do have keyname=valueInfo, with readarray, you can process it like this:
readarray -t lines < "$fileName"
for line in "${lines[#]}"; do
key=${line%%=*}
value=${line#*=}
ary[$key]=$value ## Or simply ary[${line%%=*}]=${line#*=}
done
Using a while read loop can also be an option:
while IFS= read -r line; do
ary[${line%%=*}]=${line#*=}
done < "$fileName"
Or
while IFS== read -r key value; do
ary[$key]=$value
done < "$fileName"

Passing An Array From One Bash Script to Another

I am new to writing Shell Scripts and am having some difficulties.
What I Want To Achieve
I have an array of strings in scriptOne.sh that I want to pass to scriptTwo.sh
What I Have Done So Far
I can execute the second script from inside the first using ./scriptTwo.sh and I have passed string variables from one to the other using ./scriptTwo.sh $variableOne.
The issues are when I try to pass an array variable it doesn't get passed. I have managed to get it to pass the first entry of the array using ./scriptTwo.sh "${array[#]}" however this is only one of the entries and I need all of them.
Thanks in advance for your help
Your way of passing the array is correct
./scriptTwo.sh "${array[#]}"
The problem is probably in the way how you receive it. In scriptTwo.sh, use
array=("$#")
I think we can directly read the array content from sriptOne.sh if you use a declare command.
I declare an Associative array named wes_batch_array in scriptOne.sh and I want to pass it to scriptTwo.sh so I put the command below in scriptTwo.sh:
declare -A wes_batch_array=$(awk -F '=' '{if ($1 ~ /^declare -A wes_batch_array/) {for (i=2;i<NF;i++) {printf $i"=";} printf $NF"\n";}}' < scriptOne.sh)
I've tested this command and it works fine.
If you do not use the declare command. You can echo the content of array to another middle temp file and use the awk command above to grab the content of bash array.
The way I solved this problem was to step through the array in script1.sh, then pass the array elements to script2.sh using a for loop. In my case I was passing many arrays with varying numbers of elements to the same script2.sh on different servers.
So for example:
script1.sh:
records="111111111 222222222 333333333"
myRecordsArray=($records)
for (( i=0 ; i < $(#myRecordsArray[#]} ; i++ )); do
./script2.sh ${myRecordsArray[i]}
done
script2.sh:
main () {
<some code here>
}
myRecord=$1
main $myRecord

Elegant use of arrays in ksh

I'm trying build an sort of property set in ksh.
Thought the easiest way to do so was using arrays but the syntax is killing me.
What I want is to
Build an arbitrary sized array in a config file with a name and a property.
Iterate for each item in that list and get that property.
I theory what I wish I could do is something like
MONITORINGSYS={
SYS1={NAME="GENERATOR" MONITORFUNC="getGeneratorStatus"}
SYS2={NAME="COOLER" MONITORFUNC="getCoolerStatus"}
}
Then later on, be able to do something like:
for CURSYS in $MONITORINGSYS
do
CSYSNAME=$CURSYS.NAME
CSYSFUNC=$CURSYS.MONITORFUNC
REPORT="$REPORT\n$CSYSNAME"
CSYSSTATUS=CSYSFUNC $(date)
REPORT="$REPORT\t$CSYSSTATUS"
done
echo $REPORT
Well, that's not real programming, but I guess you got the point..
How do I do that?
[EDIT]
I do not mean I want to use associative arrays. I only put this way to make my question more clear... I.e. It would not be a problem if the loop was something like:
for CURSYS in $MONITORINGSYS
do
CSYSNAME=${CURSYS[0]}
CSYSFUNC=${CURSYS[1]}
REPORT="$REPORT\n$CSYSNAME"
CSYSSTATUS=CSYSFUNC $(date)
REPORT="$REPORT\t$CSYSSTATUS"
done
echo $REPORT
Same applies to the config file.. I'm just looking for a syntax that makes it minimally readable.
cheers
Not exactly sure what you want... Kornshell can handle both associative and indexed arrays.
However, Kornshell arrays are one dimensional. It might be possible to use indirection to emulate a two dimensional array via the use of $() and eval. I did this a couple of times in the older Perl 4.x and Perl 3.x, but it's a pain. If you want multidimensional arrays, use Python or Perl.
The only thing is that you must declare arrays via the typedef command:
$ typeset -A foohash #foohash is an associative array
$ typeset -a foolist #foolist is an integer indexed array.
Maybe your script can look something like this
typeset -a sysname
typeset -a sysfunct
sysname[1] = "GENERATOR"
sysname[2] = "COOLER"
sysfunc[1] = "getGeneratorStatus"
sysfunc[2] = "getCoolerStatus"
for CURSYS in {1..2}
do
CSYSNAME="${sysname[$CURSYS]}"
CSYSFUNC="${sysfunc[$CURSYS]}"
REPORT="$REPORT\n$CSYSNAME"
CSYSSTATUS=$(eval "CSYSFUNC $(date)")
REPORT="$REPORT\t$CSYSSTATUS"
done
echo $REPORT
ksh93 now has compound variables which can contain a mixture of indexed and associative arrays. No need to declare it as ksh will work it out itself.
#!/bin/ksh
MONITORINGSYS=(
[SYS1]=(NAME="GENERATOR" MONITORFUNC="getGeneratorStatus")
[SYS2]=(NAME="COOLER" MONITORFUNC="getCoolerStatus")
)
echo MONITORING REPORT
echo "-----------------"
for sys in ${!MONITORINGSYS[*]}; do
echo "System: $sys"
echo "Name: ${MONITORINGSYS[$sys].NAME}"
echo "Generator: ${MONITORINGSYS[$sys].MONITORFUNC}"
echo
done
Output:
MONITORING REPORT
-----------------
System: SYS1
Name: GENERATOR
Generator: getGeneratorStatus
System: SYS2
Name: COOLER
Generator: getCoolerStatus

Resources