File: nt.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 # nt [options...] [filenames...]
  27 #
  28 #
  29 # Nice Tables realigns and styles TSV (tab-separated values) data using ANSI
  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 `nt` and `nn`
  36 # vnt() {
  37 #     awk 1 "$@" | nl -b a -w 1 -v 0 | nt | 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 `nt` and `nn`
  44 # vnt() {
  45 #     awk 1 "$@" | nl -b a -w 1 -v 0 | nt | 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 '/^# +nt /, /^$/ { 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` as
 126 # 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 ' "$@"