jq icon indicating copy to clipboard operation
jq copied to clipboard

Enhance @sh to output objects as bash associative array initialisers

Open rah2501 opened this issue 4 years ago • 2 comments

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

rah2501 avatar Jan 13 '21 15:01 rah2501

Coverage Status

Coverage decreased (-16.0%) to 68.149% when pulling 264bd1f675c5a69b65e6906e354c9ee3ac9169a9 on rah2501:sh-associative-array-initialiser into 80052e5275ae8c45b20411eecdd49c945a64a412 on stedolan:master.

coveralls avatar Jan 13 '21 15:01 coveralls

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"

geirha avatar Jun 01 '21 16:06 geirha