#!/usr/bin/env bash # # Author: Georg Voell - georg.voell@standby.cloud # Version: @(#)norm-json 3.3.1 16.03.2026 (c)2026 Standby.cloud # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ # # This script can be used free of charge. Use it as is or customize as needed. It is not guaranteed to be # error free and you will not be reimbursed for any damage it may cause. # #@ Normalize JSON items and pretty print them. #@ #@Usage: norm-json [options] #@ Options: #@ -h, --help : Displays helptext. #@ -v, --version : Displays the version of the script. #@ -q, --quiet : Just write error message to LOGFILE. #@ -r, --raw : Don't convert hyphens e.g. lifecycle-state to lifecycleState #@ -i, --import : Read from a file ( = filename) - if not specified, read from stdin. #@ -s, --select : Select only the nth JSON item (starting with 1 for the first item). # # Exit codes: # 01: Unknown or wrong parameter. # 02: No **jq** or wrong version of **jq**. This script needs **jq** to format "json". # 03: No input or empty file. # 04: JSON file contains less then 2 lines. # 05: Error in JSON file. # 06: JSON file doesn't contain specified item. # 99: User interrupt. # # Update history: # # V 3.0.0 10.01.2021 New version # V 3.1.0 05.06.2023 New copyright # V 3.2.0 11.08.2024 New minor version: New normalized JSON format # V 3.2.1 01.12.2024 Convert \\ to | # V 3.2.2 14.09.2025 Small changes # V 3.3.0 19.01.2026 Revised with support of Claude Code # V 3.3.1 16.03.2026 Optimized: replaced backticks with $(); [ ] with [[ ]]; # echo|grep with [[ =~ ]]; UUOC removed; (( )) for arithmetic; # replaced [ -a ] with &&; [ -o ] with || # # Find executable bash library and source it lib=$(command -v lib.bash 2>/dev/null) if [[ -n "$lib" ]]; then source "$lib" else progdir=$(dirname "$0") if [[ -r "${progdir}/lib.bash" ]]; then source "${progdir}/lib.bash" else echo "Unexpected error: Unable to locate bash library 'lib.bash'." exit 1 fi fi # Do extra cleanup function ExtraCleanup() { filecheck -rm "${scratchfile}.pre" } # Presets raw=false qopt="" number="" param="" filename="" result="" items=0 lines=0 # Loop until all parameters are used up while [[ $# -gt 0 ]]; do pname="${1}" case "$1" in -i | --import) shift if [[ -n "$1" ]]; then if [[ -z "$filename" ]]; then filename="$1" if [[ ! -r "$filename" ]]; then errstr="Can't read from filename '$filename'." fi else errstr="Option '$pname' used more then once." fi shift else errstr="Please specify a filename after parameter '$pname'." fi ;; -s | --select) shift if [[ -n "$1" ]]; then if [[ -z "$number" ]]; then number="$1" if [[ ! "$number" =~ ^[0-9]+$ ]]; then errstr="Invalid number after option '$pname'." fi else errstr="Option '$pname' used more then once." fi shift else errstr="Please specify a number after option '$pname'." fi ;; -r | --raw) shift raw=true ;; -v | --version) shift showversion=true ;; -h | --help) shift showhelp=true ;; -q | --quiet) shift qopt="$pname" ;; *) shift errstr="Unknown parameter: '$pname'." ;; esac done # Display help or error message DisplayHelp # Check if we have jq in path and the right version result=$(check-version jq --min 1.5) jqvers=$(printf '%s' "$result" | cut -d' ' -f2) jqok=$(printf '%s' "$result" | cut -d' ' -f3) # Check if sed can convert dashes to Camel Case (requires GNU sed) sedok=$(printf 'my-test' | sed 's|-\(.\)|\U\1|g') [[ "$sedok" == "myTest" ]] && sedok="ok" if [[ "$jqok" != "ok" || "$sedok" != "ok" ]]; then result="" exitcode=2 if [[ "$jqok" != "ok" ]]; then jqok="jq" [[ "$OS" == "Darwin" ]] && result="Install 'jq' with 'brew install jq'" else jqok="sed" [[ "$OS" == "Darwin" ]] && result="Install 'gsed' with 'brew install gnu-sed'" fi errormsg $qopt $exitcode "($progstr) No '$jqok' in '$PATH' or not the required version." "$result" ClearSTDIN exit $exitcode else # Read from stdin or file - avoid UUOC: redirect directly into sed+grep if [[ -n "$filename" ]]; then sed 's|^//.*$||;s|\\\\|:|g' < "$filename" | grep . > "$scratchfile" else if [[ ! -t 0 ]]; then sed 's|^//.*$||;s|\\\\|:|g' | grep . > "$scratchfile" else exitcode=3 errormsg $qopt $exitcode "($progstr) No filename specified and stdin seems to be empty." exit $exitcode fi fi # Check JSON syntax with jq - read first char with bash built-in read -r -n 1 result < "$scratchfile" if [[ "$result" == "[" ]]; then jq '.[]' < "$scratchfile" > "${scratchfile}.pre" 2>&1 stat=$? else jq '.' < "$scratchfile" > "${scratchfile}.pre" 2>&1 stat=$? fi if [[ $stat -gt 0 ]]; then errrsn=$(head -n 1 "${scratchfile}.pre") exitcode=4 errormsg $qopt $exitcode "($progstr) Tool 'jq' detected an error in JSON input." "$errrsn" Cleanup exit $exitcode else # Check for at least 2 lines in JSON result=$(filecheck -sl "${scratchfile}.pre") if [[ -z "$result" ]]; then exitcode=4 result=$(cat "${scratchfile}.pre") if [[ -z "$result" ]]; then if [[ -n "$filename" ]]; then errormsg $qopt $exitcode "($progstr) JSON file '$filename' is empty." else errormsg $qopt $exitcode "($progstr) JSON file is empty." fi else errormsg $qopt $exitcode "($progstr) JSON file contains only one line: '$result'." fi Cleanup exit $exitcode else if [[ "$raw" == true ]]; then mv -f "${scratchfile}.pre" "$scratchfile" stat=$? else # Analyze JSON format - avoid UUOC result=$(head -n 2 "${scratchfile}.pre" | tr -d '\n' | tr -d ' ' | cut -d':' -f1) case "$result" in '{"content"') raw=true jq '.content' < "${scratchfile}.pre" > "$scratchfile" stat=$? ;; '{"items"') raw=true jq '.items' < "${scratchfile}.pre" > "$scratchfile" stat=$? ;; '{"data"') result=$(grep '^ "resources":' "${scratchfile}.pre" | head -n 10) if [[ -n "$result" ]]; then jq '.data.resources' < "${scratchfile}.pre" > "$scratchfile" else jq '.data' < "${scratchfile}.pre" > "$scratchfile" fi stat=$? ;; '{"result"') jq '.result' < "${scratchfile}.pre" > "$scratchfile" stat=$? ;; *) mv -f "${scratchfile}.pre" "$scratchfile" stat=$? ;; esac fi if [[ $stat -gt 0 ]]; then exitcode=5 result=$(cat "$scratchfile") if [[ -z "$result" ]]; then errormsg $qopt $exitcode "($progstr) JSON file is empty." else errormsg $qopt $exitcode "($progstr) JSON file contains only one line: '$result'." fi Cleanup exit $exitcode else # Check for array - use bash built-in read for first char read -r -n 1 result < "$scratchfile" if [[ "$result" == "[" ]]; then jq --slurp add "$scratchfile" > "${scratchfile}.pre" 2>/dev/null stat=$? else result=$(compare-version "1.6" "$jqvers") if [[ "$result" == "older" ]]; then # Workaround for older jq version printf '[\n' > "${scratchfile}.pre" tr -d '\n' < "$scratchfile" | sed 's|}{|},{|g' >> "${scratchfile}.pre" printf '\n]\n' >> "${scratchfile}.pre" mv -f "${scratchfile}.pre" "$scratchfile" jq --slurp add "$scratchfile" > "${scratchfile}.pre" 2>/dev/null stat=$? else jq --slurp < "$scratchfile" > "${scratchfile}.pre" 2>/dev/null stat=$? fi fi if [[ $stat -gt 0 ]]; then exitcode=4 errormsg $qopt $exitcode "($progstr) Tool 'jq' detected an error in JSON input." "Slurp conversion" Cleanup exit $exitcode else # If specified - select item if [[ -n "$number" ]]; then items=$(jq -r '. | length' < "${scratchfile}.pre") if (( number < 1 || number > items )); then if (( items == 1 )); then result="There is only item '1'" else result="Please select between '1' and '${items}'" fi exitcode=6 errormsg $qopt $exitcode "($progstr) JSON file doesn't contain specified item: '$number'." "$result" Cleanup exit $exitcode else (( number-- )) jq -M "nth(${number})" < "${scratchfile}.pre" > "$scratchfile" # Avoid UUOC: wc directly lines=$(wc -l < "$scratchfile") if (( lines > 1 )); then mv -f "$scratchfile" "${scratchfile}.pre" else jq -r . < "$scratchfile" > "${scratchfile}.pre" raw=true fi fi fi # No conversion to Camel in raw mode if [[ "$raw" == true ]]; then cat "${scratchfile}.pre" else while IFS=':' read -r key value; do if [[ -n "$key" && -n "$value" ]]; then key=$(printf '%s' "$key" | sed 's|-\([a-z]\)|\U\1|g' | sed 's|\([MGT]\)b\([s]*\)"|\1B\2"|g') printf "%s:%s\n" "$key" "$value" else printf "%s\n" "$key" fi done < "${scratchfile}.pre" fi fi fi fi fi fi # Cleanup and exit Cleanup exit $exitcode