File: clam.sh
   1 #!/bin/sh
   2 
   3 # The MIT License (MIT)
   4 #
   5 # Copyright © 2024 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 # clam
  27 # Command-Line Augmentation Module: get the best out of your shell
  28 #
  29 # This is a collection of arguably useful shell functions and shortcuts:
  30 # some of these can be real time/effort savers, letting you concentrate
  31 # on getting things done.
  32 #
  33 # You're supposed to `source` this script, so its definitions stay for
  34 # your whole shell session: for that, you can run `source clam` or
  35 # `. clam` (no quotes either way), either directly or at shell startup.
  36 #
  37 # Most of these shell functions rely on tools which are almost always
  38 # available. The functions depending on tools which are least-likely
  39 # pre-installed are
  40 #
  41 #   cancur       curl
  42 #   cgt          go
  43 #   crg          rg
  44 #   cs           bat
  45 #   csf          bat
  46 #   dic          curl
  47 #   dict         curl
  48 #   fetch        curl
  49 #   fetchjson    curl
  50 #   get          curl
  51 #   getjson      curl
  52 #   gobs         go
  53 #   gobwingui    go
  54 #   godep        go
  55 #   goimp        go
  56 #   h            hat         my own `HAndy Tools` app; commented out
  57 #   hmr          bat
  58 #   j2           python      commented out
  59 #   jc           python      uses non-built-in package; commented out
  60 #   joke         curl
  61 #   lab          book        my own script; commented out
  62 #   serve        python
  63 #   wheretrails  rg
  64 #   whichtrails  rg
  65 #
  66 #
  67 # Full list of funcs/commands added
  68 #
  69 # allfiles     show all files in a folder, digging recursively
  70 # allfolders   show all folders in a folder, digging recursively
  71 # args         emit each argument given to it on its own line
  72 # avoid        ignore lines matching the regex given
  73 # banner       show words/arguments given as a styled line
  74 # bar          emit a styled bar to visually separate different outputs
  75 # bl           Breathe Lines regularly adds extra empty lines every few
  76 # blawk        process BLocks of non-empty lines with AWK
  77 # bleak        Blue LEAK helps you debug pipes, via colored stderr lines
  78 # blow         expand tabs into spaces, using the tabstop-width given
  79 # blowtabs     expand tabs into spaces, using the tabstop-width given
  80 # breathe      Breathe regularly adds extra empty lines every few
  81 # bt           Breathe Table makes lines breathe starting from the 2nd one
  82 # c            run `cat`, which is useful, despite claims to the contrary
  83 # cancur       get the latest exchange rates from the bank of canada as TSV
  84 # cap          limit lines up to their first n bytes, line-feed excluded
  85 # cawk         Comma AWK runs AWK in CSV mode
  86 # cgt          Colored Go Test runs `go test`, making problems stand out
  87 # chopdecs     ignore trailing decimal zeros in numbers
  88 # choplf       ignore last byte if it's a line-feed
  89 # cls          CLear Screen
  90 # coco         COunt COndition, uses an AWK condition as its first arg
  91 # crep         Colored Regular Expression Printer runs `grep` in color-mode
  92 # crg          Colored RipGrep
  93 # cs           Color Syntax
  94 # csf          Color Syntax for a whole Folder, even its subfolders
  95 # debase64     DEcode BASE64 bytes
  96 # debom        ignore leading utf-8 BOM markers for each input, when present
  97 # decap        DECAPitate emits the first n lines to stderr
  98 # dedent       ignore the first n leading spaces per line, or 4 by default
  99 # dedup        deduplicate lines, keeping them in their original order
 100 # delay        delay each input line, waiting the number of seconds given
 101 # detab        expand tabs into spaces, using the tabstop-width given
 102 # dic          lookup words using an online dictionary
 103 # dict         lookup words using an online dictionary
 104 # div          divide 2 numbers both ways/orders
 105 # dt           show the current Date and Time
 106 # eg           Extended Grep
 107 # enum         enumerate all lines, starting from 1
 108 # enum1        enumerate all lines, starting from 1
 109 # enum0        enumerate all lines, starting from 0
 110 # except       ignore lines matching the regex given
 111 # fail         show an error message and fail
 112 # fetch        get/fetch bytes from the URI given
 113 # fetchjson    asks webserver at URI given to respond with a JSON payload
 114 # findtargets  show/find all targets from makefiles
 115 # first        keep only the first n lines, or just the first 1 by default
 116 # forever      keep (re)running the command given, until forced to quit
 117 # gb           Good/Bad colors lines using 1 or 2 awk conditions
 118 # gbm          Good, Bad, Meh colors lines using 1..3 awk conditions
 119 # get          get/fetch bytes from the URI given
 120 # getjson      asks webserver at URI given to respond with a JSON payload
 121 # gethelp      show help for the command given, using common help options
 122 # get          try to get/fetch JSON data from the URI given
 123 # gleak        Green LEAK helps you debug pipes, via colored stderr lines
 124 # glue         join all input lines using the separator/joiner string given
 125 # gobs         GO Build Stripped
 126 # gobwingui    GO Build WINdows GUI
 127 # godep        GO DEPendencies from the folder given
 128 # goimp        GO IMPorts from the folder given
 129 # gsub         run the awk function of the same name
 130 # gth          Go To Hell deletes telemetric data for go's vscode extension
 131 # h            run my own `hat` (HAndy Tools) cmd-line app; disabled
 132 # hawk         highlight lines matching the awk condition given
 133 # helpless     view most apps' help messages using `less`
 134 # hl           Header Less runs `less` and always shows the 1st line on top
 135 # hmr          Help Me Remember one of these commands
 136 # hv           Header View runs `less` and always shows the 1st line on top
 137 # imany        Insensitive Match ANY regex-matches lines case-insensitively
 138 # indent       indent text the number of spaces given, or 4 by default
 139 # inone        ignore lines regex-matching case-insensitively any args given
 140 # j2           reformat JSON into multiple lines with 2-space indent levels
 141 # jc           shortcut to run `jc`, a useful data-to-JSON python module
 142 # joke         show a `Dad Joke` from the web, sometimes even a funny one
 143 # l            run `less`, enabling line numbers, scrolling, and ANSI styles
 144 # lab          layout content like a book; disabled; uses my own `book` app
 145 # largs        run `xargs` taking the extra arguments from whole stdin lines
 146 # last         keep only the last n lines, or just the last 1 by default
 147 # leak         emit input lines both to stdout and stderr, to debug pipes
 148 # leako        LEAK Orange helps you debug pipes, via colored stderr lines
 149 # lineup       regroup adjacent lines into n-item tab-separated lines
 150 # lower        lowercase all lines
 151 # m            case-insensitively match the extended regex given
 152 # many         Match ANY regex-matches lines case-sensitively with its args
 153 # match        case-insensitively match the extended regex given
 154 # merrge       redirect stderr into stdout, without any ugly finger-dancing
 155 # n            Number all lines, starting from 1
 156 # n0           Number all lines, starting from 0, instead of 1
 157 # n1           Number all lines, starting from 1
 158 # nfs          Nice File Sizes
 159 # nil          emit nothing to stdout and/or discard everything from stdin
 160 # noerr        ignore stderr, without any ugly finger-dancing
 161 # none         ignore lines regex-matching case-sensitively any args given
 162 # now          show the current date and time
 163 # nth          keep only the n-th line from the input, if it has enough lines
 164 # oleak        Orange LEAK helps you debug pipes, via colored stderr lines
 165 # owe          Ok Warning Error colors lines using up to 3 awk conditions
 166 # pad80        pad lines to be 80-char wide when they are shorter than that
 167 # pawk         Print AWK expressions
 168 # plain        ignore all ANSI styles
 169 # plainend     reset ANSI styles at the end of each line
 170 # reprose      reflow/trim lines of prose (text) for improved legibility
 171 # repstr       REPeat a STRing n times, or 80 times by default
 172 # runin        run a command in the folder given
 173 # sep          emit a unique-looking separator line
 174 # serve        start a local webserver from the current folder
 175 # skip         skip the first n lines, or the first 1 by default
 176 # skipfirst    skip the first n lines, or the first 1 by default
 177 # skiplast     ignore the last n lines, or the very last 1 by default
 178 # sosi         reverse-SOrted SIzes of the files given
 179 # squeeze      aggressively get rid of extra spaces on every line
 180 # ssv2tsv      turn each run of 1+ spaces into a single tab
 181 # stripe       underline every 5th line
 182 # tawk         Tab AWK, runs AWK using tab as its IO item-separator
 183 # today        show current date in a way friendly both to people and tools
 184 # topfiles     show all files directly in a folder, without recursion
 185 # topfolders   show all folders directly in a folder, without recursion
 186 # trim         get rid of leading and trailing spaces on lines
 187 # trimprefix   ignore the prefix given from input lines which start with it
 188 # trimsuffix   ignore the suffix given from input lines which end with it
 189 # trimtrail    get rid of trailing spaces on lines
 190 # trimtrails   get rid of trailing spaces on lines
 191 # try          try running the command given
 192 # tsawk        TimeStamp lines satisfying AWK condition, ignoring the rest
 193 # unbase64     decode base64 bytes
 194 # unique       deduplicate lines, keeping them in their original order
 195 # unixify      ensure plain-text lines are unix-like
 196 # untab        expand tabs into spaces, using the tabstop-width given
 197 # up           go UP n folders, or go up 1 folder by default
 198 # upsidedown   emit input lines in reverse order, or last to first
 199 # ut           Underline Table underlines the 1st line, then 1 line every 5
 200 # v            View runs `less`, enabling scrolling and ANSI styles
 201 # verdict      run a command, showing its success/failure right after
 202 # wat          What Are These (?) shows what the names given to it do
 203 # wheretrails  find all files which have trailing spaces/CRs on their lines
 204 # whichtrails  find all files which have trailing spaces/CRs on their lines
 205 # wit          What Is This (?) shows what the name given to it does
 206 # year         show a full calendar for the current year, or the year given
 207 # ymd          show the current date in the YYYY-MM-DD format
 208 
 209 
 210 # handle help options
 211 case "$1" in
 212     -h|--h|-help|--help)
 213         # show help message, extracting the info-comment at the start
 214         # of this file, and quit
 215         awk '/^# +clam/, /^$/ { gsub(/^# ?/, ""); print }' "$0"
 216         exit 0
 217     ;;
 218 esac
 219 
 220 
 221 # use a simple shell prompt
 222 # PS1="\$ "
 223 # PS2="> "
 224 
 225 # use a simple shell prompt, showing the current folder in the title
 226 # PS1="\[\e]0;\w\a\]\$ "
 227 # PS2="> "
 228 
 229 # prevent `less` from saving searches/commands
 230 # LESSHISTFILE="-"
 231 # LESSSECURE=1
 232 
 233 # prevent the shell from saving commands
 234 # unset HISTFILE
 235 
 236 
 237 # dashed aliases of multi-word commands defined later
 238 alias all-files='allfiles'
 239 alias all-folders='allfolders'
 240 alias blow-tabs='blowtabs'
 241 alias chop-decs='chopdecs'
 242 alias chop-lf='choplf'
 243 alias fetch-json='fetchjson'
 244 alias get-help='gethelp'
 245 alias get-json='getjson'
 246 alias help-for='gethelp'
 247 alias help-less='helpless'
 248 alias line-up='lineup'
 249 alias pad-80='pad80'
 250 alias plain-end='plainend'
 251 alias rep-str='repstr'
 252 alias run-in='runin'
 253 alias skip-first='skipfirst'
 254 alias skip-last='skiplast'
 255 alias ssv-to-tsv='ssv2tsv'
 256 alias top-files='topfiles'
 257 alias top-folders='topfolders'
 258 alias trim-prefix='trimprefix'
 259 alias trim-suffix='trimsuffix'
 260 alias trim-trail='trimtrail'
 261 alias trim-trails='trimtrails'
 262 alias ts-awk='tsawk'
 263 alias upside-down='upsidedown'
 264 alias where-trails='wheretrails'
 265 alias which-trails='whichtrails'
 266 
 267 # undashed aliases for commands defined later
 268 alias bocler='cancur'
 269 
 270 
 271 # show all files in a folder, digging recursively
 272 allfiles() {
 273     local arg
 274     for arg in "$@"; do
 275         find "${arg}" -type f
 276     done
 277 
 278     if [ "$#" -eq 0 ]; then
 279         find . -type f
 280     fi
 281 }
 282 
 283 # show all folders in a folder, digging recursively
 284 allfolders() {
 285     local arg
 286     for arg in "$@"; do
 287         find "${arg}" -type d | awk 'NR > 1'
 288     done
 289 
 290     if [ "$#" -eq 0 ]; then
 291         find . -type d | awk 'NR > 1'
 292     fi
 293 }
 294 
 295 
 296 # emit each argument given as its own line of output
 297 args() {
 298     awk 'BEGIN { for (i = 1; i < ARGC; i++) print ARGV[i]; exit }' "$@"
 299 }
 300 
 301 # avoid lines matching the regex given, or avoid empty(ish) lines by default
 302 avoid() {
 303     regex="${1:-[^ *]\r?$}"
 304     shift
 305     grep -E -v "${regex}" "$@"
 306 }
 307 
 308 # show a line which clearly labels part of a shell session
 309 banner() {
 310     # printf "\x1b[7m%-80s\x1b[0m\n" "$*"
 311     printf "\x1b[48;5;253m%-80s\x1b[0m\n" "$*"
 312 }
 313 
 314 # emit a colored bar which can help visually separate different outputs
 315 bar() {
 316     printf "\x1b[48;5;253m%${1:-80}s\x1b[0m\n" " "
 317 }
 318 
 319 # Breathe Lines: separate groups of 5 lines with empty lines
 320 bl() {
 321     awk 'NR % 5 == 1 && NR != 1 { print "" } 1' "$@"
 322 }
 323 
 324 # process BLocks of non-empty lines with AWK
 325 blawk() {
 326     awk -F='' -v RS='' "$@"
 327 }
 328 
 329 # Blue LEAK emits/tees input both to stdout and stderr, coloring blue what
 330 # it emits to stderr using an ANSI-style; this cmd is useful to `debug`
 331 # pipes involving several steps
 332 bleak() {
 333     awk '{
 334         print
 335         fflush()
 336         gsub(/\x1b\[0m/, "\x1b[0m\x1b[38;5;26m")
 337         printf "\x1b[38;5;26m%s\x1b[0m\n", $0 > "/dev/stderr"
 338         fflush("/dev/stderr")
 339     }'
 340 }
 341 
 342 # expand tabs into spaces using the tabstop given
 343 blow() {
 344     local tabstop
 345     tabstop="$1"
 346     shift
 347     expand -t "${tabstop}" "$@"
 348 }
 349 
 350 # expand tabs into spaces using the tabstop given
 351 blowtabs() {
 352     local tabstop
 353     tabstop="$1"
 354     shift
 355     expand -t "${tabstop}" "$@"
 356 }
 357 
 358 # separate groups of 5 lines with empty lines, making text-rows much
 359 # easier to follow/eye-scan along, especially with tall walls of text
 360 breathe() {
 361     awk 'NR % 5 == 1 && NR != 1 { print "" } 1' "$@"
 362 }
 363 
 364 # Breathe Table: add an empty line after the first one (the header),
 365 # then separate groups of 5 lines with empty lines between them
 366 bt() {
 367     awk '(NR - 1) % 5 == 1 && NR > 1 { print "" } 1' "$@"
 368 }
 369 
 370 # `cat` can be useful, despite claims to the contrary
 371 # bytes() {
 372 #     cat "$@"
 373 # }
 374 
 375 # `cat` can be useful, despite claims to the contrary
 376 c() {
 377     cat "$@"
 378 }
 379 
 380 # CANadian CURrencies emits the Bank Of Canada's Latest Exchange Rates as a
 381 # 2-line table of tab-separated values, where the first line is the header
 382 cancur() {
 383     local b
 384     b='https://www.bankofcanada.ca/valet/observations/group/FX_RATES_DAILY/'
 385     # starting `3 days ago` ensures this works even on weekends, while
 386     # minimizing the data transmitted
 387     curl -s "${b}csv?start_date=$(date -d '3 days ago' +'%Y-%m-%d')" |
 388     # pick the header line, along with the last one, turning CSV into TSV
 389     awk '/^"date"/; END { print }' | tr -d '"' | tr ',' '\t' |
 390     # simplify/change most column names
 391     sed 's/FX//g; s/CAD//g'
 392 }
 393 
 394 # limit lines up to their first n bytes (80 by default), line-feed excluded
 395 cap() {
 396     local n
 397     n="${1:-80}"
 398     shift
 399     awk -v n="${n}" '{ print substr($0, 1, n) }' "$@"
 400 }
 401 
 402 # Comma AWK: run awk in CSV mode
 403 cawk() {
 404     awk --csv "$@"
 405 }
 406 
 407 # Colored Go Test on the folder given
 408 cgt() {
 409     go test "${1:-.}" 2>&1 | awk '
 410         /^ok/ { printf "\x1b[38;5;29m%s\x1b[0m\n", $0; fflush() }
 411         /^[-]* ?FAIL/ { printf "\x1b[38;5;1m%s\x1b[0m\n", $0; fflush() }
 412         /^\?/ { printf "\x1b[38;5;249m%s\x1b[0m\n", $0; fflush() }'
 413 }
 414 
 415 # ignore trailing decimal zeros in numbers
 416 chopdecs() {
 417     awk '
 418         {
 419             for (i = 1; i <= NF; i++) {
 420                 gsub(/(\.[0-9]+[1-9]+)0+$/, "&1")
 421                 gsub(/([0-9]+)\.0*$/, "&1")
 422             }
 423             print
 424         }
 425     ' "$@"
 426 }
 427 
 428 # ignore final life-feed from text, if it's the very last byte; all
 429 # carriage-returns in CRLF byte-pairs are also ignored
 430 choplf() {
 431     awk 'NR > 1 { print "" } { printf "%s", $0 }' "$@"
 432 }
 433 
 434 # CLear Screen
 435 cls() {
 436     clear
 437 }
 438 
 439 # COunt COndition: count how many times the AWK expression given is true
 440 coco() {
 441     local cond
 442     cond="${1:-1}"
 443     shift
 444     awk "${cond} { c++ } END { print c }" "$@"
 445 }
 446 
 447 # Colored Regular Expression Printer runs `grep` to shows colored matches
 448 crep() {
 449     grep --color=always "$@"
 450 }
 451 
 452 # Colored RipGrep: ensures app `rg` emits colors when piped
 453 crg() {
 454     rg --color=always "$@"
 455 }
 456 
 457 # Color Syntax: run syntax-coloring app `bat` without line-wrapping
 458 cs() {
 459     local cmd
 460     cmd="bat"
 461     # debian linux uses a different name for the `bat` app
 462     if [ -e "/usr/bin/batcat" ]; then
 463         cmd="batcat"
 464     fi
 465 
 466     "$cmd" --style=plain,header,numbers --theme='Monokai Extended Light' \
 467         --wrap=never --color=always "$@" |
 468     sed 's-\x1b\[38;5;70m-\x1b\[38;5;28m-g' | less -KiCRS
 469 }
 470 
 471 # Color Syntax of all files in a Folder, showing line numbers
 472 csf() {
 473     local cmd
 474     cmd="bat"
 475     # debian linux uses a different name for the `bat` app
 476     if [ -e "/usr/bin/batcat" ]; then
 477         cmd="batcat"
 478     fi
 479 
 480     find "${1:-.}" -type f -print0 | xargs --null "$cmd" \
 481         --style=plain,header,numbers --theme='Monokai Extended Light' \
 482         --wrap=never --color=always |
 483     sed 's-\x1b\[38;5;70m-\x1b\[38;5;28m-g' | less -KiCRS
 484 }
 485 
 486 # DEcode BASE64 bytes
 487 debase64() {
 488     base64 -d "$@"
 489 }
 490 
 491 # ignore leading utf-8 BOM markers for each input, when present; CRLF
 492 # byte-pairs are also turned into single LF bytes
 493 debom() {
 494     awk 'FNR == 1 { gsub(/^\xef\xbb\xbf/, "") } 1' "$@"
 495 }
 496 
 497 # DECAPitate emits the first n lines to stderr, and the rest to stdout;
 498 # the name is a reference to the standard cmd-line app `head`, which
 499 # emits only the first n lines to stdout
 500 decap() {
 501     local n
 502     n="${1:-1}"
 503     shift
 504 
 505     awk -v n="${n}" '
 506         BEGIN {
 507             if (n !~ /^-?[0-9]+$/) {
 508                 fmt = "leading arg %s isn'\''t a valid line-count\n"
 509                 printf fmt, n > "/dev/stderr"
 510                 exit 1
 511             }
 512         }
 513 
 514         NR <= n { print > "/dev/stderr"; next }
 515         1
 516     ' "$@"
 517 }
 518 
 519 # ignore up to n leading spaces for each line, or up to 4 by default
 520 dedent() {
 521     local upto
 522     upto="${1:-4}"
 523     shift
 524     awk "{ gsub(/^ {0,${upto}}/, \"\"); print }" "$@"
 525 }
 526 
 527 # DEDUPlicate prevents lines from appearing more than once
 528 dedup() {
 529     awk '!c[$0]++' "$@"
 530 }
 531 
 532 # DEDUPlicatE prevents lines from appearing more than once
 533 dedupe() {
 534     awk '!c[$0]++' "$@"
 535 }
 536 
 537 # delay each input line, waiting the number of seconds given, or wait
 538 # 1 second before each line by default
 539 delay() {
 540     local delay
 541     local line
 542     IFS='' # keep all spaces from lines
 543     delay="${1:-1}"
 544     shift
 545 
 546     awk 1 "$@" | while read -r line; do
 547         sleep "${delay}"
 548         printf "%s\n" "${line}"
 549     done
 550 }
 551 
 552 # expand tabs into spaces using the tabstop given
 553 detab() {
 554     local tabstop
 555     tabstop="$1"
 556     shift
 557     expand -t "${tabstop}" "$@"
 558 }
 559 
 560 # DICtionary definitions, using an online service
 561 dic() {
 562     curl -s "dict://dict.org/d:$*" | tr -d '\r' |
 563     awk '
 564         /^151 / { printf "\x1b[38;5;4m%s\x1b[0m\n", $0; next }
 565         /^[1-9][0-9]{2} / { printf "\x1b[38;5;244m%s\x1b[0m\n", $0; next }
 566         1
 567     '
 568 }
 569 
 570 # DICTionary definitions, using an online service
 571 dict() {
 572     curl -s "dict://dict.org/d:$*" | tr -d '\r' |
 573     awk '
 574         /^151 / { printf "\x1b[38;5;4m%s\x1b[0m\n", $0; next }
 575         /^[1-9][0-9]{2} / { printf "\x1b[38;5;244m%s\x1b[0m\n", $0; next }
 576         1
 577     '
 578 }
 579 
 580 # divide 2 numbers both ways, also showing their proper complement
 581 div() {
 582     awk -v x="${1:-1}" -v y="${2:-1}" '
 583         BEGIN {
 584             gsub(/_/, "", x)
 585             gsub(/_/, "", y)
 586             x = x + 0
 587             y = y + 0
 588             min = x < y ? x : y
 589             max = x > y ? x : y
 590             print x/y
 591             print y/x
 592             print 1 - min / max
 593             exit
 594         }
 595     ' < /dev/null
 596 }
 597 
 598 # show the current Date and Time
 599 dt() {
 600     local arg
 601 
 602     cal "$@"
 603     # further indent the last line if needed, so the current time shown
 604     # in it is center-aligned even when showing 3 months, or a whole year
 605     for arg in "$@"; do
 606         case "${arg}" in
 607             -3)
 608                 printf "%22s" " "
 609             ;;
 610             -y)
 611                 printf "%23s" " "
 612             ;;
 613         esac
 614     done
 615     printf "      \x1b[34m%s\x1b[0m\n" "$(date +'%T')"
 616 }
 617 
 618 # Extended Grep
 619 eg() {
 620     grep -E "$@"
 621 }
 622 
 623 # enumerate lines, starting from 1, and using a tab as the separator:
 624 # even empty lines are counted, unlike with `nl`
 625 enum() {
 626     awk '{ printf "%d\t", NR; print }' "$@"
 627 }
 628 
 629 # enumerate lines, starting from 1, and using a tab as the separator:
 630 # even empty lines are counted, unlike with `nl`
 631 enum1() {
 632     awk '{ printf "%d\t", NR; print }' "$@"
 633 }
 634 
 635 # enumerate lines, starting from 0, and using a tab as the separator:
 636 # even empty lines are counted, unlike with `nl`
 637 enum0() {
 638     awk '{ printf "%d\t", NR - 1; print }' "$@"
 639 }
 640 
 641 # avoid lines matching the regex given, or avoid empty(ish) lines by default
 642 except() {
 643     local regex
 644     regex="${1:-[^ *]\r?$}"
 645     shift
 646     grep -E -v "${regex}" "$@"
 647 }
 648 
 649 # show an error message and fail
 650 fail() {
 651     printf "\x1b[41m\x1b[38;5;15m %s \x1b[0m\n" "$*" >&2 && return 255
 652 }
 653 
 654 # get/fetch data from the URI given
 655 fetch() {
 656     curl -s "$@" || (
 657         printf "\x1b[31mcan't get %s\x1b[0m\n" "$@" >&2
 658         return 1
 659     )
 660 }
 661 
 662 # asks webserver at URI given to respond with a JSON payload
 663 fetchjson() {
 664     curl -s "$@" -H 'Accept: application/json' || (
 665         printf "\x1b[31mcan't get JSON from %s\x1b[0m\n" "$@" >&2
 666         return 1
 667     )
 668 }
 669 
 670 # show/find all targets from makefiles
 671 findtargets() {
 672     grep -E -i '^[a-z0-9_\.$-]+.*:' "${@:-Makefile}"
 673 }
 674 
 675 # get the first n lines, or 1 by default
 676 first() {
 677     head -n "${1:-1}" "${2:--}"
 678 }
 679 
 680 # keep (re)running the command given, until forced to quit, whether
 681 # directly or indirectly
 682 forever() {
 683     while true; do
 684         "$@" || return "$?"
 685     done
 686 }
 687 
 688 # Good/Bad: show pattern-matched lines in green and red respectively,
 689 # the rest being kept verbatim
 690 gb() {
 691     local good
 692     local bad
 693     good="${1:-0}"
 694     shift
 695     bad="${1:-0}"
 696     shift
 697 
 698     awk "
 699         ${good} {
 700             gsub(/\\x1b\\[0m/, \"\x1b[0m\\x1b[38;5;29m\")
 701             printf \"\\x1b[38;5;29m%s\x1b[0m\\n\"\$0
 702             fflush()
 703             next
 704         }
 705 
 706         ${bad} {
 707             gsub(/\\x1b\\[0m/, \"\x1b[0m\\x1b[38;5;1m\")
 708             printf \"\\x1b[38;5;1m%s\x1b[0m\\n\"\$0
 709             fflush()
 710             next
 711         }
 712 
 713         { print; fflush() }
 714     " "$@"
 715 }
 716 
 717 # Good, Bad, Meh: show pattern-matched lines in green, red, and gray
 718 # respectively; all other lines are kept verbatim
 719 gbm() {
 720     local good
 721     local bad
 722     local meh
 723     good="${1:-0}"
 724     shift
 725     bad="${1:-0}"
 726     shift
 727     meh="${1:-0}"
 728     shift
 729 
 730     awk "
 731         ${good} {
 732             gsub(/\\x1b\\[0m/, \"\x1b[0m\\x1b[38;5;29m\")
 733             printf \"\\x1b[38;5;29m%s\x1b[0m\\n\"\$0
 734             fflush()
 735             next
 736         }
 737 
 738         ${bad} {
 739             gsub(/\\x1b\\[0m/, \"\x1b[0m\\x1b[38;5;1m\")
 740             printf \"\\x1b[38;5;1m%s\x1b[0m\\n\"\$0
 741             fflush()
 742             next
 743         }
 744 
 745         ${meh} {
 746             gsub(/\\x1b\\[0m/, \"\x1b[0m\\x1b[38;5;249m\")
 747             printf \"\\x1b[38;5;249m%s\x1b[0m\\n\"\$0
 748             fflush()
 749             next
 750         }
 751 
 752         { print; fflush() }
 753     " "$@"
 754 }
 755 
 756 # get/fetch data from the URI given
 757 get() {
 758     curl -s "$@" || (
 759         printf "\x1b[31mcan't get %s\x1b[0m\n" "$@" >&2
 760         return 1
 761     )
 762 }
 763 
 764 # view most apps' help messages using `less`; this command used to be
 765 # called `helpless`
 766 gethelp() {
 767     "${1}" "${2:---help}" 2>&1 | less -KiCRS
 768 }
 769 
 770 # asks webserver at URI given to respond with a JSON payload
 771 getjson() {
 772     curl -s "$@" -H 'Accept: application/json' || (
 773         printf "\x1b[31mcan't get JSON from %s\x1b[0m\n" "$@" >&2
 774         return 1
 775     )
 776 }
 777 
 778 # Green LEAK emits/tees input both to stdout and stderr, coloring green what
 779 # it emits to stderr using an ANSI-style; this cmd is useful to `debug` pipes
 780 # involving several steps
 781 gleak() {
 782     awk '{
 783         print
 784         fflush()
 785         gsub(/\x1b\[0m/, "\x1b[0m\x1b[38;5;29m")
 786         printf "\x1b[38;5;29m%s\x1b[0m\n", $0 > "/dev/stderr"
 787         fflush("/dev/stderr")
 788     }'
 789 }
 790 
 791 # join all input lines using the separator/joiner string given; the name
 792 # `join` is already taken by a standard(ish) cmd-line app
 793 glue() {
 794     local sep
 795     sep="${1}"
 796     shift
 797 
 798     awk -v sep="${sep}" '
 799         NR > 1 { printf "%s", sep }
 800         1 { printf "%s", $0 }
 801         END { printf "\n" }
 802     ' "$@"
 803 }
 804 
 805 # GO Build Stripped: a common use-case for the go compiler
 806 gobs() {
 807     go build -ldflags "-s -w" -trimpath "$@"
 808 }
 809 
 810 # GO Build WINdows GUI: a common use-case for the go compiler
 811 gobwingui() {
 812     go build -ldflags "-s -w -H=windowsgui" -trimpath "$@"
 813 }
 814 
 815 # GO DEPendencies: shows all dependencies in a go project
 816 godep() {
 817     go list -f '{{ join .Deps "\n" }}' "$@"
 818 }
 819 
 820 # GO IMPorts: show all imports in a go project
 821 goimp() {
 822     go list -f '{{ join .Imports "\n" }}' "$@"
 823 }
 824 
 825 # transform lines using AWK's gsub func (global substitute)
 826 gsub() {
 827     local what
 828     local with
 829     what="${1}"
 830     shift
 831     with="${1}"
 832     shift
 833     # awk "{ gsub(/${what}/, \"${with}\"); print }" "$@"
 834     awk "{ gsub(${what}\"${with}\"); print }" "$@"
 835 }
 836 
 837 # Go To Hell deletes telemetric data for go's vscode extension
 838 # gth() {
 839 #     rm -r -d .../go/telemetry # exact path is installation-dependent
 840 # }
 841 
 842 # run my own `hat` (HAndy Tools) cmd-line app
 843 # h() {
 844 #     hat "$@"
 845 # }
 846 
 847 # Highlight (lines) with AWK
 848 hawk() {
 849     local cond
 850     cond="${1:-1}"
 851     shift
 852 
 853     awk "
 854         ${cond} {
 855             gsub(/\\x1b\\[0m/, \"\x1b[0m\\x1b[7m\")
 856             printf \"\\x1b[7m%s\\x1b[0m\\n\"\$0
 857             fflush()
 858             next
 859         }
 860 
 861         { print; fflush() }
 862     " "$@"
 863 }
 864 
 865 # view most apps' help messages using `less`
 866 helpless() {
 867     "${1}" "${2:---help}" 2>&1 | less -KiCRS
 868 }
 869 
 870 # Header Less runs `less` with line numbers, ANSI styles, no line-wrapping,
 871 # and using the first line as a sticky-header, so it always shows on top
 872 hl() {
 873     less --header=1 -KNiCRS "$@"
 874 }
 875 
 876 # Help Me Remember my custom shell commands
 877 hmr() {
 878     local cmd
 879     cmd="bat"
 880     # debian linux uses a different name for the `bat` app
 881     if [ -e "/usr/bin/batcat" ]; then
 882         cmd="batcat"
 883     fi
 884 
 885     "$cmd" --style=plain,header,numbers --theme='Monokai Extended Light' \
 886         --wrap=never --color=always "$(which clam)" |
 887     sed 's-\x1b\[38;5;70m-\x1b\[38;5;28m-g' | less -KiCRS
 888 }
 889 
 890 # Header View runs `less` without line numbers, with ANSI styles, with no
 891 # line-wrapping, and using the first line as a sticky-header, so it always
 892 # shows on top
 893 hv() {
 894     less --header=1 -KiCRS "$@"
 895 }
 896 
 897 # Insensitive Match ANY matches lines with any of the case-insensitive
 898 # regexes given
 899 imany() {
 900     awk '
 901         BEGIN {
 902             for (i = 1; i < ARGC; i++) {
 903                 v[i] = tolower(ARGV[i])
 904                 delete ARGV[i]
 905             }
 906         }
 907 
 908         {
 909             $0 = tolower($0)
 910             for (i = 1; i <= length(v); i++) {
 911                 if ($0 ~ v[i]) {
 912                     print
 913                     next
 914                 }
 915             }
 916         }
 917     ' "$@"
 918 }
 919 
 920 # indent each line the number of spaces given, or 4 spaces by default
 921 indent() {
 922     local n
 923     n="${1:-4}"
 924     shift
 925 
 926     awk -v n="${n}" '
 927         BEGIN {
 928             # for (i = 1; i <= n; i++) pre = pre " "
 929             pre = " "
 930             while (length(pre) < n) pre = pre pre
 931             pre = substr(pre, 1, n)
 932         }
 933 
 934         { printf pre; print }
 935     ' "$@"
 936 }
 937 
 938 # ignore lines matching any of the case-insensitive regexes given
 939 inone() {
 940     awk '
 941         BEGIN {
 942             for (i = 1; i < ARGC; i++) {
 943                 v[i] = tolower(ARGV[i])
 944                 delete ARGV[i]
 945             }
 946         }
 947 
 948         {
 949             $0 = tolower($0)
 950             for (i = 1; i <= length(v); i++) {
 951                 if ($0 ~ v[i]) {
 952                     next
 953                 }
 954             }
 955             print
 956         }
 957     ' "$@"
 958 }
 959 
 960 # reformat JSON into multiple lines with 2-space indent levels
 961 # j2() {
 962 #     cat "${1:--}" | python -c "#!/usr/bin/python3
 963 # from json import load, dump
 964 # from sys import exit, stderr, stdin, stdout
 965 # try:
 966 #     seps = (', ', ': ')
 967 #     stdout.reconfigure(newline='\n', encoding='utf-8')
 968 #     dump(load(stdin), stdout, indent=2, allow_nan=False, separators=seps)
 969 #     stdout.write('\n')
 970 # except Exception as e:
 971 #     print('\x1b[31m' + str(e) + '\x1b[0m', file=stderr)
 972 #     exit(1)
 973 # "
 974 # }
 975 
 976 # Json Converter; uses python package `jc`, which isn't built-in
 977 # jc() {
 978 #     python -m jc "$@"
 979 # }
 980 
 981 # show a `dad` JOKE from the web, some of which are even funny
 982 joke() {
 983     curl -s https://icanhazdadjoke.com | fold -s | sed 's/ *$//'
 984     # plain-text output from previous cmd doesn't end with a line-feed
 985     printf "\n"
 986 }
 987 
 988 # run `less` with line numbers, ANSI styles, and no line-wrapping; this
 989 # command used to be called `least`, but that's much longer to type
 990 l() {
 991     less -KNiCRS "$@"
 992 }
 993 
 994 # Like A Book groups lines as 2 side-by-side pages, the same way books
 995 # do it; uses my own cmd-line app `book`
 996 # lab() {
 997 #     book "$(($(tput lines) - 1))" "$@" | less -KiCRS
 998 # }
 999 
1000 # Line xARGS: `xargs` using line separators, which handles filepaths
1001 # with spaces, as long as the standard input has 1 path per line
1002 largs() {
1003     xargs -d "\n" "$@"
1004 }
1005 
1006 # get the last n lines, or 1 by default
1007 last() {
1008     tail -n "${1:-1}" "${2:--}"
1009 }
1010 
1011 # leak emits/tees input both to stdout and stderr; useful in pipes
1012 leak() {
1013     tee /dev/stderr
1014 }
1015 
1016 # LEAK Orange emits/tees input both to stdout and stderr, coloring orange
1017 # what it emits to stderr using an ANSI-style; this cmd is useful to `debug`
1018 # pipes involving several steps
1019 leako() {
1020     awk '{
1021         print
1022         fflush()
1023         gsub(/\x1b\[0m/, "\x1b[0m\x1b[38;5;166m")
1024         printf "\x1b[38;5;166m%s\x1b[0m\n", $0 > "/dev/stderr"
1025         fflush("/dev/stderr")
1026     }'
1027 }
1028 
1029 # concatenate all named input sources, ignoring trailing CRLFs into LFs,
1030 # and guaranteeing lines from different sources are accidentally joined,
1031 # by adding a line-feed when an input's last line doesn't end with one
1032 # lines() {
1033 #     # awk 1 "$@"
1034 #     awk 'FNR == 1 { gsub(/^\xef\xbb\xbf/, "") } 1' "$@"
1035 # }
1036 
1037 # regroup adjacent lines into n-item tab-separated lines
1038 lineup() {
1039     local n
1040     n="${1:-0}"
1041     shift
1042 
1043     if [ "${n}" -le 0 ]; then
1044         # printf "invalid number %s\n", "${n}" 2>&1
1045         # return 1
1046         awk '
1047             NR > 1 { printf "\t" }
1048             { printf "%s", $0 }
1049             END { if (NR > 0) print "" }
1050         ' "$@"
1051         return "$?"
1052     fi
1053 
1054     awk -v n="${n}" '
1055         NR % n != 1 { printf "\t" }
1056         { printf "%s", $0 }
1057         NR % n == 0 { print "" }
1058         END { if (NR % n != 0) print "" }
1059     ' "$@"
1060 }
1061 
1062 # make all text lowercase
1063 lower() {
1064     awk '{ print tolower($0) }' "$@"
1065 }
1066 
1067 # match the regex given, or match non-empty(ish) lines by default
1068 m() {
1069     local regex
1070     regex="${1:-[^ *]\r?$}"
1071     shift
1072     grep -E "${regex}" "$@"
1073 }
1074 
1075 # Match ANY matches lines with any of the case-sensitive regexes given
1076 many() {
1077     awk '
1078         BEGIN {
1079             for (i = 1; i < ARGC; i++) {
1080                 v[i] = ARGV[i]
1081                 delete ARGV[i]
1082             }
1083         }
1084 
1085         {
1086             for (i = 1; i <= length(v); i++) {
1087                 if ($0 ~ v[i]) {
1088                     print
1089                     next
1090                 }
1091             }
1092         }
1093     ' "$@"
1094 }
1095 
1096 # match the regex given, or match non-empty(ish) lines by default
1097 match() {
1098     local regex
1099     regex="${1:-[^ *]\r?$}"
1100     shift
1101     grep -E "${regex}" "$@"
1102 }
1103 
1104 # merge stderr into stdout without any keyboard-dancing
1105 merrge() {
1106     "$@" 2>&1
1107 }
1108 
1109 # Number all lines starting from 1, turning all CRLF into single LF bytes,
1110 # and ensuring lines aren't accidentally joined when changing input sources
1111 n() {
1112     # awk '{ printf "%6d  %s\n", NR, $0 }' "$@"
1113     awk '{ printf "%d\t%s\n", NR, $0 }' "$@"
1114 }
1115 
1116 # Number all lines starting from 0, turning all CRLF into single LF bytes,
1117 # and ensuring lines aren't accidentally joined when changing input sources
1118 n0() {
1119     # awk '{ printf "%6d  %s\n", NR - 1, $0 }' "$@"
1120     awk '{ printf "%d\t%s\n", NR - 1, $0 }' "$@"
1121 }
1122 
1123 # Number all lines starting from 1, turning all CRLF into single LF bytes,
1124 # and ensuring lines aren't accidentally joined when changing input sources
1125 n1() {
1126     # awk '{ printf "%6d  %s\n", NR, $0 }' "$@"
1127     awk '{ printf "%d\t%s\n", NR, $0 }' "$@"
1128 }
1129 
1130 # Nice File Sizes
1131 nfs() {
1132     # turn arg-list into single-item lines
1133     awk 'BEGIN { for (i = 1; i < ARGC; i++) print ARGV[i]; exit }' "$@" |
1134     # calculate file-sizes, and reverse-sort results
1135     xargs -d '\n' wc -c | sort -rn |
1136     # start output with a header-like line, and add a MiB field
1137     awk 'BEGIN { printf "%5s  %9s  %8s  name\n", "n", "bytes", "MiB" }
1138     { printf "%5d  %9d  %8.2f  %s\n", NR - 1, $1, $1 / 1048576, $2 }' |
1139     # make zeros in the MiB field stand out with a special color
1140     awk '{ gsub(/ 0.00 /, "\x1b[38;5;103m 0.00 \x1b[0m"); print }' |
1141     # make table breathe with empty lines, so tall outputs are readable
1142     awk '(NR - 2) % 5 == 1 && NR > 1 { print "" } 1'
1143 }
1144 
1145 # emit nothing to output and/or discard everything from input
1146 nil() {
1147     if [ -p /dev/stdin ]; then
1148         cat > /dev/null
1149     else
1150         head -c 0
1151     fi
1152 }
1153 
1154 # ignore stderr without any keyboard-dancing
1155 noerr() {
1156     "$@" 2> /dev/null
1157 }
1158 
1159 # none ignores lines matching any of the case-sensitive regexes given
1160 none() {
1161     awk '
1162         BEGIN {
1163             for (i = 1; i < ARGC; i++) {
1164                 v[i] = ARGV[i]
1165                 delete ARGV[i]
1166             }
1167         }
1168 
1169         {
1170             for (i = 1; i <= length(v); i++) {
1171                 if ($0 ~ v[i]) {
1172                     next
1173                 }
1174             }
1175             print
1176         }
1177     ' "$@"
1178 }
1179 
1180 # show the current date and time
1181 now() {
1182     date +'%Y-%m-%d %H:%M:%S'
1183 }
1184 
1185 # keep only the nth line from the input, if it has at least that many lines
1186 nth() {
1187     local n
1188     n="${1}"
1189     shift
1190 
1191     awk -v n="${n}" '
1192         BEGIN { if (n < 1) exit }
1193         NR == n { print; exit }
1194     ' "$@"
1195 }
1196 
1197 # Orange LEAK emits/tees input both to stdout and stderr, coloring orange
1198 # what it emits to stderr using an ANSI-style; this cmd is useful to `debug`
1199 # pipes involving several steps
1200 oleak() {
1201     awk '{
1202         print
1203         fflush()
1204         gsub(/\x1b\[0m/, "\x1b[0m\x1b[38;5;166m")
1205         printf "\x1b[38;5;166m%s\x1b[0m\n", $0 > "/dev/stderr"
1206         fflush("/dev/stderr")
1207     }'
1208 }
1209 
1210 # Ok, Warning, Error: show pattern-matched lines in green, yellow, and red
1211 # respectively; all other lines keep their original style(s), or lack of
1212 owe() {
1213     local ok
1214     local warning
1215     local error
1216     ok="${1:-0}"
1217     shift
1218     warning="${1:-0}"
1219     shift
1220     error="${1:-0}"
1221     shift
1222 
1223     awk "
1224         ${ok} {
1225             gsub(/\\x1b\\[0m/, \"\x1b[0m\\x1b[38;5;29m\")
1226             printf \"\\x1b[38;5;29m%s\x1b[0m\\n\"\$0
1227             fflush()
1228             next
1229         }
1230 
1231         ${warning} {
1232             gsub(/\\x1b\\[0m/, \"\x1b[0m\\x1b[38;5;229m\")
1233             printf \"\\x1b[38;5;229m%s\x1b[0m\\n\"\$0
1234             fflush()
1235             next
1236         }
1237 
1238         ${error} {
1239             gsub(/\\x1b\\[0m/, \"\x1b[0m\\x1b[38;5;1m\")
1240             printf \"\\x1b[38;5;1m%s\x1b[0m\\n\"\$0
1241             fflush()
1242             next
1243         }
1244 
1245         { print; fflush() }
1246     " "$@"
1247 }
1248 
1249 # pad lines to be 80-char wide when they are shorter than that
1250 pad80() {
1251     awk '{ printf "%-80s\n", $0 }' "$@"
1252 }
1253 
1254 # Print AWK expressions
1255 pawk() {
1256     local arg
1257     local shown
1258     for arg in "$@"; do
1259         shown="END { print $(echo "${arg}" | sed 's/"/\\"/g') }"
1260         printf "\x1b[48;5;253m\x1b[38;5;26m%-80s\x1b[0m\n" \
1261             "awk \"${shown}\" < /dev/null" >&2
1262         awk "END { print ${arg} }" < /dev/null
1263     done
1264 }
1265 
1266 # ignore ANSI terminal styling
1267 plain() {
1268     awk '{ gsub(/\x1b\[([0-9]*[A-HJKST]|[0-9;]*m)/, ""); print }' "$@"
1269 }
1270 
1271 # reset ANSI styles at the end of each line
1272 plainend() {
1273     awk '{ printf "%s\x1b[0m\n", $0 }' "$@"
1274 }
1275 
1276 # reflow/trim lines of prose (text) to improve its legibility: it
1277 # seems especially useful when the text is pasted from web-pages
1278 # being viewed in reader mode
1279 reprose() {
1280     awk 'FNR == 1 && NR > 1 { print "" } 1' "$@" |
1281         fold -s |
1282         sed 's/  *$//'
1283 }
1284 
1285 # REPeat a STRing n times, or 80 times by default
1286 repstr() {
1287     awk -v what="${1}" -v times="${2:-80}" 'BEGIN {
1288         if (length(what) == 0) exit 0
1289         for (i = 1; i <= times; i++) printf "%s", what
1290         printf "\n"
1291     }' < /dev/null
1292 }
1293 
1294 # RUN a command IN the folder given as the first argument
1295 runin() {
1296     local prev
1297     local res
1298 
1299     prev="${OLDPWD}"
1300     cd "${1}" || return 1
1301 
1302     shift
1303     "$@"
1304     res="$?"
1305 
1306     cd - || return 1
1307     OLDPWD="${prev}"
1308     return "${res}"
1309 }
1310 
1311 # show a unique-looking separator line; useful to run between commands
1312 # which output walls of text
1313 sep() {
1314     printf "\x1b[48;5;253m"
1315     printf "·························································"
1316     printf "·······················"
1317     printf "\x1b[0m\n"
1318 }
1319 
1320 # serve starts a local webserver from the current folder, using the port
1321 # number given, or port 8080 by default
1322 serve() {
1323     printf "\x1b[38;5;26mserving files in ${2:-$(pwd)}\x1b[0m\n" >&2
1324     python -m http.server "${1:-8080}" -d "${2:-.}"
1325 }
1326 
1327 # SKIP the first n lines, or the 1st line by default
1328 skip() {
1329     tail -n +$(("${1:-1}" + 1)) "${2:--}"
1330 }
1331 
1332 # SKIP the FIRST n lines, or the 1st line by default
1333 skipfirst() {
1334     tail -n +$(("${1:-1}" + 1)) "${2:--}"
1335 }
1336 
1337 # skip/ignore the last n lines, or only the very last line by default
1338 skiplast() {
1339     head -n -"${1:-1}" "${2:--}"
1340 }
1341 
1342 # Styled LEAK emits/tees input both to stdout and stderr, coloring what
1343 # it emits to stderr using ANSI-styles; this cmd is useful to `debug`
1344 # pipes involving several steps
1345 # sleak() {
1346 #     awk '{
1347 #         print
1348 #         fflush()
1349 #         gsub(/\x1b\[0m/, "\x1b[0m\x1b[38;5;26m")
1350 #         printf "\x1b[38;5;26m%s\x1b[0m\n", $0 > "/dev/stderr"
1351 #         fflush("/dev/stderr")
1352 #     }'
1353 # }
1354 
1355 # show the reverse-SOrted SIzes of various files
1356 sosi() {
1357     wc -c "$@" | sort -rn
1358 }
1359 
1360 # ignore leading spaces, trailing spaces, even runs of multiple spaces
1361 # in the middle of lines, as well as trailing carriage returns
1362 squeeze() {
1363     awk '
1364         {
1365             gsub(/  +/, " ")
1366             gsub(/ *\t */, "\t")
1367             gsub(/(^ +)|( *\r?$)/, "")
1368             print
1369         }
1370     ' "$@"
1371 }
1372 
1373 # ssv2tsv turns each run of 1+ spaces into a single tab, while ignoring
1374 # leading spaces in each line
1375 ssv2tsv() {
1376     awk 1 "$@" | sed 's/^  *//; s/  */\t/g'
1377 }
1378 
1379 # underline every 5th line
1380 stripe() {
1381     awk '
1382         NR % 5 == 0 && NR != 1 { printf "\x1b[4m%s\x1b[0m\n", $0; next }
1383         1
1384     ' "$@"
1385 }
1386 
1387 # Tab AWK: TSV-specific I/O settings for `awk`
1388 tawk() {
1389     awk -F "\t" -v OFS="\t" "$@"
1390 }
1391 
1392 # show current date in a specifc format, which is both people-friendly
1393 # and machine/tool/search/automation-friendly
1394 today() {
1395     date +'%Y-%m-%d %a %b %d'
1396 }
1397 
1398 # show all files directly in the folder given, without looking any deeper
1399 topfiles() {
1400     local arg
1401     for arg in "$@"; do
1402         find "${arg}" -maxdepth 1 -type f
1403     done
1404 
1405     if [ "$#" -eq 0 ]; then
1406         find . -maxdepth 1 -type f
1407     fi
1408 }
1409 
1410 # show all folders directly in the folder given, without looking any deeper
1411 topfolders() {
1412     local arg
1413     for arg in "$@"; do
1414         find "${arg}" -maxdepth 1 -type d | awk 'NR > 1'
1415     done
1416 
1417     if [ "$#" -eq 0 ]; then
1418         find . -maxdepth 1 -type d | awk 'NR > 1'
1419     fi
1420 }
1421 
1422 # ignore leading spaces, trailing spaces, and carriage returns on lines
1423 trim() {
1424     awk '{ gsub(/(^ +)|( +$)/, ""); print }' "$@"
1425 }
1426 
1427 # ignore the prefix given from input lines which start with it; input
1428 # lines which don't start with the prefix given will stay unchanged
1429 trimprefix() {
1430     local prefix
1431     prefix="${1:-}"
1432     shift
1433 
1434     awk -v pre="${prefix}" '
1435         index($0, pre) == 1 { $0 = substr($0, length(pre) + 1) }
1436         1
1437     ' "$@"
1438 }
1439 
1440 # ignore the suffix given from input lines which end with it; input
1441 # lines which don't end with the suffix given will stay unchanged
1442 trimsuffix() {
1443     local suffix
1444     suffix="${1:-}"
1445     shift
1446 
1447     awk -v suf="${suffix}" '
1448         {
1449             i = index($0, suf)
1450             if (i != 0 && i == length - length(suf) + 1) {
1451                 $0 = substr($0, 1, length - length(suf))
1452             }
1453         }
1454         1
1455     ' "$@"
1456 }
1457 
1458 # ignore trailing spaces and possibly a carriage return in all lines;
1459 # also, this command ensures separate lines from different inputs will
1460 # never join by accident
1461 trimtrail() {
1462     awk '{ gsub(/ +$/, ""); print }' "$@"
1463 }
1464 
1465 # ignore trailing spaces and possibly a carriage return in all lines;
1466 # also, this command ensures separate lines from different inputs will
1467 # never join by accident
1468 trimtrails() {
1469     awk '{ gsub(/ +$/, ""); print }' "$@"
1470 }
1471 
1472 # try running a command, emitting an explicit message to standard-error
1473 # if the command given fails
1474 try() {
1475     "$@" || (
1476         printf "%s: failure running %s\n" "$0" "$*" >&2
1477         return 255
1478     )
1479 }
1480 
1481 # TimeStamp lines satisfying an AWK condition, ignoring all other lines
1482 tsawk() {
1483     awk \
1484         # -v line="\x1b[38;5;27m%s\x1b[0m %s\n" \
1485         -v line="\x1b[48;5;255m\x1b[38;5;24m%s\x1b[0m %s\n" \
1486         -v time="%Y-%m-%d %H:%M:%S" \
1487         "${1:-1} { printf line, strftime(time), \$0; fflush() }"
1488 }
1489 
1490 # decode base64 bytes
1491 unbase64() {
1492     base64 -d "$@"
1493 }
1494 
1495 # deduplicate lines, keeping them in their original order
1496 unique() {
1497     awk '!c[$0]++' "$@"
1498 }
1499 
1500 # concatenate all named input sources, ignoring trailing CRLFs into LFs,
1501 # and guaranteeing lines from different sources are accidentally joined,
1502 # by adding a line-feed when an input's last line doesn't end with one;
1503 # also, ignore leading UTF-8 BOMs on the first line of each input, as
1504 # those are useless at best
1505 unixify() {
1506     awk 'FNR == 1 { gsub(/^\xef\xbb\xbf/, "") } 1' "$@"
1507 }
1508 
1509 # expand tabs into spaces using the tabstop given
1510 untab() {
1511     local tabstop
1512     tabstop="$1"
1513     shift
1514     expand -t "${tabstop}" "$@"
1515 }
1516 
1517 # go UP n folders, or go up 1 folder by default
1518 up() {
1519     if [ "${1:-1}" -le 0 ]; then
1520         cd .
1521         return "$?"
1522     fi
1523 
1524     cd "$(printf "%${1:-1}s" "" | sed 's- -../-g')"
1525 }
1526 
1527 # emit input lines in reverse order, or last to first
1528 upsidedown() {
1529     awk '
1530         { lines[NR] = $0 }
1531         END { for (i = NR; i >= 1; i--) print lines[i] }
1532     ' "$@"
1533 }
1534 
1535 # Underline Table: underline the first line (the header), then
1536 # underline every 5th line after that
1537 ut() {
1538     awk '
1539         (NR - 1) % 5 == 0 {
1540             gsub(/\x1b\[0m/, "\x1b[0m\x1b[4m")
1541             printf "\x1b[4m%s\x1b[0m\n", $0
1542             next
1543         }
1544 
1545         1
1546     ' "$@"
1547 }
1548 
1549 # View text: run `less` with ANSI styles and no line-wrapping
1550 v() {
1551     less -KiCRS "$@"
1552 }
1553 
1554 # run a command, showing its success/failure right after
1555 verdict() {
1556     local code
1557     local fs
1558     local msg
1559 
1560     "$@"
1561     code="$?"
1562 
1563     if [ "${code}" -eq 0 ]; then
1564         fs="\n\x1b[38;5;29m%s \x1b[48;5;29m\x1b[97m succeeded \x1b[0m\n"
1565         printf "${fs}" "$*" >&2
1566         return 0
1567     fi
1568 
1569     msg="failed with error code ${code}"
1570     printf "\n\x1b[31m%s \x1b[41m\x1b[97m ${msg} \x1b[0m\n" "$*" >&2
1571     return "${code}"
1572 }
1573 
1574 # What Are These (?) shows what the names given to it are/do
1575 wat() {
1576     local code
1577     local a
1578     local res
1579 
1580     code=0
1581     for a in "$@"; do
1582         printf "\x1b[48;5;253m\x1b[38;5;26m%-80s\x1b[0m\n" "${a}"
1583         (alias "${a}" || declare -f "${a}" || which "${a}") 2> /dev/null
1584         res="$?"
1585 
1586         if [ "${res}" -ne 0 ]; then
1587             code="${res}"
1588             printf "\x1b[31m%s not found\x1b[0m\n" "${a}"
1589         fi
1590     done
1591 
1592     return "${code}"
1593 }
1594 
1595 # find all files which have at least 1 line with trailing spaces/CRs, with
1596 # the option to limit the (fully-recursive) search to the files/folders given
1597 wheretrails() {
1598     rg -c '[ \r]+$' "${@:-.}"
1599 }
1600 
1601 # find all files which have at least 1 line with trailing spaces/CRs, with
1602 # the option to limit the (fully-recursive) search to the files/folders given
1603 whichtrails() {
1604     rg -c '[ \r]+$' "${@:-.}"
1605 }
1606 
1607 # What Is This (?) shows what the name given to it is/does
1608 wit() {
1609     (
1610         alias "${1}" || declare -f "${1}" || which "${1}"
1611     ) 2> /dev/null || (
1612         printf "\x1b[31m%s not found\x1b[0m\n" "${1}" >&2 && return 1
1613     )
1614 }
1615 
1616 # year shows a full calendar for the current year, or for the year given
1617 year() {
1618     cal -y "$@"
1619 }
1620 
1621 # show the current date in the YYYY-MM-DD format
1622 ymd() {
1623     date +'%Y-%m-%d'
1624 }