File: ntsv.sh
   1 #!/bin/sh
   2 
   3 # The MIT License (MIT)
   4 #
   5 # Copyright © 2020-2025 pacman64
   6 #
   7 # Permission is hereby granted, free of charge, to any person obtaining a copy
   8 # of this software and associated documentation files (the “Software”), to deal
   9 # in the Software without restriction, including without limitation the rights
  10 # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  11 # copies of the Software, and to permit persons to whom the Software is
  12 # furnished to do so, subject to the following conditions:
  13 #
  14 # The above copyright notice and this permission notice shall be included in
  15 # all copies or substantial portions of the Software.
  16 #
  17 # THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  18 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  19 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  20 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  21 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  22 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  23 # SOFTWARE.
  24 
  25 
  26 # ntsv [options...] [filenames...]
  27 #
  28 #
  29 # Nice Tab Separated Values realigns and styles data tables using ANSI color
  30 # sequences. When not given filepaths to read data from, this tool reads from
  31 # standard input.
  32 #
  33 # If you're using Linux or MacOS, you may also find this command useful:
  34 #
  35 # # View Nice Table(s) / Very Nice Table(s); uses my scripts `ntsv` and `nn`
  36 # vnt() {
  37 #     awk 1 "$@" | nl -b a -w 1 -v 0 | ntsv | nn |
  38 #         awk '(NR - 1) % 5 == 1 { print "" } 1' | less -MKiCRS
  39 # }
  40 #
  41 # If `less` supports the `header` option, an even more useful command is:
  42 #
  43 # # View Nice Table(s) / Very Nice Table(s); uses my scripts `ntsv` and `nn`
  44 # vnt() {
  45 #     awk 1 "$@" | nl -b a -w 1 -v 0 | ntsv | nn |
  46 #         awk '(NR - 1) % 5 == 1 { print "" } 1' | less -MKiCRS --header=1
  47 # }
  48 
  49 
  50 case "$1" in
  51     -h|--h|-help|--help)
  52         awk '/^# +ntsv /, /^$/ { gsub(/^# ?/, ""); print }' "$0"
  53         exit 0
  54     ;;
  55 esac
  56 
  57 [ "$1" = "--" ] && shift
  58 
  59 # show all non-existing files given
  60 failed=0
  61 for arg in "$@"; do
  62     if [ "${arg}" = "-" ]; then
  63         continue
  64     fi
  65     if [ ! -e "${arg}" ]; then
  66         printf "no file named \"%s\"\n" "${arg}" > /dev/stderr
  67         failed=1
  68     fi
  69 done
  70 
  71 if [ "${failed}" -gt 0 ]; then
  72     exit 2
  73 fi
  74 
  75 awk -F "\t" '
  76     function match_number(v) {
  77         return match(v, /^[+-]?[0-9]+(\.[0-9]+)?$/)
  78     }
  79 
  80     function match_dot_digits(v) {
  81         return match(v, /\.[0-9]+$/)
  82     }
  83 
  84     # show_tiles uses `pad` and `decs` as local variables
  85     function show_tiles(i, l, j, v, pad, decs) {
  86         for (j = 1; j <= l; j++) {
  87             v = data[i][j]
  88 
  89             if (v == "") {
  90                 printf "%s", "\x1b[0m○"
  91                 continue
  92             }
  93 
  94             if (!match_number(v)) {
  95                 pad = v ~ /^ | $/
  96                 v = pad ? "\x1b[38;2;196;160;0m■" : "\x1b[38;2;128;128;128m■"
  97                 printf "%s", v
  98                 continue
  99             }
 100 
 101             if (v > 0) {
 102                 decs = match_dot_digits(v)
 103                 v = decs ? "\x1b[38;2;0;135;95m■" : "\x1b[38;2;0;95;0m■"
 104                 printf "%s", v
 105                 continue
 106             }
 107 
 108             if (v < 0) {
 109                 decs = match_dot_digits(v)
 110                 v = decs ? "\x1b[38;2;215;95;95m■" : "\x1b[38;2;204;0;0m■"
 111                 printf "%s", v
 112                 continue
 113             }
 114 
 115             printf "%s", "\x1b[38;2;0;95;215m■"
 116         }
 117 
 118         printf "\x1b[0m"
 119         for (j = l + 1; j <= num_cols; j++) {
 120             printf "%s", "×"
 121         }
 122         printf "\x1b[0m"
 123     }
 124 
 125     # show_number uses `dd`, `iw`, `dpad`, `ipad`, `lpad`, `style`, and `decs`
 126     # as local variables
 127     function show_number(v, j, last, w, dd, iw, dpad, ipad, lpad, style, decs) {
 128         if (match_dot_digits(v)) {
 129             dd = RLENGTH
 130             iw = RSTART - 1
 131         } else {
 132             dd = 0
 133             iw = w
 134         }
 135 
 136         dpad = dot_decs[j] - dd
 137         ipad = int_widths[j] - iw
 138         if (ipad < 0) ipad = 0
 139         lpad = widths[j] - (ipad + w + dpad)
 140         if (lpad < 0) lpad = 0
 141 
 142         # avoid adding trailing spaces at the end of lines
 143         if (j == last) dpad = 0
 144 
 145         if (v > 0) {
 146             decs = dot_decs[j] > 0
 147             style = decs ? "\x1b[38;2;0;135;95m" : "\x1b[38;2;0;95;0m"
 148         } else if (v < 0) {
 149             decs = dot_decs[j] > 0
 150             style = decs ? "\x1b[38;2;215;95;95m" : "\x1b[38;2;204;0;0m"
 151         } else {
 152             style = "\x1b[38;2;0;95;215m"
 153         }
 154 
 155         printf "%*s%*s%s%s\x1b[0m%*s", lpad, "", ipad, "", style, v, dpad, ""
 156     }
 157 
 158     {
 159         gsub(/\r$/, "")
 160         if (num_cols < NF) num_cols = NF
 161 
 162         for (i = 1; i <= NF; i++) {
 163             data[NR][i] = $i
 164 
 165             if (match_number($i)) {
 166                 numbers[i]++
 167                 sums[i] += $i + 0
 168 
 169                 if (match_dot_digits($i)) {
 170                     dd = RLENGTH
 171                     if (dot_decs[i] < dd) dot_decs[i] = dd
 172                     iw = RSTART - 1
 173                     if (int_widths[i] < iw) int_widths[i] = iw
 174                 } else {
 175                     w = length($i)
 176                     if (int_widths[i] < w) int_widths[i] = w
 177                 }
 178 
 179                 continue
 180             }
 181 
 182             w = length($i)
 183             if (widths[i] < w) widths[i] = w
 184         }
 185     }
 186 
 187     END {
 188         # fix column-widths using the number-padding info, and the column-totals
 189         for (i = 1; i <= num_cols; i++) {
 190             if (numbers[i] > 0) {
 191                 decs = dot_decs[i] > 0 ? dot_decs[i] - 1 : 0
 192                 w = length(sprintf("%.*f", decs, sums[i]))
 193             } else {
 194                 w = 1
 195             }
 196             if (widths[i] < w) widths[i] = w
 197 
 198             w = int_widths[i] + dot_decs[i]
 199             if (widths[i] < w) widths[i] = w
 200         }
 201 
 202         for (i = 1; i <= NR; i++) {
 203             last = length(data[i])
 204             show_tiles(i, last)
 205             printf "  "
 206 
 207             for (j = 1; j <= last; j++) {
 208                 v = data[i][j]
 209 
 210                 # put 2-space gaps between columns
 211                 if (j > 1 && j < last) printf "  "
 212                 else if (j == last && v != "") printf "  "
 213 
 214                 if (!match_number(v)) {
 215                     # avoid adding trailing spaces at the end of lines
 216                     printf "%*s", (j == last) ? 0 : -widths[j], v
 217                     continue
 218                 }
 219 
 220                 show_number(v, j, last, length(v))
 221             }
 222 
 223             printf "\n"
 224         }
 225 
 226         # show extra row with the column-sums
 227         if (num_cols > 0) printf "%*s", num_cols, ""
 228         for (i = 1; i <= num_cols; i++) {
 229             printf "  "
 230             if (numbers[i] > 0) {
 231                 decs = dot_decs[i] > 0 ? dot_decs[i] - 1 : 0
 232                 s = sprintf("%.*f", decs, sums[i])
 233                 show_number(s, i, num_cols, length(s))
 234             } else {
 235                 printf "%*s", -widths[i], "-"
 236             }
 237         }
 238         if (num_cols > 0) printf "\n"
 239     }
 240 ' "$@"