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]=%s\n" "$x" "${array[$x]}" ; done
[def]="foo bar"
[abc]='foobar'
# with quoting
$ for x in "${!array[@]}"; do printf "[%q]=%q\n" "$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" "${x@Q}" "${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=%s\n" "$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
zsh
to reverse keys <=> values
In zsh
, where the primary syntax for defining a hash is hash=(k1 v1 k2 v2...)
like in perl
(newer versions also support the awkward ksh93/bash syntax for compatibility though with variations when it comes to quoting the keys)
keys=("${(@k)hash}")
values=("${(@v)hash}")
typeset -A reversed
reversed=("${(@)values:^keys}") # array zipping operator
or using a loop:
for k v ("${(@kv}hash}") reversed[$v]=$k
The @
and double quotes is to preserve empty keys and values (note that bash
associative arrays don't support empty keys). As the expansion of elements in associative arrays is in no particular order, if several elements of $hash
have the same value (which will end up being a key in $reversed
), you can't tell which key will be used as the value in $reversed
.
for your loop
You'd use the R
hash subscript flag to get elements based on value instead of key, combined with e
for exact (as opposed to wildcard) match, and then get the keys for those elements with the k
parameter expansion flag:
for value ("${(@u)hash}")
print -r "elements with '$value' as value: ${(@k)hash[(Re)$value]}"
your perl approach
zsh
(contrary to ksh93
) doesn't support arrays of arrays, but its variables can contain the NUL byte, so you could use that to separate elements if the elements don't otherwise contain NUL bytes, or use the ${(q)var}
/ ${(Q)${(z)var}}
to encode/decode a list using quoting.
typeset -A seen
for k v ("${(@kv)hash}")
seen[$v]+=" ${(q)k}"
for k v ("${(@kv)seen}")
print -r "elements with '$k' as value: ${(Q@)${(z)v}}"
ksh93
ksh93 was the first shell to introduce associative arrays in 1993. The syntax for assigning values as a whole means it's very difficult to do it programmatically contrary to zsh
, but at least it's somewhat justified in ksh93 in that ksh93
supports complex nested data structures.
In particular, here ksh93 supports arrays as values for hash elements, so you can do:
typeset -A seen
for k in "${!hash[@]}"; do
seen[${hash[$k]}]+=("$k")
done
for k in "${!seen[@]}"; do
print -r "elements with '$k' as value ${x[$k][@]}"
done
bash
bash
added support for associative arrays decades later, copied the ksh93 syntax, but not the other advanced data structures, and doesn't have any of the advanced parameter expansion operators of zsh.
In bash
, you could use the quoted list approach mentioned in the zsh using printf %q
or with newer versions ${var@Q}
.
typeset -A seen
for k in "${!hash[@]}"; do
printf -v quoted_k %q "$k"
seen[${hash[$k]}]+=" $quoted_k"
done
for k in "${!seen[@]}"; do
eval "elements=(${seen[$k]})"
echo -E "elements with '$k' as value: ${elements[@]}"
done
As noted earlier however, bash
associative arrays don't support the empty value as a key, so it won't work if some of $hash
's values are empty. You could choose to replace the empty string with some place holder like <EMPTY>
or prefix the key with some character that you'd later strip for display.
Best Answer
Shells with associative arrays
Some modern shells provide associative arrays: ksh93, bash ≥4, zsh. In ksh93 and bash, if
a
is an associative array, then"${!a[@]}"
is the array of its keys:In zsh, that syntax only works in ksh emulation mode. Otherwise you have to use zsh's native syntax:
${(k)a}
also works ifa
does not have an empty key.In zsh, you could also loop on both
k
eys andv
alues at the same time:Shells without associative arrays
Emulating associative arrays in shells that don't have them is a lot more work. If you need associative arrays, it's probably time to bring in a bigger tool, such as ksh93 or Perl.
If you do need associative arrays in a mere POSIX shell, here's a way to simulate them, when keys are restricted to contain only the characters
0-9A-Z_a-z
(ASCII digits, letters and underscore). Under this assumption, keys can be used as part of variable names. The functions below act on an array identified by a naming prefix, the “stem”, which must not contain two consecutive underscores.(Warning, untested code. Error detection for syntactically invalid stems and keys is not provided.)