6

I need to generate JSON output as below:

[{
    "Service": "service1-name",
    "AWS Account": "service1-dev",
    "AD Accounts": {
        "APP-Service1-Admin": ["a", "b"],
        "APP-Service1-View": ["c", "d"]
    }
}
]

I'm trying this shell script, but I can't insert inner info

#!/bin/bash
innerAD=$(jq -n --arg aaaa aaaa \
              --arg xxxx "xxxx" \
              '$ARGS.named'
)
inner=$(jq -n --argjson "APP-Service1-Admin" "[$innerAD]" \
              '$ARGS.named'
)
final=$(jq -n --arg Service "service1-name" \
              --arg "AWS Account" "service1-dev" \
              --argjson "AD Accounts" "[$inner]" \
              '$ARGS.named'
)
echo "$final"
2

4 Answers 4

6

One suggestion is to use --args with jq to create the two arrays and then collect these in the correct location in the main document. Note that --args is required to be the last option on the command line and that all the remaining command line arguments will become elements of the $ARGS.positional array.

{
    jq -n --arg key APP-Service1-Admin '{($key): $ARGS.positional}' --args a b
    jq -n --arg key APP-Service1-View  '{($key): $ARGS.positional}' --args c d
} |
jq -s --arg key 'AD Accounts' '{($key): add}' |
jq --arg Service service1-name --arg 'AWS account' service1-dev '$ARGS.named + .'

The first two jq invocations create a set of two JSON objects:

{
  "APP-Service1-Admin": [
    "a",
    "b"
  ]
}
{
  "APP-Service1-View": [
    "c",
    "d"
  ]
}

The third jq invocation uses -s to read that set into an array, which becomes a merged object when passed through add. The merged object is assigned to our top-level key:

{
  "AD Accounts": {
    "APP-Service1-Admin": [
      "a",
      "b"
    ],
    "APP-Service1-View": [
      "c",
      "d"
    ]
  }
}

The last jq adds the remaining top-level keys and their values to the object:

{
  "Service": "service1-name",
  "AWS account": "service1-dev",
  "AD Accounts": {
    "APP-Service1-Admin": [
      "a",
      "b"
    ],
    "APP-Service1-View": [
      "c",
      "d"
    ]
  }
}

With jo:

jo -d . \
    Service=service1-name \
    'AWS account'=service1-dev  \
    'AD Accounts.APP-Service1-Admin'="$(jo -a a b)" \
    'AD Accounts.APP-Service1-View'="$(jo -a c d)"

The "internal" object is created using .-notation (enabled with -d .), and a couple of command substitutions for creating the arrays.

Or you can drop the -d . and use a form of array notation:

jo  Service=service1-name \
    'AWS account'=service1-dev \
    'AD Account[APP-Service1-Admin]'="$(jo -a a b)" \
    'AD Account[APP-Service1-View]'="$(jo -a c d)"
2
  • Can I pass a variable on jo -a command? I have a variable like: MyVAR="'Surname1, Name1 (Role1)' 'Surname2, Name2 (Role2)'" Mar 9, 2023 at 14:25
  • 1
    @MarcoFerrara If you have an array, like myvar=('Surname1, Name1 (Role1)' 'Surname2, Name2 (Role2)'), then you would pass that to jo -a like so: jo -a "${myvar[@]}". If your variable is a single string, as you show, including literal single quotes etc., then you will want to preprocess that with some tool that is able to isolate the elements correctly and assign them to an array
    – Kusalananda
    Mar 9, 2023 at 14:33
4

I often use heredocs when creating complicated json objects in bash:

service=$(thing-what-gets-service)
account=$(thing-what-gets-account)
admin=$(jo -a $(thing-what-gets-admin))
view=$(jo -a $(thing-what-gets-view))

read -rd '' json <<EOF
[
    {
        "Service": "$service",
        "AWS Account": "$account",
        "AD Accounts": {
            "APP-Service1-Admin": $admin,
            "APP-Service1-View": $view
        }
    }
]
EOF

This uses jo to create the arrays as it's a pretty simple way to do it but it could be done differently if needed.

5
  • 1
    Yes. For this use case, a heredoc can be thought of as a special kind of quoting. It's pretty good for SQL commands, too. It's not perfect, but avoids the need to create double- and triple-backslashes to protect characters like " and ' (double-quote and single-quote).
    – Sotto Voce
    Mar 8, 2023 at 18:27
  • 1
    @SottoVoce Many database command line tools allows you to use parametrised SQL queries, e.g. psql --variable passwd='mypassword' <<<"ALTER USER me WITH PASSWORD :'passwd'" (using here-string for brevity). This takes care of quoting. jesse_b's solution here is ok only because we know that $admin and $view already are correctly encoded JSON documents, but I'm uneasy about $service and $account (he could have encoded them easily with jo too).
    – Kusalananda
    Mar 8, 2023 at 19:56
  • 1
    @Kusalananda I'm sure there are some gotchas to doing it this way but they would most likely have to be intentionally malicious as the common issues with whitespace and even new lines wouldn't cause an issue. I could have done it all with jo but the only way I know how would be a far less readable combination of unmanageable nested command substitutions.
    – jesse_b
    Mar 8, 2023 at 20:12
  • 1
    Actually I guess it would choke on newlines, but not whitespace.
    – jesse_b
    Mar 8, 2023 at 20:20
  • 1
    String concatenation is absolutely the wrong way to create JSON. But every time I have to use jq I end up spending 4 hours going through documentation to figure out how to use it, so +1 from me!
    – miken32
    Mar 9, 2023 at 17:32
2

Another jo answer:

# 2 shell arrays
admins=("a" "b")
views=("c" "d")

accounts=$(jo "${admins[@]/#/APP_Service1-Admin[]=}" "${views[@]/#/APP_Service1-View[]=}")

final=$(jo "Service=service1-name" "AWS Account=service1-dev" "AD Accounts=$accounts")

echo "$final" | jq .
{
  "Service": "service1-name",
  "AWS Account": "service1-dev",
  "AD Accounts": {
    "APP_Service1-Admin": [
      "a",
      "b"
    ],
    "APP_Service1-View": [
      "c",
      "d"
    ]
  }
}

This is the tricky bit: "${admins[@]/#/APP_Service1-Admin[]=}"

$ printf '%s\n' "${admins[@]/#/APP_Service1-Admin[]=}"
APP_Service1-Admin[]=a
APP_Service1-Admin[]=b

That lets you build up a list from a shell array.

2

xo(1) is a great utility for generating JSON on the command line. It comes from the libxo project developed by Juniper. Conveniently, it has been preinstalled on FreeBSD since 11.0-RELEASE and can output JSON and other formats like XML. This is the mechanism used by many FreeBSD administration tools (e.g., ps(1)) to support machine-friendly output formats.

Here are output examples from the manual page.

$ xo --style text "The {k:name} weighs {:weight/%d} pounds.\n" fish 6
The fish weighs 6 pounds.

$ xo --style xml "The {k:name} weighs {:weight/%d} pounds.\n" fish 6
<name>fish</name>
<weight>6</weight>

$ xo --style json "The {k:name} weighs {:weight/%d} pounds.\n" fish 6
"name": "fish",
"weight": 6
"name": "fish",

$ xo --style html "The {k:name} weighs {:weight/%d} pounds.\n" fish 6
<div class="line">
     <div class="text">The </div>
     <div class="data" data-tag="name">fish</div>
     <div class="text"> weighs </div>
     <div class="data" data-tag="weight">6</div>
     <div class="text"> pounds.</div>
</div>

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .