Is there a way to print an entire array ([key]=value) without looping over all elements?
Assume I have created an array with some elements:
declare -A array array=([a1]=1 [a2]=2 ... [b1]=bbb ... [f500]=abcdef)
I can print back the entire array with
for i in "${!array[@]}"
do
echo "${i}=${array[$i]}"
done
However, it seems bash already knows how to get all array elements in one “go” – both keys ${!array[@]} and values ${array[@]}.
Is there a way to make bash print this info without the loop?
Edit:
typeset -p array does that!
However I can’t remove both prefix and suffix in a single substitution:
a="$(typeset -p array)"
b="${a##*(}"
c="${b%% )*}"
Is there a cleaner way to get/print only the key=value portion of the output?
Answers:
Thank you for visiting the Q&A section on Magenaut. Please note that all the answers may not help you solve the issue immediately. So please treat them as advisements. If you found the post helpful (or not), leave a comment & I’ll get back to you as soon as possible.
Method 1
I think you’re asking two different things there.
Is there a way to make bash print this info without the loop?
Yes, but they are not as good as just using the loop.
Is there a cleaner way to get/print only the key=value portion of the output?
Yes, the for loop. It has the advantages that it doesn’t require external programs, is straightforward, and makes it rather easy to control the exact output format without surprises.
Any solution that tries to handle the output of declare -p (typeset -p)
has to deal with a) the possibility of the variables themselves containing parenthesis or brackets, b) the quoting that declare -p has to add to make it’s output valid input for the shell.
For example, your expansion b="${a##*(}" eats some of the values, if any key/value contains an opening parenthesis. This is because you used ##, which removes the longest prefix. Same for c="${b%% )*}". Though you could of course match the boilerplate printed by declare more exactly, you’d still have a hard time if you didn’t want all the quoting it does.
This doesn’t look very nice unless you need it.
$ declare -A array=([abc]="'foobar'" [def]='"foo bar"') $ declare -p array declare -A array='([def]=""foo bar"" [abc]="'''foobar'''" )'
With the for loop, it’s easier to choose the output format as you like:
# without quoting
$ for x in "${!array[@]}"; do printf "[%s]=%sn" "$x" "${array[$x]}" ; done
[def]="foo bar"
[abc]='foobar'
# with quoting
$ for x in "${!array[@]}"; do printf "[%q]=%qn" "$x" "${array[$x]}" ; done
[def]="foo bar"
[abc]='foobar'
From there, it’s also simple to change the output format otherwise (remove the brackets around the key, put all key/value pairs on a single line…). If you need quoting for something other than the shell itself, you’ll still need to do it by yourself, but at least you have the raw data to work on. (If you have newlines in the keys or values, you are probably going to need some quoting.)
With a current Bash (4.4, I think), you could also use printf "[%s]=%s" "${[email protected]}" "${array[$x]@Q}" instead of printf "%q=%q". It produces a somewhat nicer quoted format, but is of course a bit more work to remember to write. (And it quotes the corner case of @ as array key, which %q doesn’t quote.)
If the for loop seems too weary to write, save it a function somewhere (without quoting here):
printarr() { declare -n __p="$1"; for k in "${!__p[@]}"; do printf "%s=%sn" "$k" "${__p[$k]}" ; done ; }
And then just use that:
$ declare -A a=([a]=123 [b]="foo bar" [c]="(blah)") $ printarr a a=123 b=foo bar c=(blah)
Works with indexed arrays, too:
$ b=(abba acdc) $ printarr b 0=abba 1=acdc
Method 2
declare -p array declare -A array='([a2]="2" [a1]="1" [zz]="Hello World" [b1]="bbb" [f50]="abcd" )'
2 fork
Maybe this:
printf "%sn" "${!array[@]}"
a2
a1
f50
zz
b1
printf "%sn" "${array[@]}"
2
1
abcd
Hello World
bbb
printf "%sn" "${!array[@]}" "${array[@]}" | pr -2t
a2 2
a1 1
f50 abcd
zz Hello World
b1 bbb
3 forks
or this:
paste -d= <(printf "%sn" "${!array[@]}") <(printf "%sn" "${array[@]}")
a2=2
a1=1
f50=abcd
zz=Hello World
b1=bbb
No fork
to be compared to
for i in "${!array[@]}";do printf "%s=%sn" "$i" "${array[$i]}";done
a2=2
a1=1
f50=abcd
zz=Hello World
b1=bbb
Execution times comparission
As last syntax don’t use fork, they could be quicker:
time printf "%sn" "${!array[@]}" "${array[@]}" | pr -2t | wc
5 11 76
real 0m0.005s
user 0m0.000s
sys 0m0.000s
time paste -d= <(printf "%sn" "${!array[@]}") <(printf "%sn" "${array[@]}") | wc
5 6 41
real 0m0.008s
user 0m0.000s
sys 0m0.000s
time for i in "${!array[@]}";do printf "%s=%sn" "$i" "${array[$i]}";done | wc
5 6 41
real 0m0.002s
user 0m0.000s
sys 0m0.001s
But this affirmation doesn’t stay true if the array becomes big; if reducing forks is efficient for small process, using dedicated tools is more efficient for bigger process.
for i in {a..z}{a..z}{a..z};do array[$i]=$RANDOM;done
time printf "%sn" "${!array[@]}" "${array[@]}" | pr -2t | wc
17581 35163 292941
real 0m0.150s
user 0m0.124s
sys 0m0.036s
time paste -d= <(printf "%sn" "${!array[@]}") <(printf "%sn" "${array[@]}") | wc
17581 17582 169875
real 0m0.140s
user 0m0.000s
sys 0m0.004s
time for i in "${!array[@]}";do printf "%s=%sn" "$i" "${array[$i]}";done | wc
17581 17582 169875
real 0m0.312s
user 0m0.268s
sys 0m0.076s
Remark
As both (forked) solutions use alignment, none of them will work if any variable contains a newline. In this case, the only way is a for loop.
More robust and detailed answer at StackOverflow
Method 3
Bash 5.1 allows a very straight forward way to display associative arrays by using the K value as in ${arr[@]@K}:
$ declare -A arr
$ arr=(k1 v1 k2 v2)
$ printf "%sn" "${arr[@]@K}"
k1 "v1" k2 "v2"
From the Bash 5.1 description document:
hh. New `K’ parameter transformation to display associative arrays as key-value pairs.
It is well explained in the Bash Reference Manual → 3.5.3 Shell Parameter Expansion:
K
Produces a possibly-quoted version of the value of parameter, except that it prints the values of indexed and associative arrays as a sequence of quoted key-value pairs (see Arrays).
Method 4
If you’re looking for a shell with better associative array support, try zsh.
In zsh (where associative arrays were added in 1998, compared to 1993 for ksh93 and 2009 for bash), $var or ${(v)var} expands to the (non-empty) values of the hash, ${(k)var} to the (non-empty) keys (in the same order), and ${(kv)var} to both keys and values.
To preserve the empty values, like for arrays, you need to quote and use the @ flag.
So to print the keys and values, it’s just a matter of
printf '%s => %sn' "${(@kv)var}"
Though to account for a possibly empty hash, you should do:
(($#var)) && printf '%s => %sn' "${(@kv)var}"
Also note that zsh uses a much more sensible and useful array definition syntax than ksh93‘s (copied by bash):
typeset -A var var=(k1 v1 k2 v2 '' empty '*' star)
Which makes it a lot easier to copy or merge associative arrays:
var2=("${(@kv)var1}")
var3+=("${(@kv)var2}")
var4=("${@kv)var4}" "${(@kv)var5}")
(you can’t easily copy a hash without a loop with bash, and note that bash currently doesn’t support empty keys or key/values with NUL bytes).
See also zsh array zipping features which you’ll typically need to work with associative arrays:
keys=($(<keys.txt)) values=($(<values.txt))
hash=(${keys:^values})
Method 5
Since typeset does what you want why not just edit its output?
typeset -p array | sed s/^.*(// | tr -d ")'"" | tr "[" "n" | sed s/]=/' = '/
gives
a2 = 2 a1 = 1 b1 = bbb
Where
array='([a2]="2" [a1]="1" [b1]="bbb" )'
Verbose but it’s pretty easy to see how the formatting works: just execute the pipeline with progressively more of the sed and tr commands. Modify them to suit pretty printing tastes.
Method 6
One more option is to list all the variables and grep for the one you want.
set | grep -e '^aa='
I use this for debugging. I doubt that it is very performant since it lists all the variables.
If you were doing this often you could make it a function like this:
aap() { set | grep -e "^$1="; }
Unfortunately when we check performance using time:
$ time aap aa
aa=([0]="abc")
.
real 0m0.014s
user 0m0.003s
sys 0m0.006s
Therefore, if you were doing this very often, you’d want @F.Hauri’s NO FORKS version because it is so much faster.
All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0