#!/usr/bin/env bash # # Author: Georg Voell - georg.voell@standby.cloud # Version: @(#)convert-json 3.2.1 14.10.2024 (c)2024 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 to a human readable file. #@ #@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. # 04: Key in JSON contains dash ("-") or blank. **jq** does not work here. - this is now obsolete # 05: JSON does not start with "{". # 06: JSON contains less then 2 lines. # 07: **jq** join error. # 99: User interrupt. # # See also: # **print-table**(1), **install-scripts**(1) # # Update history: # # V 1.0.0 17.10.2017 New version # V 2.0.0 22.06.2019 Some fixes # V 3.0.0 02.06.2020 Redesign (was an include) # V 3.0.1 11.06.2020 Using library # V 3.0.2 11.01.2021 Using script "norm-jon" and removed option "strict" # V 3.0.3 27.03.2021 New option --filename (V 3.2.0: Renamed to --import) # V 3.0.4 07.04.2021 Also accept tab separated fields string # V 3.1.0 05.06.2023 New copyright # V 3.1.1 07.09.2023 Check if sed is capable of writing Camel Case # V 3.1.2 28.07.2024 Trapping ctrl-c # V 3.1.3 09.08.2024 Also use print-table with output format "json" (instead of just cat) # V 3.2.0 12.08.2024 New minor version # V 3.2.1 14.10.2024 Converted to bash # # Find executable bash library and source it lib=`which lib.bash 2>/dev/null | sed 's|^no 'lib.bash' in .*||'` if [ "$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 -le $maxlevel ]; do filecheck -rm ${scratchfile}.keys.$i let 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 [ "$key" = "" ]; then jqkey="." curlevel=0 else key=`echo "$key" | tr -s '/' | sed 's|^/||' | sed 's|/$||'` jqkey=`echo "/${key}/" | sed 's|\(\[[0-9]*\]\)*/|\"\1\.\"|g'` jqkey=`echo "$jqkey" | sed 's|^"||' | sed 's|\."$||' | sed 's|""||'` ### | sed 's|\\\|\\\\\\\\|g'` curlevel=`echo "/$key" | tr '[' '/' | tr -cd '/' | wc -c` fi keysfile="${keysfile}.$curlevel" if [ "$DEBUG_CJ" = true ]; then echo "Start: curlevel: '$curlevel' - orgkey: '$orgkey' - key: '$key' - jqkey: '$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 -lt $elements ]; do result=`"$jq" -r 'nth('$i')' "$keysfile" | tr '\n' '\t'` elkey=`echo "$result" | cut -d$'\t' -f1` eltype=`echo "$result" | cut -d$'\t' -f2` elvalue=`echo "$result" | cut -d$'\t' -f3` hasChilds=false case "$eltype" in null) elvalue="/null/" ;; object) if [ "$elvalue" != "{}" ]; then hasChilds=true fi elvalue="/{}/" ;; array) if [ "$elvalue" != "[]" ]; then hasChilds=true fi elvalue="/[]/" ;; number | boolean) elvalue="/$elvalue/" ;; esac # # Handle empty values # if [ "$elvalue" = "" ]; then # elvalue="/$elvalue/" # fi result=`echo "$elkey" | grep '^[0123456789]*$'` if [ "$result" != "" ]; then elkey=`echo "[${elkey}]"` fi if [ "$key" = "" ]; then result="$elkey" else if [ "$result" != "" ]; then result=`echo "${key}$elkey"` else result=`echo "${key}/$elkey"` fi fi if [ $level -gt $curlevel -a "$hasChilds" = true ]; then if [ "$DEBUG_CJ" = true ]; then echo "Vor Rekursion: curlevel: '$curlevel' - eltype: '$eltype' - elkey: '$elkey' - result: '$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 let i++ done fi fi } function CreateETSVFile() { local j=0 # iterate over objects in json file local objects=0 # objects in json file local result="" local firstline=true local firstelement=true result=`filecheck -sl $scratchfile` if [ "$result" != "" ]; then printf "" > ${scratchfile}.etsv objects=`"$jq" -r '. | length' $scratchfile` while [ $j -lt $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 let j++ done fi } # Preset noheader=false qopt="" ropt="" nopt="" number="" level="" filename="" # fieldsstr="" # List of fields we have to convert from json formatstr="" # Output format param="" # Loop until all parameters are used up while [ $# -gt 0 ]; do pname=${1} case "$pname" in -i | --import) shift if [ "$1" != "" ]; then if [ "$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 [ "$1" != "" ]; then if [ "$formatstr" = "" ]; then formatstr=`echo "$1" | tolower` if [ "$formatstr" != "table" -a "$formatstr" != "line" -a "$formatstr" != "json" -a "$formatstr" != "etsv" -a "$formatstr" != "tsv" ]; then errstr="Unknown format '$formatstr' after parameter '$pname'. Please choose from 'table', 'line', 'json', 'etsv' or 'tsv'." fi 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 [ "$1" != "" ]; then if [ "$number" = "" ]; then number="$1" range=`echo "$number" | grep '^[0123456789]*$'` if [ "$range" = "" ]; 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 [ "$1" != "" ]; then if [ "$level" = "" ]; then level="$1" range=`echo "$level" | grep '^[0123456789]*$'` if [ "$range" = "" ]; then errstr="Invalid number after option '$pname'." else if [ $level -lt 0 -o $level -gt $maxlevel ]; then errstr="Please choose between '0' and '$maxlevel' for option '$pname'." fi 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 paramck=`echo "$pname" | grep '^-'` # Keys don't begin with '-' if [ "$paramck" != "" ]; then errstr="Unknown option '$pname'." else if [ "$errstr" = "" ]; then if [ "$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 output to line if not specified if [ "$formatstr" = "" ]; then formatstr="line" fi if [ "$level" = "" ]; then level=0 fi # Check if he have jq in path and right version result=`check-version jq --min 1.5` jq=`echo "$result" | cut -d' ' -f1` jqvers=`echo "$result" | cut -d' ' -f2` jqversok=`echo "$result" | cut -d' ' -f3` if [ "$jqversok" != "ok" ]; then # At least jq version 1.5 is needed for 'first(inputs)', 'keys_unsorted' and 'nth(0)' exitcode=2 errormsg $qopt $exitcode "($progstr) No 'jq' in '$PATH' or version is less than '1.5'." ClearSTDIN exit $exitcode else if [ "$filename" != "" ]; then norm-json --import "$filename" $nopt $qopt $ropt > $scratchfile 2>&1 stat=$? else if [ ! -t 0 ]; then cat /dev/stdin | 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 [ "$qopt" = "" ]; then errrsn=`head -n 2 $scratchfile` printf "${errrsn}\n" ### Don't change this line fi Cleanup exit $exitcode else if [ "$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 echo "" > /tmp/keysfile fi # Create enhanced tsv file CreateETSVFile if [ "$DEBUG_CJ" = true ]; then cat ${scratchfile}.etsv > /tmp/result fi if [ "$ENVELOPE_TABLE" != false -a "$formatstr" = "json" ]; then print-table --import ${scratchfile}.etsv --output $formatstr "$param" | sed 's|\("contentItems":.*\)|\1,\n "creator": "'$progstr'"|' else if [ "$noheader" = true -a "$formatstr" = "etsv" -o "$noheader" = true -a "$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