#!/bin/bash # # Author: Georg Voell - georg.voell@standby.cloud # Version: @(#)print-table 3.2.0 11.08.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", "tsv", "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 colon (default) or tab 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 # # 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 } # 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" != "tsv" -a "$formatstr" != "json" -a "$formatstr" != "keys" \ -a "$formatstr" != "plain" ]; then errstr="Unknown format '$formatstr' after parameter '$pname'. Please choose from 'table', 'line', 'json', 'tsv', 'keys' or 'plain'." fi else errstr="Option '$pname' used more then once." fi shift else errstr="Please specify a format ('table', 'line', 'json', 'tsv', '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 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 else # Create a workfile without empty lines grep . "$filename" > $scratchfile fi # Check if we have a header line=`head -n 1 "$scratchfile" | tr ' ' ' ' | tr -d ':'` # We don't allow any space (convert them to non-braking-space) or colon (:) in column names (keys) if [ "$line" != "" ]; then headerArr=($line) columns=${#headerArr[@]} # Parameter given -> Select only what was specified if [ "$param" != "" ]; then for item in $param; do i=0 j=1 while [ $i -lt $columns ]; do if [ "$item" = "${headerArr[$i]}" ]; then if [ "$awkstr" = "" ]; then awkstr="\$$j" else awkstr="${awkstr}\"\t\"\$$j" fi fi let i++ let j++ done done # We found something to cut - create a new workfile if [ "$awkstr" != "" ]; then mv -f $scratchfile ${scratchfile}.pre cat ${scratchfile}.pre | awk -F$'\t' '{print '$awkstr'}' > $scratchfile filecheck -rm ${scratchfile}.pre line=`head -n 1 "$scratchfile" | tr ' ' ' ' | tr -d ':'` headerArr=($line) columns=${#headerArr[@]} 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) First line in file should be the header - here it is empty. Exiting." Cleanup exit $exitcode fi # 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 for item in $line; do item=`echo "$item" | tr ' ' ' '` # Convert non-breaking-space to space printf "%s\n" "$item" done Cleanup exit $exitcode fi ;; tsv) echo "$line" | tr ' ' ' ' # Convert non-breaking-space to space cat $scratchfile | tailfromline2 Cleanup exit $exitcode ;; csv) # Not yet implemented ;; 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=0 while [ $i -lt $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 read line; do i=0 j=1 # Used for cut starting with 1 while [ $i -lt $columns ]; do len=`echo "$line" | cut -d$'\t' -f$j | wc -m` let len-- if [ $len -gt ${headerArrLen[$i]} ]; then headerArrLen[$i]=$len fi let i++ let j++ done done < <(tailfromline2 $scratchfile) # Print header line i=0 while [ $i -lt $columns ]; do if [ $i -gt 0 ]; 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=0 while [ $i -lt $columns ]; do if [ $i -gt 0 ]; 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 read line; do i=0 # Used for our array starting with 0 j=1 # Used for cut starting with 1 while [ $i -lt $columns ]; do item=`echo "$line" | cut -d$'\t' -f$j` 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 0 ]; then printf " " # Print space between items fi ;; line) if [ $i -gt 0 ]; 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 0 ]; 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=$j while [ $k -le $columns ]; do 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 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 0 ]; then printf "\n" # Print newline fi printf "%s:" "${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 # Check if color string matches item rfound="" if [ "$redstr" != "" ]; then for cstr in $redstr; do if [ "$rfound" = "" ]; then rfound=`echo "$item" | grep -i '^'"$cstr"'$'` # -i = Ignore Case fi done fi yfound="" if [ "$yellowstr" != "" ]; then for cstr in $yellowstr; do if [ "$yfound" = "" ]; then yfound=`echo "$item" | grep -i '^'"$cstr"'$'` fi done fi gfound="" if [ "$greenstr" != "" ]; then for cstr in $greenstr; do if [ "$gfound" = "" ]; then gfound=`echo "$item" | grep -i '^'"$cstr"'$'` fi done fi dfound="" if [ "$graystr" != "" ]; then for cstr in $graystr; do if [ "$dfound" = "" ]; then dfound=`echo "$item" | grep -i '^'"$cstr"'$'` fi done fi bfound="" if [ "$boldstr" != "" ]; then for cstr in $boldstr; do if [ "$bfound" = "" ]; then bfound=`echo "$item" | grep -i '^'"$cstr"'$'` fi done fi # Print item if [ "$rfound" != "" ]; then printf "${red}%s${normal}" "$item" else if [ "$yfound" != "" ]; then printf "${yellow}%s${normal}" "$item" else if [ "$gfound" != "" ]; then printf "${green}%s${normal}" "$item" else if [ "$dfound" != "" ]; then printf "${gray}%s${normal}" "$item" else 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++ let j++ 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