#!/usr/bin/env bash # # Author: Georg Voell - georg.voell@standby.cloud # Version: @(#)print-table 3.2.3 30.11.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 tab separated values file (tsv) and pretty prints it. #@ First row (header) should contain the keys - all other rows are the values for the keys. #@ #@Usage: print-table [options] [keys] #@ Options: #@ -h, --help : Displays helptext. #@ -v, --version : Displays the version of the script. #@ -q, --quiet : Just write error message to LOGFILE. #@ -o, --output : Output format: can be "table" (default), "line", "etsv", "tsv", "csv", "json", "keys" or "plain". #@ -i, --import : Read from a file ( = filename) - if not specified, read from stdin. #@ -g, --green : If is the same as column, display it green. #@ -y, --yellow : If is the same as column, display it yellow. #@ -r, --red : If is the same as column, display it red. #@ -d, --darkgray : If is the same as column, display it gray. #@ -b, --bold : If is the same as column, display it bold. #@ Keys: Optional - Select the keys you want to display. Use a tab (default) or comma separated string for keys. #@ Colored output only used with output set to "table" or "line". #@ JSON output is always enveloped with "content" and amount of items (contentItems). Disable this via 'export ENVELOPE_TABLE=false' #@ #@Examples: #@ #@ printf "cmd\tvers\tos\njq\t1.6\tlinux\nrclone\t1.51.0\tlinux\n" | print-table #@ #@ Displays: #@ #@ cmd vers os #@ ------ ------ ----- #@ jq 1.6 linux #@ rclone 1.51.0 linux # # Exit codes: # 01: Unknown or wrong parameter. # 02: No file and empty stream # 03: File is empty or specified keys not found. # 99: User interrupt. # # See also: # **print-header**(1), **install-scripts**(1) # # Update history: # # V 1.0.0 10.10.2017 New version # V 2.0.0 01.06.2019 Fix for UTF8 (multibyte) chars # V 2.0.1 05.07.2019 Display colors # V 3.0.0 26.04.2020 Option vertical (now line) and Multi OS compatible # V 3.0.1 11.06.2020 Using library # V 3.0.2 21.04.2021 Rewritten using bash instead of tcsh # V 3.1.0 05.06.2023 New copyright # V 3.1.1 08.08.2024 New output types # V 3.2.0 11.08.2024 New minor version: Options --import and keys # V 3.2.1 26.10.2024 Delete leading and trailing slash when output is tsv or csv # V 3.2.2 13.11.2024 Print also non existing keys # V 3.2.3 30.11.2024 Use new ConverKeys function # # 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 # Do extra cleanup function ExtraCleanup() { filecheck -rm ${scratchfile}.pre } # Append a key to a keystring function AppendKey() { local keystring=${1} local key=${2} local result="" if [ "$key" != "" ]; then if [ "$keystring" != "" ]; then printf "%s\t%s\n" "$keystring" "$key" else echo "$key" fi else echo "$keystring" fi } # Only for testing function ReadTsvFile() { local tsvfile=${1} local header="" local line="" local key="" local value="" local i=1 local headerlen=0 local linelen=0 local stat=0 typeset -a linearr # Check if tsv file has at least two lines line=`filecheck -sl $tsvfile` if [ "$line" != "" ]; then # Read header header=`head -n 1 "$tsvfile"` headerlen=`NumKeys "$header"` if [ $headerlen -gt 0 ]; then while IFS=$'\n' read -r line; do # Check if linelen is same as headerlen linelen=`NumKeys "$line"` if [ $linelen -ne $headerlen ]; then return 3 fi # Read all columns in line i=1 unset linearr while [ $i -le $headerlen ]; do key=`GetKey "$header" "$i"` value=`GetKey "$line" "$i"` ValueToArray "$header" "$headerlen" "$key" "$value" "linearr" let i++ done # KeysToArray "$line" "$linelen" "linearr" # Print line i=0 while [ $i -le $headerlen ]; do printf "item: '%s' - value: '%s'\n" "$i" "${linearr[$i]}" let i++ done printf "\n" done < <(tailfromline2 "$tsvfile") else stat=2 fi else stat=1 fi return $stat } # Preset formatstr="" awkstr="" redstr="" greenstr="" yellowstr="" graystr="" boldstr="" filename="" param="" qopt="" # Check parameters: Loop until all parameters are used up while [ $# -gt 0 ]; do pname=${1} case "$pname" in -v | --version) shift showversion=true ;; -h | --help) shift showhelp=true ;; -q | --quiet) shift qopt="$pname" ;; -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" != "etsv" -a "$formatstr" != "tsv" -a "$formatstr" != "csv" -a "$formatstr" != "json" -a "$formatstr" != "keys" \ -a "$formatstr" != "plain" ]; then errstr="Unknown format '$formatstr' after parameter '$pname'. Please choose from 'table', 'line', 'json', 'etsv', 'tsv', 'csv', 'keys' or 'plain'." fi else errstr="Option '$pname' used more then once." fi shift else errstr="Please specify a format ('table', 'line', 'json', 'etsv', 'tsv', 'csv', 'keys' or 'plain') after parameter '$pname'." fi ;; -r | --red) shift if [ "$1" != "" ]; then if [ "$redstr" = "" ]; then redstr=`ConvertKeys "$1"` else errstr="Option '$pname' used more then once." fi shift else errstr="Please specify a string after parameter '$pname'." fi ;; -g | --green) shift if [ "$1" != "" ]; then if [ "$greenstr" = "" ]; then greenstr=`ConvertKeys "$1"` else errstr="Option '$pname' used more then once." fi shift else errstr="Please specify a string after parameter '$pname'." fi ;; -y | --yellow) shift if [ "$1" != "" ]; then if [ "$yellowstr" = "" ]; then yellowstr=`ConvertKeys "$1"` else errstr="Option '$pname' used more then once." fi shift else errstr="Please specify a string after parameter '$pname'." fi ;; -d | --darkgray) shift if [ "$1" != "" ]; then if [ "$graystr" = "" ]; then graystr=`ConvertKeys "$1"` else errstr="Option '$pname' used more then once." fi shift else errstr="Please specify a string after parameter '$pname'." fi ;; -b | --bold) shift if [ "$1" != "" ]; then if [ "$boldstr" = "" ]; then boldstr=`ConvertKeys "$1"` else errstr="Option '$pname' used more then once." fi shift else errstr="Please specify a string after parameter '$pname'." fi ;; *) shift paramck=`echo "$pname" | grep '^-'` # Keys don't begin with '-' if [ "$paramck" != "" ]; then errstr="Unknown option '$pname'." else if [ "$param" = "" ]; then param=`ConvertKeys "$pname"` else errstr="Keys were already specified '$param'. Unknown additional parameter: '$pname'." fi fi esac done # Display help or error message DisplayHelp ### Main # Set output to table if not specified if [ "$formatstr" = "" ]; then formatstr="table" fi # Check if filename is empty and create workfile if [ "$filename" != "" ]; then # Create a workfile without empty lines grep . "$filename" > $scratchfile else if [ ! -t 0 ]; then # We have a stream cat /dev/stdin | grep . > $scratchfile 2>&1 else exitcode=2 errormsg $qopt $exitcode "($progstr) Please specify a file with tab separated values (tsv)." exit $exitcode fi fi # Check if we have a header line="" result=`filecheck -sl $scratchfile` if [ "$result" != "" ]; then line=`head -n 1 "$scratchfile"` if [ "$line" != "" ]; then fc=`echo "$line" | head -c 1` if [ "$fc" = '{' -o "$fc" = '[' -o "$fc" = '"' ]; then # JSON file line="" else columns=`NumKeys "$line"` if [ $columns -lt 1 ]; then line="" else # KeysToArray "$line" "$columns" "headerArr" oldIFS="$IFS"; IFS=$'\t'; headerArr=($columns $line); IFS="$oldIFS" fi fi fi fi #ShowVariable "line" "$line" #ShowVariable "param" "$param" #ShowVariable "colums" "$colums" if [ "$line" != "" ]; then # Parameter given -> Select only what was specified if [ "$param" != "" ]; then # Check for parents in param and if so, modify param and replace parents with children i=1 param2="" num=`NumKeys "$param"` while [ $i -le $num ]; do item=`GetKey "$param" "$i"` result=`CheckKey "$line" "$item" "parent"` if [ "$result" != "" ]; then while IFS=$'\n' read -r item; do if [ "$item" != "" ]; then if [ "$formatstr" = "plain" -a $num -eq 1 ]; then item=`basename "$item"` fi param2=`AppendKey "$param2" "$item"` fi done < <(echo "$result") else param2=`AppendKey "$param2" "$item"` fi let i++ done # Did we found something? if [ "$param" != "$param2" ]; then param="$param2" num=`NumKeys "$param"` fi # for item in $param; do i=1 while [ $i -le $num ]; do j=1 found=false item=`GetKey "$param" "$i"` while [ $j -le $columns -a "$found" = false ]; do if [ "$item" = "${headerArr[$j]}" ]; then found=true if [ "$awkstr" = "" ]; then awkstr="\$$j" awkstr2="\$$j" else awkstr="${awkstr}\"\t\"\$$j" awkstr2="${awkstr2}\"\t\"\$$j" fi fi let j++ done if [ "$found" = false ]; then if [ "$awkstr" = "" ]; then awkstr="\"$item\"" awkstr2="\"//\"" else awkstr="${awkstr}\"\t$item\"" awkstr2="${awkstr2}\"\t//\"" fi fi let i++ done # We found something to cut - create a new workfile if [ "$awkstr" != "" ]; then mv -f $scratchfile ${scratchfile}.pre head -n 1 ${scratchfile}.pre | awk -F$'\t' '{print '"$awkstr"'}' > $scratchfile tailfromline2 ${scratchfile}.pre | awk -F$'\t' '{print '"$awkstr2"'}' >> $scratchfile filecheck -rm ${scratchfile}.pre line=`head -n 1 "$scratchfile"` columns=`NumKeys "$line"` if [ $columns -lt 1 ]; then line="" else # KeysToArray "$line" "$columns" "headerArr" oldIFS="$IFS"; IFS=$'\t'; headerArr=($columns $line); IFS="$oldIFS" fi else exitcode=3 errormsg $qopt $exitcode "($progstr) None of the specified keys were found. Exiting." Cleanup exit $exitcode fi fi else exitcode=3 errormsg $qopt $exitcode "($progstr) Invalid TSV file. Exiting." Cleanup exit $exitcode fi # echo "line2: '$line'" # If output is plain, just display all items in first line and exit case "$formatstr" in plain) # plain and more then one key -> display all the keys if [ $columns -gt 1 ]; then parent="" oldparent="" i=1 while [ $i -le $columns ]; do item=`GetKey "$line" "$i"` # Looking for cildren havechildren=false result=`echo "$item" | grep '^.*/.*$'` if [ "$result" != "" ]; then parent=`echo "$item" | cut -d'/' -f1` child=`echo "$item" | cut -d'/' -f2-` if [ "$parent" != "" -a "$child" != "" ]; then havechildren=true fi fi if [ "$havechildren" = true ]; then if [ "$oldparent" != "$parent" ]; then printf '%s\n' "$parent" oldparent="$parent" fi else printf '%s\n' "$item" fi let i++ done Cleanup exit $exitcode else # Check if we selected a parent result=`echo "$line" | sed 's|\t|\n|g' | grep "^${item}/"` if [ "$result" != "" ]; then echo "$result" | cut -d"/" -f2- Cleanup exit $exitcode fi fi ;; etsv) # Just cat the original file cat $scratchfile Cleanup exit $exitcode ;; tsv | csv) if [ "$formatstr" = "tsv" ]; then echo "$line" else printf '"%s"\n' "$line" | sed 's|\t|","|g' # Convert non-breaking-space to space and convert tab to comma fi while IFS=$'\n' read -r line; do i=1 while [ $i -le $columns ]; do item=`GetKey "$line" "$i"` result=`echo "$item" | grep '^/.*/$'` if [ "$result" != "" ]; then item=`echo "$item" | sed 's|^/||' | sed 's|/$||'` else if [ "$formatstr" = "csv" ]; then item=`echo '"'$item'"'` fi fi if [ $i -eq $columns ]; then printf "%s\n" "$item" else if [ "$formatstr" = "csv" ]; then printf "%s," "$item" else printf "%s\t" "$item" fi fi let i++ done done < <(tailfromline2 $scratchfile) Cleanup exit $exitcode ;; esac headerArrLen=0 # Total length of header headerArrMax=0 # Maximal length of each header item linesMax=`cat "$scratchfile" | wc -l` # Number of lines in Tab File let linesMax-- # Subtracting the header line # Determine real size of string length for all header items and the longest item i=1 while [ $i -le $columns ]; do # headerArr[$i]=`echo "${headerArr[$i]}" | tr ' ' ' '` # Convert non-breaking-space to space len=`printf "%s" "${headerArr[$i]}" | wc -m` headerArrLen[$i]=$len if [ $headerArrMax -lt $len ]; then headerArrMax=$len fi let i++ done # Setting spaces at beginning of lines for JSON if [ "$formatstr" = "json" ]; then oldparent="" havechildren=false if [ "$ENVELOPE_TABLE" = false ]; then if [ $linesMax -gt 1 ]; then spacer=" " else spacer=" " fi else if [ $linesMax -gt 1 ]; then spacer=" " else spacer=" " fi fi fi # We have to determine the length of each item in all lines if [ "$formatstr" = "table" ]; then while IFS=$'\n' read -r line; do i=1 # Used for cut starting with 1 while [ $i -le $columns ]; do item=`GetKey "$line" "$i"` result=`echo "$item" | grep '^/.*/$'` if [ "$result" != "" ]; then item=`echo "$item" | sed 's|^/||' | sed 's|/$||'` fi len=`echo "$item" | wc -m` let len-- if [ $len -gt ${headerArrLen[$i]} ]; then headerArrLen[$i]=$len fi let i++ done done < <(tailfromline2 $scratchfile) # Print header line i=1 while [ $i -le $columns ]; do if [ $i -gt 1 ]; then printf " " # Print space between items fi printf "%s" "${headerArr[$i]}" k=`printf "%s" "${headerArr[$i]}" | wc -m` if [ $k -lt ${headerArrLen[$i]} ]; then k=$((${headerArrLen[$i]} - $k)) printf "%${k}s" " " fi let i++ done # Newline printf "\n" # Print dashed line i=1 while [ $i -le $columns ]; do if [ $i -gt 1 ]; then printf " " # Print space between items fi printf "%-${headerArrLen[$i]}s" | tr ' ' '-' let i++ done # Newline printf "\n" else # Check if we have at least one line after headerline if [ "$formatstr" = "json" -a $linesMax -gt 0 ]; then if [ $linesMax -gt 1 ]; then if [ "$ENVELOPE_TABLE" = false ]; then printf '[\n {\n' else printf '{\n "content": [\n {\n' fi else if [ "$ENVELOPE_TABLE" = false ]; then printf '{\n' else printf '{\n "content": {\n' fi fi fi fi # Set defaults l=0 # Line counter nextskip=false # Needed by json and line # Print all remaining lines while IFS=$'\n' read -r line; do i=1 # Used for cut while [ $i -le $columns ]; do item=`GetKey "$line" "$i"` skip=false special=false if [ "$item" != "" ]; then if [ "$item" = "//" ]; then skip=true item="" else result=`echo "$item" | grep '^/.*/$'` if [ "$result" != "" ]; then special=true # Remove leading and trailing / item=`echo "$item" | sed 's|^/||' | sed 's|/$||'` fi fi fi case "$formatstr" in table) if [ $i -gt 1 ]; then printf " " # Print space between items fi ;; line) if [ $i -gt 1 ]; then if [ "$nextskip" = true ]; then nextskip=false else printf "\n" # Print newline fi fi if [ "$skip" = false ]; then k=`printf "%s" "${headerArr[$i]}" | wc -m` if [ $k -lt $headerArrMax ]; then k=$(($headerArrMax - $k)) printf "%${k}s" " " fi printf "%s" "${headerArr[$i]}: " # Print header else nextskip=true fi ;; json) # Do we have lines in tsv? if [ $linesMax -gt 0 ]; then # Looking for cildren result=`echo "${headerArr[$i]}" | grep '^.*/.*$'` if [ "$result" != "" ]; then parent=`echo "${headerArr[$i]}" | cut -d'/' -f1` child=`echo "${headerArr[$i]}" | cut -d'/' -f2-` if [ "$parent" != "" -a "$child" != "" ]; then havechildren=true else havechildren=false fi else parent="" child="" havechildren=false fi # Don't do this for the first item in line if [ $i -gt 1 ]; then if [ "$nextskip" = false ]; then # Prepare next item (comma if needed and newline) if [ "$havechildren" = true -a "$oldparent" != "" ]; then # We need to do that beacuse there could be items to skip needcomma=false k=$i while [ $k -le $columns ]; do # -lt also works - why? theader=${headerArr[${k}-1]} result=`echo "$theader" | grep '^.*/.*$'` if [ "$result" != "" ]; then par=`echo "$theader" | cut -d'/' -f1` chi=`echo "$theader" | cut -d'/' -f2-` if [ "$par" != "" -a "$chi" != "" ]; then if [ "$par" != "$oldparent" ]; then needcomma=false k=$columns else titem=`echo "$line" | cut -d$'\t' -f$k` if [ "$titem" = "//" ]; then needcomma=false k=$columns else needcomma=true k=$columns fi fi fi else needcomma=true k=$columns fi # echo "theader: '$theader' - titem: '$titem' - par: '$par' - oldparent: '$oldparent' - needcomma: '$needcomma'" let k++ done if [ "$needcomma" = true ]; then printf ',' fi printf '\n' else if [ "$oldparent" = "" ]; then printf ',' fi printf '\n' fi fi fi # Print spacer if [ "$skip" = false ]; then printf "%s" "$spacer" nextskip=false else nextskip=true fi # Print parent (if needed) if [ "$havechildren" = true ]; then if [ "$oldparent" = "" ]; then printf '"%s": {\n' "$parent" printf '%s' "$spacer" else if [ "$oldparent" != "$parent" ]; then if [ "$nextskip" = true ]; then printf '%s' "$spacer" fi printf '},\n' printf '%s"%s": {\n' "$spacer" "$parent" if [ "$skip" = false ]; then printf '%s' "$spacer" fi fi fi oldparent="$parent" else if [ "$oldparent" != "" ]; then printf '},\n' printf '%s' "$spacer" oldparent="" fi fi # Print header if [ "$skip" = false ]; then if [ "$havechildren" = true ]; then printf ' "%s": ' "$child" else printf '"%s": ' "${headerArr[$i]}" fi fi fi ;; keys) if [ $i -gt 1 ]; then printf "\n" # Print newline fi printf "%s\t" "${headerArr[$i]}" # Print header ;; esac if [ "$formatstr" = "json" -o "$formatstr" = "keys" -o "$formatstr" = "plain" ]; then # Don't use color coding here - print just the item if [ "$formatstr" = "json" ]; then if [ "$skip" = false ]; then if [ "$special" = true ]; then printf "%s" "$item" else printf '"%s"' "$item" fi fi else printf "%s" "$item" fi else if [ "$formatstr" = "table" -o "$formatstr" = "line" -a "$skip" = false ]; then # Print item rfound=`CheckKey "$redstr" "$item"` if [ "$rfound" != "" ]; then printf "${red}%s${normal}" "$item" else yfound=`CheckKey "$yellowstr" "$item"` if [ "$yfound" != "" ]; then printf "${yellow}%s${normal}" "$item" else gfound=`CheckKey "$greenstr" "$item"` if [ "$gfound" != "" ]; then printf "${green}%s${normal}" "$item" else dfound=`CheckKey "$graystr" "$item"` if [ "$dfound" != "" ]; then printf "${gray}%s${normal}" "$item" else bfound=`CheckKey "$boldstr" "$item"` if [ "$bfound" != "" ]; then printf "${bold}%s${normal}" "$item" else # Print item without coloring printf "%s" "$item" fi fi fi fi fi fi fi if [ "$formatstr" = "table" ]; then k=`printf "%s" "$item" | wc -m` if [ $k -lt ${headerArrLen[$i]} ]; then k=$((${headerArrLen[$i]} - $k)) printf "%${k}s" " " fi fi let i++ done # Newline if [ "$formatstr" = "json" -o "$formatstr" = "line" ]; then if [ "$nextskip" = false ]; then printf '\n' fi else printf "\n" fi let l++ if [ "$formatstr" != "table" -a $l -lt $linesMax ]; then if [ "$formatstr" = "json" ]; then if [ "$oldparent" != "" ]; then printf '%s}\n' "$spacer" oldparent="" fi # Print JSON separator between records or lines if [ "$ENVELOPE_TABLE" = false ]; then printf " },\n {\n" else printf " },\n {\n" fi else if [ "$formatstr" != "plain" -a "$formatstr" != "keys" ]; then printf "\n" # Print newline / separator between records or lines fi fi fi done < <(tailfromline2 $scratchfile) if [ "$formatstr" = "json" ]; then if [ $linesMax -gt 0 ]; then if [ "$oldparent" != "" ]; then printf '%s}\n' "$spacer" oldparent="" fi if [ $linesMax -gt 1 ]; then if [ "$ENVELOPE_TABLE" = false ]; then printf ' }\n]\n' else printf ' }\n ],\n "contentItems": %d\n}\n' "$linesMax" fi else if [ "$ENVELOPE_TABLE" = false ]; then printf '}\n' else printf ' },\n "contentItems": %d\n}\n' "$linesMax" fi fi fi fi # Cleanup and exit Cleanup exit $exitcode