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 # Nice Tables realigns and styles TSV (tab-separated values) data using ANSI
  29 # sequences. When not given filepaths to read data from, this tool reads from
  30 # standard input.
  31 #
  32 # If you're using Linux or MacOS, you may find this cmd-line shortcut useful:
  33 #
  34 # # View Nice Table(s) / Very Nice Table(s); uses my scripts `nt` and `nn`
  35 # vnt() {
  36 #     awk 1 "$@" | nl -b a -w 1 -v 0 | nt | nn |
  37 #         awk '(NR - 1) % 5 == 1 && NR > 1 { print "" } 1' | less -JMKiCRS
  38 # }
  39 
  40 
  41 case "$1" in
  42     -h|--h|-help|--help)
  43         awk '/^# +nt /, /^$/ { gsub(/^# ?/, ""); print }' "$0"
  44         exit 0
  45     ;;
  46 esac
  47 
  48 
  49 awk -F "\t" '
  50 function match_number(v) {
  51     return match(v, /^[+-]?[0-9]+(\.[0-9]+)?$/)
  52 }
  53 
  54 function match_dot_digits(v) {
  55     return match(v, /\.[0-9]+$/)
  56 }
  57 
  58 function show_tiles(i, l, j, v) {
  59     printf "\x1b[48;5;255m"
  60 
  61     for (j = 1; j <= l; j++) {
  62         v = data[i][j]
  63 
  64         if (v == "") {
  65             printf "%s", "\x1b[0m\x1b[48;5;255m○"
  66             continue
  67         }
  68 
  69         if (!match_number(v)) {
  70             v = v ~ /^ | $/ ? "\x1b[38;5;3m■" : "\x1b[0m■"
  71             printf "%s", v
  72             continue
  73         }
  74 
  75         if (v > 0) {
  76             v = match_dot_digits(v) ? "\x1b[38;5;29m■" : "\x1b[38;5;22m■"
  77             printf "%s", v
  78             continue
  79         }
  80 
  81         if (v < 0) {
  82             v = match_dot_digits(v) ? "\x1b[38;5;167m■" : "\x1b[38;5;1m■"
  83             printf "%s", v
  84             continue
  85         }
  86 
  87         printf "%s", "\x1b[38;5;26m■"
  88     }
  89 
  90     printf "\x1b[0m\x1b[48;5;255m"
  91     for (j = l + 1; j <= num_cols; j++) {
  92         printf "%s", "×"
  93     }
  94     printf "\x1b[0m"
  95 }
  96 
  97 function show_number(v, j, last, w, dd, iw, dpad, ipad, lpad, style) {
  98     if (match_dot_digits(v)) {
  99         dd = RLENGTH
 100         iw = RSTART - 1
 101     } else {
 102         dd = 0
 103         iw = w
 104     }
 105 
 106     dpad = dot_decs[j] - dd
 107     ipad = int_widths[j] - iw
 108     if (ipad < 0) ipad = 0
 109     lpad = widths[j] - (ipad + w + dpad)
 110     if (lpad < 0) lpad = 0
 111 
 112     # avoid adding trailing spaces at the end of lines
 113     if (j == last) dpad = 0
 114 
 115     if (v > 0) {
 116         style = dot_decs[j] > 0 ? "\x1b[38;5;29m" : "\x1b[38;5;22m"
 117     } else if (v < 0) {
 118         style = dot_decs[j] > 0 ? "\x1b[38;5;167m" : "\x1b[38;5;1m"
 119     } else {
 120         style = "\x1b[38;5;26m"
 121     }
 122 
 123     printf "%*s%*s%s%s\x1b[0m%*s", lpad, "", ipad, "", style, v, dpad, ""
 124 }
 125 
 126 {
 127     gsub(/\r$/, "")
 128     if (num_cols < NF) num_cols = NF
 129 
 130     for (i = 1; i <= NF; i++) {
 131         data[NR][i] = $i
 132 
 133         if (match_number($i)) {
 134             numbers[i]++
 135             sums[i] += $i + 0
 136 
 137             if (match_dot_digits($i)) {
 138                 dd = RLENGTH
 139                 if (dot_decs[i] < dd) dot_decs[i] = dd
 140                 iw = RSTART - 1
 141                 if (int_widths[i] < iw) int_widths[i] = iw
 142             } else {
 143                 w = length($i)
 144                 if (int_widths[i] < w) int_widths[i] = w
 145             }
 146 
 147             continue
 148         }
 149 
 150         w = length($i)
 151         if (widths[i] < w) widths[i] = w
 152     }
 153 }
 154 
 155 END {
 156     # fix column-widths using the number-padding info, and the column-totals
 157     for (i = 1; i <= num_cols; i++) {
 158         if (numbers[i] > 0) {
 159             decs = dot_decs[i] > 0 ? dot_decs[i] - 1 : 0
 160             w = length(sprintf("%.*f", decs, sums[i]))
 161         } else {
 162             w = 1
 163         }
 164         if (widths[i] < w) widths[i] = w
 165 
 166         w = int_widths[i] + dot_decs[i]
 167         if (widths[i] < w) widths[i] = w
 168     }
 169 
 170     for (i = 1; i <= NR; i++) {
 171         last = length(data[i])
 172         show_tiles(i, last)
 173         printf "  "
 174 
 175         for (j = 1; j <= last; j++) {
 176             if (j > 1) printf "  " # put 2-space gaps between columns
 177 
 178             v = data[i][j]
 179 
 180             if (!match_number(v)) {
 181                 # avoid adding trailing spaces at the end of lines
 182                 printf "%*s", (j == last) ? 0 : -widths[j], v
 183                 continue
 184             }
 185 
 186             show_number(v, j, last, length(v))
 187         }
 188 
 189         printf "\n"
 190     }
 191 
 192     # show extra row with the column-sums
 193     if (num_cols > 0) printf "%*s", num_cols, ""
 194     for (i = 1; i <= num_cols; i++) {
 195         printf "  "
 196         if (numbers[i] > 0) {
 197             decs = dot_decs[i] > 0 ? dot_decs[i] - 1 : 0
 198             s = sprintf("%.*f", decs, sums[i])
 199             show_number(s, i, num_cols, length(s))
 200         } else {
 201             printf "%*s", -widths[i], "-"
 202         }
 203     }
 204     printf "\n"
 205 }
 206 ' "$@"