5

Basically I have a bash script that at one point makes an API call and a cert and key are generated and returned in json. I pipe it to jq and can select either the cert or the key and store it in a variable.

Something like this:

CERT=$(API call | jq -r '.certificate')
or
KEY=$(API call | jq -r '.key')

I want to store each in its own variable but I can't make the call twice because it will generate a new cert/key.

I know that I can just store both in a file and then manipulate after to accomplish my task but I am curious if jq offers a direct way to selectively store each value in its own variable?

peak
  • 105,803
  • 17
  • 152
  • 177
jlarr
  • 53
  • 1
  • 3
  • Might .certificate contain a literal NUL (\u0000) character? "In bash, you can't store the NULL-character in a variable" so the question seems to presume not, but .... [https://stackoverflow.com/questions/6570531/assign-string-containing-null-character-0-to-a-variable-in-bash] – peak Apr 12 '21 at 03:29

5 Answers5

3

You could store the original JSON in a variable (instead of a file), and then extract the key and cert from that:

apiResult=$(API call)
cert=$(jq -r '.certificate' <<<"$apiResult")
key=$(jq -r '.key' <<<"$apiResult")

Notes: I recommend using lower- or mixed-case variables, to avoid accidental conflicts with the many all-caps names with special meanings. Also, <<< is a bashism, and won't work in all other shells; if you need this be portable to e.g. dash, use something like key=$(printf '%s\n' "$apiResult" | jq -r '.key').

Gordon Davisson
  • 118,432
  • 16
  • 123
  • 151
1

If neither of the values contain a newline, you can easily use read:

$ read cert key < <( echo '{"certificate": "foo", "key": "bar"}' | jq -r .certificate,.key | tr \\n ' ')
$ echo $cert
foo
$ echo $key
bar

or:

$ { read cert; read key; } << EOF
> $( echo '{"certificate": "foo", "key": "bar"}' | jq -r .certificate,.key )
> EOF
$ echo $cert:$key
foo:bar

The certificate almost certainly will contain newlines, so you many want to serialize that data (eg, base64 encode it). But you're probably better off using Gordon Davisson's approach.

William Pursell
  • 204,365
  • 48
  • 270
  • 300
  • The two solutions using `read` also make some assumptions about whitespace and backslash. If there is a need to serialize the data, it would be because of NUL, but then using bash variables becomes problematic ... – peak Apr 13 '21 at 06:30
0

Unless there are NUL characters in the strings, there should be no need to call jq more than once, or to serialize the data.

In the simplest case, you could proceed along the following lines:

{ IFS= read -r certificate
  IFS= read -r key
  echo "certificate=$certificate"
  echo "key=$key"
} < <(API call | jq -r '.certificate, .key')

If the values do not contain NUL but might contain newline characters, then you could use NUL as the delimiter. For the sake of variety, we could also use a while loop:

while IFS= read -r -d $'\0' certificate
do
  IFS= read -r -d $'\0' key
  echo "certificate=$certificate"
  echo "key=$key"
done < <(API call | jq -rj '[.certificate, .key] | join("\u0000")')

Conversely ...

If the values of interest might contain literal NUL values ("\u0000"), then the question seems to be problematic, as bash variables in effect cannot contain literal NULs.

If any of the values of interest might contain literal NUL values, then here are two strategies for extracting the "raw" string equivalents into separate files:

  1. Save the JSON output in a (temporary) file, and invoke jq -r once per value of interest in the obvious way.

  2. Set up a bash pipeline starting with:

    API call | jq -r '.certificate, .key | @base64`

    and continuing with a loop in which each line is decoded, e.g. using base64 --decode or jq's @base64d.

The second strategy might make sense if the API call produces a very large JSON document.

peak
  • 105,803
  • 17
  • 152
  • 177
0

Say you created a program that output shell commands.

$ data_source | jq -r '@sh "certificate=\( .certificate )\nkey=\( .key )"'
certificate='the certificate'
key='the key'

Then, all you would need to do is evaluate them.

eval "$( data_source | jq -r '@sh "certificate=\( .certificate )\nkey=\( .key )"' )"

If you were dealing with many variables, you could use the following:

eval "$(
   data_source |
   jq -r '
      { "certificate": "certificate", "key": "key" } as $map | 
      ( $map | keys_unsorted[] ) as $shell_var |
      $shell_var + @sh "=\( .[$map[$shell_var]] )"
   '
)"

You could even use paths.

eval "$(
   data_source |
   jq -r '
      { "certificate": [ "certificate" ], "key": [ "key" ] } as $map | 
      ( $map | keys_unsorted[] ) as $shell_var |
      $shell_var + @sh "=\( getpath($map[$shell_var]) )"
   '
)"
ikegami
  • 367,544
  • 15
  • 269
  • 518
0

You can output text like

VAR1=something
VAR2=somethingElse

and eval it. This way you'll have 2 new variables:

eval `API_CALL | jq -r '"VAR1="+.var1,"VAR2="+.var2'