File: clam.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 # clam
  27 #
  28 # Command-Line Augmentation Module (clam): get the best out of your shell
  29 #
  30 #
  31 # This is a collection of arguably useful shell functions and shortcuts:
  32 # some of these extra commands can be real time/effort savers, ideally
  33 # letting you concentrate on getting things done.
  34 #
  35 # Some of these commands depend on my other scripts from the `pac-tools`,
  36 # others either rely on widely-preinstalled command-line apps, or ones
  37 # which are available on most of the major command-line `package` managers.
  38 #
  39 # Among these commands, you'll notice a preference for lines whose items
  40 # are tab-separated instead of space-separated, and unix-style lines, which
  41 # always end with a line-feed, instead of a CRLF byte-pair. This convention
  42 # makes plain-text data-streams less ambiguous and generally easier to work
  43 # with, especially when passing them along pipes.
  44 #
  45 # To use this script, you're supposed to `source` it, so its definitions
  46 # stay for your whole shell session: for that, you can run `source clam` or
  47 # `. clam` (no quotes either way), either directly or at shell startup.
  48 #
  49 # This script is compatible with `bash`, `zsh`, and even `dash`, which is
  50 # debian linux's default non-interactive shell. Some of its commands even
  51 # seem to work on busybox's shell.
  52 
  53 
  54 case "$1" in
  55     -h|--h|-help|--help)
  56         # show help message, using the info-comment from this very script
  57         awk '
  58             /^case / { exit }
  59             /^# +clam$/, /^$/ { gsub(/^# ?/, ""); print }
  60         ' "$0"
  61         exit 0
  62     ;;
  63 esac
  64 
  65 
  66 # dash doesn't support regex-matching syntax, forcing to use case statements
  67 case "$0" in
  68     -bash|-dash|-sh|bash|dash|sh)
  69         # script is being sourced with bash or dash, which is good
  70         :
  71     ;;
  72     *)
  73         case "$ZSH_EVAL_CONTEXT" in
  74             *:file)
  75                 # script is being sourced with zsh, which is good
  76                 :
  77             ;;
  78             *)
  79                 # script is being run normally, which is a waste of time
  80 printf "\e[48;2;255;255;135m\e[38;2;0;0;0mDon't run this script, source it instead: to do that,\e[0m\n"
  81 printf "\e[48;2;255;255;135m\e[38;2;0;0;0mrun 'source clam' or '. clam' (no quotes either way).\e[0m\n"
  82                 # failing during shell-startup may deny shell access, so exit
  83                 # with a 0 error-code to declare success
  84                 exit 0
  85             ;;
  86         esac
  87     ;;
  88 esac
  89 
  90 
  91 # n-column-layout shortcuts, using my tool `bsbs` (Book-like Side By Side)
  92 alias 1='bsbs 1'
  93 alias 2='bsbs 2'
  94 alias 3='bsbs 3'
  95 alias 4='bsbs 4'
  96 alias 5='bsbs 5'
  97 alias 6='bsbs 6'
  98 alias 7='bsbs 7'
  99 alias 8='bsbs 8'
 100 alias 9='bsbs 9'
 101 alias 0='bsbs 10'
 102 
 103 alias a=avoid
 104 
 105 # find name from the local `apt` database of installable packages
 106 aptfind() {
 107     local arg
 108     local gap=0
 109     local options='-JMKiCRS'
 110 
 111     if [ $# -eq 1 ]; then
 112         options='--header=1 -JMKiCRS'
 113     fi
 114 
 115     for arg in "$@"; do
 116         [ "${gap}" -gt 0 ] && printf "\n"
 117         gap=1
 118         printf "\e[7m%-80s\e[0m\n\n" "${arg}"
 119 
 120         # despite warnings, the `search` command has been around for years
 121         apt search "${arg}" 2>/dev/null |
 122             grep -E -A 1 "^[a-z0-9-]*${arg}" | sed -u 's/^--$//'
 123     done | less "${options}"
 124 }
 125 
 126 # APT UPdate/grade
 127 aptup() { sudo apt update && sudo apt upgrade "$@"; sudo -k; }
 128 
 129 # emit each argument given as its own line of output
 130 args() { awk 'BEGIN { for (i = 1; i < ARGC; i++) print ARGV[i]; exit }' "$@"; }
 131 
 132 # avoid/ignore lines which match any of the regexes given
 133 avoid() {
 134     awk '
 135         BEGIN {
 136             for (i = 1; i < ARGC; i++) {
 137                 e[i] = ARGV[i]
 138                 delete ARGV[i]
 139             }
 140         }
 141 
 142         {
 143             for (i = 1; i < ARGC; i++) if ($0 ~ e[i]) next
 144             print; fflush()
 145             got++
 146         }
 147 
 148         END { exit(got == 0) }
 149     ' "${@:-^\r?$}"
 150 }
 151 
 152 # show an ansi-styled BANNER-like line
 153 banner() { printf "\e[7m%-$(tput cols)s\e[0m\n" "$*"; }
 154 
 155 # emit a colored bar which can help visually separate different outputs
 156 bar() {
 157     [ "${1:-80}" -gt 0 ] &&
 158         printf "\e[48;2;218;218;218m%${1:-80}s\e[0m\n" ""
 159 }
 160 
 161 # process Blocks/paragraphs of non-empty lines with AWK
 162 bawk() { stdbuf -oL awk -F='' -v RS='' "$@"; }
 163 
 164 # play a repeating and annoying high-pitched beep sound a few times a second,
 165 # lasting the number of seconds given, or for 1 second by default; uses my
 166 # script `waveout`
 167 beeps() {
 168     local f='sin(2_000 * tau * t) * (t % 0.5 < 0.0625)'
 169     waveout "${1:-1}" "${2:-1} * $f" | mpv --really-quiet -
 170 }
 171 
 172 # start by joining all arguments given as a tab-separated-items line of output,
 173 # followed by all lines from stdin verbatim
 174 begintsv() {
 175     awk '
 176         BEGIN {
 177             for (i = 1; i < ARGC; i++) {
 178                 if (i > 1) printf "\t"
 179                 printf "%s", ARGV[i]
 180                 delete ARGV[i]
 181             }
 182             if (ARGC > 1) printf "\n"
 183             fflush()
 184         }
 185         { print; fflush() }
 186     ' "$@"
 187 }
 188 
 189 # play a repeating synthetic-bell-like sound lasting the number of seconds
 190 # given, or for 1 second by default; uses my tool `waveout`
 191 bell() {
 192     local f='sin(880*tau*u) * exp(-10*u)'
 193     waveout "${1:-1}" "${2:-1} * $f" | mpv --really-quiet -
 194 }
 195 
 196 # Breathe Header 5: add an empty line after the first one (the header),
 197 # then separate groups of 5 lines with empty lines between them
 198 bh5() {
 199     awk '(NR - 1) % 5 == 1 && NR > 1 { print "" } { print; fflush() }' "$@"
 200 }
 201 
 202 # recursively find all files with at least the number of bytes given; when
 203 # not given a minimum byte-count, the default is 100 binary megabytes
 204 bigfiles() {
 205     local n
 206     n="$(echo "${1:-104857600}" | sed -E 's-_--g; s-\.[0-9]+$--')"
 207     [ $# -gt 0 ] && shift
 208 
 209     local arg
 210     for arg in "${@:-.}"; do
 211         if [ ! -d "${arg}" ]; then
 212             printf "\e[38;2;204;0;0mno folder named %s\e[0m\n" "${arg}" >&2
 213             return 1
 214         fi
 215         stdbuf -oL find "${arg}" -type f -size "$n"c -o -size +"$n"c
 216     done
 217 }
 218 
 219 # Breathe Lines 5: separate groups of 5 lines with empty lines
 220 bl5() {
 221     awk 'NR % 5 == 1 && NR != 1 { print "" } { print; fflush() }' "$@"
 222 }
 223 
 224 # emit a line with a repeating block-like symbol in it
 225 blocks() { [ "${1:-80}" -gt 0 ] && printf "%${1:-80}s\n" "" | sed 's- -█-g'; }
 226 
 227 # Book-like MANual, lays out `man` docs as pairs of side-by-side pages; uses
 228 # my tool `bsbs`
 229 bman() {
 230     local w
 231     w="$(tput cols)"
 232     w="$((w / 2 - 4))"
 233     if [ "$w" -lt 65 ]; then
 234         w=65
 235     fi
 236     MANWIDTH="$w" man "$@" | bsbs 2
 237 }
 238 
 239 # BOOK-like MANual, lays out `man` docs as pairs of side-by-side pages; uses
 240 # my tool `bsbs`
 241 bookman() {
 242     local w
 243     w="$(tput cols)"
 244     w="$((w / 2 - 4))"
 245     if [ "$w" -lt 65 ]; then
 246         w=65
 247     fi
 248     MANWIDTH="$w" man "$@" | bsbs 2
 249 }
 250 
 251 # split lines using the regex given, turning them into single-item lines
 252 breakdown() {
 253     local sep="${1:- }"
 254     [ $# -gt 0 ] && shift
 255     awk -v FPAT="${sep}" '{ for (i = 1; i <= NF; i++) print $i; fflush() }' "$@"
 256 }
 257 
 258 # play a busy-phone-line sound lasting the number of seconds given, or for 1
 259 # second by default; uses my tool `waveout`
 260 busy() {
 261     local f='min(1, exp(-90*(u-0.5))) * (sin(480*tau*t) + sin(620*tau*t)) / 2'
 262     waveout "${1:-1}" "${2:-1} * $f" | mpv --really-quiet -
 263 }
 264 
 265 c() { cat "$@"; }
 266 
 267 # CAlculator with Nice numbers runs my tool `ca` and colors results with
 268 # my tool `nn`, alternating styles to make long numbers easier to read
 269 can() {
 270     local arg
 271     for arg in "$@"; do
 272         printf "\e[7m%s\e[0m\n" "${arg}" >&2
 273         ca "${arg}" | nn --gray
 274     done
 275 }
 276 
 277 # uppercase the first letter on each line, and lowercase all later letters
 278 capitalize() {
 279     awk '{ print; fflush() }' "$@" | sed -E -u 's-^(.*)-\L\1-; s-^(.)-\u\1-'
 280 }
 281 
 282 # conCATenate Lines guarantees no lines are ever accidentally joined
 283 # across inputs, always emitting a line-feed at the end of every line
 284 catl() { awk '{ print; fflush() }' "$@"; }
 285 
 286 # Count with AWK: count the times the AWK expression/condition given is true
 287 cawk() {
 288     local cond="${1:-1}"
 289     [ $# -gt 0 ] && shift
 290     awk "
 291         { low = lower = tolower(\$0) }
 292         ${cond} { count++ }
 293         END { print count }
 294     " "$@"
 295 }
 296 
 297 # Compile C Optimized
 298 cco () { cc -Wall -O3 -s -fanalyzer -march=native -mtune=native -flto "$@"; }
 299 
 300 # Compile C Plus Plus Optimized
 301 cppo () { c++ -Wall -O3 -s -fanalyzer -march=native -mtune=native -flto "$@"; }
 302 
 303 # center-align lines of text, using the current screen width
 304 center() {
 305     awk -v width="$(tput cols)" '
 306         {
 307             gsub(/\r$/, "")
 308             lines[NR] = $0
 309             s = $0
 310             gsub(/\x1b\[[0-9;]*[A-Za-z]/, "", s) # ANSI style-changers
 311             l = length(s)
 312             if (maxlen < l) maxlen = l
 313         }
 314 
 315         END {
 316             n = (width - maxlen) / 2
 317             if (n % 1) n = n - (n % 1)
 318             fmt = sprintf("%%%ds%%s\n", (n > 0) ? n : 0)
 319             for (i = 1; i <= NR; i++) printf fmt, "", lines[i]
 320         }
 321     ' "$@"
 322 }
 323 
 324 # Colored Go Test on the folder given; uses my command `jawk`
 325 cgt() { go test "${1:-.}" 2>&1 | jawk '/^ok/' '/^[-]* ?FAIL/' '/^\?/'; }
 326 
 327 # ignore final life-feed from text, if it's the very last byte; also ignore
 328 # all trailing carriage-returns
 329 choplf() {
 330     awk '
 331         FNR == 1 { gsub(/^\xef\xbb\xbf/, "") }
 332         NR > 1 { print ""; fflush() }
 333         { gsub(/\r$/, ""); printf "%s", $0; fflush() }
 334     ' "$@"
 335 }
 336 
 337 # Color Json using the `jq` app, allowing an optional filepath as the data
 338 # source, and even an optional transformation formula
 339 cj() { jq -C "${2:-.}" "${1:--}"; }
 340 
 341 # clean the screen, after running the command given
 342 clean() {
 343     local res
 344     if [ -p /dev/stdout ]; then
 345         "$@"
 346         return $?
 347     fi
 348 
 349     tput smcup
 350     "$@"
 351     res=$?
 352     tput rmcup
 353     return "${res}"
 354 }
 355 
 356 # Colored Live/Line-buffered RipGrep ensures results show up immediately,
 357 # also emitting colors when piped
 358 clrg() { rg --color=always --line-buffered "$@"; }
 359 
 360 # CLear Screen, like the old dos command of the same name
 361 cls() { clear; }
 362 
 363 # Colored RipGrep ensures app `rg` emits colors when piped
 364 crg() { rg --color=always --line-buffered "$@"; }
 365 
 366 # emit a line with a repeating cross-like symbol in it
 367 crosses() {
 368     [ "${1:-80}" -gt 0 ] && printf "%${1:-80}s\n" "" | sed 's- -×-g'
 369 }
 370 
 371 # split lines using the string given, turning them into single-item lines
 372 crumble() {
 373     local sep="${1:- }"
 374     [ $# -gt 0 ] && shift
 375     awk -F "${sep}" '{ for (i = 1; i <= NF; i++) print $i; fflush() }' "$@"
 376 }
 377 
 378 # Color Syntax: run syntax-coloring app `bat` without line-wrapping
 379 cs() {
 380     local cmd="bat"
 381     # debian linux uses a different name for the `bat` app
 382     if [ -e "/usr/bin/batcat" ]; then
 383         cmd="batcat"
 384     fi
 385 
 386     "${cmd}" --style=plain,header,numbers \
 387         --theme='Monokai Extended Light' \
 388         --wrap=never --color=always --paging=never "$@" |
 389             # make colors readable even on light backgrounds
 390             sed -e 's-\x1b\[38;5;70m-\x1b[38;5;28m-g' \
 391                 -e 's-\x1b\[38;5;214m-\x1b[38;5;208m-g' \
 392                 -e 's-\x1b\[38;5;228m-\x1b[48;5;228m-g' |
 393             less -JMKiCRS
 394 }
 395 
 396 # Color Syntax of all files in a Folder, showing line numbers
 397 csf() {
 398     local cmd="bat"
 399     # debian linux uses a different name for the `bat` app
 400     if [ -e "/usr/bin/batcat" ]; then
 401         cmd="batcat"
 402     fi
 403 
 404     find "${1:-.}" -type f -print0 |
 405         xargs --null "${cmd}" \
 406             --style=plain,header,numbers --theme='Monokai Extended Light' \
 407             --wrap=never --color=always |
 408         # make colors readable even on light backgrounds
 409         sed -e 's-\x1b\[38;5;70m-\x1b[38;5;28m-g' \
 410             -e 's-\x1b\[38;5;214m-\x1b[38;5;208m-g' \
 411             -e 's-\x1b\[38;5;228m-\x1b[48;5;228m-g' |
 412         less -JMKiCRS
 413 }
 414 
 415 # Change Units turns common US units into international ones; uses my
 416 # scripts `bu` (Better Units) and `nn` (Nice Numbers)
 417 cu() {
 418     bu "$@" | awk '
 419         NF == 5 || (NF == 4 && $NF == "s") { print $(NF-1), $NF }
 420         NF == 4 && $NF != "s" { print $NF }
 421     ' | nn --gray
 422 }
 423 
 424 # CURL Silent spares you the progress bar, but still tells you about errors
 425 curls() { curl --show-error -s "$@"; }
 426 
 427 # listen to streaming DANCE music
 428 dance() {
 429     printf "streaming \e[7mDance Wave Retro\e[0m\n"
 430     # mpv --quiet https://retro.dancewave.online/retrodance.mp3
 431     mpv --really-quiet https://retro.dancewave.online/retrodance.mp3
 432 }
 433 
 434 # emit a line with a repeating dash-like symbol in it
 435 dashes() { [ "${1:-80}" -gt 0 ] && printf "%${1:-80}s\n" "" | sed 's- -—-g'; }
 436 
 437 # DEcode BASE64-encoded data, or even base64-encoded data-URIs, by ignoring
 438 # the leading data-URI declaration, if present
 439 debase64() { sed -E 's-^data:.{0,50};base64,--' "${1:--}" | base64 -d; }
 440 
 441 # ignore whole-comment lines, or just trailing unix-style comments in them
 442 decomment() {
 443     awk '/^ *#/ { next } { gsub(/ *#.*$/, ""); print; fflush(); }' "$@"
 444 }
 445 
 446 # turn Comma-Separated-Values tables into tab-separated-values tables
 447 # decsv() { xsv fmt -t '\t' "$@"; }
 448 
 449 # DEDUPlicate prevents lines from appearing more than once
 450 dedup() { awk '!c[$0]++ { print; fflush() }' "$@"; }
 451 
 452 # dictionary-DEFine the word given, using an online service
 453 alias def=define
 454 
 455 # dictionary-define the word given, using an online service
 456 define() {
 457     local arg
 458     local gap=0
 459     local options='-JMKiCRS'
 460 
 461     if [ $# -eq 0 ]; then
 462         printf "\e[38;2;204;0;0mdefine: no names given\e[0m\n" >&2
 463         return 1
 464     fi
 465 
 466     if [ $# -eq 1 ]; then
 467         options='--header=1 -JMKiCRS'
 468     fi
 469 
 470     for arg in "$@"; do
 471         [ "${gap}" -gt 0 ] && printf "\n"
 472         gap=1
 473         printf "\e[7m%-80s\x1b[0m\n" "${arg}"
 474         curl -s "dict://dict.org/d:${arg}" | awk '
 475             { gsub(/\r$/, "") }
 476             /^151 / {
 477                 printf "\x1b[38;2;52;101;164m%s\x1b[0m\n", $0; fflush()
 478                 next
 479             }
 480             /^[1-9][0-9]{2} / {
 481                 printf "\x1b[38;2;128;128;128m%s\x1b[0m\n", $0; fflush()
 482                 next
 483             }
 484             { print; fflush() }
 485         '
 486     done | less "${options}"
 487 }
 488 
 489 # turn JSON Lines into a proper json array
 490 dejsonl() { jq -s -M "${@:-.}"; }
 491 
 492 # convert lines of Space(s)-Separated Values into lines of tab-separated values
 493 dessv() {
 494     awk '
 495         FNR == 1 { gsub(/^\xef\xbb\xbf/, "") }
 496 
 497         {
 498             gsub(/\r$/, "")
 499             for (i = 1; i <= NF; i++) {
 500                 if (i > 1) printf "\t"
 501                 printf "%s", $i
 502             }
 503             printf "\n"; fflush()
 504         }
 505     ' "$@"
 506 }
 507 
 508 # expand tabs each into up to the number of space given, or 4 by default
 509 detab() { expand -t "${1:-4}"; }
 510 
 511 # ignore trailing spaces, as well as trailing carriage returns
 512 detrail() { awk '{ gsub(/ *\r?$/, ""); print; fflush() }' "$@"; }
 513 
 514 # turn UTF-16 data into UTF-8
 515 deutf16() { iconv -f utf16 -t utf8 "$@"; }
 516 
 517 # DICtionary-define the word given locally
 518 dic() {
 519     local arg
 520     local gap=0
 521     local options='-JMKiCRS'
 522 
 523     if [ $# -eq 0 ]; then
 524         printf "\e[38;2;204;0;0mdic: no names given\e[0m\n" >&2
 525         return 1
 526     fi
 527 
 528     if [ $# -eq 1 ]; then
 529         options='--header=1 -JMKiCRS'
 530     fi
 531 
 532     for arg in "$@"; do
 533         [ "${gap}" -gt 0 ] && printf "\n"
 534         gap=1
 535         printf "\e[7m%-80s\x1b[0m\n" "${arg}"
 536         dict "${arg}" 2>&1 | awk '
 537             NR == 1 && /^No definitions found for / { err = 1 }
 538             err { printf "\x1b[38;2;204;0;0m%s\x1b[0m\n", $0; next }
 539             1
 540         '
 541     done | less "${options}"
 542 }
 543 
 544 # DIVide 2 numbers 3 ways, including the complement
 545 div() {
 546     awk -v a="${1:-1}" -v b="${2:-1}" '
 547         BEGIN {
 548             gsub(/_/, "", a)
 549             gsub(/_/, "", b)
 550             if (a > b) { c = a; a = b; b = c }
 551             c = 1 - a / b
 552             if (0 <= c && c <= 1) printf "%f\n%f\n%f\n", a / b, b / a, c
 553             else printf "%f\n%f\n", a / b, b / a
 554             exit
 555         }'
 556 }
 557 
 558 # emit a line with a repeating dot-like symbol in it
 559 dots() { [ "${1:-80}" -gt 0 ] && printf "%${1:-80}s\n" "" | sed 's- -·-g'; }
 560 
 561 # ignore/remove all matched regexes given on all stdin lines
 562 drop() {
 563     awk '
 564         BEGIN {
 565             for (i = 1; i < ARGC; i++) {
 566                 e[i] = ARGV[i]
 567                 delete ARGV[i]
 568             }
 569         }
 570 
 571         {
 572             for (i = 1; i < ARGC; i++) gsub(e[i], "")
 573             print; fflush()
 574         }
 575     ' "${@:-\r$}"
 576 }
 577 
 578 # show the current Date and Time
 579 dt() {
 580     printf "\e[38;2;78;154;6m%s\e[0m  \e[38;2;52;101;164m%s\e[0m\n" \
 581         "$(date +'%a %b %d')" "$(date +%T)"
 582 }
 583 
 584 # show the current Date, Time, and a Calendar with the 3 `current` months
 585 dtc() {
 586     {
 587         # show the current date/time center-aligned
 588         printf "%20s\e[38;2;78;154;6m%s\e[0m  \e[38;2;52;101;164m%s\e[0m\n\n" \
 589             "" "$(date +'%a %b %d')" "$(date +%T)"
 590         # debian linux has a different `cal` app which highlights the day
 591         if [ -e "/usr/bin/ncal" ]; then
 592             # fix debian/ncal's weird way to highlight the current day
 593             ncal -C -3 | sed -E 's/_\x08(.)/\x1b[7m\1\x1b[0m/g'
 594         else
 595             cal -3
 596         fi
 597     } | less -JMKiCRS
 598 }
 599 
 600 e() { echo "$@"; }
 601 
 602 # edit plain-text files
 603 edit() { micro "$@"; }
 604 
 605 # EDit RUN shell commands, using an interactive editor; uses my tool `leak`
 606 edrun() {
 607     # dash doesn't support the process-sub syntax
 608     # . <( micro -readonly true -filetype shell | leak --inv )
 609     micro -readonly true -filetype shell | leak --inv | . /dev/fd/0
 610 }
 611 
 612 # show all empty files in a folder, digging recursively
 613 emptyfiles() {
 614     local arg
 615     for arg in "${@:-.}"; do
 616         if [ ! -d "${arg}" ]; then
 617             printf "\e[38;2;204;0;0mno folder named %s\e[0m\n" "${arg}" >&2
 618             return 1
 619         fi
 620         stdbuf -oL find "${arg}" -type f -empty
 621     done
 622 }
 623 
 624 # show all empty folders in a folder, digging recursively
 625 emptyfolders() {
 626     local arg
 627     for arg in "${@:-.}"; do
 628         if [ ! -d "${arg}" ]; then
 629             printf "\e[38;2;204;0;0mno folder named %s\e[0m\n" "${arg}" >&2
 630             return 1
 631         fi
 632         stdbuf -oL find "${arg}" -type d -empty
 633     done
 634 }
 635 
 636 # follow all lines from stdin with a final line, made by joining all arguments
 637 # given as tab-separated-items
 638 endtsv() {
 639     awk '
 640         BEGIN {
 641             for (i = 1; i < ARGC; i++) {
 642                 item[i] = ARGV[i]
 643                 delete ARGV[i]
 644             }
 645         }
 646 
 647         { print; fflush() }
 648 
 649         END {
 650             for (i = 1; i < ARGC; i++) {
 651                 if (i > 1) printf "\t"
 652                 printf "%s", item[i]
 653             }
 654             if (ARGC > 1) printf "\n"
 655             fflush()
 656         }
 657     ' "$@"
 658 }
 659 
 660 # Extended Plain Interactive Grep
 661 epig() { ugrep --color=never -Q -E "$@"; }
 662 
 663 # Extended Plain Recursive Interactive Grep
 664 eprig() { ugrep --color=never -Q -E "$@"; }
 665 
 666 # ignore/remove all matched regexes given on all stdin lines
 667 alias erase=drop
 668 
 669 # Editor Read-Only
 670 ero() { micro -readonly true "$@"; }
 671 
 672 # Expand 4 turns each tab into up to 4 spaces
 673 expand4() { expand -t 4 "$@"; }
 674 
 675 # convert EURos into CAnadian Dollars, using the latest official exchange
 676 # rates from the bank of canada; during weekends, the latest rate may be
 677 # from a few days ago; the default amount of euros to convert is 1, when
 678 # not given
 679 eur2cad() {
 680     local site='https://www.bankofcanada.ca/valet/observations/group'
 681     local csv_rates="${site}/FX_RATES_DAILY/csv"
 682     local url
 683     url="${csv_rates}?start_date=$(date -d '3 days ago' +'%Y-%m-%d')"
 684     curl -s "${url}" | awk -F, -v amount="$(echo "${1:-1}" | sed 's-_--g')" '
 685         /EUR/ { for (i = 1; i <= NF; i++) if($i ~ /EUR/) j = i }
 686         END { gsub(/"/, "", $j); if (j != 0) printf "%.2f\n", amount * $j }'
 687 }
 688 
 689 # Faint (gray) lines with AWK
 690 fawk() {
 691     local cond="${1:-1}"
 692     [ $# -gt 0 ] && shift
 693 
 694     awk '
 695         { low = lower = tolower($0) }
 696 
 697         '"${cond}"' {
 698             gsub(/\x1b\[0m/, "\x1b[0m\x1b[38;2;168;168;168m")
 699             printf "\x1b[38;2;168;168;168m%s\x1b[0m\n", $0; fflush()
 700             next
 701         }
 702 
 703         { print; fflush() }
 704     ' "$@"
 705 }
 706 
 707 # fetch/web-request all URIs given, using protcol HTTPS when none is given
 708 fetch() {
 709     local a
 710     for a in "$@"; do
 711         case "$a" in
 712             file://*|https://*|http://*) curl --show-error -s "$a";;
 713             ftp://*|ftps://*|sftp://*) curl --show-error -s "$a";;
 714             dict://*|telnet://*) curl --show-error -s "$a";;
 715             data:*) echo "$a" | sed -E 's-^data:.{0,50};base64,--' | base64 -d;;
 716             *) curl --show-error -s "https://$a";;
 717         esac
 718     done
 719 }
 720 
 721 # run the Fuzzy Finder (fzf) in multi-choice mode, with custom keybindings
 722 ff() { fzf -m --bind ctrl-a:select-all,ctrl-space:toggle "$@"; }
 723 
 724 # FInd FIles
 725 fifi() {
 726     local arg
 727     local what="${1:-.}"
 728     [ $# -gt 0 ] && shift
 729 
 730     for arg in "${@:-.}"; do
 731         if [ ! -d "${arg}" ]; then
 732             printf "\e[38;2;204;0;0mno folder named %s\e[0m\n" "${arg}" >&2
 733             return 1
 734         fi
 735         stdbuf -oL find "${arg}" -type f
 736     done | awk -v what="${what}" '
 737         BEGIN {
 738             m = "this variant of AWK lacks case-insensitive regex-matching"
 739             if (IGNORECASE == "") {
 740                 printf("\x1b[38;2;204;0;0m%s\x1b[0m\n", m) > "/dev/stderr"
 741                 exit 125
 742             }
 743             IGNORECASE = 1
 744         }
 745 
 746         # BEGIN { what = tolower(what) }
 747         # tolower($0) ~ what { got++; print; fflush() }
 748         $0 ~ what { got++; print; fflush() }
 749         END { exit(got == 0) }
 750     '
 751 }
 752 
 753 filemime() { file --mime-type "$@"; }
 754 
 755 # show all files in folders, digging recursively
 756 files() {
 757     local arg
 758     for arg in "${@:-.}"; do
 759         if [ -f "${arg}" ]; then
 760             printf "%s\n" "$(realpath "${arg}")"
 761             continue
 762         fi
 763         if [ -d "${arg}" ]; then
 764             stdbuf -oL find "${arg}" -type f
 765         fi
 766     done | awk '!c[$0]++ { print; fflush() }'
 767 }
 768 
 769 # recursively find all files with fewer bytes than the number given
 770 filesunder() {
 771     local n
 772     n="$(echo "${1:-4097}" | sed -E 's-_--g; s-\.[0-9]+$--')"
 773     [ $# -gt 0 ] && shift
 774 
 775     local arg
 776     for arg in "${@:-.}"; do
 777         if [ ! -d "${arg}" ]; then
 778             printf "\e[38;2;204;0;0mno folder named %s\e[0m\n" "${arg}" >&2
 779             return 1
 780         fi
 781         stdbuf -oL find "${arg}" -type f -size -"$n"c
 782     done
 783 }
 784 
 785 # get the first n lines, or 1 by default
 786 first() { head -n "${1:-1}" "${2:--}"; }
 787 
 788 # fix lines, ignoring leading UTF-8_BOMs (byte-order-marks) on each input's
 789 # first line, turning all end-of-line CRLF byte-pairs into single line-feeds,
 790 # and ensuring each input's last line ends with a line-feed; trailing spaces
 791 # are also ignored
 792 fixlines() {
 793     awk '
 794         FNR == 1 { gsub(/^\xef\xbb\xbf/, "") }
 795         { gsub(/ *\r?$/, ""); print; fflush() }
 796     ' "$@"
 797 }
 798 
 799 # try to run the command given using line-buffering for its (standard) output
 800 flushlines() { stdbuf -oL "$@"; }
 801 
 802 # show all folders in other folders, digging recursively
 803 folders() {
 804     local arg
 805     for arg in "${@:-.}"; do
 806         if [ -f "${arg}" ]; then
 807             continue
 808         fi
 809         if [ ! -d "${arg}" ]; then
 810             printf "\e[31mno folder named %s\e[0m\n" "${arg}" > /dev/stderr
 811         fi
 812         stdbuf -oL find "${arg}" -type d | awk '!/^\.$/ { print; fflush() }'
 813     done | awk '!c[$0]++ { print; fflush() }'
 814 }
 815 
 816 # show total sizes for the all folders given, digging recursively
 817 foldersizes() {
 818     local arg
 819     for arg in "${@:-.}"; do
 820         if [ ! -d "${arg}" ]; then
 821             printf "\e[38;2;204;0;0mno folder named %s\e[0m\n" "${arg}" >&2
 822             return 1
 823         fi
 824         du "${arg}" | sort -rnk1
 825     done | awk '{ $1 *= 1024; print; fflush() }' | sed -u 's-^ *--; s-  *-\t-1'
 826 }
 827 
 828 # start from the line number given, skipping all previous ones
 829 fromline() { tail -n +"${1:-1}" "${2:--}"; }
 830 
 831 # convert FeeT into meters
 832 ft() {
 833     echo "${@:-1}" | sed -E 's-_--g; s- +-\n-g' |
 834         awk '/./ { printf "%.2f\n", 0.3048 * $0; fflush() }'
 835 }
 836 
 837 # convert FeeT² (squared) into meters²
 838 ft2() {
 839     echo "${@:-1}" | sed -E 's-_--g; s- +-\n-g' |
 840         awk '/./ { printf "%.2f\n", 0.09290304 * $0 }'
 841 }
 842 
 843 # convert a mix of FeeT and INches into meters
 844 ftin() {
 845     local ft="${1:-0}"
 846     ft="$(echo "${ft}" | sed 's-_--g')"
 847     local in="${2:-0}"
 848     in="$(echo "${in}" | sed 's-_--g')"
 849     awk "BEGIN { print 0.3048 * ${ft} + 0.0254 * ${in}; exit }"
 850 }
 851 
 852 # convert GALlons into liters
 853 gal() {
 854     echo "${@:-1}" | sed -E 's-_--g; s- +-\n-g' |
 855         awk '/./ { printf "%.2f\n", 3.785411784 * $0; fflush() }'
 856 }
 857 
 858 # convert binary GigaBytes into bytes
 859 gb() {
 860     echo "${@:-1}" | sed -E 's-_--g; s- +-\n-g' |
 861         awk '/./ { printf "%.4f\n", 1073741824 * $0; fflush() }' |
 862         sed 's-\.00*$--'
 863 }
 864 
 865 # Good, Bad, Meh colors lines using up to 3 regular expressions, keeping all
 866 # other input lines verbatim
 867 gbm() {
 868     local good="$1"
 869     local bad="$2"
 870     local meh="$3"
 871     [ $# -gt 0 ] && shift
 872     [ $# -gt 0 ] && shift
 873     [ $# -gt 0 ] && shift
 874 
 875     awk '
 876         BEGIN {
 877             gotgood = ARGC > 1 && ARGV[1] != ""
 878             gotbad = ARGC > 2 && ARGV[2] != ""
 879             gotmeh = ARGC > 3 && ARGV[3] != ""
 880             good = ARGV[1]
 881             bad = ARGV[2]
 882             meh = ARGV[3]
 883             delete ARGV[1]
 884             delete ARGV[2]
 885             delete ARGV[3]
 886         }
 887 
 888         gotgood && $0 ~ good {
 889             # code to use a color-blind-friendlier blue, instead of green
 890             # gsub(/\x1b\[0m/, "\x1b[0m\x1b[38;2;0;95;215m")
 891             # printf "\x1b[38;2;0;95;215m%s\x1b[0m\n", $0
 892             gsub(/\x1b\[0m/, "\x1b[0m\x1b[38;2;0;135;95m")
 893             printf "\x1b[38;2;0;135;95m%s\x1b[0m\n", $0; fflush()
 894             next
 895         }
 896 
 897         gotbad && $0 ~ bad {
 898             gsub(/\x1b\[0m/, "\x1b[0m\x1b[38;2;204;0;0m")
 899             printf "\x1b[38;2;204;0;0m%s\x1b[0m\n", $0; fflush()
 900             next
 901         }
 902 
 903         gotmeh && $0 ~ meh {
 904             gsub(/\x1b\[0m/, "\x1b[0m\x1b[38;2;168;168;168m")
 905             printf "\x1b[38;2;168;168;168m%s\x1b[0m\n", $0; fflush()
 906             next
 907         }
 908 
 909         { print; fflush() }
 910     ' "${good}" "${bad}" "${meh}" "$@"
 911 }
 912 
 913 # glue/stick together various lines, only emitting a line-feed at the end; an
 914 # optional argument is the output-item-separator, which is empty by default
 915 glue() {
 916     local sep="${1:-}"
 917     [ $# -gt 0 ] && shift
 918     awk -v sep="${sep}" '
 919         NR > 1 { printf "%s", sep }
 920         { gsub(/\r/, ""); printf "%s", $0; fflush() }
 921         END { if (NR > 0) print ""; fflush() }
 922     ' "$@"
 923 }
 924 
 925 # GO Build Stripped: a common use-case for the go compiler
 926 gobs() { go build -ldflags "-s -w" -trimpath "$@"; }
 927 
 928 # GO DEPendencieS: show all dependencies in a go project
 929 godeps() { go list -f '{{ join .Deps "\n" }}' "$@"; }
 930 
 931 # GO IMPortS: show all imports in a go project
 932 goimps() { go list -f '{{ join .Imports "\n" }}' "$@"; }
 933 
 934 # go to the folder picked using an interactive TUI; uses my tool `bf`
 935 goto() {
 936     local where
 937     where="$(bf "${1:-.}")"
 938     if [ $? -ne 0 ]; then
 939         return 0
 940     fi
 941 
 942     where="$(realpath "${where}")"
 943     if [ ! -d "${where}" ]; then
 944         where="$(dirname "${where}")"
 945     fi
 946     cd "${where}" || return
 947 }
 948 
 949 # GRoup via AWK groups lines using common results of the AWK expression given
 950 grawk() {
 951     local code="${1:-\$0}"
 952     [ $# -gt 0 ] && shift
 953 
 954     awk '
 955         { low = lower = tolower($0) }
 956 
 957         {
 958             k = '"${code}"'
 959             if (!(k in groups)) ordkeys[++okl] = k
 960             groups[k][length(groups[k]) + 1] = $0
 961         }
 962 
 963         END {
 964             for (i = 1; i <= okl; i++) {
 965                 k = ordkeys[i]
 966                 n = length(groups[k])
 967                 for (j = 1; j <= n; j++) print groups[k][j]
 968             }
 969         }
 970     ' "$@"
 971 }
 972 
 973 # Style lines using a GRAY-colored BACKground
 974 grayback() {
 975     awk '
 976         {
 977             gsub(/\x1b\[0m/, "\x1b[0m\x1b[48;2;218;218;218m")
 978             printf "\x1b[48;2;218;218;218m%s\x1b[0m\n", $0; fflush()
 979         }
 980     ' "$@"
 981 }
 982 
 983 # Global extended regex SUBstitute, using the AWK function of the same name:
 984 # arguments are used as regex/replacement pairs, in that order
 985 gsub() {
 986     awk '
 987         BEGIN {
 988             for (i = 1; i < ARGC; i++) {
 989                 args[++n] = ARGV[i]
 990                 delete ARGV[i]
 991             }
 992         }
 993         {
 994             for (i = 1; i <= n; i += 2) gsub(args[i], args[i + 1])
 995             print; fflush()
 996         }
 997     ' "$@"
 998 }
 999 
1000 # show Help laid out on 2 side-by-side columns; uses my tool `bsbs`
1001 h2() { naman "$@" | bsbs 2; }
1002 
1003 # Highlight (lines) with AWK
1004 hawk() {
1005     local cond="${1:-1}"
1006     [ $# -gt 0 ] && shift
1007     awk '
1008         { low = lower = tolower($0) }
1009         '"${cond}"' {
1010             gsub(/\x1b\[0m/, "\x1b[0m\x1b[7m")
1011             printf "\x1b[7m%s\x1b[0m\n", $0; fflush()
1012             next
1013         }
1014         { print; fflush() }
1015     ' "$@"
1016 }
1017 
1018 # play a heartbeat-like sound lasting the number of seconds given, or for 1
1019 # second by default; uses my tool `waveout`
1020 heartbeat() {
1021     local a='sin(v[0]*tau*exp(-20*v[1]))*exp(-2*v[1])'
1022     local b='((12, u), (8, (u-0.25)%1))'
1023     local f="sum($a for v in $b) / 2"
1024     waveout "${1:-1}" "${2:-1} * $f" | mpv --really-quiet -
1025 }
1026 
1027 # Highlighted-style ECHO
1028 hecho() { printf "\e[7m%s\e[0m\n" "$*"; }
1029 
1030 # show each byte as a pair of HEXadecimal (base-16) symbols
1031 hexify() {
1032     cat "$@" | od -x -A n |
1033         awk '{ gsub(/ +/, ""); printf "%s", $0; fflush() } END { printf "\n" }'
1034 }
1035 
1036 # HIghlighted-style ECHO
1037 hiecho() { printf "\e[7m%s\e[0m\n" "$*"; }
1038 
1039 # highlight lines
1040 highlight() {
1041     awk '
1042         {
1043             gsub(/\x1b\[0m/, "\x1b[0m\x1b[7m")
1044             printf "\x1b[7m%s\x1b[0m\n", $0; fflush()
1045         }
1046     ' "$@"
1047 }
1048 
1049 # HIghlight LEAK emits/tees input both to stdout and stderr, highlighting what
1050 # it emits to stderr using an ANSI-style; this cmd is useful to `debug` pipes
1051 # involving several steps
1052 hileak() {
1053     awk '
1054         {
1055             gsub(/\x1b\[[0-9;]*[A-Za-z]/, "")
1056             printf "\x1b[7m%s\x1b[0m\n", $0 > "/dev/stderr"
1057             print; fflush()
1058         }
1059     ' "$@"
1060 }
1061 
1062 # highlight lines
1063 alias hilite=highlight
1064 
1065 # Help Me Remember my custom shell commands
1066 hmr() {
1067     local cmd="bat"
1068     # debian linux uses a different name for the `bat` app
1069     if [ -e "/usr/bin/batcat" ]; then
1070         cmd="batcat"
1071     fi
1072 
1073     "$cmd" \
1074         --style=plain,header,numbers --theme='Monokai Extended Light' \
1075         --wrap=never --color=always "$(which clam)" |
1076             sed 's-\x1b\[38;5;70m-\x1b\[38;5;28m-g' | less -JMKiCRS
1077 }
1078 
1079 # convert seconds into a colon-separated Hours-Minutes-Seconds triple
1080 hms() {
1081     echo "${@:-0}" | sed -E 's-_--g; s- +-\n-g' | awk '/./ {
1082         x = $0
1083         h = (x - x % 3600) / 3600
1084         m = (x % 3600) / 60
1085         s = x % 60
1086         printf "%02d:%02d:%05.2f\n", h, m, s; fflush()
1087     }'
1088 }
1089 
1090 # find all hyperlinks inside HREF attributes in the input text
1091 href() {
1092     awk '
1093         BEGIN { e = "href=\"[^\"]+\"" }
1094         {
1095             for (s = $0; match(s, e); s = substr(s, RSTART + RLENGTH)) {
1096                 print substr(s, RSTART + 6, RLENGTH - 7); fflush()
1097             }
1098         }
1099     ' "$@"
1100 }
1101 
1102 # Index all lines starting from 0, using a tab right after each line number
1103 i() { stdbuf -oL nl -b a -w 1 -v 0 "$@"; }
1104 
1105 alias ia=iavoid
1106 
1107 # avoid/ignore lines which case-insensitively match any of the regexes given
1108 iavoid() {
1109     awk '
1110         BEGIN {
1111             if (IGNORECASE == "") {
1112                 m = "this variant of AWK lacks case-insensitive regex-matching"
1113                 printf("\x1b[38;2;204;0;0m%s\x1b[0m\n", m) > "/dev/stderr"
1114                 exit 125
1115             }
1116             IGNORECASE = 1
1117 
1118             for (i = 1; i < ARGC; i++) {
1119                 e[i] = ARGV[i]
1120                 delete ARGV[i]
1121             }
1122         }
1123 
1124         {
1125             for (i = 1; i < ARGC; i++) if ($0 ~ e[i]) next
1126             print; fflush(); got++
1127         }
1128 
1129         END { exit(got == 0) }
1130     ' "${@:-^\r?$}"
1131 }
1132 
1133 # case-Insensitively DEDUPlicate prevents lines from appearing more than once
1134 idedup() { awk '!c[tolower($0)]++ { print; fflush() }' "$@"; }
1135 
1136 # ignore/remove all case-insensitively matched regexes given on all stdin lines
1137 idrop() {
1138     awk '
1139         BEGIN {
1140             if (IGNORECASE == "") {
1141                 m = "this variant of AWK lacks case-insensitive regex-matching"
1142                 printf("\x1b[38;2;204;0;0m%s\x1b[0m\n", m) > "/dev/stderr"
1143                 exit 125
1144             }
1145             IGNORECASE = 1
1146 
1147             for (i = 1; i < ARGC; i++) { e[i] = ARGV[i]; delete ARGV[i] }
1148         }
1149 
1150         {
1151             for (i = 1; i < ARGC; i++) gsub(e[i], "")
1152             print; fflush()
1153         }
1154     ' "${@:-\r$}"
1155 }
1156 
1157 # ignore/remove all case-insensitively matched regexes given on all stdin lines
1158 alias ierase=idrop
1159 
1160 # ignore command in a pipe: this allows quick re-editing of pipes, while
1161 # still leaving signs of previously-used steps, as a memo
1162 ignore() { cat; }
1163 
1164 alias im=imatch
1165 
1166 # only keep lines which case-insensitively match any of the regexes given
1167 imatch() {
1168     awk '
1169         BEGIN {
1170             if (IGNORECASE == "") {
1171                 m = "this variant of AWK lacks case-insensitive regex-matching"
1172                 printf("\x1b[38;2;204;0;0m%s\x1b[0m\n", m) > "/dev/stderr"
1173                 exit 125
1174             }
1175             IGNORECASE = 1
1176 
1177             for (i = 1; i < ARGC; i++) {
1178                 e[i] = ARGV[i]
1179                 delete ARGV[i]
1180             }
1181         }
1182 
1183         {
1184             for (i = 1; i < ARGC; i++) {
1185                 if ($0 ~ e[i]) {
1186                     print; fflush()
1187                     got++
1188                     next
1189                 }
1190             }
1191         }
1192 
1193         END { exit(got == 0) }
1194     ' "${@:-[^\r]}"
1195 }
1196 
1197 # start each non-empty line with extra n spaces
1198 indent() {
1199     awk '
1200         BEGIN {
1201             n = ARGV[1] + 0
1202             delete ARGV[1]
1203             fmt = sprintf("%%%ds%%s\n", (n > 0) ? n : 0)
1204         }
1205 
1206         /^\r?$/ { print ""; fflush(); next }
1207         { gsub(/\r$/, ""); printf(fmt, "", $0); fflush() }
1208     ' "$@"
1209 }
1210 
1211 # Interactive Read-Only Top
1212 irot() { htop --readonly "$@"; }
1213 
1214 # emit each word-like item from each input line on its own line; when a file
1215 # has tabs on its first line, items are split using tabs alone, which allows
1216 # items to have spaces in them
1217 items() {
1218     awk '
1219         FNR == 1 { FS = ($0 ~ /\t/) ? "\t" : " "; $0 = $0 }
1220         { gsub(/\r$/, ""); for (i = 1; i <= NF; i++) print $i; fflush() }
1221     ' "$@"
1222 }
1223 
1224 # case-insensitively deduplicate lines, keeping them in their original order:
1225 # the checking/matching is case-insensitive, but each first match is output
1226 # exactly as is
1227 iunique() { awk '!c[tolower($0)]++ { print; fflush() }' "$@"; }
1228 
1229 alias j0=json0
1230 
1231 alias j2=json2
1232 
1233 # Judge with AWK colors lines green/red/gray, using up to 3 AWK conditions
1234 jawk() {
1235     local good="${1:-0}"
1236     local bad="${2:-0}"
1237     local meh="${3:-0}"
1238 
1239     [ $# -gt 0 ] && shift
1240     [ $# -gt 0 ] && shift
1241     [ $# -gt 0 ] && shift
1242 
1243     awk '
1244         { low = lower = tolower($0) }
1245 
1246         '"${good}"' {
1247             # code to use a color-blind-friendlier blue, instead of green
1248             # gsub(/\x1b\[0m/, "\x1b[0m\x1b[38;2;0;95;215m")
1249             # printf "\x1b[38;2;0;95;215m%s\x1b[0m\n", $0; fflush()
1250 
1251             gsub(/\x1b\[0m/, "\x1b[0m\x1b[38;2;0;135;95m")
1252             printf "\x1b[38;2;0;135;95m%s\x1b[0m\n", $0; fflush()
1253             next
1254         }
1255 
1256         '"${bad}"' {
1257             gsub(/\x1b\[0m/, "\x1b[0m\x1b[38;2;204;0;0m")
1258             printf "\x1b[38;2;204;0;0m%s\x1b[0m\n", $0; fflush()
1259             next
1260         }
1261 
1262         '"${meh}"' {
1263             gsub(/\x1b\[0m/, "\x1b[0m\x1b[38;2;168;168;168m")
1264             printf "\x1b[38;2;168;168;168m%s\x1b[0m\n", $0; fflush()
1265             next
1266         }
1267 
1268         { print; fflush() }
1269     ' "$@"
1270 }
1271 
1272 # listen to streaming JAZZ music
1273 jazz() {
1274     printf "streaming \e[7mSmooth Jazz Instrumental\e[0m\n"
1275     # mpv https://stream.zeno.fm/00rt0rdm7k8uv
1276     mpv --quiet https://stream.zeno.fm/00rt0rdm7k8uv
1277 }
1278 
1279 # Json Lines turns JSON top-level arrays into multiple individually-JSON lines
1280 # using the `jq` app, keeping all other top-level values as single line JSON
1281 # outputs, allowing an optional filepath as the data source
1282 jl() { jq -c -C ".[]" "${1:--}"; }
1283 
1284 # show a `dad` JOKE from the web, sometimes even a very funny one
1285 joke() {
1286     curl --show-error -s https://icanhazdadjoke.com | fold -s |
1287         awk '{ gsub(/ *\r?$/, ""); print }'
1288 }
1289 
1290 # shrink/compact JSON using the `jq` app, allowing an optional filepath, and
1291 # even an optional transformation formula after that
1292 json0() { jq -c -M "${2:-.}" "${1:--}"; }
1293 
1294 # show JSON data on multiple lines, using 2 spaces for each indentation level,
1295 # allowing an optional filepath, and even an optional transformation formula
1296 # after that
1297 json2() { jq --indent 2 -M "${2:-.}" "${1:--}"; }
1298 
1299 # JSON Lines turns JSON top-level arrays into multiple individually-JSON lines
1300 # using the `jq` app, keeping all other top-level values as single line JSON
1301 # outputs, allowing an optional filepath as the data source
1302 jsonl() { jq -c -C ".[]" "${1:--}"; }
1303 
1304 # emit the given number of random/junk bytes, or 1024 junk bytes by default
1305 junk() { head -c "$(echo "${1:-1024}" | sed 's-_--g')" /dev/urandom; }
1306 
1307 # convert binary KiloBytes into bytes
1308 kb() {
1309     echo "${@:-1}" | sed -E 's-_--g; s- +-\n-g' |
1310         awk '/./ { printf "%.2f\n", 1024 * $0; fflush() }' |
1311         sed 's-\.00*$--'
1312 }
1313 
1314 # run `less`, showing line numbers, among other settings
1315 l() { less -JMKNiCRS "$@"; }
1316 
1317 # LABEL/precede data with an ANSI-styled line
1318 label() { printf "\e[7m%-*s\e[0m\n" "$(($(tput cols) - 2))" "$*"; cat -; }
1319 
1320 # find the LAN (local-area network) IP address for this device
1321 lanip() { hostname -I; }
1322 
1323 # play a stereotypical once-a-second laser sound for the number of seconds
1324 # given, or for 1 second (once) by default; uses my tool `waveout`
1325 laser() {
1326     local f='sin(100 * tau * exp(-40 * u))'
1327     waveout "${1:-1}" "${2:-1} * $f" | mpv --really-quiet -
1328 }
1329 
1330 # get the last n lines, or 1 by default
1331 last() { tail -n "${1:-1}" "${2:--}"; }
1332 
1333 # convert pounds (LB) into kilograms
1334 lb() {
1335     echo "${@:-1}" | sed -E 's-_--g; s- +-\n-g' |
1336         awk '/./ { printf "%.2f\n", 0.45359237 * $0; fflush() }'
1337 }
1338 
1339 # convert a mix of pounds (LB) and weight-ounces (OZ) into kilograms
1340 lboz() {
1341     local lb="${1:-0}"
1342     lb="$(echo "${lb}" | sed 's-_--g')"
1343     local oz="${2:-0}"
1344     oz="$(echo "${oz}" | sed 's-_--g')"
1345     awk "BEGIN { print 0.45359237 * ${lb} + 0.028349523 * ${oz}; exit }"
1346 }
1347 
1348 # turn the first n space-separated fields on each line into tab-separated
1349 # ones; this behavior is useful to make the output of many cmd-line tools
1350 # into TSV, since filenames are usually the last fields, and these may
1351 # contain spaces which aren't meant to be split into different fields
1352 leadtabs() {
1353     local n="$1"
1354     local cmd
1355     cmd="$([ "${1:-80}" -gt 0 ] && printf "%${1:-80}s\n" "")"
1356     cmd="s-^ *--; s- *\\r?\$--; $(echo "${cmd}" | sed 's/ /s- +-\\t-1;/g')"
1357     sed -u -E "${cmd}"
1358 }
1359 
1360 # limit stops at the first n bytes, or 1024 bytes by default
1361 limit() { head -c "$(echo "${1:-1024}" | sed 's-_--g')" "${2:--}"; }
1362 
1363 # Less with Header runs `less` with line numbers, ANSI styles, no line-wraps,
1364 # and using the first n lines as a sticky-header (1 by default), so they
1365 # always show on top
1366 lh() {
1367     local n="${1:-1}"
1368     [ $# -gt 0 ] && shift
1369     less --header="$n" -JMKNiCRS "$@"
1370 }
1371 
1372 lh1() { less --header=1 -JMKNiCRS "$@"; }
1373 
1374 lh2() { less --header=2 -JMKNiCRS "$@"; }
1375 
1376 # LINe-Buffered Output
1377 linbo() { stdbuf -oL "$@"; }
1378 
1379 # ensure lines are never accidentally joined across files, by always emitting
1380 # a line-feed at the end of each line
1381 lines() { awk '{ print; fflush() }' "$@"; }
1382 
1383 # regroup adjacent lines into n-item tab-separated lines
1384 lineup() {
1385     local n="${1:-0}"
1386     [ $# -gt 0 ] && shift
1387 
1388     if [ "$n" -le 0 ]; then
1389         awk '
1390             NR > 1 { printf "\t" }
1391             { printf "%s", $0; fflush() }
1392             END { if (NR > 0) print "" }
1393         ' "$@"
1394         return $?
1395     fi
1396 
1397     awk -v n="$n" '
1398         NR % n != 1 && n > 1 { printf "\t" }
1399         { printf "%s", $0; fflush() }
1400         NR % n == 0 { print ""; fflush() }
1401         END { if (NR % n != 0) print "" }
1402     ' "$@"
1403 }
1404 
1405 # find all hyperLINKS (https:// and http://) in the input text
1406 alias links=weblinks
1407 
1408 # LOAD data from the filename or URI given; uses my tool `get`
1409 load() { get "$@"; }
1410 
1411 # LOcal SERver webserves files in a folder as localhost, using the port
1412 # number given, or port 8080 by default
1413 loser() {
1414     printf "\e[7mserving files in %s\e[0m\n" "${2:-$(pwd)}" >&2
1415     python3 -m http.server "${1:-8080}" -d "${2:-.}"
1416 }
1417 
1418 # LOWercase all ASCII symbols
1419 low() { awk '{ print tolower($0); fflush() }' "$@"; }
1420 
1421 # LOWERcase all ASCII symbols
1422 lower() { awk '{ print tolower($0); fflush() }' "$@"; }
1423 
1424 # Listen To Youtube
1425 alias lty=yap
1426 
1427 alias m=match
1428 
1429 # only keep lines which match any of the regexes given
1430 match() {
1431     awk '
1432         BEGIN {
1433             for (i = 1; i < ARGC; i++) {
1434                 e[i] = ARGV[i]
1435                 delete ARGV[i]
1436             }
1437         }
1438 
1439         {
1440             for (i = 1; i < ARGC; i++) {
1441                 if ($0 ~ e[i]) {
1442                     print; fflush()
1443                     got++
1444                     next
1445                 }
1446             }
1447         }
1448 
1449         END { exit(got == 0) }
1450     ' "${@:-[^\r]}"
1451 }
1452 
1453 # convert binary MegaBytes into bytes
1454 mb() {
1455     echo "${@:-1}" | sed -E 's-_--g; s- +-\n-g' |
1456         awk '/./ { printf "%.2f\n", 1048576 * $0; fflush() }' |
1457         sed 's-\.00*$--'
1458 }
1459 
1460 # Multi-Core MAKE runs `make` using all cores
1461 mcmake() { make -j "$(nproc)" "$@"; }
1462 
1463 # convert MIles into kilometers
1464 mi() {
1465     echo "${@:-1}" | sed -E 's-_--g; s- +-\n-g' |
1466         awk '/./ { printf "%.2f\n", 1.609344 * $0; fflush() }'
1467 }
1468 
1469 # convert MIles² (squared) into kilometers²
1470 mi2() {
1471     echo "${@:-1}" | sed -E 's-_--g; s- +-\n-g' |
1472         awk '/./ { printf "%.2f\n", 2.5899881103360 * $0 }'
1473 }
1474 
1475 # Make In Folder
1476 mif() {
1477     local code
1478     pushd "${1:-.}" > /dev/null || return
1479     [ $# -gt 0 ] && shift
1480     make "$@"
1481     code=$?
1482     popd > /dev/null || return "${code}"
1483     return "${code}"
1484 }
1485 
1486 # MINimize DECimalS ignores all trailing decimal zeros in numbers, and even
1487 # the decimal dots themselves, when decimals in a number are all zeros
1488 mindecs() {
1489     awk '{ gsub(/\r$/, ""); print; fflush() }' "$@" |
1490         sed -u -E 's-([0-9]+)\.0+\W-\1-g; s-([0-9]+\.[0-9]*[1-9])0+\W-\1-g'
1491 }
1492 
1493 # run `make`
1494 mk() { make "$@"; }
1495 
1496 # convert Miles Per Hour into kilometers per hour
1497 mph() {
1498     echo "${@:-1}" | sed -E 's-_--g; s- +-\n-g' |
1499         awk '/./ { printf "%.2f\n", 1.609344 * $0 }'
1500 }
1501 
1502 # Number all lines, using a tab right after each line number
1503 n() { stdbuf -oL nl -b a -w 1 -v 1 "$@"; }
1504 
1505 # NArrow MANual, keeps `man` narrow, even if the window/tab is wide when run
1506 naman() {
1507     local w
1508     w="$(tput cols)"
1509     w="$((w / 2 - 4))"
1510     if [ "$w" -lt 80 ]; then
1511         w=80
1512     fi
1513     MANWIDTH="$w" man "$@"
1514 }
1515 
1516 # Not AND sorts its 2 inputs, then finds lines not in common
1517 nand() {
1518     # comm -3 <(sort "$1") <(sort "$2")
1519     # dash doesn't support the process-sub syntax
1520     (sort "$1" | (sort "$2" | (comm -3 /dev/fd/3 /dev/fd/4) 4<&0) 3<&0)
1521 }
1522 
1523 # listen to streaming NEW WAVE music
1524 newwave() {
1525     printf "streaming \e[7mNew Wave radio\e[0m\n"
1526     mpv --quiet https://puma.streemlion.com:2910/stream
1527 }
1528 
1529 # Nice CAlculator runs my tool `ca` and colors results with my tool `nn`,
1530 # alternating styles to make long numbers easier to read
1531 nica() {
1532     local arg
1533     for arg in "$@"; do
1534         printf "\e[7m%s\e[0m\n" "${arg}" >&2
1535         ca "${arg}" | nn --gray
1536     done
1537 }
1538 
1539 # NIce(r) COlumns makes the output of many commands whose output starts with
1540 # a header line easier to read; uses my tool `nn`
1541 nico() {
1542     awk '
1543         (NR - 1) % 5 == 1 && NR > 1 { print "" }
1544         { printf "%5d  %s\n", NR - 1, $0; fflush() }
1545     ' "$@" | nn --gray | less -JMKiCRS
1546 }
1547 
1548 # emit nothing to output and/or discard everything from input
1549 nil() {
1550     if [ $# -gt 0 ]; then
1551         "$@" > /dev/null
1552     else
1553         cat < /dev/null
1554     fi
1555 }
1556 
1557 # Nice Json colors data using the `jq` app, allowing an optional filepath as
1558 # the data source, and even an optional transformation formula
1559 nj() { jq -C "${2:-.}" "${1:--}"; }
1560 
1561 # Nice Json Lines colors data using the `jq` app, allowing an optional filepath
1562 # as the data source
1563 njl() { jq -c -C ".[]" "${1:--}"; }
1564 
1565 # pipe-run my tools `nj` (Nice Json) and `nn` (Nice Numbers)
1566 njnn() { nj "$@" | nn --gray; }
1567 
1568 # Nice JSON Lines colors data using the `jq` app, allowing an optional filepath
1569 # as the data source
1570 njsonl() { jq -c -C ".[]" "${1:--}"; }
1571 
1572 # Narrow MANual, keeps `man` narrow, even if the window/tab is wide when run
1573 nman() {
1574     local w
1575     w="$(tput cols)"
1576     w="$((w / 2 - 4))"
1577     if [ "$w" -lt 80 ]; then
1578         w=80
1579     fi
1580     MANWIDTH="$w" man "$@"
1581 }
1582 
1583 # convert Nautical MIles into kilometers
1584 nmi() {
1585     echo "${@:-1}" | sed -E 's-_--g; s- +-\n-g' |
1586         awk '/./ { printf "%.2f\n", 1.852 * $0; fflush() }'
1587 }
1588 
1589 # play a white-noise sound lasting the number of seconds given, or for 1
1590 # second by default; uses my tool `waveout`
1591 noise() { waveout "${1:-1}" "${2:-0.05} * random()" | mpv --really-quiet -; }
1592 
1593 # show the current date and time
1594 now() { date +'%Y-%m-%d %H:%M:%S'; }
1595 
1596 # Nice Print Python result; uses my tool `nn`
1597 npp() { python -c "print($1)" | nn "${2:---gray}"; }
1598 
1599 # Nice `Process Show` shows/lists all current processes shown by `ps` in a
1600 # way that's easier to read; uses my tool `nn`
1601 nps() {
1602     local res
1603     local code
1604     res="$(ps "${@:-aux}")"
1605     code=$?
1606     if [ "${code}" -ne 0 ]; then
1607         return "${code}"
1608     fi
1609 
1610     echo "${res}" | awk '
1611         BEGIN {
1612             d = strftime("%a %b %d")
1613             t = strftime("%H:%M:%S")
1614             # printf "\x1b[7m%30s%s  %s%30s\x1b[0m\n\n", "", d, t, ""
1615             fmt = "\x1b[38;2;128;128;128m\x1b[7m%30s%s  %s%30s\x1b[0m\n\n"
1616             printf fmt, "", d, t, ""
1617         }
1618 
1619         (NR - 1) % 5 == 1 && NR > 1 { print "" }
1620 
1621         $1 == "root" {
1622             gsub(/^/, "\x1b[38;2;52;101;164m")
1623             gsub(/ +/, "&\x1b[0m\x1b[38;2;52;101;164m")
1624             gsub(/$/, "\x1b[0m")
1625         }
1626 
1627         {
1628             gsub(/ \? /, "\x1b[38;2;135;135;175m&\x1b[0m")
1629             gsub(/0\.0/, "\x1b[38;2;135;135;175m&\x1b[0m")
1630             gsub(/0:00/, "\x1b[38;2;135;135;175m&\x1b[0m")
1631             printf "%3d  %s\n", NR - 1, $0
1632         }
1633     ' | nn --gray | less -JMKiCRS
1634 }
1635 
1636 # Nice Size, using my tools `nn` and `cext`
1637 ns() { wc -c "$@" | nn --gray | cext; }
1638 
1639 # Nice Systemctl Status
1640 nss() {
1641     systemctl status "$@" 2>&1 | sed 's-\x1b\[[^A-Za-z][A-Za-z]--g' | sed -E \
1642         -e 's-(^[^ ] )([^ ]+\.service)-\1\x1b[7m\2\x1b[0m-' \
1643         -e 's- (enabled)- \x1b[38;2;0;135;95m\x1b[7m\1\x1b[0m-g' \
1644         -e 's- (disabled)- \x1b[38;2;215;95;0m\x1b[7m\1\x1b[0m-g' \
1645         -e 's- (active \(running\))- \x1b[38;2;0;135;95m\x1b[7m\1\x1b[0m-g' \
1646         -e 's- (inactive \(dead\))- \x1b[38;2;204;0;0m\x1b[7m\1\x1b[0m-g' \
1647         -e 's-^(Unit .* could not .*)$-\x1b[38;2;204;0;0m\x1b[7m\1\x1b[0m\n-' \
1648         -e 's-(\[WARN\].*)$-\x1b[38;2;215;95;0m\x1b[7m\1\x1b[0m\n-' \
1649         -e 's-(\[ERR\].*)$-\x1b[38;2;204;0;0m\x1b[7m\1\x1b[0m\n-' |
1650             less -JMKiCRS
1651 }
1652 
1653 # Nice TimeStamp
1654 nts() {
1655     ts '%Y-%m-%d %H:%M:%S' |
1656         sed -u 's-^-\x1b[48;2;218;218;218m\x1b[38;2;0;95;153m-; s- -\x1b[0m\t-2'
1657 }
1658 
1659 # emit nothing to output and/or discard everything from input
1660 null() {
1661     if [ $# -gt 0 ]; then
1662         "$@" > /dev/null
1663     else
1664         cat < /dev/null
1665     fi
1666 }
1667 
1668 # NULl-terminate LINES ends each stdin line with a null byte, instead of a
1669 # line-feed byte
1670 # nullines() {
1671 #     awk -v ORS='\000' '
1672 #         FNR == 1 { gsub(/^\xef\xbb\xbf/, "") }
1673 #         { gsub(/\r$/, ""); print; fflush() }
1674 #     ' "$@"
1675 # }
1676 
1677 # (Nice) What Are These (?) shows what the names given to it are/do, coloring
1678 # the syntax of shell functions
1679 nwat() {
1680     local arg
1681     local gap=0
1682 
1683     if [ $# -eq 0 ]; then
1684         printf "\e[38;2;204;0;0mnwat: no names given\e[0m\n" >&2
1685         return 1
1686     fi
1687 
1688     local cmd="bat"
1689     # debian linux uses a different name for the `bat` app
1690     if [ -e "/usr/bin/batcat" ]; then
1691         cmd="batcat"
1692     fi
1693 
1694     for arg in "$@"; do
1695         [ "${gap}" -gt 0 ] && printf "\n"
1696         gap=1
1697         printf "\e[48;2;218;218;218m%-80s\e[0m\n" "${arg}"
1698         # printf "\e[7m%-80s\e[0m\n" "${arg}"
1699 
1700         while alias "${arg}" > /dev/null 2> /dev/null; do
1701             arg="$(alias "${arg}" | sed -E "s-^[^=]+=['\"](.+)['\"]\$-\\1-")"
1702         done
1703 
1704         if type "${arg}" > /dev/null 2> /dev/null; then
1705             type "${arg}" | awk 'NR == 1 && / is a function$/ { next } 1' |
1706                 "$cmd" -l sh --style=plain --theme='Monokai Extended Light' \
1707                     --wrap=never --color=always |
1708                         sed 's-\x1b\[38;5;70m-\x1b\[38;5;28m-g'
1709         else
1710             printf "\e[38;2;204;0;0m%s not found\e[0m\n" "${arg}"
1711         fi
1712     done | less -JMKiCRS
1713 }
1714 
1715 # Nice Weather Forecast gets weather forecasts, using ANSI styles and almost
1716 # filling the terminal's current width
1717 alias nwf=weather
1718 
1719 # Nice Zoom Json, using my tools `zj`, and `nj`
1720 nzj() { zj "$@" | nj; }
1721 
1722 # Plain text, by ignoring ANSI terminal styling
1723 alias p=plain
1724 
1725 # Print Awk expression
1726 pa() { awk "BEGIN { print ${1:-0}; exit }"; }
1727 
1728 # Paragraph AWK runs `awk` in block/paragraph/multiline input-mode
1729 pawk() { stdbuf -oL awk -F='' -v RS='' "$@"; }
1730 
1731 # Plain `fd`
1732 pfd() { fd --color=never "$@"; }
1733 
1734 # pick lines, using all the 1-based line-numbers given
1735 picklines() {
1736     awk '
1737         BEGIN { m = ARGC - 1; if (ARGC == 1) exit 0 }
1738         BEGIN { for (i = 1; i <= m; i++) { p[i] = ARGV[i]; delete ARGV[i] } }
1739         { l[++n] = $0 }
1740         END {
1741             for (i = 1; i <= m; i++) {
1742                 j = p[i]
1743                 if (j < 0) j += NR + 1
1744                 if (0 < j && j <= NR) print l[j]
1745             }
1746         }
1747     ' "$@"
1748 }
1749 
1750 # Plain Interactive Grep
1751 pig() { ugrep --color=never -Q -E "$@"; }
1752 
1753 # make text plain, by ignoring ANSI terminal styling
1754 plain() { awk '{ gsub(/\x1b\[[0-9;]*[A-Za-z]/, ""); print; fflush() }' "$@"; }
1755 
1756 # play audio/video media
1757 play() { mpv "${@:--}"; }
1758 
1759 # Print Python result
1760 pp() { python -c "print($1)"; }
1761 
1762 # PRecede (input) ECHO, prepends a first line to stdin lines
1763 precho() { echo "$@" && cat /dev/stdin; }
1764 
1765 # PREcede (input) MEMO, prepends a first highlighted line to stdin lines
1766 prememo() {
1767     awk '
1768         BEGIN {
1769             if (ARGC > 1) printf "\x1b[7m"
1770             for (i = 1; i < ARGC; i++) {
1771                 if (i > 1) printf " "
1772                 printf "%s", ARGV[i]
1773                 delete ARGV[i]
1774             }
1775             if (ARGC > 1) printf "\x1b[0m\n"
1776             fflush()
1777         }
1778         { print; fflush() }
1779     ' "$@"
1780 }
1781 
1782 # start by joining all arguments given as a tab-separated-items line of output,
1783 # followed by all lines from stdin verbatim
1784 alias pretsv=begintsv
1785 
1786 # Plain Recursive Interactive Grep
1787 prig() { ugrep --color=never -r -Q -E "$@"; }
1788 
1789 # show/list all current processes
1790 processes() {
1791     local res
1792     res="$(ps aux)"
1793     echo "${res}" | awk '!/ps aux$/' | sed -E \
1794         -e 's- +-\t-1; s- +-\t-1; s- +-\t-1; s- +-\t-1; s- +-\t-1' \
1795         -e 's- +-\t-1; s- +-\t-1; s- +-\t-1; s- +-\t-1; s- +-\t-1'
1796 }
1797 
1798 # Play Youtube Audio
1799 alias pya=yap
1800 
1801 # Quiet ignores stderr, without any ugly keyboard-dancing
1802 q() { "$@" 2> /dev/null; }
1803 
1804 # Quiet MPV
1805 qmpv() { mpv --quiet "${@:--}"; }
1806 
1807 # ignore stderr, without any ugly keyboard-dancing
1808 quiet() { "$@" 2> /dev/null; }
1809 
1810 # Reset the screen, which empties it and resets the current style
1811 r() { reset; }
1812 
1813 # keep only lines between the 2 line numbers given, inclusively
1814 rangelines() {
1815     { [ "$#" -eq 2 ] || [ "$#" -eq 3 ]; } && [ "${1}" -le "${2}" ] &&
1816         { tail -n +"${1:-1}" "${3:--}" | head -n "$(("${2}" - "${1}" + 1))"; }
1817 }
1818 
1819 # RANdom MANual page
1820 ranman() {
1821     find "/usr/share/man/man${1:-1}" -type f | shuf -n 1 | xargs basename |
1822         sed 's-\.gz$--' | xargs man
1823 }
1824 
1825 # Red AWK styles lines satisfying an AWK condition/expression red, keeping all
1826 # other lines the same
1827 rawk() {
1828     local cond="${1:-1}"
1829     [ $# -gt 0 ] && shift
1830 
1831     awk '
1832         { low = lower = tolower($0) }
1833 
1834         '"${cond}"' {
1835             gsub(/\x1b\[0m/, "\x1b[0m\x1b[38;2;204;0;0m")
1836             printf "\x1b[38;2;204;0;0m%s\x1b[0m\n", $0; fflush()
1837             next
1838         }
1839 
1840         { print; fflush() }
1841     ' "$@"
1842 }
1843 
1844 # play a ready-phone-line sound lasting the number of seconds given, or for 1
1845 # second by default; uses my tool `waveout`
1846 ready() {
1847     local f='0.5 * sin(350*tau*t) + 0.5 * sin(450*tau*t)'
1848     waveout "${1:-1}" "${2:-1} * $f" | mpv --really-quiet -
1849 }
1850 
1851 # reflow/trim lines of prose (text) to improve its legibility: it's especially
1852 # useful when the text is pasted from web-pages being viewed in reader mode
1853 reprose() {
1854     local w="${1:-80}"
1855     [ $# -gt 0 ] && shift
1856     awk '
1857         FNR == 1 && NR > 1 { print "" }
1858         { gsub(/\r$/, ""); print; fflush() }
1859     ' "$@" | fold -s -w "$w" | sed -u -E 's- *\r?$--'
1860 }
1861 
1862 # Run In Folder
1863 rif() {
1864     local code
1865     pushd "${1:-.}" > /dev/null || return
1866     [ $# -gt 0 ] && shift
1867     "$@"
1868     code=$?
1869     popd > /dev/null || return "${code}"
1870     return "${code}"
1871 }
1872 
1873 # play a ringtone-style sound lasting the number of seconds given, or for 1
1874 # second by default; uses my tool `waveout`
1875 ringtone() {
1876     local f='sin(2048 * tau * t) * exp(-50 * (t%0.1))'
1877     waveout "${1:-1}" "${2:-1} * $f" | mpv --really-quiet -
1878 }
1879 
1880 # Read-Only Editor
1881 roe() { micro -readonly true "$@"; }
1882 
1883 # Read-Only Micro (text editor)
1884 rom() { micro -readonly true "$@"; }
1885 
1886 # show a RULER-like width-measuring line
1887 ruler() {
1888     [ "${1:-80}" -gt 0 ] && printf "%${1:-80}s\n" "" | sed -E \
1889         's- {10}-····╵····│-g; s- -·-g; s-·····-····╵-'
1890 }
1891 
1892 # voice-synthesize plain-text; only works on the windows subsystem for linux
1893 # say() {
1894 #     awk '{ print; fflush() }' "$@" | powershell.exe -noprofile -command '
1895 #         Add-Type -AssemblyName System.Speech
1896 #         $syn = New-Object -TypeName System.Speech.Synthesis.SpeechSynthesizer
1897 #         foreach ($line in $Input) { $syn.Speak($line) }
1898 #     ' | cat
1899 # }
1900 
1901 # SystemCTL; `sysctl` is already taken for a separate/unrelated app
1902 sctl() { systemctl "$@"; }
1903 
1904 # Silent CURL spares you the progress bar, but still tells you about errors
1905 scurl() { curl --show-error -s "$@"; }
1906 
1907 # show a unique-looking SEParator line; useful to run between commands
1908 # which output walls of text
1909 sep() {
1910     [ "${1:-80}" -gt 0 ] &&
1911         printf "\e[48;2;218;218;218m%${1:-80}s\e[0m\n" "" | sed 's- -·-g'
1912 }
1913 
1914 # webSERVE files in a folder as localhost, using the port number given, or
1915 # port 8080 by default
1916 serve() {
1917     printf "\e[7mserving files in %s\e[0m\n" "${2:-$(pwd)}" >&2
1918     python3 -m http.server "${1:-8080}" -d "${2:-.}"
1919 }
1920 
1921 # SET DIFFerence sorts its 2 inputs, then finds lines not in the 2nd input
1922 setdiff() {
1923     # comm -23 <(sort "$1") <(sort "$2")
1924     # dash doesn't support the process-sub syntax
1925     (sort "$1" | (sort "$2" | (comm -23 /dev/fd/3 /dev/fd/4) 4<&0) 3<&0)
1926 }
1927 
1928 # SET INtersection, sorts its 2 inputs, then finds common lines
1929 setin() {
1930     # comm -12 <(sort "$1") <(sort "$2")
1931     # dash doesn't support the process-sub syntax
1932     (sort "$1" | (sort "$2" | (comm -12 /dev/fd/3 /dev/fd/4) 4<&0) 3<&0)
1933 }
1934 
1935 # SET SUBtraction sorts its 2 inputs, then finds lines not in the 2nd input
1936 setsub() {
1937     # comm -23 <(sort "$1") <(sort "$2")
1938     # dash doesn't support the process-sub syntax
1939     (sort "$1" | (sort "$2" | (comm -23 /dev/fd/3 /dev/fd/4) 4<&0) 3<&0)
1940 }
1941 
1942 # Show Files (and folders), coloring folders and links; uses my tool `nn`
1943 sf() {
1944     local arg
1945     local gap=0
1946     local options='-JMKiCRS'
1947 
1948     if [ $# -le 1 ]; then
1949         options='--header=1 -JMKiCRS'
1950     fi
1951 
1952     for arg in "${@:-.}"; do
1953         [ "${gap}" -gt 0 ] && printf "\n"
1954         printf "\e[7m%s\e[0m\n\n" "$(realpath "${arg}")"
1955         gap=1
1956 
1957         ls -al --file-type --color=never --time-style iso "${arg}" | awk '
1958             BEGIN {
1959                 drep = "\x1b[38;2;0;135;255m\x1b[48;2;228;228;228m&\x1b[0m"
1960                 lrep = "\x1b[38;2;0;135;95m\x1b[48;2;228;228;228m&\x1b[0m"
1961             }
1962 
1963             NR < 4 { next }
1964             (NR - 3) % 5 == 1 && (NR - 3) > 1 { print "" }
1965 
1966             {
1967                 gsub(/^(d[rwx-]+)/, drep)
1968                 gsub(/^(l[rwx-]+)/, lrep)
1969                 printf "%6d  %s\n", NR - 3, $0; fflush()
1970             }
1971         '
1972     done | nn --gray | less "${options}"
1973 }
1974 
1975 # Show File Sizes, using my tools `nn` and `cext`
1976 sfs() {
1977     # turn arg-list into single-item lines
1978     printf "%s\x00" "$@" |
1979     # calculate file-sizes, and reverse-sort results
1980     xargs -0 wc -c | sort -rn |
1981     # add/realign fields to improve legibility
1982     awk '
1983         # start output with a header-like line, and add a MiB field
1984         BEGIN { printf "%6s  %10s  %8s  name\n", "n", "bytes", "MiB"; fflush() }
1985         # make table breathe with empty lines, so tall outputs are readable
1986         (NR - 1) % 5 == 1 && NR > 1 { print "" }
1987         # emit regular output lines
1988         {
1989             printf "%6d  %10d  %8.2f  ", NR - 1, $1, $1 / 1048576
1990             # first field is likely space-padded
1991             gsub(/^ */, "")
1992             # slice line after the first field, as filepaths can have spaces
1993             $0 = substr($0, length($1) + 1)
1994             # first field is likely space-padded
1995             gsub(/^ /, "")
1996             printf "%s\n", $0; fflush()
1997         }
1998     ' |
1999     # make zeros in the MiB field stand out with a special color
2000     awk '
2001         {
2002             gsub(/ 00*\.00* /, "\x1b[38;2;135;135;175m&\x1b[0m")
2003             print; fflush()
2004         }
2005     ' |
2006     # make numbers nice, alternating styles along 3-digit groups
2007     nn --gray |
2008     # color-code file extensions
2009     cext |
2010     # make result interactively browsable
2011     less -JMKiCRS
2012 }
2013 
2014 # SHOW a command, then RUN it
2015 showrun() { printf "\e[7m%s\e[0m\n" "$*"; "$@"; }
2016 
2017 # SHell-QUOTE each line from the input(s): this is useful to make lines of
2018 # single-filepaths compatible with `xargs`, since standard shell settings
2019 # get in the way of filepaths with spaces and other special symbols in them
2020 shquote() {
2021     awk '
2022         {
2023             s = $0
2024             gsub(/\r$/, "", s)
2025             gsub(/\\/, "\\\\", s)
2026             gsub(/"/, "\\\"", s)
2027             gsub(/`/, "\\`", s)
2028             gsub(/\$/, "\\$", s)
2029             printf "\"%s\"\n", s; fflush()
2030         }
2031     ' "$@"
2032 }
2033 
2034 # skip the first n lines, or the 1st line by default
2035 skip() { tail -n +$(("${1:-1}" + 1)) "${2:--}"; }
2036 
2037 # skip the last n lines, or the last line by default
2038 skiplast() { head -n -"${1:-1}" "${2:--}"; }
2039 
2040 # SLOW/delay lines from the standard-input, waiting the number of seconds
2041 # given for each line, or waiting 1 second by default
2042 slow() {
2043     local seconds="${1:-1}"
2044     (
2045         IFS="$(printf "\n")"
2046         while read -r line; do
2047             sleep "${seconds}"
2048             printf "%s\n" "${line}"
2049         done
2050     )
2051 }
2052 
2053 # Show Latest Podcasts, using my tools `podfeed` and `si`
2054 slp() {
2055     local title
2056     title="Latest Podcast Episodes as of $(date +'%F %T')"
2057     podfeed -title "${title}" "$@" | si
2058 }
2059 
2060 # recursively find all files with fewer bytes than the number given
2061 smallfiles() {
2062     local n
2063     n="$(echo "${1:-4097}" | sed -E 's-_--g; s-\.[0-9]+$--')"
2064     [ $# -gt 0 ] && shift
2065 
2066     local arg
2067     for arg in "${@:-.}"; do
2068         if [ ! -d "${arg}" ]; then
2069             printf "\e[38;2;204;0;0mno folder named %s\e[0m\n" "${arg}" >&2
2070             return 1
2071         fi
2072         stdbuf -oL find "${arg}" -type f -size -"$n"c
2073     done
2074 }
2075 
2076 # Stdbuf Output Line-buffered
2077 sol() { stdbuf -oL "$@"; }
2078 
2079 # emit the first line as is, sorting all lines after that, using the
2080 # `sort` command, passing all/any arguments/options to it
2081 sortrest() {
2082     awk -v sort="sort $*" '
2083         FNR == 1 { gsub(/^\xef\xbb\xbf/, "") }
2084         { gsub(/\r$/, "") }
2085         NR == 1 { print; fflush() }
2086         NR > 1 { print | sort }
2087     '
2088 }
2089 
2090 # SORt Tab-Separated Values: emit the first line as is, sorting all lines after
2091 # that, using the `sort` command in TSV (tab-separated values) mode, passing
2092 # all/any arguments/options to it
2093 sortsv() {
2094     awk -v sort="sort -t \"$(printf '\t')\" $*" '
2095         FNR == 1 { gsub(/^\xef\xbb\xbf/, "") }
2096         { gsub(/\r$/, "") }
2097         NR == 1 { print; fflush() }
2098         NR > 1 { print | sort }
2099     '
2100 }
2101 
2102 # emit a line with the number of spaces given in it
2103 spaces() { [ "${1:-80}" -gt 0 ] && printf "%${1:-80}s\n" ""; }
2104 
2105 # ignore leading spaces, trailing spaces, even runs of multiple spaces
2106 # in the middle of lines, as well as trailing carriage returns
2107 squeeze() {
2108     awk '
2109         FNR == 1 { gsub(/^\xef\xbb\xbf/, "") }
2110         {
2111             gsub(/^ +| *\r?$/, "")
2112             gsub(/ *\t */, "\t")
2113             gsub(/  +/, " ")
2114             print; fflush()
2115         }
2116     ' "$@"
2117 }
2118 
2119 # SQUeeze and stOMP, by ignoring leading spaces, trailing spaces, even runs
2120 # of multiple spaces in the middle of lines, as well as trailing carriage
2121 # returns, while also turning runs of empty lines into single empty lines,
2122 # and ignoring leading/trailing empty lines, effectively also `squeezing`
2123 # lines vertically
2124 squomp() {
2125     awk '
2126         FNR == 1 { gsub(/^\xef\xbb\xbf/, "") }
2127         /^\r?$/ { empty = 1; next }
2128         empty { if (n > 0) print ""; empty = 0 }
2129         {
2130             gsub(/^ +| *\r?$/, "")
2131             gsub(/ *\t */, "\t")
2132             gsub(/  +/, " ")
2133             print; fflush()
2134             n++
2135         }
2136     ' "$@"
2137 }
2138 
2139 # turn runs of empty lines into single empty lines, effectively squeezing
2140 # paragraphs vertically, so to speak; runs of empty lines both at the start
2141 # and at the end are ignored
2142 stomp() {
2143     awk '
2144         /^\r?$/ { empty = 1; next }
2145         empty { if (n > 0) print ""; empty = 0 }
2146         { print; fflush(); n++ }
2147     ' "$@"
2148 }
2149 
2150 # turn SUDo privileges OFF right away: arguments also cause `sudo` to run with
2151 # what's given, before relinquishing existing privileges
2152 sudoff() {
2153     local code=0
2154     if [ $# -gt 0 ]; then
2155         sudo "$@"
2156         code=$?
2157     fi
2158     sudo -k
2159     return "${code}"
2160 }
2161 
2162 # append a final Tab-Separated-Values line with the sums of all columns from
2163 # the input table(s) given; items from first lines aren't counted/added
2164 sumtsv() {
2165     awk -F "\t" '
2166         # FNR == 1 { gsub(/^\xef\xbb\xbf/, "") }
2167 
2168         {
2169             gsub(/\r$/, "")
2170             print; fflush()
2171             if (width < NF) width = NF
2172         }
2173 
2174         FNR > 1 { for (i = 1; i <= NF; i++) sums[i] += $i + 0 }
2175 
2176         END {
2177             for (i = 1; i <= width; i++) {
2178                 if (i > 1) printf "\t"
2179                 printf "%s", sums[i] ""
2180             }
2181             if (width > 0) printf "\n"
2182         }
2183     ' "$@"
2184 }
2185 
2186 # Time the command given
2187 alias t=time
2188 
2189 # show a reverse-sorted tally of all lines read, where ties are sorted
2190 # alphabetically
2191 tally() {
2192     awk -v sortcmd="sort -t \"$(printf '\t')\" -rnk2 -k1d" '
2193         # reassure users by instantly showing the header
2194         BEGIN { print "value\ttally"; fflush() }
2195         { gsub(/\r$/, ""); t[$0]++ }
2196         END { for (k in t) { printf("%s\t%d\n", k, t[k]) | sortcmd } }
2197     ' "$@"
2198 }
2199 
2200 # Tab AWK: TSV-specific I/O settings for `awk`
2201 tawk() { stdbuf -oL awk -F "\t" -v OFS="\t" "$@"; }
2202 
2203 # Titled conCATenate Lines highlights each filename, before emitting its lines
2204 tcatl() {
2205     awk '
2206         FNR == 1 { printf "\x1b[7m%s\x1b[0m\n", FILENAME; fflush() }
2207         FNR == 1 { gsub(/^\xef\xbb\xbf/, "") }
2208         { gsub(/\r$/, ""); print; fflush() }
2209     ' "$@"
2210 }
2211 
2212 # turn documents into plain-TEXT
2213 text() { pandoc -s -t plain "$@"; }
2214 
2215 # run `top` without showing any of its output after quitting it
2216 tip() { tput smcup; top "$@"; tput rmcup; }
2217 
2218 # show current date in a specifc format
2219 today() { date +'%Y-%m-%d %a %b %d'; }
2220 
2221 # get the first n lines, or 1 by default
2222 toline() { head -n "${1:-1}" "${2:--}"; }
2223 
2224 # lowercase all ASCII symbols
2225 tolower() { awk '{ print tolower($0); fflush() }' "$@"; }
2226 
2227 # play a tone/sine-wave sound lasting the number of seconds given, or for 1
2228 # second by default: after the optional duration, the next optional arguments
2229 # are the volume and the tone-frequency; uses my tool `waveout`
2230 tone() {
2231     waveout "${1:-1}" "${2:-1} * sin(${3:-440} * 2 * pi * t)" |
2232         mpv --really-quiet -
2233 }
2234 
2235 # get the processes currently using the most cpu
2236 topcpu() {
2237     local n="${1:-10}"
2238     [ "$n" -gt 0 ] && ps aux | awk '
2239         NR == 1 { print; fflush() }
2240         NR > 1 { print | "sort -rnk3" }
2241     ' | head -n "$(("$n" + 1))"
2242 }
2243 
2244 # show all files directly in the folder given, without looking any deeper
2245 topfiles() {
2246     local arg
2247     for arg in "${@:-.}"; do
2248         if [ ! -d "${arg}" ]; then
2249             printf "\e[38;2;204;0;0mno folder named %s\e[0m\n" "${arg}" >&2
2250             return 1
2251         fi
2252         stdbuf -oL find "${arg}" -maxdepth 1 -type f
2253     done
2254 }
2255 
2256 # show all folders directly in the folder given, without looking any deeper
2257 topfolders() {
2258     local arg
2259     for arg in "${@:-.}"; do
2260         if [ ! -d "${arg}" ]; then
2261             printf "\e[38;2;204;0;0mno folder named %s\e[0m\n" "${arg}" >&2
2262             return 1
2263         fi
2264         stdbuf -oL find "${arg}" -maxdepth 1 -type d |
2265             awk '!/^\.$/ { print; fflush() }'
2266     done
2267 }
2268 
2269 # get the processes currently using the most memory
2270 topmemory() {
2271     local n="${1:-10}"
2272     [ "$n" -gt 0 ] && ps aux | awk '
2273         NR == 1 { print; fflush() }
2274         NR > 1 { print | "sort -rnk6" }
2275     ' | head -n "$(("$n" + 1))"
2276 }
2277 
2278 # transpose (switch) rows and columns from tables
2279 transpose() {
2280     awk '
2281         { gsub(/\r$/, "") }
2282 
2283         FNR == 1 { FS = ($0 ~ /\t/) ? "\t" : " "; $0 = $0 }
2284 
2285         {
2286             for (i = 1; i <= NF; i++) lines[i][NR] = $i
2287             if (maxitems < NF) maxitems = NF
2288         }
2289 
2290         END {
2291             for (j = 1; j <= maxitems; j++) {
2292                 for (i = 1; i <= NR; i++) {
2293                     if (i > 1) printf "\t"
2294                     printf "%s", lines[j][i]
2295                 }
2296                 printf "\n"
2297             }
2298         }
2299     ' "$@"
2300 }
2301 
2302 # ignore leading/trailing spaces, as well as trailing carriage returns
2303 trim() { awk '{ gsub(/^ +| *\r?$/, ""); print; fflush() }' "$@"; }
2304 
2305 # try running a command, emitting an explicit message to standard-error
2306 # if the command given fails
2307 try() {
2308     local code
2309     "$@"
2310     code=$?
2311 
2312     if [ "${code}" -ne 0 ]; then
2313         printf "\n\e[38;2;204;0;0m%s \e[48;2;204;0;0m\e[38;2;255;255;255m failed with error code %d \e[0m\n" "$*" "${code}"
2314     fi >&2
2315     return "${code}"
2316 }
2317 
2318 # Time Verbosely the command given
2319 tv() { /usr/bin/time -v "$@"; }
2320 
2321 # Unique via AWK, avoids lines duplicating the expression given
2322 uawk() {
2323     local code="${1:-\$0}"
2324     [ $# -gt 0 ] && shift
2325     awk '!c['"${code}"']++ { print; fflush() }' "$@"
2326 }
2327 
2328 # Underline Every 5 lines: make groups of 5 lines stand out by underlining
2329 # the last line of each such group
2330 ue5() {
2331     awk '
2332         NR % 5 == 0 && NR != 1 {
2333             gsub(/\x1b\[0m/, "\x1b[0m\x1b[4m")
2334             printf("\x1b[4m%s\x1b[0m\n", $0); fflush()
2335             next
2336         }
2337         { print; fflush() }
2338     ' "$@"
2339 }
2340 
2341 # deduplicate lines, keeping them in their original order
2342 unique() { awk '!c[$0]++ { print; fflush() }' "$@"; }
2343 
2344 # skip the first/leading n bytes
2345 unleaded() { tail -c +$(("$1" + 1)) "${2:--}"; }
2346 
2347 # go UP n folders, or go up 1 folder by default
2348 up() {
2349     if [ "${1:-1}" -le 0 ]; then
2350         cd .
2351     else
2352         cd "$(printf "%${1:-1}s" "" | sed 's- -../-g')" || return $?
2353     fi
2354 }
2355 
2356 # convert United States Dollars into CAnadian Dollars, using the latest
2357 # official exchange rates from the bank of canada; during weekends, the
2358 # latest rate may be from a few days ago; the default amount of usd to
2359 # convert is 1, when not given
2360 usd2cad() {
2361     local site='https://www.bankofcanada.ca/valet/observations/group'
2362     local csv_rates="${site}/FX_RATES_DAILY/csv"
2363     local url
2364     url="${csv_rates}?start_date=$(date -d '3 days ago' +'%Y-%m-%d')"
2365     curl -s "${url}" | awk -F, -v amount="$(echo "${1:-1}" | sed 's-_--g')" '
2366         /USD/ { for (i = 1; i <= NF; i++) if($i ~ /USD/) j = i }
2367         END { gsub(/"/, "", $j); if (j != 0) printf "%.2f\n", amount * $j }'
2368 }
2369 
2370 # View with `less`
2371 v() { less -JMKiCRS "$@"; }
2372 
2373 # run a command, showing its success/failure right after
2374 verdict() {
2375     local code
2376     "$@"
2377     code=$?
2378 
2379     if [ "${code}" -eq 0 ]; then
2380         printf "\n\e[38;2;0;135;95m%s \e[48;2;0;135;95m\e[38;2;255;255;255m succeeded \e[0m\n" "$*"
2381     else
2382         printf "\n\e[38;2;204;0;0m%s \e[48;2;204;0;0m\e[38;2;255;255;255m failed with error code %d \e[0m\n" "$*" "${code}"
2383     fi >&2
2384     return "${code}"
2385 }
2386 
2387 # run `cppcheck` with even stricter options
2388 vetc() {
2389     cppcheck --enable=portability --enable=style --check-level=exhaustive "$@"
2390 }
2391 
2392 # run `cppcheck` with even stricter options, also checking for c89 compliance
2393 vetc89() {
2394     cppcheck --enable=portability --enable=style \
2395         --check-level=exhaustive --std=c89 "$@"
2396 }
2397 
2398 # run `cppcheck` with even stricter options
2399 vetcpp() {
2400     cppcheck --enable=portability --enable=style --check-level=exhaustive "$@"
2401 }
2402 
2403 # check shell scripts for common gotchas, avoiding complaints about using
2404 # the `local` keyword, which is widely supported in practice
2405 vetshell() { shellcheck -e 3043 "$@"; }
2406 
2407 # View with Header runs `less` without line numbers, with ANSI styles, no
2408 # line-wraps, and using the first n lines as a sticky-header (1 by default),
2409 # so they always show on top
2410 vh() {
2411     local n="${1:-1}"
2412     [ $# -gt 0 ] && shift
2413     less --header="$n" -JMKiCRS "$@"
2414 }
2415 
2416 vh1() { less --header=1 -JMKiCRS "$@"; }
2417 
2418 vh2() { less --header=2 -JMKiCRS "$@"; }
2419 
2420 # View Nice Hexadecimals; uses my tool `nh`
2421 vnh() { nh "$@" | less -JMKiCRS; }
2422 
2423 # View Nice Json / Very Nice Json; uses my tools `nj` and `nn`
2424 vnj() { nj "$@" | less -JMKiCRS; }
2425 
2426 # View Very Nice Json with Nice Numbers; uses my tools `nj` and `nn`
2427 vnjnn() { nj "$@" | nn --gray | less -JMKiCRS; }
2428 
2429 # View Nice Numbers; uses my tool `nn`
2430 vnn() { nn "${@:---gray}" | less -JMKiCRS; }
2431 
2432 # View Nice Table / Very Nice Table; uses my tools `nt` and `nn`
2433 vnt() {
2434     awk '{ gsub(/\r$/, ""); printf "%d\t%s\n", NR - 1, $0; fflush() }' "$@" |
2435         nt | nn --gray |
2436         awk '(NR - 1) % 5 == 1 && NR > 1 { print "" } { print; fflush() }' |
2437         less -JMKiCRS
2438 }
2439 
2440 # View Text, turning documents into plain-text if needed
2441 vt() {
2442     local arg
2443     local gap=0
2444     local options='-JMKiCRS'
2445 
2446     if [ $# -eq 1 ]; then
2447         options='--header=1 -JMKiCRS'
2448     fi
2449 
2450     if [ $# -eq 0 ]; then
2451         pandoc -s -t plain - 2>&1 | less -JMKiCRS
2452     else
2453         for arg in "$@"; do
2454             [ "${gap}" -eq 1 ] && printf "\n"
2455             gap=1
2456             printf "\e[7m%-80s\e[0m\n" "${arg}"
2457             pandoc -s -t plain "${arg}" 2>&1 | awk 1
2458         done | less "${options}"
2459     fi
2460 }
2461 
2462 # What Are These (?) shows what the names given to it are/do
2463 wat() {
2464     local arg
2465     local gap=0
2466 
2467     if [ $# -eq 0 ]; then
2468         printf "\e[38;2;204;0;0mwat: no names given\e[0m\n" >&2
2469         return 1
2470     fi
2471 
2472     for arg in "$@"; do
2473         [ "${gap}" -gt 0 ] && printf "\n"
2474         gap=1
2475         # printf "\e[48;2;218;218;218m%-80s\e[0m\n" "${arg}"
2476         printf "\e[7m%-80s\e[0m\n" "${arg}"
2477 
2478         while alias "${arg}" > /dev/null 2> /dev/null; do
2479             arg="$(alias "${arg}" | sed -E "s-^[^=]+=['\"](.+)['\"]\$-\\1-")"
2480         done
2481 
2482         if type "${arg}" > /dev/null 2> /dev/null; then
2483             type "${arg}" | awk 'NR == 1 && / is a function$/ { next } 1'
2484         else
2485             printf "\e[38;2;204;0;0m%s not found\e[0m\n" "${arg}"
2486         fi
2487     done | less -JMKiCRS
2488 }
2489 
2490 # Word-Count TSV, runs the `wc` app using all stats, emitting tab-separated
2491 # lines instead
2492 wctsv() {
2493     printf "file\tbytes\tlines\tcharacters\twords\tlongest\n"
2494     stdbuf -oL wc -cmlLw "${@:--}" | sed -E -u \
2495         's-^ *([^ ]*) *([^ ]*) *([^ ]*) *([^ ]*) *([^ ]*) *([^\r]*)$-\6\t\4\t\1\t\3\t\2\t\5-' |
2496         awk '
2497             NR > 1 { print prev; fflush() }
2498             { prev = $0 }
2499             END { if (NR == 1 || !/^total\t/) print }
2500         '
2501 }
2502 
2503 # get weather forecasts, almost filling the terminal's current width
2504 weather() {
2505     local gap=0
2506     local width="$(($(tput cols) - 2))"
2507     local place
2508 
2509     for place in "$@"; do
2510         [ "${gap}" -gt 0 ] && printf "\n"
2511         gap=1
2512 
2513         # printf "\e[48;2;218;218;218m%-${width}s\e[0m\n" "${place}"
2514         printf "\e[7m%-${width}s\e[0m\n" "${place}"
2515 
2516         printf "%s~%s\r\n\r\n" "${place}" "${width}" |
2517         curl --show-error -s telnet://graph.no:79 |
2518         sed -u -E \
2519             -e 's/ *\r?$//' \
2520             -e '/^\[/d' \
2521             -e 's/^ *-= *([^=]+) +=- *$/\1\n/' \
2522             -e 's/-/\x1b[38;2;196;160;0m●\x1b[0m/g' \
2523             -e 's/^( +)\x1b\[38;2;196;160;0m●\x1b\[0m/\1-/g' \
2524             -e 's/\|/\x1b[38;2;52;101;164m█\x1b[0m/g' \
2525             -e 's/#/\x1b[38;2;218;218;218m█\x1b[0m/g' \
2526             -e 's/([=\^][=\^]*)/\x1b[38;2;164;164;164m\1\x1b[0m/g' \
2527             -e 's/\*/○/g' \
2528             -e 's/_/\x1b[48;2;216;200;0m_\x1b[0m/g' \
2529             -e 's/([0-9][0-9]\/[0-9][0-9])/\x1b[7m\1\x1b[0m/g' | awk 1
2530     done | less -JMKiCRS
2531 }
2532 
2533 # find all WEB/hyperLINKS (https:// and http://) in the input text
2534 weblinks() {
2535     awk '
2536         BEGIN { e = "https?://[A-Za-z0-9+_.:%-]+(/[A-Za-z0-9+_.%/,#?&=-]*)*" }
2537         {
2538             # match all links in the current line
2539             for (s = $0; match(s, e); s = substr(s, RSTART + RLENGTH)) {
2540                 print substr(s, RSTART, RLENGTH); fflush()
2541             }
2542         }
2543     ' "$@"
2544 }
2545 
2546 # recursively find all files with trailing spaces/CRs
2547 wheretrails() { rg -c --line-buffered '[ \r]+$' "${@:-.}"; }
2548 
2549 # recursively find all files with trailing spaces/CRs
2550 whichtrails() { rg -c --line-buffered '[ \r]+$' "${@:-.}"; }
2551 
2552 # run `xargs`, using zero/null bytes as the extra-arguments terminator
2553 x0() { xargs -0 "$@"; }
2554 
2555 # run `xargs`, using whole lines as extra arguments
2556 xl() {
2557     awk -v ORS='\000' '
2558         FNR == 1 { gsub(/^\xef\xbb\xbf/, "") }
2559         { gsub(/\r$/, ""); print; fflush() }
2560     ' | xargs -0 "$@"
2561 }
2562 
2563 # Youtube AAC audio
2564 ya() { yt-dlp -x --audio-format best "$@"; }
2565 
2566 # Youtube AAC audio
2567 yaac() { yt-dlp -f 140 "$@"; }
2568 
2569 # Youtube MP4 video
2570 ymp4() { yt-dlp -f 22 "$@"; }
2571 
2572 # Youtube Audio Player
2573 # yap() {
2574 #     local url
2575 #     # some youtube URIs end with extra playlist/tracker parameters
2576 #     url="$(echo "$1" | sed 's-&.*--')"
2577 #     mpv "$(yt-dlp -x --audio-format aac --get-url "${url}" 2> /dev/null)"
2578 # }
2579 
2580 # Youtube Audio Player
2581 yap() {
2582     local url
2583     # some youtube URIs end with extra playlist/tracker parameters
2584     url="$(echo "$1" | sed 's-&.*--')"
2585     mpv "$(yt-dlp -x --audio-format best --get-url "${url}" 2> /dev/null)"
2586 }
2587 
2588 # Youtube Download AAC audio
2589 ydaac() { yt-dlp -f 140 "$@"; }
2590 
2591 # Youtube Download MP4 video
2592 ydmp4() { yt-dlp -f 22 "$@"; }
2593 
2594 # show a calendar for the current YEAR, or for the year given
2595 year() {
2596     {
2597         # show the current date/time center-aligned
2598         printf "%20s\e[38;2;78;154;6m%s\e[0m  \e[38;2;52;101;164m%s\e[0m\n\n" \
2599             "" "$(date +'%a %b %d %Y')" "$(date +%T)"
2600         # debian linux has a different `cal` app which highlights the day
2601         if [ -e "/usr/bin/ncal" ]; then
2602             # fix debian/ncal's weird way to highlight the current day
2603             ncal -C -y "$@" | sed -E 's/_\x08(.)/\x1b[7m\1\x1b[0m/g'
2604         else
2605             cal -y "$@"
2606         fi
2607     } | less -JMKiCRS
2608 }
2609 
2610 # show the current date in the YYYY-MM-DD format
2611 ymd() { date +'%Y-%m-%d'; }
2612 
2613 # YouTube DownLoad Plus
2614 ytdlp() { yt-dlp "$@"; }
2615 
2616 # YouTube Url
2617 ytu() {
2618     local url
2619     # some youtube URIs end with extra playlist/tracker parameters
2620     url="$(echo "$1" | sed 's-&.*--')"
2621     [ $# -gt 0 ] && shift
2622     yt-dlp "$@" --get-url "${url}"
2623 }
2624 
2625 # underline every few lines: make groups of 5 lines (by default) stand out by
2626 # underlining the last line of each
2627 alias zebra=ue5
2628 
2629 # Zero/null-terminate LINES ends each stdin line with a null byte, instead of
2630 # a line-feed byte
2631 zlines() {
2632     awk -v ORS='\000' '
2633         FNR == 1 { gsub(/^\xef\xbb\xbf/, "") }
2634         { gsub(/\r$/, ""); print; fflush() }
2635     ' "$@"
2636 }