web-dev-qa-db-fra.com

Créer un JSON en utilisant jq à partir de clés et de valeurs séparées par des tubes dans bash

J'essaie de créer un objet JSON à partir d'une chaîne dans bash. La chaîne est la suivante. 

CONTAINER|CPU%|MEMUSAGE/LIMIT|MEM%|NETI/O|BLOCKI/O|PIDS
nginx_container|0.02%|25.09MiB/15.26GiB|0.16%|0B/0B|22.09MB/4.096kB|0

La sortie provient de la commande docker stats et mon objectif final est de publier des métriques personnalisées sur aws cloudwatch. Je voudrais formater cette chaîne comme json. 

{
    "CONTAINER":"nginx_container",
    "CPU%":"0.02%", 
    ....
}

J'ai déjà utilisé jq command auparavant et il semble que cela devrait bien fonctionner dans ce cas, mais je n'ai pas encore trouvé de bonne solution. Autre que le codage en dur des noms de variables et l'indexation à l'aide de sed ou awk. Ensuite, créer un JSON à partir de zéro. Toute suggestion serait appréciée. Merci. 

14
michael_65

Prérequis

Pour tout ce qui suit, il est supposé que votre contenu se trouve dans une variable Shell nommée s:

s='CONTAINER|CPU%|MEMUSAGE/LIMIT|MEM%|NETI/O|BLOCKI/O|PIDS
nginx_container|0.02%|25.09MiB/15.26GiB|0.16%|0B/0B|22.09MB/4.096kB|0'

Quoi (jq moderne)

# thanks to @JeffMercado and @chepner for refinements, see comments
jq -Rn '
( input  | split("|") ) as $keys |
( inputs | split("|") ) as $vals |
[[$keys, $vals] | transpose[] | {key:.[0],value:.[1]}] | from_entries
' <<<"$s"

Comment (jq moderne)

Cela nécessite un tout nouveau (probablement 1,5?) jq pour fonctionner et constitue un bloc de code dense. Pour le décomposer:

  • Utiliser -n empêche jq de lire stdin par lui-même, laissant l'intégralité du flux d'entrée disponible pour être lu par input et inputs, le premier permettant de lire une seule ligne et le second de lire toutes les lignes restantes. (-R, pour une entrée brute, provoque la lecture de lignes textuelles plutôt que d'objets JSON).
  • Avec [$keys, $vals] | transpose[], nous générons des paires [key, value] (en termes Python, compresser les deux listes).
  • Avec {key:.[0],value:.[1]}, nous transformons chaque paire [key, value] en un objet de la forme {"key": key, "value": value}
  • Avec from_entries, nous combinons ces paires en objets contenant ces clés et valeurs.

Quoi (assisté par Shell)

Cela fonctionnera avec une jq beaucoup plus ancienne que la précédente, et constitue une approche facile à adopter pour les scénarios dans lesquels une solution native -jq peut être plus difficile à traiter:

{
   IFS='|' read -r -a keys # read first line into an array of strings

   ## read each subsequent line into an array named "values"
   while IFS='|' read -r -a values; do

    # setup: positional arguments to pass in literal variables, query with code    
    jq_args=( )
    jq_query='.'

    # copy values into the arguments, reference them from the generated code    
    for idx in "${!values[@]}"; do
        [[ ${keys[$idx]} ]] || continue # skip values with no corresponding key
        jq_args+=( --arg "key$idx"   "${keys[$idx]}"   )
        jq_args+=( --arg "value$idx" "${values[$idx]}" )
        jq_query+=" | .[\$key${idx}]=\$value${idx}"
    done

    # run the generated command
    jq "${jq_args[@]}" "$jq_query" <<<'{}'
  done
} <<<"$s"

Comment (assisté par Shell)

La commande invoquée jq décrite ci-dessus est similaire à:

jq --arg key0   'CONTAINER' \
   --arg value0 'nginx_container' \
   --arg key1   'CPU%' \
   --arg value1 '0.0.2%' \
   --arg key2   'MEMUSAGE/LIMIT' \
   --arg value2 '25.09MiB/15.26GiB' \
   '. | .[$key0]=$value0 | .[$key1]=$value1 | .[$key2]=$value2' \
   <<<'{}'

... en passant chaque clé et valeur hors bande (de sorte qu'elle est traitée comme une chaîne littérale plutôt que analysée comme JSON), puis en y faisant référence individuellement.


Résultat

L'une ou l'autre de ces réponses émettra:

{
  "CONTAINER": "nginx_container",
  "CPU%": "0.02%",
  "MEMUSAGE/LIMIT": "25.09MiB/15.26GiB",
  "MEM%": "0.16%",
  "NETI/O": "0B/0B",
  "BLOCKI/O": "22.09MB/4.096kB",
  "PIDS": "0"
}

Pourquoi

En bref: Parce qu'il est garanti de générer un JSON valide en sortie .

Considérez ce qui suit comme exemple pour casser des approches plus naïves:

s='key ending in a backslash\
value "with quotes"'

Bien sûr, ce sont des scénarios inattendus, mais jq sait comment les gérer:

{
  "key ending in a backslash\\": "value \"with quotes\""
}

... alors qu'une implémentation qui ne comprenait pas les chaînes JSON pourrait facilement émettre:

{
  "key ending in a backslash\": "value "with quotes""
}
32
Charles Duffy

json_template='{"CONTAINER":"%s","CPU%":"%s","MEMUSAGE/LIMIT":"%s", "MEM%":"%s","NETI/O":"%s","BLOCKI/O":"%s","PIDS":"%s"}' json_string=$(printf "$json_template" "nginx_container" "0.02%" "25.09MiB/15.26GiB" "0.16%" "0B/0B" "22.09MB/4.096kB" "0") echo "$json_string"

N'utilise pas jq mais peut utiliser args et environment dans les valeurs.

CONTAINER=nginx_container json_template='{"CONTAINER":"%s","CPU%":"%s","MEMUSAGE/LIMIT":"%s", "MEM%":"%s","NETI/O":"%s","BLOCKI/O":"%s","PIDS":"%s"}' json_string=$(printf "$json_template" "$CONTAINER" "$1" "25.09MiB/15.26GiB" "0.16%" "0B/0B" "22.09MB/4.096kB" "0") echo "$json_string"

1
NoamG

Voici une solution qui utilise les options -R et -s avec transpose :

   split("\n")                       # [ "CONTAINER...", "nginx_container|0.02%...", ...]
 | (.[0]    | split("|")) as $keys   # [ "CONTAINER", "CPU%", "MEMUSAGE/LIMIT", ... ]
 | (.[1:][] | split("|"))            # [ "nginx_container", "0.02%", ... ] [ ... ] ...
 | select(length > 0)                # (remove empty [] caused by trailing newline)
 | [$keys, .]                        # [ ["CONTAINER", ...], ["nginx_container", ...] ] ...
 | [ transpose[] | {(.[0]):.[1]} ]   # [ {"CONTAINER": "nginx_container"}, ... ] ...
 | add                               # {"CONTAINER": "nginx_container", "CPU%": "0.02%" ...
1
jq170727
JSONSTR=""
declare -a JSONNAMES=()
declare -A JSONARRAY=()
LOOPNUM=0

cat ~/newfile | while IFS=: read CONTAINER CPU MEMUSE MEMPC NETIO BLKIO PIDS; do
    if [[ "$LOOPNUM" = 0 ]]; then
        JSONNAMES=("$CONTAINER" "$CPU" "$MEMUSE" "$MEMPC" "$NETIO" "$BLKIO" "$PIDS")
        LOOPNUM=$(( LOOPNUM+1 ))
    else
        echo "{ \"${JSONNAMES[0]}\": \"${CONTAINER}\", \"${JSONNAMES[1]}\": \"${CPU}\", \"${JSONNAMES[2]}\": \"${MEMUSE}\", \"${JSONNAMES[3]}\": \"${MEMPC}\", \"${JSONNAMES[4]}\": \"${NETIO}\", \"${JSONNAMES[5]}\": \"${BLKIO}\", \"${JSONNAMES[6]}\": \"${PIDS}\" }"
    fi 
done

Résultats:

{ "CONTAINER": "nginx_container", "CPU%": "0.02%", "MEMUSAGE/LIMIT": "25.09MiB/15.26GiB", "MEM%": "0.16%", "NETI/O": "0B/0B", "BLOCKI/O": "22.09MB/4.096kB", "PIDS": "0" }
1
Nick Bull

Vous pouvez demander à docker de vous donner des données JSON en premier lieu

docker stats --format "{{json .}}"

Pour plus d'informations, voir: https://docs.docker.com/config/formatting/

0
MatrixManAtYrService

Si vous commencez avec des données tabulaires, je pense qu'il est plus logique d'utiliser quelque chose qui fonctionne avec les données tabulaires de manière native, comme sqawk pour le transformer en json, puis d'utiliser jq work avec davantage.

echo 'CONTAINER|CPU%|MEMUSAGE/LIMIT|MEM%|NETI/O|BLOCKI/O|PIDS
nginx_container|0.02%|25.09MiB/15.26GiB|0.16%|0B/0B|22.09MB/4.096kB|0' \
        | sqawk -FS '[|]' -RS '\n' -output json 'select * from a' header=1 \
        | jq '.[] | with_entries(select(.key|test("^a.*")|not))'

    {
      "CONTAINER": "nginx_container",
      "CPU%": "0.02%",
      "MEMUSAGE/LIMIT": "25.09MiB/15.26GiB",
      "MEM%": "0.16%",
      "NETI/O": "0B/0B",
      "BLOCKI/O": "22.09MB/4.096kB",
      "PIDS": "0"
    }

Sans jq, sqawk donne un peu trop:

[
  {
    "anr": "1",
    "anf": "7",
    "a0": "nginx_container|0.02%|25.09MiB/15.26GiB|0.16%|0B/0B|22.09MB/4.096kB|0",
    "CONTAINER": "nginx_container",
    "CPU%": "0.02%",
    "MEMUSAGE/LIMIT": "25.09MiB/15.26GiB",
    "MEM%": "0.16%",
    "NETI/O": "0B/0B",
    "BLOCKI/O": "22.09MB/4.096kB",
    "PIDS": "0",
    "a8": "",
    "a9": "",
    "a10": ""
  }
]
0
MatrixManAtYrService