This question already has answers here:
Reading output of a command into an array in Bash
(4 answers)
Closed 1 year ago.
I'm new to bash scripting and trying to learn.
I have a variable $temp, that contains what I get from an API call, which is a long list of SHA1 hashes. I want to take what is in $temp and create an array that I can loop through and compare each entry to a string I have defined. Somehow, I can not seem to get it to work. The array I create always only contains only one element. It is like the rest of the string in $temp is being ignored.
I have tried multiple approaches, and can't get anything working the way I want it to.
Here's the code snippet:
temp=$(curl --silent --request GET "https://api.pwnedpasswords.com/range/$shortendHash")
echo "Temp is: $temp"
declare -A myArr
read myArr <<<$temp
echo ${myArr[#]}
echo ${myArr[0]}
echo ${myArr[1]}
When I run this, I get a full list of hashes, when I echo $temp (line 2), something like this:
"Temp is: 002C3CADF9FC86069F09961B10E0EDEFC45:1
003BE35BD9466D19258E79C8665AFB072F6:1
0075B2ADF782F9677BDDF8CC5778900D986:8
00810352C02B145FF304FCBD5BEF4F7AA9F:1
013720F40B99D6BCF6F08BD7271523EBB49:1
01C894A55EBCB6048A745B37EC2D989F102:1
030B46D53696C8889910EE0F9EB42CEAE97:4
03126B56153F0564F4A6ED6E734604C67E8:2
043196099707FCB0C01F8C2111086A92A0B:1
0452CABBEF8438F358F0BD8089108D0910E:5
0490255B206A2C0377EBD080723BF72DDAE:2
05AD1141C41237E061460DB5CA5412C1A48:4
05C1D058E4439F8F005EFB32E7553E6AA5B:1
067AF515CC712AC4ACA012311979DBC7C9A:2 ... and so on.."
But the
echo ${myArr[#]}
echo ${myArr[0]}
echo ${myArr[1]}
returns (first value is ${myArr[#]}, second is ${myArr[0]}, and last one is an empty line, indicating that there is no value at array position 1):
002C3CADF9FC86069F09961B10E0EDEFC45:1
002C3CADF9FC86069F09961B10E0EDEFC45:1
I hope someone can help with this.
Thanks a lot in advance!
declare -A myArr # creates an associative array (hash)
declare -a myArr # creates an array
Neither is necessary here. read can create the array for you. See help declare and help read.
temp=$(curl --silent --request GET "https://api.pwnedpasswords.com/range/$shortendHash")
# -a: assign the words read to sequential indices of the array
read -a myArr <<<"$temp"
declare -p myArr
Output:
declare -a myArr=([0]="002C3CADF9FC86069F09961B10E0EDEFC45:1" [1]="003BE35BD9466D19258E79C8665AFB072F6:1" [2]="0075B2ADF782F9677BDDF8CC5778900D986:8" [3]="00810352C02B145FF304FCBD5BEF4F7AA9F:1" [4]="013720F40B99D6BCF6F08BD7271523EBB49:1" [5]="01C894A55EBCB6048A745B37EC2D989F102:1" [6]="030B46D53696C8889910EE0F9EB42CEAE97:4" [7]="03126B56153F0564F4A6ED6E734604C67E8:2" [8]="043196099707FCB0C01F8C2111086A92A0B:1" [9]="0452CABBEF8438F358F0BD8089108D0910E:5" [10]="0490255B206A2C0377EBD080723BF72DDAE:2" [11]="05AD1141C41237E061460DB5CA5412C1A48:4" [12]="05C1D058E4439F8F005EFB32E7553E6AA5B:1" [13]="067AF515CC712AC4ACA012311979DBC7C9A:2")
Assuming ...
$(curl --silent --request GET "https://api.pwnedpasswords.com/range/$shortendHash")
Returns ...
002C3CADF9FC86069F09961B10E0EDEFC45:1
003BE35BD9466D19258E79C8665AFB072F6:1
0075B2ADF782F9677BDDF8CC5778900D986:8
00810352C02B145FF304FCBD5BEF4F7AA9F:1
013720F40B99D6BCF6F08BD7271523EBB49:1
01C894A55EBCB6048A745B37EC2D989F102:1
030B46D53696C8889910EE0F9EB42CEAE97:4
03126B56153F0564F4A6ED6E734604C67E8:2
043196099707FCB0C01F8C2111086A92A0B:1
0452CABBEF8438F358F0BD8089108D0910E:5
0490255B206A2C0377EBD080723BF72DDAE:2
05AD1141C41237E061460DB5CA5412C1A48:4
05C1D058E4439F8F005EFB32E7553E6AA5B:1
067AF515CC712AC4ACA012311979DBC7C9A:2
Then assign this to an array by wrapping in a set of parens:
# unset current variable
$ unset myArr
# no need to issue a `declare`; the `myArr=( ... )` will automatically create `myArr` as a normal integer-indexed array
$ myArr=( $(curl --silent --request GET "https://api.pwnedpasswords.com/range/$shortendHash") )
# verify array contents:
$ typeset myArr
declare -a myarr=([0]="002C3CADF9FC86069F09961B10E0EDEFC45:1" [1]="003BE35BD9466D19258E79C8665AFB072F6:1" [2]="0075B2ADF782F9677BDDF8CC5778900D986:8" [3]="00810352C02B145FF304FCBD5BEF4F7AA9F:1" [4]="013720F40B99D6BCF6F08BD7271523EBB49:1" [5]="01C894A55EBCB6048A745B37EC2D989F102:1" [6]="030B46D53696C8889910EE0F9EB42CEAE97:4" [7]="03126B56153F0564F4A6ED6E734604C67E8:2" [8]="043196099707FCB0C01F8C2111086A92A0B:1" [9]="0452CABBEF8438F358F0BD8089108D0910E:5" [10]="0490255B206A2C0377EBD080723BF72DDAE:2" [11]="05AD1141C41237E061460DB5CA5412C1A48:4" [12]="05C1D058E4439F8F005EFB32E7553E6AA5B:1" [13]="067AF515CC712AC4ACA012311979DBC7C9A:2")
Assuming OP wants to populate the array from the variable $temp, and assuming the white space in $temp consists of linefeeds and not spaces:
$ mapfile -t myArr <<< "${temp}"
$ typeset -p myArr
declare -a x=([0]="002C3CADF9FC86069F09961B10E0EDEFC45:1" [1]="003BE35BD9466D19258E79C8665AFB072F6:1" [2]="0075B2ADF782F9677BDDF8CC5778900D986:8" [3]="00810352C02B145FF304FCBD5BEF4F7AA9F:1" [4]="013720F40B99D6BCF6F08BD7271523EBB49:1" [5]="01C894A55EBCB6048A745B37EC2D989F102:1" [6]="030B46D53696C8889910EE0F9EB42CEAE97:4" [7]="03126B56153F0564F4A6ED6E734604C67E8:2" [8]="043196099707FCB0C01F8C2111086A92A0B:1" [9]="0452CABBEF8438F358F0BD8089108D0910E:5" [10]="0490255B206A2C0377EBD080723BF72DDAE:2" [11]="05AD1141C41237E061460DB5CA5412C1A48:4" [12]="05C1D058E4439F8F005EFB32E7553E6AA5B:1" [13]="067AF515CC712AC4ACA012311979DBC7C9A:2")
Related
I declare an associative array:
$ declare -A dikv
Initialize it with some key/value pairs:
$ dikv=( ["k1"]="v1" ["k2"]="v2" ["k3"]="v3" ["k4"]="v4")
Then I can save the contents to a file:
$ declare -p dikv > /tmp/dikv.saved
This is the content of /tmp/dikv.saved:
$ cat /tmp/dikv.saved
declare -A dikv=([k4]="v4" [k1]="v1" [k2]="v2" [k3]="v3" )
Now, in a new shell environment, I can load the saved associative array:
$ source /tmp/dikv.saved
And the content is properly accesible:
$ echo "${!dikv[#]}"
k4 k1 k2 k3
$ echo "${dikv[#]}"
v4 v1 v2 v3
This works as expected, nice.
Now I want to do exactly the same but using a bash function:
#! /bin/bash
declare -A dikv
backup_dictionary()
{
local -n dict_ref=$1
FILE=$2
echo "${!dict_ref[#]}"
echo "${dict_ref[#]}"
declare -p dict_ref > $FILE
}
dikv=( ["k1"]="v1" ["k2"]="v2" ["k3"]="v3" ["k4"]="v4")
backup_dictionary dikv /tmp/dikv.saved
As you can see, I pass the associative array to the function using local -n. When I run this code, the echo's inside the function print the content of the associative array properly. So, as far as I understand, the associative array has been properly passed as argument.
However, this statement is not working as expected:
$ declare -p dict_ref > $FILE
This is the content of $FILE:
$ cat /tmp/dikv.saved
declare -n dict_ref="dikv"
I expected to see something like this:
dikv=( ["k1"]="v1" ["k2"]="v2" ["k3"]="v3" ["k4"]="v4")
As it happens when not using a bash funcion. Can you explain what happens here? And what should be the proper way to fix this? Thanks!
Your dict_ref stores the string dikv. Hence if you do a declare -p dict_ref, I would expect to see as output something like
declare -n dict_ref=dikv
Note that dereferencing (due to the -n declaration) occurs during parameter expansion, i.e. when you do a $dict_ref.
You could do a
declare -p "$1" >$FILE
The file /tmp/file.csv contains the following:
name,age,gender
bob,21,m
jane,32,f
The CSV file will always have headers.. but might contain a different number of fields:
id,title,url,description
1,foo name,foo.io,a cool foo site
2,bar title,http://bar.io,a great bar site
3,baz heading,https://baz.io,some description
In either case, I want to convert my CSV data into an array of associative arrays..
What I need
So, I want a Bash 4.3 function that takes CSV as piped input and sends the array to stdout:
/tmp/file.csv:
name,age,gender
bob,21,m
jane,32,f
It needs to be used in my templating system, like this:
{{foo | csv_to_array | foo2}}
^ this is a fixed API, I must use that syntax .. foo2 must receive the array as standard input.
The csv_to_array func must do it's thing, so that afterwards I can do this:
$ declare -p row1; declare -p row2; declare -p new_array;
and it would give me this:
declare -A row1=([gender]="m" [name]="bob" [age]="21" )
declare -A row2=([gender]="f" [name]="jane" [age]="32" )
declare -a new_array=([0]="row1" [1]="row2")
..Once I have this array structure (an indexed array of associative array names), I have a shell-based templating system to access them, like so:
{{#new_array}}
Hi {{item.name}}, you are {{item.age}} years old.
{{/new_array}}
But I'm struggling to generate the arrays I need..
Things I tried:
I have already tried using this as a starting point to get the array structure I need:
while IFS=',' read -r -a my_array; do
echo ${my_array[0]} ${my_array[1]} ${my_array[2]}
done <<< $(cat /tmp/file.csv)
(from Shell: CSV to array)
..and also this:
cat /tmp/file.csv | while read line; do
line=( ${line//,/ } )
echo "0: ${line[0]}, 1: ${line[1]}, all: ${line[#]}"
done
(from https://www.reddit.com/r/commandline/comments/1kym4i/bash_create_array_from_one_line_in_csv/cbu9o2o/)
but I didn't really make any progress in getting what I want out the other end...
EDIT:
Accepted the 2nd answer, but I had to hack the library I am using to make either solution work..
I'll be happy to look at other answers, which do not export the declare commands as strings, to be run in the current env, but instead somehow hoist the resultant arrays of the declare commands to the current env (the current env is wherever the function is run from).
Example:
$ cat file.csv | csv_to_array
$ declare -p row2 # gives the data
So, to be clear, if the above ^ works in a terminal, it'll work in the library I'm using without the hacks I had to add (which involved grepping STDIN for ^declare -a and using source <(cat); eval $STDIN... in other functions)...
See my comments on the 2nd answer for more info.
The approach is straightforward:
Read the column headers into an array
Read the file line by line, in each line …
Create a new associative array and register its name in the array of array names
Read the fields and assign them according to the column headers
In the last step we cannot use read -a, mapfile, or things like these since they only create regular arrays with numbers as indices, but we want an associative array instead, so we have to create the array manually.
However, the implementation is a bit convoluted because of bash's quirks.
The following function parses stdin and creates arrays accordingly.
I took the liberty to rename your array new_array to rowNames.
#! /bin/bash
csvToArrays() {
IFS=, read -ra header
rowIndex=0
while IFS= read -r line; do
((rowIndex++))
rowName="row$rowIndex"
declare -Ag "$rowName"
IFS=, read -ra fields <<< "$line"
fieldIndex=0
for field in "${fields[#]}"; do
printf -v quotedFieldHeader %q "${header[fieldIndex++]}"
printf -v "$rowName[$quotedFieldHeader]" %s "$field"
done
rowNames+=("$rowName")
done
declare -p "${rowNames[#]}" rowNames
}
Calling the function in a pipe has no effect. Bash executes the commands in a pipe in a subshell, therefore you won't have access to the arrays created by someCommand | csvToArrays. Instead, call the function as either one of the following
csvToArrays < <(someCommand) # when input comes from a command, except "cat file"
csvToArrays < someFile # when input comes from a file
Bash scripts like these tend to be very slow. That's the reason why I didn't bother to extract printf -v quotedFieldHeader … from the inner loop even though it will do the same work over and over again.
I think the whole templating thing and everything related would be way easier to program and faster to execute in languages like python, perl, or something like that.
The following script:
csv_to_array() {
local -a values
local -a headers
local counter
IFS=, read -r -a headers
declare -a new_array=()
counter=1
while IFS=, read -r -a values; do
new_array+=( row$counter )
declare -A "row$counter=($(
paste -d '' <(
printf "[%s]=\n" "${headers[#]}"
) <(
printf "%q\n" "${values[#]}"
)
))"
(( counter++ ))
done
declare -p new_array ${!row*}
}
foo2() {
source <(cat)
declare -p new_array ${!row*} |
sed 's/^/foo2: /'
}
echo "==> TEST 1 <=="
cat <<EOF |
id,title,url,description
1,foo name,foo.io,a cool foo site
2,bar title,http://bar.io,a great bar site
3,baz heading,https://baz.io,some description
EOF
csv_to_array |
foo2
echo "==> TEST 2 <=="
cat <<EOF |
name,age,gender
bob,21,m
jane,32,f
EOF
csv_to_array |
foo2
will output:
==> TEST 1 <==
foo2: declare -a new_array=([0]="row1" [1]="row2" [2]="row3")
foo2: declare -A row1=([url]="foo.io" [description]="a cool foo site" [id]="1" [title]="foo name" )
foo2: declare -A row2=([url]="http://bar.io" [description]="a great bar site" [id]="2" [title]="bar title" )
foo2: declare -A row3=([url]="https://baz.io" [description]="some description" [id]="3" [title]="baz heading" )
==> TEST 2 <==
foo2: declare -a new_array=([0]="row1" [1]="row2")
foo2: declare -A row1=([gender]="m" [name]="bob" [age]="21" )
foo2: declare -A row2=([gender]="f" [name]="jane" [age]="32" )
The output comes from foo2 function.
The csv_to_array function first reads the headaers. Then for each read line it adds new element into new_array array and also creates a new associative array with the name row$index with elements created from joining the headers names with values read from the line. On the end the output from declare -p is outputted from the function.
The foo2 function sources the standard input, so the arrays come into scope for it. It outputs then those values again, prepending each line with foo2:.
I've got an array bucket1=('10' '22' 'bucket1')
As you can see, one of the elements is the name of the array bucket1
Now I'm creating a new array by copying bucket1:
array1=("${bucket1[#]}")
Now I'm changing one of the elements in array1:
array1[1]='30'
echo ${array1[#]} gives 10 30 bucket1
Now I want to feed that change back to the array bucket1, but without knowing that array1 was created from bucket1. Instead I want to use the third element of array1, namely bucket1.
Something like:
declare -a ${array1[2]}=${array1[#]}
So that I end up with new bucket1 array, containing ('10' '30' 'bucket1')
In short:
I want to copy an array, alter the copied array, apply the changes from the copied array in the original array using one of the elements from the copied array as the name of the original array.
It this possible?
bucket1=(10 20 bucket1)
tmp=("${bucket1[#]}")
tmp[1]=30
declare -a "${tmp[2]}"=("${tmp[#]}")
bash: syntax error near unexpected token `('
Hmm that doesn't work. Try one-by-one
for i in ${!tmp[#]}; do declare "${tmp[2]}[$i]"="${tmp[i]}"; done
echo ${bucket1[1]}
30
This is MUCH easier in ksh93
$ bucket1=(10 20 bucket1)
$ nameref tmp=bucket1
$ tmp[1]=30
$ echo ${bucket1[1]}
30
You can use read -ra instead of declare here:
$> bucket1=('10' '22' 'bucket1')
$> array1=("${bucket1[#]}")
$> array1[1]='30 50'
$> declare -p array1
declare -a array1='([0]="10" [1]="30 50" [2]="bucket1")'
$> IFS=$'^G' && read -ra "${array1[2]}" < <(printf "%s^G" "${array1[#]}")
$> declare -p "${array1[2]}"
declare -a bucket1='([0]="10" [1]="30 50" [2]="bucket1")'
$> declare -p bucket1
declare -a bucket1='([0]="10" [1]="30 50" [2]="bucket1")'
All these declare -p have been used to print the array contents and can be removed in real script.
^G is typed using ControlVG together.
With a little work, you can get the value of the array in a form suitable for use in the argument to declare.
IFS="=" read _ value <<< "$(set | grep '^array1=')"
declare -a "${array1[2]}=$value"
The quotes around the command substitution are necessary to work around a bug that is fixed in bash 4.3. However, if you have that version of bash, you can use named references to simplify this:
declare -n tmp=${array1[2]}
tmp=("${array1[#]}")
Try this:
unset ${array1[2]}
declare -a ${array1[2]}="`echo ${array1[#]}`"
First we clear the array and then the output of echo will be stored in the new array name.
I have an array of applications, initialized like this:
depends=$(cat ~/Depends.txt)
When I try to parse the list and copy it to a new array using,
for i in "${depends[#]}"; do
if [ $i #isn't installed ]; then
newDepends+=("$i")
fi
done
What happens is that only the first element of depends winds up on newDepends.
for i in "${newDepends[#]}"; do
echo $i
done
^^ This would output just one thing. So I'm trying to figure out why my for loop is is only moving the first element. The whole list is originally on depends, so it's not that, but I'm all out of ideas.
a=(foo bar "foo 1" "bar two") #create an array
b=("${a[#]}") #copy the array in another one
for value in "${b[#]}" ; do #print the new array
echo "$value"
done
The simplest way to copy a non-associative array in bash is to:
arrayClone=("${oldArray[#]}")
or to add elements to a preexistent array:
someArray+=("${oldArray[#]}")
Newlines/spaces/IFS in the elements will be preserved.
For copying associative arrays, Isaac's solutions work great.
The solutions given in the other answers won't work for associative arrays, or for arrays with non-contiguous indices. Here are is a more general solution:
declare -A arr=([this]=hello [\'that\']=world [theother]='and "goodbye"!')
temp=$(declare -p arr)
eval "${temp/arr=/newarr=}"
diff <(echo "$temp") <(declare -p newarr | sed 's/newarr=/arr=/')
# no output
And another:
declare -A arr=([this]=hello [\'that\']=world [theother]='and "goodbye"!')
declare -A newarr
for idx in "${!arr[#]}"; do
newarr[$idx]=${arr[$idx]}
done
diff <(echo "$temp") <(declare -p newarr | sed 's/newarr=/arr=/')
# no output
Try this: arrayClone=("${oldArray[#]}")
This works easily.
array_copy() {
set -- "$(declare -p $1)" "$2"
eval "$2=${1#*=}"
}
# Usage examples:
these=(apple banana catalog dormant eagle fruit goose hat icicle)
array_copy these those
declare -p those
declare -A src dest
source=(["It's a 15\" spike"]="and it's 1\" thick" [foo]=bar [baz]=qux)
array_copy src dest
declare -p dest
Note: when copying associative arrays, the destination must already exist as an associative array. If not, array_copy() will create it as a standard array and try to interpret the key names from the associative source as arithmetic variable names, with ugly results.
Isaac Schwabacher's solution is more robust in this regard, but it can't be tidily wrapped up in a function because its eval step evaluates an entire declare statement and bash treats those as equivalent to local when they're inside a function. This could be worked around by wedging the -g option into the evaluated declare but that might give the destination array more scope than it's supposed to have. Better, I think, to have array_copy() perform only the actual copy into an explicitly scoped destination.
You can copy an array by inserting the elements of the first array into the copy by specifying the index:
#!/bin/bash
array=( One Two Three Go! );
array_copy( );
let j=0;
for (( i=0; i<${#array[#]}; i++)
do
if [[ $i -ne 1 ]]; then # change the test here to your 'isn't installed' test
array_copy[$j]="${array[$i]}
let i+=1;
fi
done
for k in "${array_copy[#]}"; do
echo $k
done
The output of this would be:
One
Three
Go!
A useful document on bash arrays is on TLDP.
Problem is to copy array in function to be visible in parent code. This solution works for indexed arrays and if before copying are predefined as declare -A ARRAY, works also for associative arrays.
function array_copy
# $1 original array name
# $2 new array name with the same content
{
local INDEX
eval "
for INDEX in \"\${!$1[#]}\"
do
$2[\"\$INDEX\"]=\"\${$1[\$INDEX]}\"
done
"
}
Starting with Bash 4.3, you can do this
$ alpha=(bravo charlie 'delta 3' '' foxtrot)
$ declare -n golf=alpha
$ echo "${golf[2]}"
delta 3
Managed to copy an array into another.
firstArray=()
secondArray=()
firstArray+=("Element1")
firstArray+=("Element2")
secondArray+=("${firstArray[#]}")
for element in "${secondArray[#]}"; do
echo "${element}"
done
I've found that this works for me (mostly :)) ...
eval $(declare -p base | sed "s,base,target,")
extending the sed command to edit any switches as necessary e.g. if the new structure has to be writeable, to edit out read-only (-r).
I've discovered what was wrong.. My if isn't installed test is two for loops that remove excess characters from file names, and spits them out if they exist on a certain web server. What it wasn't doing was removing a trailing hyphen. So, when it tested it online for availability, they were parsed out. Because "file" exists, but "file-" doesn't.
How do you pass an associative array as an argument to a function? Is this possible in Bash?
The code below is not working as expected:
function iterateArray
{
local ADATA="${#}" # associative array
for key in "${!ADATA[#]}"
do
echo "key - ${key}"
echo "value: ${ADATA[$key]}"
done
}
Passing associative arrays to a function like normal arrays does not work:
iterateArray "$A_DATA"
or
iterateArray "$A_DATA[#]"
If you're using Bash 4.3 or newer, the cleanest way is to pass the associative array by name reference and then access it inside your function using a name reference with local -n. For example:
function foo {
local -n data_ref=$1
echo ${data_ref[a]} ${data_ref[b]}
}
declare -A data
data[a]="Fred Flintstone"
data[b]="Barney Rubble"
foo data
You don't have to use the _ref suffix; that's just what I picked here. You can call the reference anything you want so long as it's different from the original variable name (otherwise youll get a "circular name reference" error).
I had exactly the same problem last week and thought about it for quite a while.
It seems, that associative arrays can't be serialized or copied. There's a good Bash FAQ entry to associative arrays which explains them in detail. The last section gave me the following idea which works for me:
function print_array {
# eval string into a new associative array
eval "declare -A func_assoc_array="${1#*=}
# proof that array was successfully created
declare -p func_assoc_array
}
# declare an associative array
declare -A assoc_array=(["key1"]="value1" ["key2"]="value2")
# show associative array definition
declare -p assoc_array
# pass associative array in string form to function
print_array "$(declare -p assoc_array)"
Based on
Florian Feldhaus's solution:
# Bash 4+ only
function printAssocArray # ( assocArrayName )
{
var=$(declare -p "$1")
eval "declare -A _arr="${var#*=}
for k in "${!_arr[#]}"; do
echo "$k: ${_arr[$k]}"
done
}
declare -A conf
conf[pou]=789
conf[mail]="ab\npo"
conf[doo]=456
printAssocArray "conf"
The output will be:
doo: 456
pou: 789
mail: ab\npo
Update, to fully answer the question, here is an small section from my library:
Iterating an associative array by reference
shopt -s expand_aliases
alias array.getbyref='e="$( declare -p ${1} )"; eval "declare -A E=${e#*=}"'
alias array.foreach='array.keys ${1}; for key in "${KEYS[#]}"'
function array.print {
array.getbyref
array.foreach
do
echo "$key: ${E[$key]}"
done
}
function array.keys {
array.getbyref
KEYS=(${!E[#]})
}
# Example usage:
declare -A A=([one]=1 [two]=2 [three]=3)
array.print A
This we a devlopment of my earlier work, which I will leave below.
#ffeldhaus - nice response, I took it and ran with it:
t()
{
e="$( declare -p $1 )"
eval "declare -A E=${e#*=}"
declare -p E
}
declare -A A='([a]="1" [b]="2" [c]="3" )'
echo -n original declaration:; declare -p A
echo -n running function tst:
t A
# Output:
# original declaration:declare -A A='([a]="1" [b]="2" [c]="3" )'
# running function tst:declare -A E='([a]="1" [b]="2" [c]="3" )'
You can only pass associative arrays by name.
It's better (more efficient) to pass regular arrays by name also.
Here is a solution I came up with today using eval echo ... to do the indirection:
print_assoc_array() {
local arr_keys="\${!$1[#]}" # \$ means we only substitute the $1
local arr_val="\${$1[\"\$k\"]}"
for k in $(eval echo $arr_keys); do #use eval echo to do the next substitution
printf "%s: %s\n" "$k" "$(eval echo $arr_val)"
done
}
declare -A my_arr
my_arr[abc]="123"
my_arr[def]="456"
print_assoc_array my_arr
Outputs on bash 4.3:
def: 456
abc: 123
yo:
#!/bin/bash
declare -A dict
dict=(
[ke]="va"
[ys]="lu"
[ye]="es"
)
fun() {
for i in $#; do
echo $i
done
}
fun ${dict[#]} # || ${dict[key]} || ${!dict[#] || ${dict[$1]}
eZ
Here's another way: you can manually serialize the associative array as you pass it to a function, then deserialize it back into a new associative array inside the function:
1. Manual passing (via serialization/deserialization) of the associative array
Here's a full, runnable example from my eRCaGuy_hello_world repo:
array_pass_as_bash_parameter_2_associative.sh:
# Print an associative array using manual serialization/deserialization
# Usage:
# # General form:
# print_associative_array array_length array_keys array_values
#
# # Example:
# # length indices (keys) values
# print_associative_array "${#array1[#]}" "${!array1[#]}" "${array1[#]}"
print_associative_array() {
i=1
# read 1st argument, the array length
array_len="${#:$i:1}"
((i++))
# read all key:value pairs into a new associative array
declare -A array
for (( i_key="$i"; i_key<$(($i + "$array_len")); i_key++ )); do
i_value=$(($i_key + $array_len))
key="${#:$i_key:1}"
value="${#:$i_value:1}"
array["$key"]="$value"
done
# print the array by iterating through all of the keys now
for key in "${!array[#]}"; do
value="${array["$key"]}"
echo " $key: $value"
done
}
# Let's create and load up an associative array and print it
declare -A array1
array1["a"]="cat"
array1["b"]="dog"
array1["c"]="mouse"
# length indices (keys) values
print_associative_array "${#array1[#]}" "${!array1[#]}" "${array1[#]}"
Sample output:
a: cat
b: dog
c: mouse
Explanation:
For a given function named print_associative_array, here is the general form:
# general form
print_associative_array array_length array_keys array_values
For an array named array1, here is how to obtain the array length, indices (keys), and values:
array length: "${#array1[#]}"
all of the array indices (keys in this case, since it's an associative array): "${!array1[#]}"
all of the array values: "${array1[#]}"
So, an example call to print_associative_array would look like this:
# example call
# length indices (keys) values
print_associative_array "${#array1[#]}" "${!array1[#]}" "${array1[#]}"
Putting the length of the array first is essential, as it allows us to parse the incoming serialized array as it arrives into the print_associative_array function inside the magic # array of all incoming arguments.
To parse the # array, we'll use array slicing, which is described as follows (this snippet is copy-pasted from my answer here):
# array slicing basic format 1: grab a certain length starting at a certain
# index
echo "${#:2:5}"
# │ │
# │ └────> slice length
# └──────> slice starting index (zero-based)
2. [Better technique than above!] Pass the array by reference
...as #Todd Lehman explains in his answer here
# Print an associative array by passing the array by reference
# Usage:
# # General form:
# print_associative_array2 array
# # Example
# print_associative_array2 array1
print_associative_array2() {
# declare a local **reference variable** (hence `-n`) named `array_reference`
# which is a reference to the value stored in the first parameter
# passed in
local -n array_reference="$1"
# print the array by iterating through all of the keys now
for key in "${!array_reference[#]}"; do
value="${array_reference["$key"]}"
echo " $key: $value"
done
}
echo 'print_associative_array2 array1'
print_associative_array2 array1
echo ""
echo "OR (same thing--quotes don't matter in this case):"
echo 'print_associative_array2 "array1"'
print_associative_array2 "array1"
Sample output:
print_associative_array2 array1
a: cat
b: dog
c: mouse
OR (same thing--quotes don't matter in this case):
print_associative_array2 "array1"
a: cat
b: dog
c: mouse
See also:
[my answer] a more-extensive demo of me serializing/deserializing a regular "indexed" bash array in order to pass one or more of them as parameters to a function: Passing arrays as parameters in bash
[my answer] a demo of me passing a regular "indexed" bash array by reference: Passing arrays as parameters in bash
[my answer] array slicing: Unix & Linux: Bash: slice of positional parameters
[my question] Why do the man bash pages state the declare and local -n attribute "cannot be applied to array variables", and yet it can?
Excellent. The simple solution described by #Todd Lehman, solved my associative array passing problem. I had to pass 3 parameters, an integer, an associative array and an indexed array to a function.
A while ago I'd read that since arrays in bash are not first-class entities, arrays could not be passed as arguments to functions. Clearly that's not the whole truth after all. I've just implemented a solution where a function handles those parameters, something like this...
function serve_quiz_question() {
local index="$1"; shift
local -n answers_ref=$1; shift
local questions=( "$#" )
current_question="${questions[$index]}"
echo "current_question: $current_question"
#...
current_answer="${answers_ref[$current_question]}"
echo "current_answer: $current_answer"
}
declare -A answers
answers[braveheart]="scotland"
answers[mr robot]="new york"
answers[tron]="vancouver"
answers[devs]="california"
# integers would actually be assigned to index \
# by iterating over a random sequence, not shown here.
index=2
declare -a questions=( "braveheart" "devs" "mr robot" "tron" )
serve_quiz_question "$index" answers "${questions[#]}"
As the local variables get assigned, I had to shift the positional parameters away, in order to end with the ( "$#" ) assigning what's left to the indexed questions array.
The indexed array is needed so that we can reliably iterate over all the questions, either in random or ordered sequence. Associative arrays are not ordered data structures, so are not meant for any sort of predictable iteration.
Output:
current_question: mr robot
current_answer: new york
From the best Bash guide ever:
declare -A fullNames
fullNames=( ["lhunath"]="Maarten Billemont" ["greycat"]="Greg Wooledge" )
for user in "${!fullNames[#]}"
do
echo "User: $user, full name: ${fullNames[$user]}."
done
I think the issue in your case is that $# is not an associative array: "#: Expands to all the words of all the positional parameters. If double quoted, it expands to a list of all the positional parameters as individual words."