Iteration
Iterating over JSON arrays via Bash associative-arrays requires two more functions to the json-utils
to json2array and array2json in the top section.
A basic form of itereratino is getkeys which returns the keys from any given level of the path. It will work for objects and arrays.
function getkeys {
local -n arr
arr=$1
local keys
keys=$(for key in "${!arr[@]}"; do
if [[ $key =~ $2 ]]; then
echo "$key"
fi
done | sort -u)
if [[ -z $keys ]]; then
echo "Error: $2 is not a valid key" >&2
return 1;
fi
echo "$keys"
}
The return 1
part is something I had to add after my initial attempt as I got more familiar with the “logic programing” style of bash, to move to an alternative block of code if the function doesn’t receive a valid key, which could create the key or whatever.
Here are some examples to illustrate how they are used.
#!/bin/bash
source /usr/local/lib/json-utils.sh
declare -A myarr
json2array myarr "$(< ../musicevent.json)"
ExampleGroup 'extract an array from a JSON object'
Example 'list all elements in the JSON "performer": [...] array'
When call getkeys myarr '"performer"'
The output should equal '"performer",0,"name"
"performer",0,"sameAs",0
"performer",0,"sameAs",1
"performer",0,"@type"
"performer",1,"image"
"performer",1,"name"
"performer",1,"sameAs"
"performer",1,"@type"'
The status should be success
The error should be blank
End
Example 'list all elements in the JSON "offers" object, which is not an array in the given example'
When call getkeys myarr '"offers"'
The output should equal '"offers","availability"
"offers","price"
"offers","priceCurrency"
"offers","@type"
"offers","url"'
The status should be success
The error should be blank
End
Example 'what happens if getkeys receives a typo key?'
When call getkeys myarr '"performers"'
The stderr should equal 'Error: "performers" is not a valid key'
The status should be failure
End
End
I then built on my getkeys function to write a function arraylength needed in the next section appending. This function represents my first attempt at throwing errors in bash functions. It checks the given key is indeed a JSON array.
function arraylength {
local rows
local cols
readarray -t rows <<< "$(getkeys "$1" "$2")"
if [[ ${rows[*]} ]]; then
readarray -d ',' -t cols <<< "${rows[-1]}"
if [[ ! ${cols[1]} =~ ^[0-9]+$ ]]; then
echo "Error: $2 is not an array" >&2
return 2
fi
echo $(( cols[1] + 1 ))
else
return $?
fi
}
The above exits with 1 with the error is unset key, and 2 if the key is not an array. This is handy for routing different errors to different handlers:
function munge_performer {
local len
len="$(arraylength event_arr '"performer"')"
case $? in
0) update_performers "$len" ;;
1) echo "no entry for performer" ;;
2) object2array event_arr '"performer"'
update_performers 1
;;
esac
}
#!/bin/bash
source /usr/local/lib/json-utils.sh
declare -A myarr
json2array myarr "$(< ../musicevent.json)"
ExampleGroup 'extract an array from a JSON object'
Example 'list all elements in the JSON "performer": [...] array'
When call getkeys myarr '"performer"'
The output should equal '"performer",0,"name"
"performer",0,"sameAs",0
"performer",0,"sameAs",1
"performer",0,"@type"
"performer",1,"image"
"performer",1,"name"
"performer",1,"sameAs"
"performer",1,"@type"'
The status should be success
The error should be blank
End
Example 'list all elements in the JSON "offers" object, which is not an array in the given example'
When call getkeys myarr '"offers"'
The output should equal '"offers","availability"
"offers","price"
"offers","priceCurrency"
"offers","@type"
"offers","url"'
The status should be success
The error should be blank
End
Example 'arraylength uses getkeys to get JSON array length'
When call arraylength myarr '"performer"'
The output should equal 2
The status should be success
The error should be blank
End
Example 'Unset key should throw an error'
When call arraylength myarr '"performers"'
The stderr should equal 'Error: "performers" is not a valid key'
The status should be failure
End
Example 'arraylength of "offers" in the given example should throw an error'
When call arraylength myarr '"offers"'
The stderr should equal 'Error: "offers" is not an array'
The status should be failure
End
End
A reminder of what the JSON array looks like:
{
"@context": "https://schema.org",
"@type": "MusicEvent",
"location": {
"@type": "MusicVenue",
"name": "Chicago Symphony Center",
"address": "220 S. Michigan Ave, Chicago, Illinois, USA"
},
"name": "Shostakovich Leningrad",
"offers": {
"@type": "Offer",
"url": "/examples/ticket/12341234",
"price": "40",
"priceCurrency": "USD",
"availability": "https://schema.org/InStock"
},
"performer": [
{
"@type": "MusicGroup",
"name": "Chicago Symphony Orchestra",
"sameAs": [
"http://cso.org/",
"http://en.wikipedia.org/wiki/Chicago_Symphony_Orchestra"
]
},
{
"@type": "Person",
"image": "/examples/jvanzweden_s.jpg",
"name": "Jaap van Zweden",
"sameAs": "http://www.jaapvanzweden.com/"
}
],
"startDate": "2014-05-23T20:00",
"workPerformed": [
{
"@type": "CreativeWork",
"name": "Britten Four Sea Interludes and Passacaglia from Peter Grimes",
"sameAs": "http://en.wikipedia.org/wiki/Peter_Grimes"
},
{
"@type": "CreativeWork",
"name": "Shostakovich Symphony No. 7 (Leningrad)",
"sameAs": "http://en.wikipedia.org/wiki/Symphony_No._7_(Shostakovich)"
}
]
}
For real events as scrapped by my scripts offers tends to be an array, and this illustrates a common snag of getting an object when I want an array. I wrote this function to transform a given object to an array.
This function moves objects such as “offers” into an array:
# object2array myarr '"offers"'
function object2array {
declare -n arr
arr=$1
local rows
local cols
local key
readarray -t rows <<< "$(getkeys "$1" "$2")"
for key in "${rows[@]}"; do
readarray -d ',' -t cols <<< "$key"
if [[ ${cols[1]} =~ ^[0-9]+$ ]]; then
echo "Error: $2 is already an array" >&2
return 1
fi
arr["${cols[0]},0,${cols[@]:1}"]="${arr["$key"]}"
unset arr["$key"]
done
}
The unset arr["$key"]
breaks shellcheck’s rule SC2184, but changing the quotes causes shellspec to fail.
#!/bin/bash
source /usr/local/lib/json-utils.sh
declare -A myarr
json2array myarr "$(< ../musicevent.json)"
ExampleGroup 'place an object in an array'
Example 'move offers into an array'
object2array myarr '"offers"'
When call array2json myarr
The output should equal '{
"@context": "https://schema.org",
"@type": "MusicEvent",
"location": {
"@type": "MusicVenue",
"address": "220 S. Michigan Ave, Chicago, Illinois, USA",
"name": "Chicago Symphony Center"
},
"name": "Shostakovich Leningrad",
"offers": [
{
"@type": "Offer",
"availability": "https://schema.org/InStock",
"price": "40",
"priceCurrency": "USD",
"url": "/examples/ticket/12341234"
}
],
"performer": [
{
"@type": "MusicGroup",
"name": "Chicago Symphony Orchestra",
"sameAs": [
"http://cso.org/",
"http://en.wikipedia.org/wiki/Chicago_Symphony_Orchestra"
]
},
{
"@type": "Person",
"image": "/examples/jvanzweden_s.jpg",
"name": "Jaap van Zweden",
"sameAs": "http://www.jaapvanzweden.com/"
}
],
"startDate": "2014-05-23T20:00",
"workPerformed": [
{
"@type": "CreativeWork",
"name": "Britten Four Sea Interludes and Passacaglia from Peter Grimes",
"sameAs": "http://en.wikipedia.org/wiki/Peter_Grimes"
},
{
"@type": "CreativeWork",
"name": "Shostakovich Symphony No. 7 (Leningrad)",
"sameAs": "http://en.wikipedia.org/wiki/Symphony_No._7_(Shostakovich)"
}
]
}'
The status should be success
The error should be blank
End
Example 'object2array on an array should throw an error'
When call object2array myarr '"workPerformed"'
The stderr should equal 'Error: "workPerformed" is already an array'
The status should be failure
End
End