First a working example with arrays
json_array() {
local -n array="${1}"
readarray -d $'\0' -t array < <(
# Create nul delimited array entry using jq
jq -cjn --argjson arr "$array" '$arr|map(tostring)|.[]+"\u0000"'
)
}
> unset arr; arr='["a", "b", "c"]'; json_array arr; echo "${arr[0]} ${arr[1]} ${arr[2]}"
a b c
Now I'm trying to do something similar with dict, convert a json dict into a bash associative array
json_dict() {
local -n dict="${1}"
declare -A hash_table
append_to_hash_table() {
shift
{ read -r key; read -r value; } <<<"$1"
hash_table+=([$key]="$value")
}
readarray -d $'\0' -c 1 -C append_to_hash_table < <(
# Create nul delimited dict entry using jq
jq -cjn --argjson d "$dict" '$d|to_entries|map("\(.key)\n\(.value|tostring|#sh)")|.[]+"\u0000"'
)
# Here hash_table contain the correct output
dict=""
dict="$hash_table"
}
> unset arr; arr='{"a": "aa", "l": "bb", "c": "ccccc"}'; json_dict arr; echo "${arr[#]}"
Nothing
It seems dict="$hash_table" doesn't correctly update the refname,
How can I make bash dict refname point to hash_table?
There's no need for readarray here: You can have two separate NUL-delimited reads as part of your while loop.
See the below answer demonstrated at https://replit.com/#CharlesDuffy2/GrandioseDraftyArguments#main.sh
while IFS= read -r -d '' key && IFS= read -r -d '' value; do
hash_table[$key]=$value
done < <(jq -cjn --argjson d "$arr" \
'$d | to_entries[] | ( .key, "\u0000", .value, "\u0000")')
Putting this into context:
json_dict() {
declare key value in_value="${!1}"
unset "$1" # FIXME: Better to take a $2 for output variable
declare -g -A "$1"
declare -n hash_table="$1"
while IFS= read -r -d '' key && IFS= read -r -d '' value; do
hash_table[$key]=$value
done < <(
jq -cjn --argjson d "$in_value" \
'$d | to_entries[] | ( .key, "\u0000", .value, "\u0000")'
)
}
arr='{"a": "aa", "l": "bb", "c": "ccccc"}'
json_dict arr
declare -p arr
...emits as output:
declare -A arr=([a]="aa" [c]="ccccc" [l]="bb" )
That said, to answer the question exactly as-asked, thus using readarray:
json_dict() {
declare -a pieces=()
readarray -d '' pieces < <(
jq -cjn --argjson d "${!1}" \
'$d | to_entries[] | ( .key, "\u0000", .value, "\u0000")'
)
unset "$1"
declare -g -A "$1"
declare -n hash_table="$1"
set -- "${pieces[#]}"
while (( $# )); do
hash_table[$1]=$2
{ shift && shift; } || return
done
}
arr='{"a": "aa", "l": "bb", "c": "ccccc"}'
json_dict arr
declare -p arr
Wouldn't it be simpler to just use declare and have jq create the contents using #sh for shell conformity?
Indexed array:
unset arr; arr='["a", "b", "c"]'
declare -a arr="($(jq -r #sh <<< "$arr"))"
Associative array:
unset arr; arr='{"a": "aa", "l": "bb", "c": "ccccc"}'
declare -A arr="($(jq -r 'to_entries[] | #sh "[\(.key)]=\(.value)"' <<< "$arr"))"
Reacting to an edit request: The above requires the values to be strings (numbers and booleans will kind of work, too), but other structures need to be brought into a stringy format first. In particular, following the request, #json could be used to encode an arbitrary JSON content as a string. However, keep in mind that in doing so, the bash array's items will be JSON-encoded, which means that also the simple cases (strings like aa from above) will be encoded (e.g. as "aa", including the quotes, as required by JSON). If this is what you want, go for it:
Indexed array with JSON-encoded values:
unset arr; arr='["a", {}, [null, {"a": true}]]'
declare -a arr="($(jq -r 'map(#json) | #sh' <<< "$arr"))"
Associative array with JSON-encoded values:
unset arr; arr='{"a": "aa", "b": {"l": "bb", "c": "ccccc"}}'
declare -A arr="($(jq -r 'to_entries[] | #sh "[\(.key)]=\(.value | #json)"' <<< "$arr"))"
Related
I have script
#!/bin/bash
ARR=("a" "b")
collection_init_msg=$( jq -n --arg arr $ARR '{arr: [$arr]}')
echo some command "$collection_init_msg"
that should convert the ARR and print it as a JSON array.
Current result
some command {
"arr": [
"a"
]
}
What I want is:
some command {
"arr": [
"a", "b"
]
}
#!/bin/bash
ARR=("a" "b")
collection_init_msg=$( jq -nc '{arr: $ARGS.positional}' --args "${ARR[#]}" )
echo "$collection_init_msg"
In answer to the supplementary question in the comment: one could do worse than:
jq -n --argjson a1 $(jq -nc '$ARGS.positional' --args "${A1[#]}") '
{$a1, a2: $ARGS.positional}' --args "${A2[#]}"
How to parse and convert string list to JSON string array in shell command?
'["test1","test2","test3"]'
to
test1
test2
test3
I tried like below:
string=$1
array=${string#"["}
array=${array%"]"}
IFS=',' read -a array <<< $array;
echo "${array[#]}"
Any other optimized way?
As bash and jq are tagged, this solution relies on both (without summoning eval). The input string is expected to be in $string, the output array is generated into ${array[#]}. It is robust wrt spaces, newlines, quotes, etc. as it uses NUL as delimiter.
mapfile -d '' array < <(jq -j '.[] + "\u0000"' <<< "$string")
Testing
string='["has spaces\tand tabs","has a\nnewline","has \"quotes\""]'
mapfile -d '' array < <(jq -j '.[] + "\u0000"' <<< "$string")
printf '==>%s<==\n' "${array[#]}"
==>has spaces and tabs<==
==>has a
newline<==
==>has "quotes"<==
eval "array=($( jq -r 'map( #sh ) | join(" ")' <<<"$json" ))"
I want to transform JSON file into bash array of strings that i will later be able to iterate over. My JSON structure is as follows:
[
{
"USERID": "TMCCP",
"CREATED_DATE": "31/01/2020 17:52"
},
{
"USERID": "TMCCP",
"CREATED_DATE": "31/01/2020 17:52"
}
]
And this is my bash script:
test_cases=($(jq -c '.[]' data.json))
echo ${test_cases[0]}
echo ${test_cases[1]}
echo ${test_cases[2]}
echo ${test_cases[3]}
As you can see it returns array with 4 elements instead of 2. Output:
{"USERID":"TMCCP","CREATED_DATE":"31/01/2020
17:52"}
{"USERID":"TMCCP","CREATED_DATE":"31/01/2020
17:52"}
For some reason having whitespace in date field causes some parsing issues. Any idea how to get over this?
Use readarray instead.
$ readarray -t test_cases < <(jq -c '.[]' file)
$ declare -p test_cases
declare -a test_cases=([0]="{\"USERID\":\"TMCCP\",\"CREATED_DATE\":\"31/01/2020 17:52\"}" [1]="{\"USERID\":\"TMCCP\",\"CREATED_DATE\":\"31/01/2020 17:52\"}")
And read can be used as shown below where readarray is unavailable.
IFS=$'\n' read -d '' -a test_cases < <(jq -c '.[]' file)
Use readarray to populate the array, rather than using an unquoted command substitution; bash doesn't care about JSON quoting when it splits the result into separate words.
readarray -t test_cases < <(jq -c '.[]' data.json)
In bash 3.2 (which is what you appear to be stuck with), you need something slightly more unwieldy
while IFS= read -r line; do
test_cases+=("$line")
done < <(jq -c '.[]' data.json)
I am trying to create a new array of strings without null elements from an array of strings with null elements.
Code
#!/bin/bash
inlist=(a b c d) # inlist to be processed
outlist=(a b) # outlist to be deleted from inlist
for i in "${outlist[#]}"; do
inlist=( "${inlist[#]/$i}" ) # use outlist to remove elements from inlist
done
for i in "${!inlist[#]}"; do # create new inlist without null elements
# if []; then
templist+=( "${inlist[i]}" )
# fi
done
inlist=("${templist[#]}")
unset templist
for i in "${!inlist[#]}"; do
echo "$i" "${inlist[i]}"
done
Unexpected result
0
1
2 c
3 d
Expected result
0 c
1 d
Once the array handling is working, I want to then extend the script to handle lists of files, something like
Extension
mapfile -t inlist < inlist.txt
mapfile -t outlist < outlist.txt
inlist.txt
file1.txt
file2.txt
file3.txt
file4.txt
outlist1.txt
file1.txt
file2.txt
I am learning bash and working through some of the basic concepts around operators, expansion and substitution.
Appreciate any explanations or verbose code suggestions.
The problem seems to be the for loop not ignoring null elements when adding them to temporary array.
Thanks in advance
templist still has all the same null strings as inlist. You want something like
for i in "${inlist[#]}"; do
if [ -n "$i" ]; then
templist+=( "$i" )
fi
done
Now inlist=("${templist[#]}") will reset inlist as desired.
You could also use
for i in "${!inlist[#]}"; do
if [ -z "${inlist[i]}" ]; then
unset "inlist[i]"
fi
done
which leaves inlist in a slightly different state:
$ declare -p inlist
declare -a inlist=([2]="c" [3]="d")
but inlist=("${inlist[#]}") will ignore the actual indices when building the new array.
Give your two input files,
$ comm -23 inlist.txt outlist.txt
file3.txt
file4.txt
Use join or comm to extract elements that aren't in one list, but are in the other.
Below I printf the arrays as zero separated streams, sort them, then comm on them and then readarray into inlist.
inlist=(a b c d)
outlist=(a b)
IFS= readarray -d '' inlist < <(comm -z -23 <(printf "%s\0" "${inlist[#]}" | sort -z) <(printf "%s\0" "${outlist[#]}" | sort -z))
declare -p inlist
will output:
declare -a inlist=([0]="c" [1]="d")
Notes:
this will probably be very fast
-z for comm is a gnu extension
you will lose the element order, as elements are sorted before comm.
On bash version pre 4.4 that doesn't have -d option with readarray, you can read the array line by line and append to an array:
inlist=(a b c d)
outlist=(a b)
while IFS= read -d '' -r a; do
tmplist+=("$a")
done < <(comm -z -23 <(printf "%s\0" "${inlist[#]}" | sort -z) <(printf "%s\0" "${outlist[#]}" | sort -z))
declare -p tmplist
I parsed a json file with jq like this :
# cat test.json | jq '.logs' | jq '.[]' | jq '._id' | jq -s
It returns an array like this : [34,235,436,546,.....]
Using bash script i described an array :
# declare -a msgIds = ...
This array uses () instead of [] so when I pass the array given above to this array it won't work.
([324,32,45..]) this causes problem. If i remove the jq -s, an array forms with only 1 member in it.
Is there a way to solve this issue?
We can solve this problem by two ways. They are:
Input string:
// test.json
{
"keys": ["key1","key2","key3"]
}
Approach 1:
1) Use jq -r (output raw strings, not JSON texts) .
KEYS=$(jq -r '.keys' test.json)
echo $KEYS
# Output: [ "key1", "key2", "key3" ]
2) Use #sh (Converts input string to a series of space-separated strings). It removes square brackets[], comma(,) from the string.
KEYS=$(<test.json jq -r '.keys | #sh')
echo $KEYS
# Output: 'key1' 'key2' 'key3'
3) Using tr to remove single quotes from the string output. To delete specific characters use the -d option in tr.
KEYS=$((<test.json jq -r '.keys | #sh')| tr -d \')
echo $KEYS
# Output: key1 key2 key3
4) We can convert the comma-separated string to the array by placing our string output in a round bracket().
It also called compound Assignment, where we declare the array with a bunch of values.
ARRAYNAME=(value1 value2 .... valueN)
#!/bin/bash
KEYS=($((<test.json jq -r '.keys | #sh') | tr -d \'\"))
echo "Array size: " ${#KEYS[#]}
echo "Array elements: "${KEYS[#]}
# Output:
# Array size: 3
# Array elements: key1 key2 key3
Approach 2:
1) Use jq -r to get the string output, then use tr to delete characters like square brackets, double quotes and comma.
#!/bin/bash
KEYS=$(jq -r '.keys' test.json | tr -d '[],"')
echo $KEYS
# Output: key1 key2 key3
2) Then we can convert the comma-separated string to the array by placing our string output in a round bracket().
#!/bin/bash
KEYS=($(jq -r '.keys' test.json | tr -d '[]," '))
echo "Array size: " ${#KEYS[#]}
echo "Array elements: "${KEYS[#]}
# Output:
# Array size: 3
# Array elements: key1 key2 key3
To correctly parse values that have spaces, newlines (or any other arbitrary characters) just use jq's #sh filter and bash's declare -a. (No need for a while read loop or any other pre-processing)
// foo.json
{"data": ["A B", "C'D", ""]}
str=$(jq -r '.data | #sh' foo.json)
declare -a arr="($str)" # must be quoted like this
$ declare -p arr
declare -a arr=([0]="A B" [1]="C'D" [2]="")
The reason that this works correctly is that #sh will produce a space-separated list of shell-quoted words:
$ echo "$str"
'A B' 'C'\''D' ''
and this is exactly the format that declare expects for an array definition.
Use jq -r to output a string "raw", without JSON formatting, and use the #sh formatter to format your results as a string for shell consumption. Per the jq docs:
#sh:
The input is escaped suitable for use in a command-line for a POSIX shell. If the input is an array, the output will be a series of space-separated strings.
So can do e.g.
msgids=($(<test.json jq -r '.logs[]._id | #sh'))
and get the result you want.
From the jq FAQ (https://github.com/stedolan/jq/wiki/FAQ):
𝑸: How can a stream of JSON texts produced by jq be converted into a bash array of corresponding values?
A: One option would be to use mapfile (aka readarray), for example:
mapfile -t array <<< $(jq -c '.[]' input.json)
An alternative that might be indicative of what to do in other shells is to use read -r within a while loop. The following bash script populates an array, x, with JSON texts. The key points are the use of the -c option, and the use of the bash idiom while read -r value; do ... done < <(jq .......):
#!/bin/bash
x=()
while read -r value
do
x+=("$value")
done < <(jq -c '.[]' input.json)
++ To resolve this, we can use a very simple approach:
++ Since I am not aware of you input file, I am creating a file input.json with the following contents:
input.json:
{
"keys": ["key1","key2","key3"]
}
++ Use jq to get the value from the above file input.json:
Command: cat input.json | jq -r '.keys | #sh'
Output: 'key1' 'key2' 'key3'
Explanation: | #sh removes [ and "
++ To remove ' ' as well we use tr
command: cat input.json | jq -r '.keys | #sh' | tr -d \'
Explanation: use tr delete -d to remove '
++ To store this in a bash array we use () with `` and print it:
command:
KEYS=(`cat input.json | jq -r '.keys | #sh' | tr -d \'`)
To print all the entries of the array: echo "${KEYS[*]}"