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