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.
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'
# 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"
Cela nécessite un tout nouveau (probablement 1,5?) jq
pour fonctionner et constitue un bloc de code dense. Pour le décomposer:
-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).[$keys, $vals] | transpose[]
, nous générons des paires [key, value]
(en termes Python, compresser les deux listes).{key:.[0],value:.[1]}
, nous transformons chaque paire [key, value]
en un objet de la forme {"key": key, "value": value}
from_entries
, nous combinons ces paires en objets contenant ces clés et valeurs.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"
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.
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"
}
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""
}
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"
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%" ...
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" }
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/
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": ""
}
]