File: ncol-compat.sh 1 #!/bin/sh 2 3 # The MIT License (MIT) 4 # 5 # Copyright (c) 2026 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 # ncol [options...] [files...] 27 # 28 # Nice COLumns realigns and styles data tables using ANSI color sequences. In 29 # particular, all auto-detected numbers are styled so they're easier to read 30 # at a glance. Input tables can be either lines of space-separated values or 31 # tab-separated values, and are auto-detected using the first non-empty line. 32 # 33 # When not given filepaths to read data from, this tool reads from standard 34 # input by default. 35 # 36 # For positive numbers, a colorblind-friendly blue is used instead of green 37 # if either environment variable COLORBLIND or COLOR_BLIND is declared and set 38 # to 1. 39 # 40 # The options are, available both in single and double-dash versions 41 # 42 # -h, -help show this help message 43 # -m, -max-columns use the row with the most items for the item-count 44 45 46 # This version is compatible with busybox/alpine-linux. 47 48 maxcols=0 49 50 for arg in "$@"; do 51 if [ "${arg}" = '--' ]; then 52 shift 53 continue 54 fi 55 56 case "${arg}" in 57 -h|--h|-help|--help) 58 awk '/^# +ncol /, /^$/ { gsub(/^# ?/, ""); print }' "$0" 59 exit 0 60 ;; 61 62 -m|--m|-maxcols|--maxcols|-maxcolumns|--maxcolumns|-max-columns|\ 63 --max-columns) 64 maxcols=1 65 shift 66 continue 67 ;; 68 esac 69 70 break 71 done 72 73 # show all non-existing files given 74 failed=0 75 for arg in "$@"; do 76 if [ "${arg}" = "-" ]; then 77 continue 78 fi 79 if [ ! -e "${arg}" ]; then 80 printf "no file named \"%s\"\n" "${arg}" > /dev/stderr 81 failed=1 82 fi 83 done 84 85 if [ "${failed}" -gt 0 ]; then 86 exit 2 87 fi 88 89 awk -v maxcols="${maxcols}" ' 90 BEGIN { 91 if (SUBSEP == "") SUBSEP = "\034" 92 93 # normal positive-style is green, colorblind-friendly positive-style 94 # becomes the same blue as the zero-style 95 cb = ENVIRON["COLORBLIND"] != 0 || ENVIRON["COLOR_BLIND"] != 0 96 97 pdtile = cb ? "\x1b[38;2;0;95;215m■" : "\x1b[38;2;0;155;95m■" 98 pitile = cb ? "\x1b[38;2;0;75;235m■" : "\x1b[38;2;0;135;0m■" 99 pdrgb = cb ? "0;95;215" : "0;135;95" 100 pirgb = cb ? "0;75;235" : "0;155;0" 101 } 102 103 # always ignore trailing carriage-returns 104 { gsub(/\r$/, "") } 105 106 # first non-empty line auto-detects SSV vs. TSV, and the column-count 107 ncols == 0 { ncols = NF; if (/\t/) { FS = "\t"; $0 = $0 } } 108 109 ncols > 0 { 110 if (maxcols && width < NF) width = NF; 111 nitems[++nrows] = NF 112 113 for (i = 1; i <= NF; i++) { 114 data[nrows SUBSEP i] = $i 115 116 plain = $i 117 gsub(/\x1b\[[0-9;]*[A-Za-z]/, "", plain) 118 w = length(plain) 119 if (widths[i] < w) widths[i] = w 120 121 # handle non-numbers 122 if (!match(plain, /^[+-]?[0-9]+(\.[0-9]+)?$/)) continue 123 124 numbers[i]++ 125 sums[i] += plain 126 127 # count `dot-decimals` trail in the number 128 if (!match(plain, /\./)) continue 129 130 dd = w - (RSTART - 1) 131 if (dot_decs[i] < dd) dot_decs[i] = dd 132 } 133 } 134 135 END { 136 # fix column-widths using number-padding info and the column-totals 137 for (i = 1; i <= ncols; i++) { 138 w = 1 139 if (numbers[i]) { 140 decs = dot_decs[i] ? dot_decs[i] - 1 : 0 141 fmt = sprintf("%%.%df", decs) 142 w = length(sprintf(fmt, sums[i])) 143 } 144 if (widths[i] < w) widths[i] = w 145 } 146 147 if (nrows == 0 || ncols == 0) exit 148 149 # add fake-row with all the column-sums 150 nrows++ 151 for (i = 1; i <= ncols; i++) { 152 data[nrows SUBSEP i] = "-" 153 if (numbers[i]) { 154 decs = dot_decs[i] ? dot_decs[i] - 1 : 0 155 fmt = sprintf("%%.%df", decs) 156 data[nrows SUBSEP i] = sprintf(fmt, sums[i]) 157 } 158 } 159 160 for (i = 1; i <= nrows; i++) { 161 n = nitems[i] 162 163 # show tiles, except for the last fake-row with the sums 164 for (j = 1; i < nrows && j <= n; j++) { 165 v = data[i SUBSEP j] 166 167 if (v == "") { 168 printf "\x1b[0m○" 169 continue 170 } 171 172 if (!match(v, /^[+-]?[0-9]+(\.[0-9]+)?$/)) { 173 if (v ~ /^ | $/) printf "\x1b[38;2;196;160;0m■" 174 else printf "\x1b[38;2;128;128;128m■" 175 continue 176 } 177 178 if (v > 0) { 179 if (match(v, /\./)) printf pdtile 180 else printf pitile 181 continue 182 } 183 184 if (v < 0) { 185 if (match(v, /\./)) printf "\x1b[38;2;215;95;95m■" 186 else printf "\x1b[38;2;204;0;0m■" 187 continue 188 } 189 190 printf "\x1b[38;2;0;95;215m■" 191 } 192 193 # show tiles for missing trailing fields, except for the fake-row 194 if (i < nrows) { 195 extra = ncols - nitems[i] 196 if (extra > 0) printf "\x1b[0m" 197 for (j = 1; j <= extra; j++) printf "×" 198 printf "\x1b[0m " 199 } else for (j = 1; j <= ncols + 2; j++) printf " " 200 201 due = 0 202 203 # show/realign row fields 204 for (j = 1; j <= ncols; j++) { 205 v = data[i SUBSEP j] 206 207 # put 2-space gaps between columns 208 if (1 < j) due += 2 209 210 if (v ~ /^ *$/) { 211 due += widths[j] 212 continue 213 } 214 215 plain = v 216 gsub(/\x1b\[[0-9;]*[A-Za-z]/, "", plain) 217 w = length(plain) 218 219 # handle non-numbers 220 if (!match(plain, /^[+-]?[0-9]+(\.[0-9]+)?$/)) { 221 for (k = 1; k <= due; k++) printf " " 222 printf "%s", v 223 due = widths[j] - w 224 continue 225 } 226 227 # count `dot-decimals` trail in the number 228 dd = match(plain, /\./) ? w - (RSTART - 1) : 0 229 230 rpad = dot_decs[j] - dd 231 lpad = widths[j] - (w + rpad) + due 232 233 if (plain > 0) rgb = dot_decs[j] ? pdrgb : pirgb 234 else if (plain < 0) rgb = dot_decs[j] ? "215;95;95" : "204;0;0" 235 else rgb = "0;95;215" 236 237 for (k = 1; k <= lpad; k++) printf " " 238 printf "\x1b[38;2;%sm%s\x1b[0m", rgb, v 239 due = rpad 240 } 241 242 # treat extra fields as part of the last one 243 last = nitems[i] 244 for (j = ncols + 1; j <= last; j++) printf " %s", data[i SUBSEP j] 245 246 print "" 247 } 248 } 249 ' "$@" | sed -E \ 250 -e 's-([0-9]{1,3})([0-9]{3})([0-9]{3})([0-9]{3})([0-9]{3})([0-9]{3})([0-9]{3})\[0m-\1\2\3\4\5\6\7-g' \ 251 -e 's-([0-9]{1,3})([0-9]{3})([0-9]{3})([0-9]{3})([0-9]{3})([0-9]{3})\[0m-\1\2\3\4\5\6-g' \ 252 -e 's-([0-9]{1,3})([0-9]{3})([0-9]{3})([0-9]{3})([0-9]{3})\[0m-\1\2\3\4\5-g' \ 253 -e 's-([0-9]{1,3})([0-9]{3})([0-9]{3})([0-9]{3})\[0m-\1\2\3\4-g' \ 254 -e 's-([0-9]{1,3})([0-9]{3})([0-9]{3})\[0m-\1\2\3-g' \ 255 -e 's-([0-9]{1,3})([0-9]{3})\[0m-\1\2-g'