#!/usr/bin/env bash # # Author: Georg Voell - georg.voell@standby.cloud # Version: @(#)convert-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. # #@ Reads a JSON file and converts it into a human readable format. #@ #@Usage: convert-json [options] [keys] #@ Options: #@ -h, --help : Displays helptext. #@ -v, --version : Displays the version of the script. #@ -n, --noheader : Don't write a header (needed for concatenating files) in output "tsv" or "etsv". #@ -q, --quiet : Just write error message to LOGFILE. #@ -r, --raw : Don't convert JSON e.g. hyphens: lifecycle-state to lifecycleState #@ -o, --output : Output format: can be "line" (default), "json", "etsv", "tsv" or "table". #@ -i, --import : JSON file to read from - if not specified, read from stdin. #@ -s, --select : Select only the nth JSON item (starting with 1 for the first record). #@ -l, --level : Depth for analyzing JSON file (0 is default). #@ Keys: Optional - Select the keys you want to convert. Use a colon (or tab) separated string for keys. #@ If empty - display all keys in JSON. # # Exit codes: # 01: Unknown or wrong parameter. # 02: **jq** not found. This script needs **jq** to perform. # 03: JSON error. # 05: JSON does not start with "{". # 06: JSON contains less then 2 lines. # 07: **jq** join error. # 99: User interrupt. # # Update history: # # V 1.0.0 17.10.2017 New version # V 3.0.0 02.06.2020 Redesign # V 3.0.2 11.01.2021 Using script "norm-json" # V 3.1.0 05.06.2023 New copyright # V 3.2.0 12.08.2024 New minor version # V 3.2.1 14.10.2024 Converted to bash # 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 [[ =~ ]]; replaced let with (( )); # UUOC removed; replaced [ -a ] with && and [ -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 # Defined maximal depth to analyze JSON structure readonly maxlevel=3 # Do extra cleanup function ExtraCleanup() { local i=0 filecheck -rm "${scratchfile}.object" filecheck -rm "${scratchfile}.etsv" while (( i <= maxlevel )); do filecheck -rm "${scratchfile}.keys.${i}" (( i++ )) done } function DisplayElements() { local displayheader="${1}" local key="${2}" local i=0 local stat=0 local elements=0 local curlevel=0 local hasChilds=false local keysfile="${scratchfile}.keys" local result="" local jqkey="" local elkey="" local eltype="" local elvalue="" local orgkey="${2}" if [[ -r "${scratchfile}.object" ]]; then if [[ -z "$key" ]]; then jqkey="." curlevel=0 else # Clean up key path with bash built-ins where possible key=$(printf '%s' "$key" | tr -s '/' | sed 's|^/||' | sed 's|/$||') jqkey=$(printf '/%s/' "$key" | sed 's|\(\[[0-9]*\]\)*/|"\1."|g') jqkey=$(printf '%s' "$jqkey" | sed 's|^"||' | sed 's|\."$||' | sed 's|""||') # Count array depth using bash built-in (no wc subshell) local tmp="/${key}" tmp="${tmp//[^[/]}" # keep only '[' and '/' curlevel=${#tmp} # More accurate: count '[' chars local tmp2="${key//[^\[]/}" local arr_depth=${#tmp2} local slash_depth="${key//[^\/]/}" curlevel=$(( ${#slash_depth} + arr_depth )) fi keysfile="${keysfile}.${curlevel}" if [[ "$DEBUG_CJ" == true ]]; then printf "Start: curlevel: '%s' - orgkey: '%s' - key: '%s' - jqkey: '%s'\n" \ "$curlevel" "$orgkey" "$key" "$jqkey" >> /tmp/keysfile fi elements=$("$jq" -r "${jqkey} | length" "${scratchfile}.object") stat=$? if [[ $stat -gt 0 ]]; then ShowVariable "(after length) key" "$key" ShowVariable "(after length) jqkey" "$jqkey" fi "$jq" "${jqkey} | keys_unsorted, map(type), map(.)" "${scratchfile}.object" > "$keysfile" stat=$? if [[ $stat -gt 0 ]]; then ShowVariable "(after map) key" "$key" ShowVariable "(after map) jqkey" "$jqkey" fi if [[ "$DEBUG_CJ" == true ]]; then cat "$keysfile" >> /tmp/keysfile fi if [[ -r "$keysfile" ]]; then i=0 while (( i < elements )); do # Avoid UUOC: redirect directly result=$("$jq" -r "nth(${i})" "$keysfile" | tr '\n' '\t') elkey="${result%%$'\t'*}" local rest="${result#*$'\t'}" eltype="${rest%%$'\t'*}" elvalue="${rest#*$'\t'}" elvalue="${elvalue%%$'\t'*}" hasChilds=false case "$eltype" in null) elvalue="/null/" ;; object) [[ "$elvalue" != "{}" ]] && hasChilds=true elvalue="/{}/"; ;; array) [[ "$elvalue" != "[]" ]] && hasChilds=true elvalue="/[]/"; ;; number | boolean) elvalue="/${elvalue}/" ;; esac # Check if elkey is a pure number (use bash regex, no subshell) if [[ "$elkey" =~ ^[0-9]+$ ]]; then elkey="[${elkey}]" result="${key}${elkey}" else if [[ -z "$key" ]]; then result="$elkey" else result="${key}/${elkey}" fi fi if (( level > curlevel )) && [[ "$hasChilds" == true ]]; then if [[ "$DEBUG_CJ" == true ]]; then printf "Vor Rekursion: curlevel: '%s' - eltype: '%s' - elkey: '%s' - result: '%s'\n" \ "$curlevel" "$eltype" "$elkey" "$result" >> /tmp/keysfile fi DisplayElements "$displayheader" "$result" >> "${scratchfile}.etsv" else if [[ "$displayheader" == true ]]; then if [[ "$firstelement" == true ]]; then firstelement=false printf "%s" "$result" >> "${scratchfile}.etsv" else printf "\t%s" "$result" >> "${scratchfile}.etsv" fi else if [[ "$firstelement" == true ]]; then firstelement=false printf "%s" "$elvalue" >> "${scratchfile}.etsv" else printf "\t%s" "$elvalue" >> "${scratchfile}.etsv" fi fi fi (( i++ )) done fi fi } function CreateETSVFile() { local j=0 local objects=0 local result="" local firstline=true local firstelement=true result=$(filecheck -sl "$scratchfile") if [[ -n "$result" ]]; then printf "" > "${scratchfile}.etsv" # Avoid UUOC: redirect directly objects=$("$jq" -r '. | length' "$scratchfile") while (( j < objects )); do "$jq" "nth(${j})" "$scratchfile" > "${scratchfile}.object" if [[ "$firstline" == true ]]; then # Print header line firstline=false DisplayElements "true" "" >> "${scratchfile}.etsv" printf "\n" >> "${scratchfile}.etsv" fi firstelement=true DisplayElements "false" "" >> "${scratchfile}.etsv" printf "\n" >> "${scratchfile}.etsv" (( j++ )) done fi } # Preset noheader=false qopt="" ropt="" nopt="" number="" level="" filename="" formatstr="" param="" # Loop until all parameters are used up while [[ $# -gt 0 ]]; do pname="${1}" case "$pname" 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 ;; -o | --output) shift if [[ -n "$1" ]]; then if [[ -z "$formatstr" ]]; then formatstr=$(ToLower "$1") case "$formatstr" in table|line|json|etsv|tsv) ;; *) errstr="Unknown format '$formatstr' after parameter '$pname'. Please choose from 'table', 'line', 'json', 'etsv' or 'tsv'." ;; esac else errstr="Option '$pname' used more then once." fi shift else errstr="Please specify a format ('table', 'line', 'json', 'etsv' or 'tsv') 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'." else nopt="$pname $number" fi else errstr="Option '$pname' used more then once." fi shift else errstr="Please specify a number after option '$pname'." fi ;; -l | --level) shift if [[ -n "$1" ]]; then if [[ -z "$level" ]]; then level="$1" if [[ ! "$level" =~ ^[0-9]+$ ]]; then errstr="Invalid number after option '$pname'." elif (( level < 0 || level > maxlevel )); then errstr="Please choose between '0' and '$maxlevel' for 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 ropt="$pname" ;; -q | --quiet) shift qopt="$pname" ;; -n | --noheader) shift noheader=true ;; -v | --version) shift showversion=true ;; -h | --help) shift showhelp=true ;; *) shift if [[ "$pname" == -* ]]; then errstr="Unknown option '$pname'." else if [[ -z "$errstr" ]]; then if [[ -z "$param" ]]; then param="$pname" else errstr="Keys were already specified '$param'. Unknown additional parameter: '$pname'." fi fi fi ;; esac done # Display help or error message DisplayHelp # Set defaults if [[ -z "$formatstr" ]]; then formatstr="line"; fi if [[ -z "$level" ]]; then level=0; fi # Check if we have jq in path and right version result=$(check-version jq --min 1.5) jq=$(printf '%s' "$result" | cut -d' ' -f1) jqvers=$(printf '%s' "$result" | cut -d' ' -f2) jqversok=$(printf '%s' "$result" | cut -d' ' -f3) if [[ "$jqversok" != "ok" ]]; then exitcode=2 errormsg $qopt $exitcode "($progstr) No 'jq' in '$PATH' or version is less than '1.5'." ClearSTDIN exit $exitcode else if [[ -n "$filename" ]]; then norm-json --import "$filename" $nopt $qopt $ropt > "$scratchfile" 2>&1 stat=$? else if [[ ! -t 0 ]]; then # Avoid UUOC: feed stdin directly to norm-json norm-json $nopt $qopt $ropt > "$scratchfile" 2>&1 stat=$? else exitcode=3 errormsg $qopt $exitcode "($progstr) No filename specified and stdin seems to be empty." exit $exitcode fi fi if [[ $stat -gt 0 ]]; then exitcode=3 if [[ -z "$qopt" ]]; then errrsn=$(head -n 2 "$scratchfile") printf '%s\n' "$errrsn" fi Cleanup exit $exitcode else if [[ -n "$nopt" ]]; then mv -f "$scratchfile" "${scratchfile}.object" printf '[\n' > "$scratchfile" cat "${scratchfile}.object" >> "$scratchfile" printf ']\n' >> "$scratchfile" rm -f "${scratchfile}.object" fi if [[ "$DEBUG_CJ" == true ]]; then printf '' > /tmp/keysfile fi # Create enhanced tsv file CreateETSVFile if [[ "$DEBUG_CJ" == true ]]; then cat "${scratchfile}.etsv" > /tmp/result fi if [[ "$ENVELOPE_TABLE" != false && "$formatstr" == "json" ]]; then print-table --import "${scratchfile}.etsv" --output "$formatstr" "$param" \ | sed 's|\("contentItems":.*\)|\1,\n "creator": "'"$progstr"'"|' else if [[ "$noheader" == true && ( "$formatstr" == "etsv" || "$formatstr" == "tsv" ) ]]; then print-table --import "${scratchfile}.etsv" --output "$formatstr" "$param" | tailfromline2 else print-table --import "${scratchfile}.etsv" --output "$formatstr" "$param" fi fi fi fi # Cleanup and exit Cleanup exit $exitcode