jq
jq copied to clipboard
Enhance @sh to output objects as bash associative array initialisers
Bash allows associative arrays to be initialised using declarations of the form
name=([foo]=1 [bar]=42 [baz]="hello")
At present, the @sh filter can format arrays into space-separated strings:
$ ip -j addr | jq -c -r '.[] | {ifname, addr_info: .addr_info[]} | {ifname, family: .addr_info["family"], address: .addr_info["local"]} | [.[]] | @sh' | head -n 3
'lo' 'inet' '127.0.0.1'
'lo' 'inet6' '::1'
'eth0' 'inet' '93.93.131.233'
We alter the @sh filter to output objects as well, in a format suitable for use as bash associative array initialisers:
$ ip -j addr | jq -c -r '.[] | {ifname, addr_info: .addr_info[]} | {ifname, family: .addr_info["family"], address: .addr_info["local"]} | @sh' | head -n 3
[ifname]='lo' [family]='inet' [address]='127.0.0.1'
[ifname]='lo' [family]='inet6' [address]='::1'
[ifname]='eth0' [family]='inet' [address]='93.93.131.233'
Allowing us to use symbolic names to reference values:
$ ip -j addr | jq -c -r '.[] | {ifname, addr_info: .addr_info[]} | {ifname, family: .addr_info["family"], address: .addr_info["local"]} | @sh' | while read initialiser; do declare -A iface; eval iface=($initialiser); echo "Interface ${iface[ifname]} has ${iface[family]} address ${iface[address]}"; done | head -n 3
Interface lo has inet address 127.0.0.1
Interface lo has inet6 address ::1
Interface eth0 has inet address 93.93.131.233
Coverage decreased (-16.0%) to 68.149% when pulling 264bd1f675c5a69b65e6906e354c9ee3ac9169a9 on rah2501:sh-associative-array-initialiser into 80052e5275ae8c45b20411eecdd49c945a64a412 on stedolan:master.
I see some problems with this.
Firstly, the current @sh
produces a string that can be safely injected into a context where a word is expected in any sh shell. This change causes it to also produce strings that can only be injected into certain contexts of a bash script. In that regard, creating a new @bash
format would be better
Second, keys of a json object can contain characters that are special to bash, but you only quoted the value, not the key. The key must be quoted as well.
It's also already possible to achieve the goal of populating an associative array from a single level object, with for example to_entries | map(@sh"data[\(.key)]=\(.value)")
:
ip -j addr show dev lo |
jq -r '
.[] |
{ifname, addr_info: .addr_info[]} |
{ifname, family: .addr_info["family"], address: .addr_info["local"]} |
to_entries |
map(@sh"data[\(.key)]=\(.value)") |
join(" ") + "\u0000"
' |
while read -rd '' entry; do
declare -A data=()
eval "$entry"
printf 'Interface %s has %s address %s\n' "${data[ifname]}" "${data[family]}" "${data[address]}"
done
Interface lo has inet address 127.0.0.1
Interface lo has inet6 address ::1
Though in this case I'd more likely just set three variables instead; @sh"ifname=\(.ifname) family=\(.family) address=\(.address)\u0000"