File: clam.sh
   1 #!/bin/sh
   2 
   3 # The MIT License (MIT)
   4 #
   5 # Copyright © 2024 pacman64
   6 #
   7 # Permission is hereby granted, free of charge, to any person obtaining a copy
   8 # of this software and associated documentation files (the “Software”), to deal
   9 # in the Software without restriction, including without limitation the rights
  10 # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  11 # copies of the Software, and to permit persons to whom the Software is
  12 # furnished to do so, subject to the following conditions:
  13 #
  14 # The above copyright notice and this permission notice shall be included in
  15 # all copies or substantial portions of the Software.
  16 #
  17 # THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  18 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  19 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  20 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  21 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  22 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  23 # SOFTWARE.
  24 
  25 
  26 # clam
  27 #
  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. It even seems to work with
  51 # 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 '/^# +clam/, /^$/ { gsub(/^# ?/, ""); print }' "$0"
  59         exit 0
  60     ;;
  61 esac
  62 
  63 # dash doesn't support regex-matching syntax, forcing to use case statements
  64 case "$0" in
  65     -bash|-dash|-sh|bash|dash|sh)
  66         # script is being sourced with bash or dash, which is good
  67         :;;
  68     *)
  69         case "$ZSH_EVAL_CONTEXT" in
  70             *:file)
  71                 # script is being sourced with zsh, which is good
  72                 :;;
  73             *)
  74                 # script is being run normally, which is a waste of time
  75 printf "\e[48;5;228m\e[30mDon't run this script, source it instead: to do that,\e[0m\n"
  76 printf "\e[48;5;228m\e[30mrun 'source clam' or '. clam' (no quotes either way).\e[0m\n"
  77                 # failing during shell-startup may deny shell access, so don't
  78                 # exit with a non-0 error code
  79             ;;
  80         esac
  81     ;;
  82 esac
  83 
  84 
  85 # use a simple shell prompt; this setting works on both bash and zsh
  86 # export PS1="\$ "
  87 # export PS2="> "
  88 
  89 # use a simple shell prompt, showing the current folder as the tab label;
  90 # this setting is bash-specific
  91 # export PS1="\[\e]0;\w\a\]$ "
  92 # export PS2="> "
  93 
  94 # prevent `less` from saving/remembering commands
  95 # export LESSHISTFILE="-"
  96 # export LESSSECURE=1
  97 
  98 # prevent the shell from saving/remembering commands across live sessions
  99 # unset HISTFILE
 100 
 101 # avoid saving nodejs command-history to file
 102 # export NODE_REPL_HISTORY=""
 103 # export NODE_REPL_HISTORY_SIZE=0
 104 
 105 # in the future, this may be how you tell `go` to mind its own biz
 106 # export GOTELEMETRY=off
 107 
 108 
 109 # alias all-files='allfiles'
 110 # alias all-folders='allfolders'
 111 # alias begin-tsv='begintsv'
 112 # alias big-files='bigfiles'
 113 # alias blow-tabs='blowtabs'
 114 # alias but-first='butfirst'
 115 # alias but-last='butlast'
 116 # alias chop-decs='chopdecs'
 117 # alias chop-lf='choplf'
 118 # alias first-bytes='firstbytes'
 119 # alias first-lines='firstlines'
 120 # alias from-line='fromline'
 121 # alias last-bytes='lastbytes'
 122 # alias last-lines='lastlines'
 123 # alias line-up='lineup'
 124 # alias pre-tsv='pretsv'
 125 # alias range-lines='rangelines'
 126 # alias ring-tone='ringtone'
 127 # alias show-run='showrun'
 128 # alias skip-bytes='skipbytes'
 129 # alias skip-lines='skiplines'
 130 # alias small-files='smallfiles'
 131 # alias sort-rest='sortrest'
 132 # alias to-line='toline'
 133 # alias to-lower='tolower'
 134 # alias to-upper='toupper'
 135 # alias top-files='topfiles'
 136 # alias top-folders='topfolders'
 137 # alias trim-end='trimend'
 138 # alias trim-ends='trimends'
 139 # alias trim-trail='trimtrail'
 140 # alias trim-trails='trimtrails'
 141 # alias ts-awk='tsawk'
 142 # alias tsv-dedup='tsvdedup'
 143 # alias where-trails='wheretrails'
 144 # alias which-trails='whichtrails'
 145 # alias yd-aac='ydaac'
 146 # alias yd-mp4='ydmp4'
 147 
 148 
 149 # n-Column-layout shortcuts, using my script `sbs` (Side By Side)
 150 # 1c() { sbs 1 "$@"; }
 151 # 2c() { sbs 2 "$@"; }
 152 # 3c() { sbs 3 "$@"; }
 153 # 4c() { sbs 4 "$@"; }
 154 # 5c() { sbs 5 "$@"; }
 155 # 6c() { sbs 6 "$@"; }
 156 # 7c() { sbs 7 "$@"; }
 157 # 8c() { sbs 8 "$@"; }
 158 # 9c() { sbs 9 "$@"; }
 159 
 160 # n-Column-layout shortcuts, using my script `sbs` (Side By Side)
 161 c1() { sbs 1 "$@"; }
 162 c2() { sbs 2 "$@"; }
 163 c3() { sbs 3 "$@"; }
 164 c4() { sbs 4 "$@"; }
 165 c5() { sbs 5 "$@"; }
 166 c6() { sbs 6 "$@"; }
 167 c7() { sbs 7 "$@"; }
 168 c8() { sbs 8 "$@"; }
 169 c9() { sbs 9 "$@"; }
 170 
 171 # shortcuts for my script `ca`, an arbitrary-precision calculator, using
 172 # various decimal precisions
 173 ca0() { ca "scale=0; $*"; }
 174 ca1() { ca "scale=1; $*"; }
 175 ca2() { ca "scale=2; $*"; }
 176 ca3() { ca "scale=3; $*"; }
 177 ca4() { ca "scale=4; $*"; }
 178 ca5() { ca "scale=5; $*"; }
 179 ca6() { ca "scale=6; $*"; }
 180 ca7() { ca "scale=7; $*"; }
 181 ca8() { ca "scale=8; $*"; }
 182 ca9() { ca "scale=9; $*"; }
 183 ca10() { ca "scale=10; $*"; }
 184 ca20() { ca "scale=20; $*"; }
 185 ca30() { ca "scale=30; $*"; }
 186 ca40() { ca "scale=40; $*"; }
 187 ca50() { ca "scale=50; $*"; }
 188 ca60() { ca "scale=60; $*"; }
 189 ca70() { ca "scale=70; $*"; }
 190 ca80() { ca "scale=80; $*"; }
 191 ca90() { ca "scale=90; $*"; }
 192 
 193 # expand Tabs up to n spaces each
 194 t1() { expand -t 1 "$@"; }
 195 t2() { expand -t 2 "$@"; }
 196 t3() { expand -t 3 "$@"; }
 197 t4() { expand -t 4 "$@"; }
 198 t5() { expand -t 5 "$@"; }
 199 t6() { expand -t 6 "$@"; }
 200 t7() { expand -t 7 "$@"; }
 201 t8() { expand -t 8 "$@"; }
 202 t9() { expand -t 9 "$@"; }
 203 
 204 # expand TABs up to n spaces each
 205 tab1() { expand -t 1 "$@"; }
 206 tab2() { expand -t 2 "$@"; }
 207 tab3() { expand -t 3 "$@"; }
 208 tab4() { expand -t 4 "$@"; }
 209 tab5() { expand -t 5 "$@"; }
 210 tab6() { expand -t 6 "$@"; }
 211 tab7() { expand -t 7 "$@"; }
 212 tab8() { expand -t 8 "$@"; }
 213 tab9() { expand -t 9 "$@"; }
 214 
 215 a() { awk "$@"; }
 216 
 217 # show all files in a folder, digging recursively
 218 allfiles() {
 219     local arg
 220     for arg in "${@:-.}"; do
 221         find "${arg}" -type f
 222     done
 223 }
 224 
 225 # show all folders in a folder, digging recursively
 226 allfolders() {
 227     local arg
 228     for arg in "${@:-.}"; do
 229         find "${arg}" -type d | awk 'NR > 1'
 230     done
 231 }
 232 
 233 # show only the lines which appear on all the inputs given
 234 and() {
 235     awk '
 236     FNR == 1 { filenum++ }
 237 
 238     {
 239         lines[n++] = $0
 240         if (last[$0] != filenum) {
 241             last[$0] = filenum
 242             tally[$0]++
 243         }
 244     }
 245 
 246     END {
 247         n = ARGC - 1
 248         for (i = 1; i <= NR; i++) {
 249             l = lines[i]
 250             if (tally[l] == n) {
 251                 tally[l] = -1
 252                 print l
 253             }
 254         }
 255     }' "$@"
 256 }
 257 
 258 # emit each argument given as its own line of output
 259 args() { awk 'BEGIN { for (i = 1; i < ARGC; i++) print ARGV[i]; exit }' "$@"; }
 260 
 261 # keep visible ASCII bytes as they are, turning (almost) all other bytes into
 262 # ASCII spaces, by default; tabs, line-feeds, carriage-returns, and escapes
 263 # are also kept as given, to avoid messing up lines and ANSI-styles
 264 # asciify() { tr -c '\11\12\15\33\41-\176' "${1:- }"; }
 265 
 266 # turn UTF-8 into visible pseudo-ASCII, where variants of latin letters become
 267 # their basic ASCII counterparts, and where non-ASCII symbols become question
 268 # marks, one question mark for each code-point byte
 269 asciify() { iconv -f utf-8 -t ascii//translit "$@"; }
 270 
 271 # avoid/ignore lines which match any of the regexes given
 272 avoid() {
 273     awk '
 274         BEGIN { for (i = 1; i < ARGC; i++) { re[i] = ARGV[i]; delete ARGV[i] } }
 275         { for (i in re) if ($0 ~ re[i]) { next } }
 276         { print; fflush() }' "${@:-^$}"
 277 }
 278 
 279 b() { bc "$@"; }
 280 
 281 # emit a line with a repeating ball-like symbol in it
 282 balls() { [ "${1:-80}" -gt 0 ] && printf "%${1:-80}s\n" " " | sed 's- -●-g'; }
 283 
 284 # show an ansi-styled BANNER-like line
 285 # banner() { printf "\e[48;5;253m%s\e[0m\n" "$*"; }
 286 
 287 # show an ansi-styled BANNER-like line
 288 banner() { printf "\e[7m%s\e[0m\n" "$*"; }
 289 
 290 # emit a colored bar which can help visually separate different outputs
 291 bar() { [ "${1:-80}" -gt 0 ] && printf "\e[48;5;253m%${1:-80}s\e[0m\n" " "; }
 292 
 293 # process Blocks/paragraphs of non-empty lines with AWK
 294 bawk() { awk -F='' -v RS='' "$@"; }
 295 
 296 # Background-colored ECHO
 297 becho() { printf "\e[48;5;253m%s\e[0m\n" "$*"; }
 298 
 299 # play a repeating and annoying high-pitched beep sound a few times a second,
 300 # lasting the number of seconds given, or for 1 second by default; uses my
 301 # script `waveout`
 302 beeps() {
 303     local f='sin(2_000 * tau * t) * (t % 0.5 < 0.0625)'
 304     waveout "${1:-1}" "${2:-1} * $f" | mpv --really-quiet -
 305 }
 306 
 307 # start by joining all arguments given as a tab-separated-items line of output,
 308 # followed by all lines from stdin verbatim
 309 begintsv() {
 310     awk 'BEGIN {
 311         for (i = 1; i < ARGC; i++) {
 312             if (i > 1) { printf "\t" }
 313             printf "%s", ARGV[i]
 314             delete ARGV[i]
 315         }
 316         printf "\n"
 317     }
 318     { print; fflush() }' "$@"
 319 }
 320 
 321 # play a repeating synthetic-bell-like sound lasting the number of seconds
 322 # given, or for 1 second by default; uses my script `waveout`
 323 bell() {
 324     local f='sin(880*tau*u) * exp(-10*u)'
 325     waveout "${1:-1}" "${2:-1} * $f" | mpv --really-quiet -
 326 }
 327 
 328 # play a repeating sound with synthetic-bells, lasting the number of seconds
 329 # given, or for 1 second by default; uses my script `waveout`
 330 bells() {
 331     local f="sum(sin(880*tau*v)*exp(-10*v) for v in (u, (u-0.25)%1)) / 2"
 332     waveout "${1:-1}" "${2:-1} * $f" | mpv --really-quiet -
 333 }
 334 
 335 # Breathe Header: add an empty line after the first one (the header), then
 336 # separate groups of 5 lines (by default) with empty lines between them
 337 bh() {
 338     local n="${1:-5}"
 339     [ $# -gt 0 ] && shift
 340     awk -v n="$n" '
 341         (NR - 1) % n == 1 && NR > 1 { print "" }
 342         { print; fflush() }' "$@"
 343 }
 344 
 345 # recursively find all files with at least the number of bytes given
 346 bigfiles() {
 347     local n
 348     n="$(echo "${1}" | sed 's-_--g')"
 349     find "${2:-.}" -size "$n"c -o -size +"$n"c
 350 }
 351 
 352 # Breathe Lines: separate groups of 5 lines (by default) with empty lines
 353 bl() {
 354     local n="${1:-5}"
 355     [ $# -gt 0 ] && shift
 356     awk -v n="$n" '
 357         NR % n == 1 && NR != 1 { print "" }
 358         { print; fflush() }' "$@"
 359 }
 360 
 361 # process BLocks/paragraphs of non-empty lines with AWK
 362 blawk() { awk -F='' -v RS='' "$@"; }
 363 
 364 # Blue LEAK emits/tees input both to stdout and stderr, coloring blue what
 365 # it emits to stderr using an ANSI-style; this cmd is useful to `debug`
 366 # pipes involving several steps
 367 bleak() {
 368     awk '{
 369         gsub(/\x1b\[([0-9]*[A-HJKSThl]|[0-9;]*m)/, "")
 370         printf "\x1b[38;5;26m%s\x1b[0m\n", $0 > "/dev/stderr"
 371         print
 372         fflush()
 373     }' "$@"
 374 }
 375 
 376 # emit a line with a repeating block-like symbol in it
 377 blocks() { [ "${1:-80}" -gt 0 ] && printf "%${1:-80}s\n" " " | sed 's- -█-g'; }
 378 
 379 # expand all tabs into runs of spaces
 380 blow() {
 381     local tabstop="${1:-4}"
 382     [ $# -gt 0 ] && shift
 383     expand -t "${tabstop}" "$@"
 384 }
 385 
 386 # expand all tabs into runs of spaces
 387 blowtabs() {
 388     local tabstop="${1:-4}"
 389     [ $# -gt 0 ] && shift
 390     expand -t "${tabstop}" "$@"
 391 }
 392 
 393 # BREATHE lines: separate groups of 5 lines (by default) with empty lines
 394 breathe() {
 395     local n="${1:-5}"
 396     [ $# -gt 0 ] && shift
 397     awk -v n="$n" '
 398         NR % n == 1 && NR != 1 { print "" }
 399         { print; fflush() }' "$@"
 400 }
 401 
 402 # show a reverse-sorted tally of all lines read, where ties are sorted
 403 # alphabetically, and where trailing bullets are added to quickly make
 404 # the tally counts comparable at a glance
 405 bully() {
 406     printf "value\ttally\tbullets\n"
 407     awk '
 408 { tally[$0]++ }
 409 
 410 END {
 411     # find the max tally, which is needed to build the bullets-string
 412     max = 0
 413     for (k in tally) {
 414         if (max < tally[k]) max = tally[k]
 415     }
 416 
 417     # make enough bullets for all tallies: this loop makes growing the
 418     # string a task with complexity O(n * log n), instead of a naive
 419     # O(n**2), which can slow-down things when tallies are high enough
 420     bullets = "•"
 421     for (n = max; n > 1; n /= 2) {
 422         bullets = bullets bullets
 423     }
 424 
 425     # emit unsorted output lines to the sort cmd, which will emit the
 426     # final reverse-sorted tally lines
 427     for (k in tally) {
 428         s = substr(bullets, 1, tally[k])
 429         printf "%s\t%d\t%s\n", k, tally[k], s
 430     }
 431 }
 432     ' "$@" | sort -t "$(printf "\t")" -rnk2 -k1d
 433 }
 434 
 435 # play a busy-phone-line sound lasting the number of seconds given, or for 1
 436 # second by default; uses my script `waveout`
 437 busy() {
 438     # local f='(u < 0.5) * (sin(480*tau * t) + sin(620*tau * t)) / 2'
 439     local f='min(1, exp(-90*(u-0.5))) * (sin(480*tau*t) + sin(620*tau*t)) / 2'
 440     # local f='(sin(350*tau*t) + sin(450*tau*t)) / 2 * min(1, exp(-90*(u-0.5)))'
 441     waveout "${1:-1}" "${2:-1} * $f" | mpv --really-quiet -
 442 }
 443 
 444 # keep all BUT the FIRST (skip) n lines, or skip just the 1st line by default
 445 butfirst() { tail -n +$(("${1:-1}" + 1)) "${2:--}"; }
 446 
 447 # keep all BUT the LAST n lines, or skip just the last line by default
 448 butlast() { head -n -"${1:-1}" "${2:--}"; }
 449 
 450 c() { cat "$@"; }
 451 
 452 # CAlculator with Nice numbers runs my script `ca` and colors results with
 453 # my script `nn`, alternating styles to make long numbers easier to read
 454 can() { ca "$@" | nn; }
 455 
 456 # conCATenate Lines
 457 catl() {
 458     awk '
 459         FNR == 1 { gsub(/^\xef\xbb\xbf/, "") }
 460         { print; fflush() }' "$@" | sed -E 's-\r$--'
 461 }
 462 
 463 # Csv AWK: CSV-specific input settings for `awk`
 464 # cawk() { awk --csv -v OFS="\t" "$@"; }
 465 
 466 # Csv AWK: CSV-specific input settings for `awk`
 467 cawk() { awk --csv "$@"; }
 468 
 469 # Colored Go Test on the folder given; uses my script `gbm`
 470 cgt() { go test "${1:-.}" 2>&1 | gbm '/^ok/' '/^[-]* ?FAIL/' '/^\?/'; }
 471 
 472 # lookup docs/examples using the curl-friendly website cheat.sh
 473 cheat() {
 474     local cmd="bat"
 475     # debian linux uses a different name for the `bat` app
 476     if [ -e "/usr/bin/batcat" ]; then
 477         cmd="batcat"
 478     fi
 479 
 480     if [ -z "$1" ]; then
 481         curl -s cheat.sh/ |
 482             awk '1 { gsub(/\x1b\[([0-9]*[A-HJKSThl]|[0-9;]*m)/, ""); print }'
 483         return
 484     fi
 485 
 486     # all but the top-level page can be shell-syntax colored
 487     curl -s "cheat.sh/$1" |
 488         awk '1 { gsub(/\x1b\[([0-9]*[A-HJKSThl]|[0-9;]*m)/, ""); print }' |
 489         "$cmd" -l sh \
 490             --style=plain --theme='Monokai Extended Light' \
 491             --wrap=never --color=always - |
 492         sed 's-\x1b\[38;5;70m-\x1b\[38;5;28m-g'
 493 }
 494 
 495 # CHOP DECimalS ignores all trailing decimal zeros in numbers, even the
 496 # decimal dots themselves, when decimals in a number are all zeros
 497 chopdecs() {
 498     awk 1 "$@" | sed -E 's-(\.[0-9]+[1-9]+)0+$-\1-g; s-([0-9]+)\.0*$-\1-g'
 499 }
 500 
 501 # ignore final life-feed from text, if it's the very last byte; also ignore
 502 # all trailing carriage-returns
 503 choplf() {
 504     awk '
 505         FNR == 1 { gsub(/^\xef\xbb\xbf/, "") }
 506         NR > 1 { print ""; fflush() }
 507         { gsub(/\r$/, ""); printf "%s", $0 }
 508     ' "$@"
 509 }
 510 
 511 # Color Json using the `jq` app, allowing an optional filepath as the data
 512 # source, and even an optional transformation formula
 513 cj() { jq -C "${2:-.}" "${1:--}"; }
 514 
 515 # show a live digital clock
 516 clock() { watch -n 1 echo 'Press Ctrl + C to quit this clock'; }
 517 
 518 # CLear Screen, like the old dos command of the same name
 519 cls() { clear; }
 520 
 521 # COunt COndition: count how many times the AWK expression given is true
 522 coco() {
 523     local cond="${1:-1}"
 524     [ $# -gt 0 ] && shift
 525     awk "
 526         { low = lower = tolower(\$0) }
 527         ${cond} { count++ }
 528         END { print count }
 529     " "$@"
 530 }
 531 
 532 # COlor SYntax: run syntax-coloring app `bat` without line-wrapping
 533 cosy() {
 534     local cmd="bat"
 535     # debian linux uses a different name for the `bat` app
 536     if [ -e "/usr/bin/batcat" ]; then
 537         cmd="batcat"
 538     fi
 539 
 540     "$cmd" --style=plain,header,numbers --theme='Monokai Extended Light' \
 541         --wrap=never --color=always "$@" |
 542     sed 's-\x1b\[38;5;70m-\x1b\[38;5;28m-g' | less -JMKiCRS
 543 }
 544 
 545 # COlor SYntax of all files in a FOlder, showing line numbers
 546 cosyfo() {
 547     local cmd="bat"
 548     # debian linux uses a different name for the `bat` app
 549     if [ -e "/usr/bin/batcat" ]; then
 550         cmd="batcat"
 551     fi
 552 
 553     find "${1:-.}" -type f -print0 | xargs --null "$cmd" \
 554         --style=plain,header,numbers --theme='Monokai Extended Light' \
 555         --wrap=never --color=always |
 556     sed 's-\x1b\[38;5;70m-\x1b\[38;5;28m-g' | less -JMKiCRS
 557 }
 558 
 559 # Colored RipGrep ensures app `rg` emits colors when piped
 560 crg() { rg --color=always "$@"; }
 561 
 562 # emit a line with a repeating cross-like symbol in it
 563 crosses() {
 564     [ "${1:-80}" -gt 0 ] && printf "%${1:-80}s\n" " " | sed 's- -×-g'
 565 }
 566 
 567 # Color Syntax: run syntax-coloring app `bat` without line-wrapping
 568 cs() {
 569     local cmd="bat"
 570     # debian linux uses a different name for the `bat` app
 571     if [ -e "/usr/bin/batcat" ]; then
 572         cmd="batcat"
 573     fi
 574 
 575     "$cmd" --style=plain,header,numbers --theme='Monokai Extended Light' \
 576         --wrap=never --color=always "$@" |
 577     sed 's-\x1b\[38;5;70m-\x1b\[38;5;28m-g' | less -JMKiCRS
 578 }
 579 
 580 # Color Syntax of all files in a Folder, showing line numbers
 581 csf() {
 582     local cmd="bat"
 583     # debian linux uses a different name for the `bat` app
 584     if [ -e "/usr/bin/batcat" ]; then
 585         cmd="batcat"
 586     fi
 587 
 588     find "${1:-.}" -type f -print0 | xargs --null "$cmd" \
 589         --style=plain,header,numbers --theme='Monokai Extended Light' \
 590         --wrap=never --color=always |
 591     sed 's-\x1b\[38;5;70m-\x1b\[38;5;28m-g' | less -JMKiCRS
 592 }
 593 
 594 # Change Units turns common US units into international ones; uses my
 595 # scripts `bu` (Better Units) and `nn` (Nice Numbers)
 596 cu() {
 597     bu "$@" | awk '
 598         NF == 5 { print $(NF-1), $NF }
 599         NF == 4 && $NF == "s" { print $(NF-1), $NF }
 600         NF == 4 && $NF != "s" { print $NF }
 601     ' | nn
 602 }
 603 
 604 # run CURL in Silent/quiet mode
 605 curls() { curl -s "$@"; }
 606 
 607 # Date and time
 608 d() {
 609     printf "\e[32m%s\e[0m  \e[34m%s\e[0m\n" "$(date +'%a %b %d')" "$(date +%T)"
 610 }
 611 
 612 # Date and time
 613 # d() { date "$@"; }
 614 
 615 # emit a line with a repeating ball-like symbol in it
 616 dashes() { [ "${1:-80}" -gt 0 ] && printf "%${1:-80}s\n" " " | sed 's- -—-g'; }
 617 
 618 # DEcode BASE-64-encoded data
 619 debase64() { base64 -d "$@"; }
 620 
 621 # DEDUPlicate prevents lines from appearing more than once
 622 dedup() { awk '!c[$0]++' "$@"; }
 623 
 624 # DEDUPlicatE prevents lines from appearing more than once
 625 dedupe() { awk '!c[$0]++' "$@"; }
 626 
 627 # dictionary-DEFine the word given, using an online service
 628 def() {
 629     curl -s "dict://dict.org/d:$*" | sed 's-\r--g' | awk '
 630         { gsub(/\r/, "") }
 631         /^151 / { printf "\x1b[38;5;4m%s\x1b[0m\n", $0; next }
 632         /^[1-9][0-9]{2} / { printf "\x1b[38;5;244m%s\x1b[0m\n", $0; next }
 633         { print; fflush() }' | less -JMKiCRS
 634 }
 635 
 636 # dictionary-define the word given, using an online service
 637 define() {
 638     curl -s "dict://dict.org/d:$*" | sed 's-\r--g' | awk '
 639         { gsub(/\r/, "") }
 640         /^151 / { printf "\x1b[38;5;4m%s\x1b[0m\n", $0; next }
 641         /^[1-9][0-9]{2} / { printf "\x1b[38;5;244m%s\x1b[0m\n", $0; next }
 642         { print; fflush() }' | less -JMKiCRS
 643 }
 644 
 645 # DEcompress GZip-encoded data
 646 degz() { zcat "$@"; }
 647 
 648 # DEcompress GZIP-encoded data
 649 degzip() { zcat "$@"; }
 650 
 651 # turn json lines into a proper json array
 652 dejsonl() { jq -s -M "${@:-.}"; }
 653 
 654 # delay lines being piped from standard input; the delay is given as a number
 655 # of seconds, and is by default 1, when not given
 656 delaylines() {
 657     (
 658         IFS="$(printf "\n")"
 659         while read -r l; do
 660             sleep "${1:-1}"
 661             printf "%s\n" "${l}"
 662         done
 663     )
 664 }
 665 
 666 # turn lines of Space-Separated Values into lines of Tab-Separated Values
 667 dessv() { awk 1 "$@" | sed -E 's-^ +--; s- *\r?--; s- +-\t-g'; }
 668 
 669 # turn UTF-16 data into UTF-8
 670 deutf16() { iconv -f utf16 -t utf8 "$@"; }
 671 
 672 # lookup words using an online DICtionary
 673 dic() {
 674     curl -s "dict://dict.org/d:$*" | awk '
 675         { gsub(/\r/, "") }
 676         /^151 / { printf "\x1b[38;5;4m%s\x1b[0m\n", $0; next }
 677         /^[1-9][0-9]{2} / { printf "\x1b[38;5;244m%s\x1b[0m\n", $0; next }
 678         { print; fflush() }' | less -JMKiCRS
 679 }
 680 
 681 # lookup words using an online DICTionary
 682 dict() {
 683     curl -s "dict://dict.org/d:$*" | awk '
 684         { gsub(/\r/, "") }
 685         /^151 / { printf "\x1b[38;5;4m%s\x1b[0m\n", $0; next }
 686         /^[1-9][0-9]{2} / { printf "\x1b[38;5;244m%s\x1b[0m\n", $0; next }
 687         { print; fflush() }' | less -JMKiCRS
 688 }
 689 
 690 # DIVide 2 numbers 3 ways, including the complement
 691 div() {
 692     awk -v a="${1:-1}" -v b="${2:-1}" '
 693         BEGIN {
 694             gsub(/_/, "", a); gsub(/_/, "", b)
 695             if (a > b) { c = a; a = b; b = c; }
 696             printf "%f\n%f\n%f\n", a / b, b / a, 1 - a / b
 697             exit
 698         }'
 699 }
 700 
 701 # get/fetch data from the filename or URI given; named `dog` because dogs can
 702 # `fetch` things for you
 703 dog() {
 704     case "$1" in
 705         http://*|https://*|ftp://*|ftps://*|sftp://*|dict://*)
 706             [ -e "$1" ] && cat "$1" || curl -s "$1";;
 707         file://*)
 708             [ -e "$1" ] && cat "$1" || cat "$(echo "$1" | sed 's-file://--1')";;
 709         *)
 710             cat "$1";;
 711     esac 2> /dev/null || {
 712         printf "\e[31mcan't get %s\e[0m\n" "$1" >&2
 713         return 1
 714     }
 715 }
 716 
 717 # emit a line with a repeating dot-like symbol in it
 718 dots() { [ "${1:-80}" -gt 0 ] && printf "%${1:-80}s\n" " " | sed 's- -·-g'; }
 719 
 720 # show the current Date and Time
 721 dt() {
 722     printf "\e[32m%s\e[0m  \e[34m%s\e[0m\n" "$(date +'%a %b %d')" "$(date +%T)"
 723 }
 724 
 725 # show the current Date, Time, and a Calendar with the 3 `current` months
 726 dtc() {
 727     # show the current time center-aligned
 728     printf "%22s\e[32m%s\e[0m  \e[34m%s\e[0m\n" \
 729         " " "$(date +'%a %b %d')" "$(date +%T)"
 730 
 731     printf "\n"
 732 
 733     # debian linux has a different `cal` app which highlights the day
 734     if [ -e "/usr/bin/ncal" ]; then
 735         ncal -C -3
 736     else
 737         cal -3
 738     fi
 739 }
 740 
 741 # Download from Youtube
 742 dy() { yt-dlp "$@"; }
 743 
 744 # Download Youtube AAC audio
 745 dyaac() { yt-dlp -f 140 "$@"; }
 746 
 747 # Download Youtube MP4 video
 748 dymp4() { yt-dlp -f 22 "$@"; }
 749 
 750 # Evaluate Awk expression
 751 ea() { awk "BEGIN { print ${1:-0}; exit }"; }
 752 
 753 # Evaluate Awk expression
 754 # ea() { awk --bignum "BEGIN { print ${1:-0}; exit }"; }
 755 
 756 # run a command for each of the files given as lines from standard input
 757 each() {
 758     (
 759         IFS="$(printf "\n")"
 760         while read -r name; do
 761             "$@" < "${name}"
 762             err="$?"
 763             if [ "${err}" -ne 0 ]; then
 764                 exit "${err}"
 765             fi
 766         done
 767     )
 768 }
 769 
 770 # ECHO Background, using gray-colored background
 771 echob() { printf "\e[48;5;253m%s\e[0m\n" "$*"; }
 772 
 773 # Green ECHO, using a green style
 774 gecho() { printf "\e[38;5;29m%s\e[0m\n" "$*"; }
 775 
 776 # ECHO Highlighted, using inverted style
 777 echoh() { printf "\e[7m%s\e[0m\n" "$*"; }
 778 
 779 # ECHO Inverted, using inverted style
 780 echoi() { printf "\e[7m%s\e[0m\n" "$*"; }
 781 
 782 # ECHO Lines emits each argument given as its own line of output
 783 echol() {
 784     awk 'BEGIN { for (i = 1; i < ARGC; i++) print ARGV[i]; exit }' "$@"
 785 }
 786 
 787 # edit plain-text files
 788 # edit() { micro "$@"; }
 789 
 790 # edit plain-text files
 791 # edit() { tilde "$@"; }
 792 
 793 # Extended-mode Grep, enabling its full regex syntax
 794 # eg() { grep -E "$@"; }
 795 
 796 # Extended-mode Grep, enabling its full regex syntax
 797 eg() { grep -E --line-buffered "$@"; }
 798 
 799 # Echo Lines emits each argument given as its own line of output
 800 el() { awk 'BEGIN { for (i = 1; i < ARGC; i++) print ARGV[i]; exit }' "$@"; }
 801 
 802 # Evaluate Nodejs expression
 803 en() {
 804     local expr="${1:-null}"
 805     expr="$(echo "${expr}" | sed 's-\\-\\\\-g; s-`-\`-g')"
 806     node -e "console.log(${expr})" | sed 's-\x1b\[[^A-Za-z]+[A-Za-z]--g'
 807 }
 808 
 809 # Evaluate Python expression
 810 ep() { python -c "print(${1:-None})"; }
 811 
 812 # Evaluate Ruby expression
 813 er() { ruby -e "puts ${1:-nil}"; }
 814 
 815 # Editor Read-Only
 816 ero() { micro -readonly true "$@"; }
 817 
 818 # Extended-mode Sed, enabling its full regex syntax
 819 es() { sed -E -u "$@"; }
 820 
 821 # f() { find "$@"; }
 822 
 823 # run the fuzzy Finder (fzf) in multi-choice mode, with custom keybindings
 824 f() { fzf -m --bind ctrl-a:select-all,ctrl-space:toggle "$@"; }
 825 
 826 # convert Fahrenheit into Celsius
 827 # f2c() {
 828 #     echo "${@:-1}" | sed 's-_--g; s-  *-\n-g' |
 829 #         awk '{ print ($0 - 32) * 9.0/5.0 }'
 830 # }
 831 
 832 # convert FAHRenheit into celsius
 833 fahr() {
 834     echo "${@:-1}" | sed 's-_--g; s-  *-\n-g' |
 835         awk '{ print ($0 - 32) * 9.0/5.0 }'
 836 }
 837 
 838 # convert fahrenheit into celsius
 839 fahrenheit() {
 840     echo "${@:-1}" | sed 's-_--g; s-  *-\n-g' |
 841         awk '{ print ($0 - 32) * 9.0/5.0 }'
 842 }
 843 
 844 # Faint (lines) with AWK, using a gray color
 845 fawk() {
 846     local cond="${1:-1}"
 847     [ $# -gt 0 ] && shift
 848 
 849     awk "${cond} {
 850             gsub(/\\x1b\\[0m/, \"\x1b[0m\\x1b[38;5;248m\")
 851             printf \"\\x1b[38;5;248m%s\\x1b[0m\\n\", \$0
 852             fflush()
 853             next
 854         }
 855 
 856         {
 857             print
 858             fflush()
 859         }" "$@"
 860 }
 861 
 862 # run the Fuzzy Finder (fzf) in multi-choice mode, with custom keybindings
 863 ff() { fzf -m --bind ctrl-a:select-all,ctrl-space:toggle "$@"; }
 864 
 865 # deduplicate lines by the field given, keeping them in their original order
 866 fieldedup() {
 867     local n="${1:-0}"
 868     [ $# -gt 0 ] && shift
 869     awk "!c[(\$${n} >= 0) ? \$${n} : \$(NF + ${n} - 1)]++" "$@"
 870 }
 871 
 872 # show all files in a folder, digging recursively
 873 files() {
 874     local arg
 875     for arg in "${@:-.}"; do
 876         find "${arg}" -type f
 877     done
 878 }
 879 
 880 # get the first n lines, or 1 by default
 881 first() { head -n "${1:-1}" "${2:--}"; }
 882 
 883 # limit data up to the first n bytes
 884 firstbytes() { head -c "$1" "${2:--}"; }
 885 
 886 # get the first n lines, or 1 by default
 887 firstlines() { head -n "${1:-1}" "${2:--}"; }
 888 
 889 # fix lines, ignoring leading UTF-8_BOMs (byte-order-marks) on each input's
 890 # first line, turning all end-of-line CRLF byte-pairs into single line-feeds,
 891 # and ensuring each input's last line ends with a line-feed
 892 fixlines() {
 893     awk '
 894         FNR == 1 { gsub(/^\xef\xbb\xbf/, "") }
 895         { print; fflush() }' "$@" | sed -E 's-\r$--'
 896 }
 897 
 898 # Faint LEAK emits/tees input both to stdout and stderr, coloring gray what
 899 # it emits to stderr using an ANSI-style; this cmd is useful to `debug` pipes
 900 # involving several steps
 901 fleak() {
 902     awk '{
 903         gsub(/\x1b\[([0-9]*[A-HJKSThl]|[0-9;]*m)/, "")
 904         printf "\x1b[38;5;248m%s\x1b[0m\n", $0 > "/dev/stderr"
 905         print
 906         fflush()
 907     }' "$@"
 908 }
 909 
 910 # Faster Nice Hex tries to speed-up my script `nh` via `pypy`
 911 fnh() { pypy3 "$(which nh)" "$@"; }
 912 
 913 # show all folders in a folder, digging recursively
 914 folders() {
 915     local arg
 916     for arg in "${@:-.}"; do
 917         find "${arg}" -type d | awk 'NR > 1'
 918     done
 919 }
 920 
 921 # make text lines look "FRAMEd" by padding them to the current terminal
 922 # width and using ANSI-style background colors; uses my script `tl`
 923 frame() {
 924     local color="${1:-green}"
 925     [ $# -gt 0 ] && shift
 926     tl "${color}back(f'{plain(line):$(tput cols)}')" "$@"
 927 }
 928 
 929 # start from the line number given, skipping all previous ones
 930 fromline() { tail -n +"${1:-1}" "${2:--}"; }
 931 
 932 # convert FeeT into meters
 933 ft() {
 934     echo "${@:-1}" | sed 's-_--g; s-  *-\n-g' | awk '{ print 0.3048 * $0 }'
 935 }
 936 
 937 # convert FeeT² (squared) into meters²
 938 ft2() {
 939     echo "${@:-1}" | sed 's-_--g; s-  *-\n-g' | awk '{ print 0.09290304 * $0 }'
 940 }
 941 
 942 # convert a mix of FeeT and INches into meters
 943 ftin() {
 944     local ft="${1:-0}"
 945     ft="$(echo "${ft}" | sed 's-_--g')"
 946     local in="${2:-0}"
 947     in="$(echo "${in}" | sed 's-_--g')"
 948     awk "BEGIN { print 0.3048 * ${ft} + 0.0254 * ${in}; exit }"
 949 }
 950 
 951 # Faster Transform Json, by using pypy instead of standard python
 952 ftj() { pypy3 "$(which tj)" "$@"; }
 953 
 954 # Faster Transform Lines, by using pypy instead of standard python
 955 ftl() { pypy3 "$(which tl)" "$@"; }
 956 
 957 # run the FuZzy finder (fzf) in multi-choice mode, with custom keybindings
 958 fz() { fzf -m --bind ctrl-a:select-all,ctrl-space:toggle "$@"; }
 959 
 960 # Get/fetch data from the filename or URI given
 961 # g() {
 962 #     case "$1" in
 963 #         http://*|https://*|ftp://*|ftps://*|sftp://*|dict://*)
 964 #             [ -e "$1" ] && cat "$1" || curl -s "$1";;
 965 #         file://*)
 966 #             [ -e "$1" ] && cat "$1" || cat "$(echo "$1" | sed 's-file://--1')";;
 967 #         *)
 968 #             cat "$1";;
 969 #     esac 2> /dev/null || {
 970 #         printf "\e[31mcan't get %s\e[0m\n" "$1" >&2
 971 #         return 1
 972 #     }
 973 # }
 974 
 975 # run `grep` in extended mode, enabling its full regex syntax
 976 # g() { grep -E "$@"; }
 977 
 978 # run `grep` in extended mode, enabling its full regex syntax
 979 g() { grep -E --line-buffered "$@"; }
 980 
 981 # convert GALlons into liters
 982 gal() {
 983     echo "${@:-1}" | sed 's-_--g; s-  *-\n-g' | awk '{ print 3.785411784 * $0 }'
 984 }
 985 
 986 # GCCGO Build Stripped: a niche use-case for the go/gccgo compilers
 987 gccgobs() { go build -compiler gccgo -gccgoflags "-s -O2" -trimpath "$@"; }
 988 
 989 # Green-colored ECHO
 990 gecho() { printf "\e[38;5;29m%s\e[0m\n" "$*"; }
 991 
 992 # GET/fetch data from the filename or URI given
 993 # get() {
 994 #     case "$1" in
 995 #         http://*|https://*|ftp://*) [ -e "$1" ] && cat "$1" || wget -O - "$1";;
 996 #         *) cat "$1";;
 997 #     esac 2> /dev/null || {
 998 #         printf "\e[31mcan't get %s\e[0m\n" "$1" >&2
 999 #         return 1
1000 #     }
1001 # }
1002 
1003 # GET/fetch data from the filename or URI given
1004 get() {
1005     case "$1" in
1006         http://*|https://*|ftp://*|ftps://*|sftp://*|dict://*)
1007             [ -e "$1" ] && cat "$1" || curl -s "$1";;
1008         file://*)
1009             [ -e "$1" ] && cat "$1" || cat "$(echo "$1" | sed 's-file://--1')";;
1010         *)
1011             cat "$1";;
1012     esac 2> /dev/null || {
1013         printf "\e[31mcan't get %s\e[0m\n" "$1" >&2
1014         return 1
1015     }
1016 }
1017 
1018 # Green LEAK emits/tees input both to stdout and stderr, coloring green what
1019 # it emits to stderr using an ANSI-style; this cmd is useful to `debug` pipes
1020 # involving several steps
1021 gleak() {
1022     awk '{
1023         gsub(/\x1b\[([0-9]*[A-HJKSThl]|[0-9;]*m)/, "")
1024         printf "\x1b[38;5;29m%s\x1b[0m\n", $0 > "/dev/stderr"
1025         print
1026         fflush()
1027     }' "$@"
1028 }
1029 
1030 # GO Build Stripped: a common use-case for the go compiler
1031 gobs() { go build -ldflags "-s -w" -trimpath "$@"; }
1032 
1033 # GO DEPendencies: shows all dependencies in a go project
1034 # godep() { go list -f '{{ join .Deps "\n" }}' "$@"; }
1035 
1036 # GO DEPendencieS: shows all dependencies in a go project
1037 godeps() { go list -f '{{ join .Deps "\n" }}' "$@"; }
1038 
1039 # GO IMPorts: show all imports in a go project
1040 goimp() { go list -f '{{ join .Imports "\n" }}' "$@"; }
1041 
1042 # GO IMPortS: show all imports in a go project
1043 goimps() { go list -f '{{ join .Imports "\n" }}' "$@"; }
1044 
1045 # Grep, Recursive Interactive and Plain
1046 grip() { ugrep -r -Q --color=never -E "$@"; }
1047 
1048 # transform lines using AWK's `gsub` (Global-regex SUBstitution) function
1049 gsub() {
1050     local what="$1"
1051     local with="$2"
1052     [ $# -gt 0 ] && shift
1053     [ $# -gt 0 ] && shift
1054     awk "{ gsub(/${what}/, \"${with}\"); print; fflush() }" "$@"
1055 }
1056 
1057 # show Help for the command given
1058 h() { "$@" --help || "$@" -h || "$@" --h || "$@" -help || "$@" help; }
1059 
1060 # Highlight (lines) with AWK
1061 hawk() {
1062     local cond="${1:-1}"
1063     [ $# -gt 0 ] && shift
1064 
1065     awk "${cond} {
1066             gsub(/\\x1b\\[0m/, \"\x1b[0m\\x1b[7m\")
1067             printf \"\\x1b[7m%s\\x1b[0m\\n\", \$0
1068             fflush()
1069             next
1070         }
1071 
1072         {
1073             print
1074             fflush()
1075         }" "$@"
1076 }
1077 
1078 # play a heartbeat-like sound lasting the number of seconds given, or for 1
1079 # second by default; uses my script `waveout`
1080 # heartbeat() {
1081 #     local f='sum(sin(10*tau*exp(-20*v))*exp(-2*v) for v in (u, (u-0.25)%1))/2'
1082 #     waveout "${1:-1}" "${2:-1} * $f" | mpv --really-quiet -
1083 # }
1084 
1085 # play a heartbeat-like sound lasting the number of seconds given, or for 1
1086 # second by default; uses my script `waveout`
1087 heartbeat() {
1088     local a='sin(v[0]*tau*exp(-20*v[1]))*exp(-2*v[1])'
1089     local b='((12, u), (8, (u-0.25)%1))'
1090     local f="sum($a for v in $b) / 2"
1091     waveout "${1:-1}" "${2:-1} * $f" | mpv --really-quiet -
1092 }
1093 
1094 # Highlighted-style ECHO
1095 hecho() { printf "\e[7m%s\e[0m\n" "$*"; }
1096 
1097 # show each byte as a pair of HEXadecimal (base-16) symbols
1098 hexify() {
1099     cat "$@" | od -x -A n | sed 's- --g' |
1100         awk '{ printf "%s", $0 } END { printf "\n" }'
1101 }
1102 
1103 # HIghlighted-style ECHO
1104 hiecho() { printf "\e[7m%s\e[0m\n" "$*"; }
1105 
1106 highlight() {
1107     awk '{
1108         gsub(/\x1b\[[0-9;]*[A-Za-z]/, "")
1109         printf "\x1b[7m%s\x1b[0m\n", $0
1110         fflush()
1111     }' "$@"
1112 }
1113 
1114 # HIghlight LEAK emits/tees input both to stdout and stderr, highlighting what
1115 # it emits to stderr using an ANSI-style; this cmd is useful to `debug` pipes
1116 # involving several steps
1117 hileak() {
1118     awk '{
1119         gsub(/\x1b\[([0-9]*[A-HJKSThl]|[0-9;]*m)/, "")
1120         printf "\x1b[7m%s\x1b[0m\n", $0 > "/dev/stderr"
1121         print
1122         fflush()
1123     }' "$@"
1124 }
1125 
1126 hilite() {
1127     awk '{
1128         gsub(/\x1b\[[0-9;]*[A-Za-z]/, "")
1129         printf "\x1b[7m%s\x1b[0m\n", $0
1130         fflush()
1131     }' "$@"
1132 }
1133 
1134 # Header Less runs `less` with line numbers, ANSI styles, no line-wrapping,
1135 # and using the first line as a sticky-header, so it always shows on top
1136 hl() { less --header=1 -JMKNiCRS "$@"; }
1137 
1138 # Highlight LEAK emits/tees input both to stdout and stderr, highlighting what
1139 # it emits to stderr using an ANSI-style; this cmd is useful to `debug` pipes
1140 # involving several steps
1141 hleak() {
1142     awk '{
1143         gsub(/\x1b\[([0-9]*[A-HJKSThl]|[0-9;]*m)/, "")
1144         printf "\x1b[7m%s\x1b[0m\n", $0 > "/dev/stderr"
1145         print
1146         fflush()
1147     }' "$@"
1148 }
1149 
1150 # Help Me Remember my custom shell commands
1151 hmr() {
1152     local cmd="bat"
1153     # debian linux uses a different name for the `bat` app
1154     if [ -e "/usr/bin/batcat" ]; then
1155         cmd="batcat"
1156     fi
1157 
1158     "$cmd" \
1159         --style=plain,header,numbers --theme='Monokai Extended Light' \
1160         --wrap=never --color=always "$(which clam)" |
1161             sed 's-\x1b\[38;5;70m-\x1b\[38;5;28m-g' | less -JMKiCRS
1162 }
1163 
1164 # convert seconds into a colon-separated Hours-Minutes-Seconds triple
1165 hms() {
1166     echo "${@:-0}" | sed 's-_--g; s-  *-\n-g' | awk '{
1167         x = $0
1168         h = (x - x % 3600) / 3600
1169         m = (x % 3600) / 60
1170         s = x % 60
1171         printf "%02d:%02d:%05.2f\n", h, m, s
1172     }'
1173 }
1174 
1175 # Header View runs `less` without line numbers, with ANSI styles, with no
1176 # line-wrapping, and using the first line as a sticky-header, so it always
1177 # shows on top
1178 hv() { less --header=1 -JMKiCRS "$@"; }
1179 
1180 # Index all lines starting from 0, using a tab right after each line number
1181 i() { nl -b a -w 1 -v 0 "$@"; }
1182 
1183 # avoid/ignore lines which case-insensitively match any of the regexes given
1184 iavoid() {
1185     awk '
1186         BEGIN { for (i = 1; i < ARGC; i++) { re[i] = ARGV[i]; delete ARGV[i] } }
1187         { l = tolower($0); for (i in re) if (l ~ re[i]) { next } }
1188         { print; fflush() }' "${@:-^$}"
1189 }
1190 
1191 # pipe-identity operation which ignores all arguments given, which is useful
1192 # to quickly disable a step along a long pipe of commands (`idem` is latin
1193 # for `same`)
1194 idem() { cat; }
1195 
1196 # pipe-IDENtity operation which ignores all arguments given, which is useful
1197 # to quickly disable a step along a long pipe of commands
1198 iden() { cat; }
1199 
1200 # pipe-identity operation which ignores all arguments given, which is useful
1201 # to quickly disable a step along a long pipe of commands
1202 identity() { cat; }
1203 
1204 # Inverted-style ECHO
1205 iecho() { printf "\e[7m%s\e[0m\n" "$*"; }
1206 
1207 # Invert LEAK emits/tees input both to stdout and stderr, inverse-styling what
1208 # it emits to stderr using an ANSI-style; this cmd is useful to `debug` pipes
1209 # involving several steps
1210 ileak() {
1211     awk '{
1212         gsub(/\x1b\[([0-9]*[A-HJKSThl]|[0-9;]*m)/, "")
1213         printf "\x1b[7m%s\x1b[0m\n", $0 > "/dev/stderr"
1214         print
1215         fflush()
1216     }' "$@"
1217 }
1218 
1219 # only keep lines which case-insensitively match any of the regexes given
1220 imatch() {
1221     awk '
1222         BEGIN {
1223             for (i = 1; i < ARGC; i++) { re[i] = ARGV[i]; delete ARGV[i] }
1224         }
1225 
1226         {
1227             l = tolower($0)
1228             for (i in re) if (l ~ re[i]) { print; fflush(); next }
1229         }' "${@:-.}"
1230 }
1231 
1232 # emit each word-like item from each input line on its own line
1233 items() { awk '{ for (i = 1; i <= NF; i++) print $i; fflush() }' "$@"; }
1234 
1235 # shrink/compact Json data, allowing an optional filepath
1236 # j0() { python -m json.tool --compact "${1:--}"; }
1237 
1238 # shrink/compact Json using the `jq` app, allowing an optional filepath, and
1239 # even an optional transformation formula after that
1240 # j0() { jq -c -M "${2:-.}" "${1:--}"; }
1241 
1242 # show Json data on multiple lines, using 2 spaces for each indentation level,
1243 # allowing an optional filepath
1244 # j2() { python -m json.tool --indent 2 "${1:--}"; }
1245 
1246 # show Json data on multiple lines, using 2 spaces for each indentation level,
1247 # allowing an optional filepath, and even an optional transformation formula
1248 # after that
1249 # j2() { jq --indent 2 -M "${2:-.}" "${1:--}"; }
1250 
1251 # listen to streaming JAZZ music
1252 # jazz() {
1253 #     printf "streaming \e[7mSmooth Jazz Instrumental\e[0m\n"
1254 #     mpv https://stream.zeno.fm/00rt0rdm7k8uv
1255 # }
1256 
1257 # listen to streaming JAZZ music
1258 jazz() {
1259     printf "streaming \e[7mSmooth Jazz Instrumental\e[0m\n"
1260     mpv --quiet https://stream.zeno.fm/00rt0rdm7k8uv
1261 }
1262 
1263 # show a `dad` JOKE from the web, sometimes even a very funny one
1264 joke() {
1265     curl -s https://icanhazdadjoke.com | fold -s | sed -E 's- *\r?$--'
1266     # plain-text output from previous cmd doesn't end with a line-feed
1267     printf "\n"
1268 }
1269 
1270 # shrink/compact JSON data, allowing an optional filepath
1271 # json0() { python -m json.tool --compact "${1:--}"; }
1272 
1273 # shrink/compact JSON using the `jq` app, allowing an optional filepath, and
1274 # even an optional transformation formula after that
1275 json0() { jq -c -M "${2:-.}" "${1:--}"; }
1276 
1277 # show JSON data on multiple lines, using 2 spaces for each indentation level,
1278 # allowing an optional filepath
1279 # json2() { python -m json.tool --indent 2 "${1:--}"; }
1280 
1281 # show JSON data on multiple lines, using 2 spaces for each indentation level,
1282 # allowing an optional filepath, and even an optional transformation formula
1283 # after that
1284 json2() { jq --indent 2 -M "${2:-.}" "${1:--}"; }
1285 
1286 # emit the given number of random/junk bytes, or 1024 junk bytes by default
1287 junk() { head -c "$(echo "${1:-1024}" | sed 's-_--g')" /dev/urandom; }
1288 
1289 # run `less`, showing line numbers, among other settings
1290 l() { less -JMKNiCRS "$@"; }
1291 
1292 # Like A Book groups lines as 2 side-by-side pages, the same way books
1293 # do it; uses my script `book`
1294 lab() { book "$(($(tput lines) - 1))" "$@" | less -JMKiCRS; }
1295 
1296 # Line xARGS: `xargs` using line separators, which handles filepaths
1297 # with spaces, as long as the standard input has 1 path per line
1298 largs() { xargs -d '\n' "$@"; }
1299 
1300 # get the last n lines, or 1 by default
1301 # last() { tail -n "${1:-1}" "${2:--}"; }
1302 
1303 # get up to the last given number of bytes
1304 lastbytes() { tail -c "${1:-1}" "${2:--}"; }
1305 
1306 # get the last n lines, or 1 by default
1307 lastlines() { tail -n "${1:-1}" "${2:--}"; }
1308 
1309 # turn UTF-8 into its latin-like subset, where variants of latin letters stay
1310 # as given, and where all other symbols become question marks, one question
1311 # mark for each code-point byte
1312 latinize() {
1313     iconv -f utf-8 -t latin-1//translit "$@" | iconv -f latin-1 -t utf-8
1314 }
1315 
1316 # convert pounds (LB) into kilograms
1317 lb() {
1318     echo "${@:-1}" | sed 's-_--g; s-  *-\n-g' | awk '{ print 0.45359237 * $0 }'
1319 }
1320 
1321 # convert pounds (LBS) into kilograms
1322 lbs() {
1323     echo "${@:-1}" | sed 's-_--g; s-  *-\n-g' | awk '{ print 0.45359237 * $0 }'
1324 }
1325 
1326 # convert a mix of pounds (LB) and weight-ounces (OZ) into kilograms
1327 lboz() {
1328     local lb="${1:-0}"
1329     lb="$(echo "${lb}" | sed 's-_--g')"
1330     local oz="${2:-0}"
1331     oz="$(echo "${oz}" | sed 's-_--g')"
1332     awk "BEGIN { print 0.45359237 * ${lb} + 0.028349523 * ${oz}; exit }"
1333 }
1334 
1335 # leak stdin to stderr, before also copying it to stdout: its main use is to
1336 # inspect/debug the intermediate stages of a `pipelined` shell command
1337 # leak() {
1338 #     awk '{
1339 #         gsub(/\x1b\[([0-9]*[A-HJKSThl]|[0-9;]*m)/, "")
1340 #         printf "\x1b[38;5;248m%s\x1b[0m\n", $0 > "/dev/stderr"
1341 #         print
1342 #         fflush()
1343 #     }' "$@"
1344 # }
1345 
1346 # LEAK Orange emits/tees input both to stdout and stderr, coloring orange what
1347 # it emits to stderr using an ANSI-style; this cmd is useful to `debug` pipes
1348 # involving several steps
1349 leako() {
1350     awk '{
1351         gsub(/\x1b\[([0-9]*[A-HJKSThl]|[0-9;]*m)/, "")
1352         printf "\x1b[38;5;166m%s\x1b[0m\n", $0 > "/dev/stderr"
1353         print
1354         fflush()
1355     }' "$@"
1356 }
1357 
1358 # run `less`, showing line numbers, among other settings
1359 least() { less -JMKNiCRS "$@"; }
1360 
1361 # Lines ECHO emits each argument given as its own line of output
1362 lecho() {
1363     awk 'BEGIN { for (i = 1; i < ARGC; i++) print ARGV[i]; exit }' "$@"
1364 }
1365 
1366 # limit stops at the first n bytes, or 1024 bytes by default
1367 limit() { head -c "$(echo "${1:-1024}" | sed 's-_--g')" "${2:--}"; }
1368 
1369 # Less with Header runs `less` with line numbers, ANSI styles, no line-wraps,
1370 # and using the first line as a sticky-header, so it always shows on top
1371 lh() { less --header=1 -JMKNiCRS "$@"; }
1372 
1373 # regroup adjacent lines into n-item tab-separated lines
1374 lineup() {
1375     local n="${1:-0}"
1376     [ $# -gt 0 ] && shift
1377 
1378     if [ "$n" -le 0 ]; then
1379         awk '
1380             NR > 1 { printf "\t" }
1381             { printf "%s", $0 }
1382             END { if (NR > 0) print "" }
1383         ' "$@"
1384         return $?
1385     fi
1386 
1387     awk -v n="$n" '
1388         NR % n != 1 { printf "\t" }
1389         { printf "%s", $0 }
1390         NR % n == 0 { print ""; fflush() }
1391         END { if (NR % n != 0) print "" }
1392     ' "$@"
1393 }
1394 
1395 # LOAD data from the filename or URI given
1396 load() {
1397     case "$1" in
1398         http://*|https://*|ftp://*|ftps://*|sftp://*|dict://*)
1399             [ -e "$1" ] && cat "$1" || curl -s "$1";;
1400         file://*)
1401             [ -e "$1" ] && cat "$1" || cat "$(echo "$1" | sed 's-file://--1')";;
1402         *)
1403             cat "$1";;
1404     esac 2> /dev/null || {
1405         printf "\e[31mcan't load %s\e[0m\n" "$1" >&2
1406         return 1
1407     }
1408 }
1409 
1410 # LOwercase line, check (awk) COndition: on each success, the original line
1411 # is output with its original letter-casing, as its lower-cased version is
1412 # only a convenience meant for the condition
1413 loco() {
1414     local cond="${1:-1}"
1415     [ $# -gt 0 ] && shift
1416     awk "
1417         {
1418             line = orig = original = \$0
1419             low = lower = tolower(\$0)
1420             \$0 = lower
1421         }
1422         ${cond} { print line; fflush() }
1423     " "$@"
1424 }
1425 
1426 # LOcal SERver webserves files in a folder as localhost, using the port
1427 # number given, or port 8080 by default
1428 loser() {
1429     # printf "\e[38;5;26mserving files in %s\e[0m\n" "${2:-$(pwd)}" >&2
1430     printf "\e[7mserving files in %s\e[0m\n" "${2:-$(pwd)}" >&2
1431     python3 -m http.server "${1:-8080}" -d "${2:-.}"
1432 }
1433 
1434 # LOWERcase all ASCII symbols
1435 lower() { awk '{ print tolower($0); fflush() }' "$@"; }
1436 
1437 # LOWERCASE all ASCII symbols
1438 lowercase() { awk '{ print tolower($0); fflush() }' "$@"; }
1439 
1440 # LiSt files, showing 4096-byte filesystem Page counts
1441 lsp() { ls -s --block-size=4096 "$@"; }
1442 
1443 # Listen To Youtube
1444 lty() {
1445     local url
1446     # some youtube URIs end with extra playlist/tracker parameters
1447     url="$(echo "$1" | sed 's-&.*--')"
1448     # mpv "$(yt-dlp -f 140 --get-url "${url}" 2> /dev/null)"
1449     mpv "$(yt-dlp -x --audio-format aac --get-url "${url}" 2> /dev/null)"
1450 }
1451 
1452 # Match lines with any of the regexes given
1453 m() {
1454     awk '
1455         BEGIN { for (i = 1; i < ARGC; i++) { re[i] = ARGV[i]; delete ARGV[i] } }
1456         { for (i in re) if ($0 ~ re[i]) { print; fflush(); next } }' "${@:-.}"
1457 }
1458 
1459 # only keep lines which match any of the regexes given
1460 match() {
1461     awk '
1462         BEGIN { for (i = 1; i < ARGC; i++) { re[i] = ARGV[i]; delete ARGV[i] } }
1463         { for (i in re) if ($0 ~ re[i]) { print; fflush(); next } }' "${@:-.}"
1464 }
1465 
1466 # merge stderr into stdout, without any ugly keyboard-dancing
1467 merrge() { "$@" 2>&1; }
1468 
1469 # convert MIles into kilometers
1470 mi() {
1471     echo "${@:-1}" | sed 's-_--g; s-  *-\n-g' | awk '{ print 1.609344 * $0 }'
1472 }
1473 
1474 # Make In Folder
1475 # mif() {
1476 #     pushd "${1:-.}" > /dev/null || return
1477 #     [ $# -gt 0 ] && shift
1478 #     make "$@"
1479 #     popd > /dev/null || return
1480 # }
1481 
1482 # convert MIles² (squared) into kilometers²
1483 mi2() {
1484     echo "${@:-1}" | sed 's-_--g; s-  *-\n-g' |
1485         awk '{ print 2.5899881103360 * $0 }'
1486 }
1487 
1488 # convert Miles Per Hour into kilometers per hour
1489 mph() {
1490     echo "${@:-1}" | sed 's-_--g; s-  *-\n-g' | awk '{ print 1.609344 * $0 }'
1491 }
1492 
1493 # Number all lines, starting from the number given, or 1 by default
1494 # n() {
1495 #     local n="${1:-1}"
1496 #     [ $# -gt 0 ] && shift
1497 #     awk -v n="$n" '{ printf "%d\t%s\n", NR - 1 + n, $0; fflush() }' "$@"
1498 # }
1499 
1500 # Number all lines, starting from the number given, or 1 by default
1501 # n() {
1502 #     local n="${1:-1}"
1503 #     [ $# -gt 0 ] && shift
1504 #     nl -b a -w 1 -v "$n" "$@"
1505 # }
1506 
1507 # Number all lines, using a tab right after each line number
1508 n() { nl -b a -w 1 "$@"; }
1509 
1510 # Nice Byte Count, using my scripts `nn` and `cext`
1511 nbc() { wc -c "$@" | nn | cext; }
1512 
1513 # Nice numbers CAlculator runs my script `ca` and colors results with my
1514 # script `nn`, alternating styles to make long numbers easier to read
1515 nca() { ca "$@" | nn; }
1516 
1517 # No (standard) Error ignores stderr, without any ugly keyboard-dancing
1518 ne() { "$@" 2> /dev/null; }
1519 
1520 # No (standard) ERRor ignores stderr, without any ugly keyboard-dancing
1521 nerr() { "$@" 2> /dev/null; }
1522 
1523 # Nice File Sizes, using my scripts `nn` and `cext`
1524 nfs() {
1525     # turn arg-list into single-item lines
1526     awk 'BEGIN { for (i = 1; i < ARGC; i++) print ARGV[i]; exit }' "$@" |
1527     # calculate file-sizes, and reverse-sort results
1528     xargs -d '\n' wc -c | sort -rn |
1529     # start output with a header-like line, and add a MiB field
1530     awk 'BEGIN { printf "%6s  %10s  %8s  name\n", "n", "bytes", "MiB" }
1531     { printf "%6d  %10d  %8.2f  %s\n", NR - 1, $1, $1 / 1048576, $2 }' |
1532     # make zeros in the MiB field stand out with a special color
1533     awk '{ gsub(/ 0.00 /, "\x1b[38;5;103m 0.00 \x1b[0m"); print }' |
1534     # make numbers nice, alternating styles along 3-digit groups
1535     nn |
1536     # color-code file extensions
1537     cext |
1538     # make table breathe with empty lines, so tall outputs are readable
1539     awk '(NR - 2) % 5 == 1 && NR > 1 { print "" } 1'
1540 }
1541 
1542 # Nice Hex Faster tries to speed-up my script `nh` via `pypy`
1543 nhf() { pypy3 "$(which nh)" "$@"; }
1544 
1545 # NIce numbers CAlculator runs my script `ca` and colors results with my
1546 # script `nn`, alternating styles to make long numbers easier to read
1547 nica() { ca "$@" | nn; }
1548 
1549 # emit nothing to output and/or discard everything from input
1550 nil() {
1551     if [ -p /dev/stdin ]; then
1552         cat > /dev/null
1553     else
1554         head -c 0
1555     fi
1556 }
1557 
1558 # convert Nautical MIles into kilometers
1559 nmi() {
1560     echo "${@:-1}" | sed 's-_--g; s-  *-\n-g' | awk '{ print 1.852 * $0 }'
1561 }
1562 
1563 # NO (standard) ERRor ignores stderr, without any ugly keyboard-dancing
1564 noerr() { "$@" 2> /dev/null; }
1565 
1566 # play a white-noise sound lasting the number of seconds given, or for 1
1567 # second by default; uses my script `waveout`
1568 noice() { waveout "${1:-1}" "${2:-0.05} * random()" | mpv --really-quiet -; }
1569 
1570 # play a white-noise sound lasting the number of seconds given, or for 1
1571 # second by default; uses my script `waveout`
1572 noise() { waveout "${1:-1}" "${2:-0.05} * random()" | mpv --really-quiet -; }
1573 
1574 # show the current date and time
1575 now() { date +'%Y-%m-%d %H:%M:%S'; }
1576 
1577 # Nice Size, using my scripts `nn` and `cext`
1578 ns() { wc -c "$@" | nn | cext; }
1579 
1580 # No Standard Error ignores stderr, without any ugly keyboard-dancing
1581 nse() { "$@" 2> /dev/null; }
1582 
1583 # Nice Transform Json, using my scripts `tj`, and `nj`
1584 # ntj() { tj "$@" | nj; }
1585 
1586 # Nice Transform Json, using my scripts `tj`, `nj`, and `nn`
1587 ntj() { tj "$@" | nj | nn; }
1588 
1589 # Nice numbers Word-Count runs `wc` and colors results with my script `nn`,
1590 # alternating styles to make long numbers easier to read
1591 nwc() { wc "$@" | nn; }
1592 
1593 # Nice Zoom Json, using my scripts `zj`, and `nj`
1594 # nzj() { zj "$@" | nj; }
1595 
1596 # Nice Zoom Json, using my scripts `zj`, `nj`, and `nn`
1597 nzj() { zj "$@" | nj | nn; }
1598 
1599 # unify Output by merging stderr into stdout, without any keyboard-dancing
1600 o() { "$@" 2>&1; }
1601 
1602 # Orange LEAK emits/tees input both to stdout and stderr, coloring orange what
1603 # it emits to stderr using an ANSI-style; this cmd is useful to `debug` pipes
1604 # involving several steps
1605 oleak() {
1606     awk '{
1607         gsub(/\x1b\[([0-9]*[A-HJKSThl]|[0-9;]*m)/, "")
1608         printf "\x1b[38;5;166m%s\x1b[0m\n", $0 > "/dev/stderr"
1609         print
1610         fflush()
1611     }' "$@"
1612 }
1613 
1614 # emit each unique line only the first time, ignoring all later occurrences
1615 once() { awk '!c[$0]++' "$@"; }
1616 
1617 # open/pop-up files, GUI file-explorers for folders, and even web-browsers
1618 # for any URIs given; only works on the windows subsystem for linux
1619 # open() {
1620 #     local arg
1621 #     for arg in "${@:-.}"; do
1622 #         cmd.exe /c start "$(echo "$arg" | sed 's-/$--; s-/-\\-g')"
1623 #     done | cat
1624 # }
1625 
1626 # emit each unique line only the first time, ignoring all later occurrences;
1627 # this is equivalent to an `or` operation on sets of lines
1628 or() { awk '!c[$0]++' "$@"; }
1629 
1630 # make text Plain, by ignoring ANSI terminal styling
1631 p() {
1632     awk '{
1633         # ignore notifications (code 9) and hyperlinks (code 8)
1634         gsub(/\x1b\](8|9);[^\x07]*\x07/, "")
1635         # ignore cursor-movers and style-changers
1636         gsub(/\x1b\[([0-9]*[A-HJKSThl]|[0-9;]*m)/, "")
1637 
1638         print
1639         fflush()
1640     }' "$@"
1641 }
1642 
1643 # ignore all arguments given, which is useful to quickly disable a step along
1644 # a long pipe of commands
1645 pass() { cat; }
1646 
1647 # ignore all arguments given, which is useful to quickly disable a step along
1648 # a long pipe of commands
1649 passthru() { cat; }
1650 
1651 # Paragraph AWK runs `awk` in block/paragraph/multiline input-mode
1652 pawk() { awk -F='' -v RS='' "$@"; }
1653 
1654 # Plain Interactive Grep
1655 pig() { ugrep --color=never -Q -E "$@"; }
1656 
1657 # make text plain, by ignoring ANSI terminal styling
1658 plain() {
1659     awk '{
1660         # ignore notifications (code 9) and hyperlinks (code 8)
1661         gsub(/\x1b\](8|9);[^\x07]*\x07/, "")
1662         # ignore cursor-movers and style-changers
1663         gsub(/\x1b\[([0-9]*[A-HJKSThl]|[0-9;]*m)/, "")
1664 
1665         print
1666         fflush()
1667     }' "$@"
1668 }
1669 
1670 # play audio/video media, possibly using a GUI
1671 play() { mpv "${@:--}"; }
1672 
1673 # Purple LEAK emits/tees input both to stdout and stderr, coloring purple what
1674 # it emits to stderr using an ANSI-style; this cmd is useful to `debug` pipes
1675 # involving several steps
1676 pleak() {
1677     awk '{
1678         gsub(/\x1b\[([0-9]*[A-HJKSThl]|[0-9;]*m)/, "")
1679         printf "\x1b[38;5;99m%s\x1b[0m\n", $0 > "/dev/stderr"
1680         print
1681         fflush()
1682     }' "$@"
1683 }
1684 
1685 # PLay data from standard INput
1686 plin() { mpv "$@" -; }
1687 
1688 # Paused MPV
1689 pmpv() { mpv --pause "${@:--}"; }
1690 
1691 # Print Python result
1692 pp() { python -c "print($1)"; }
1693 
1694 # PRint AWK result
1695 # prawk() { awk "BEGIN { print ${1:-0}; exit }"; }
1696 
1697 # PRint AWK result
1698 # prawk() { awk --bignum "BEGIN { print ${1:-0}; exit }"; }
1699 
1700 # start by joining all arguments given as a tab-separated-items line of output,
1701 # followed by all lines from stdin verbatim
1702 pretsv() {
1703     awk 'BEGIN {
1704         for (i = 1; i < ARGC; i++) {
1705             if (i > 1) { printf "\t" }
1706             printf "%s", ARGV[i]
1707             delete ARGV[i]
1708         }
1709         printf "\n"
1710     }
1711     { print; fflush() }' "$@"
1712 }
1713 
1714 # Plain Recursive Interactive Grep
1715 prig() { ugrep --color=never -r -Q -E "$@"; }
1716 
1717 # Print RUBY expression
1718 # pruby() { ruby -e "puts ${1:-nil}"; }
1719 
1720 # PRint pYTHON result
1721 # prython() { python -c "print(${1:-None})"; }
1722 
1723 # Play Youtube Audio
1724 pya() {
1725     local url
1726     # some youtube URIs end with extra playlist/tracker parameters
1727     url="$(echo "$1" | sed 's-&.*--')"
1728     # mpv "$(yt-dlp -f 140 --get-url "${url}" 2> /dev/null)"
1729     mpv "$(yt-dlp -x --audio-format aac --get-url "${url}" 2> /dev/null)"
1730 }
1731 
1732 # Quiet ignores stderr, without any ugly keyboard-dancing
1733 q() { "$@" 2> /dev/null; }
1734 
1735 # Quiet MPV
1736 qmpv() { mpv --quiet "${@:--}"; }
1737 
1738 # ignore stderr, without any ugly keyboard-dancing
1739 quiet() { "$@" 2> /dev/null; }
1740 
1741 # Quiet cURL
1742 qurl() { curl -s "$@"; }
1743 
1744 # Reset the screen, which empties it and resets the current style
1745 r() { reset; }
1746 
1747 # keep only lines between the 2 line numbers given, inclusively
1748 rangelines() {
1749     { [ "$#" -eq 2 ] || [ "$#" -eq 3 ]; } && [ "${1}" -le "${2}" ] &&
1750         { tail -n +"${1:-1}" "${3:--}" | head -n "$(("${2}" - "${1}" + 1))"; }
1751 }
1752 
1753 # RANdom MANual page
1754 ranman() {
1755     find "/usr/share/man/man${1:-1}" -type f | shuf -n 1 | xargs basename |
1756         sed 's-\.gz$--g' | xargs man
1757 }
1758 
1759 # play a ready-phone-line sound lasting the number of seconds given, or for 1
1760 # second by default; uses my script `waveout`
1761 ready() {
1762     local f='0.5 * sin(350*tau*t) + 0.5 * sin(450*tau*t)'
1763     waveout "${1:-1}" "${2:-1} * $f" | mpv --really-quiet -
1764 }
1765 
1766 # emit a line using the symbol given, REPeated a number of times (1 by default)
1767 rep() {
1768     local what="${1}"
1769     local times="${2:-1}"
1770     [ "${times}" -gt 0 ] && printf "%${times}s\n" " " |
1771         if [ "${what}" = ' ' ]; then
1772             cat
1773         elif [ "${what}" = '-' ]; then
1774             sed "s/ /${what}/g"
1775         else
1776             sed "s- -${what}-g"
1777         fi
1778 }
1779 
1780 # reflow/trim lines of prose (text) to improve its legibility: it's especially
1781 # useful when the text is pasted from web-pages being viewed in reader mode
1782 reprose() {
1783     local w="${1:-80}"
1784     [ $# -gt 0 ] && shift
1785     awk 'FNR == 1 && NR > 1 { print "" } { print; fflush() }' "$@" |
1786         fold -s -w "$w" | sed -E 's- *\r?$--'
1787 }
1788 
1789 # change color/style of lines, using my script `tl`
1790 restyle() {
1791     local style="$1"
1792     [ $# -gt 0 ] && shift
1793     tl "${style}(plain(l))" "$@"
1794 }
1795 
1796 # play a ringtone-style sound lasting the number of seconds given, or for 1
1797 # second by default; uses my script `waveout`
1798 ring() {
1799     local f='sin(2048 * tau * t) * exp(-50 * (t%0.1))'
1800     waveout "${1:-1}" "${2:-1} * $f" | mpv --really-quiet -
1801 }
1802 
1803 # play a ringtone-style sound lasting the number of seconds given, or for 1
1804 # second by default; uses my script `waveout`
1805 ringtone() {
1806     local f='sin(2048 * tau * t) * exp(-50 * (t%0.1))'
1807     waveout "${1:-1}" "${2:-1} * $f" | mpv --really-quiet -
1808 }
1809 
1810 # Read-Only Micro (text editor)
1811 rom() { micro -readonly true "$@"; }
1812 
1813 # Right STRIP ignores trailing spaces, as well as trailing carriage returns
1814 rstrip() { awk 1 "$@" | sed -E 's- *\r?$--'; }
1815 
1816 # Right TRIM ignores trailing spaces, as well as trailing carriage returns
1817 rtrim() { awk 1 "$@" | sed -E 's- *\r?$--'; }
1818 
1819 # show a RULER-like width-measuring line
1820 ruler() {
1821     [ "${1:-80}" -gt 0 ] && printf "%${1:-80}s\n" " " | sed -E \
1822         's- {5}-···· -g; s/(···· ){2}/····╵····│/g; s- -·-g; s-·{5}-····╵-'
1823 }
1824 
1825 # RUN a command, after showing/anNOUNCE-ing it
1826 runnounce() { printf "\e[7m%s\e[0m\n" "$*" && "$@"; }
1827 
1828 # run `sed` in extended mode, enabling its full regex syntax
1829 s() { sed -E -u "$@"; }
1830 
1831 # voice-synthesize plain-text; only works on the windows subsystem for linux
1832 # say() {
1833 #     awk 1 "$@" | powershell.exe -noprofile -command '
1834 # Add-Type -AssemblyName System.Speech
1835 # $syn = New-Object -TypeName System.Speech.Synthesis.SpeechSynthesizer
1836 #
1837 # foreach ($line in $Input) {
1838 #     $syn.Speak($line)
1839 # }' | cat
1840 # }
1841 
1842 # Silent CURL
1843 scurl() { curl -s "$@"; }
1844 
1845 # show a unique-looking SEParator line; useful to run between commands
1846 # which output walls of text
1847 sep() {
1848     [ "${1:-80}" -gt 0 ] &&
1849         printf "\e[48;5;253m%${1:-80}s\e[0m\n" " " | sed 's- -·-g'
1850 }
1851 
1852 # webSERVE files in a folder as localhost, using the port number given, or
1853 # port 8080 by default
1854 serve() {
1855     # printf "\e[38;5;26mserving files in %s\e[0m\n" "${2:-$(pwd)}" >&2
1856     printf "\e[7mserving files in %s\e[0m\n" "${2:-$(pwd)}" >&2
1857     python3 -m http.server "${1:-8080}" -d "${2:-.}"
1858 }
1859 
1860 # SET DIFference
1861 setdif() {
1862     awk '
1863         FNR == 1 { filenum++ }
1864 
1865         {
1866             if (filenum == 1) {
1867                 lines[++n] = $0
1868             } else {
1869                 avoid[$0]++
1870             }
1871         }
1872 
1873         END {
1874             for (i = 1; i <= n; i++) {
1875                 l = lines[i]
1876                 if (avoid[l] == 0) {
1877                     avoid[l] = 1
1878                     print l
1879                 }
1880             }
1881         }' "$@"
1882 }
1883 
1884 # SET DIFFerence sorts its 2 inputs, then finds lines not in the 2nd input
1885 setdiff() {
1886     # comm -23 <(sort "$1") <(sort "$2")
1887     # dash doesn't support the process-sub syntax
1888     (sort "$1" | (sort "$2" | (comm -23 /dev/fd/3 /dev/fd/4) 4<&0) 3<&0)
1889 }
1890 
1891 # SET INtersection, sorts its 2 inputs, then finds common lines
1892 setin() {
1893     # comm -12 <(sort "$1") <(sort "$2")
1894     # dash doesn't support the process-sub syntax
1895     (sort "$1" | (sort "$2" | (comm -12 /dev/fd/3 /dev/fd/4) 4<&0) 3<&0)
1896 }
1897 
1898 # SET SUBtraction sorts its 2 inputs, then finds lines not in the 2nd input
1899 setsub() {
1900     # comm -23 <(sort "$1") <(sort "$2")
1901     # dash doesn't support the process-sub syntax
1902     (sort "$1" | (sort "$2" | (comm -23 /dev/fd/3 /dev/fd/4) 4<&0) 3<&0)
1903 }
1904 
1905 # Show Files (and folders), coloring folders and links
1906 sf() {
1907     ls -al --file-type --color=never --time-style iso "$@" | awk '
1908         /^d/ { printf "\x1b[38;5;33m%s\x1b[0m\n", $0; next }
1909         /^l/ { printf "\x1b[38;5;29m%s\x1b[0m\n", $0; next }
1910         1'
1911 }
1912 
1913 # SHOW a command, then RUN it
1914 showrun() { printf "\e[7m%s\e[0m\n" "$*" && "$@"; }
1915 
1916 # start from the line number given, skipping all previous ones
1917 sinceline() { tail -n +"${1:-1}" "${2:--}"; }
1918 
1919 # skip the first n lines, or the 1st line by default
1920 skip() { tail -n +$(("${1:-1}" + 1)) "${2:--}"; }
1921 
1922 # skip the first n bytes
1923 skipbytes() { tail -c +$(("$1" + 1)) "${2:--}"; }
1924 
1925 # skip the last n lines, or the last line by default
1926 skiplast() { head -n -"${1:-1}" "${2:--}"; }
1927 
1928 # skip the last n bytes
1929 skiplastbytes() { head -c -"$1" "${2:--}"; }
1930 
1931 # skip the first n lines, or the 1st line by default
1932 skiplines() { tail -n +$(("${1:-1}" + 1)) "${2:--}"; }
1933 
1934 # Styled LEAK runs my script `leak`
1935 sleak() { leak "$@"; }
1936 
1937 # Show Latest Podcasts, using my scripts `podfeed` and `si`
1938 slp() {
1939     local title
1940     title="Latest Podcast Episodes as of $(date +'%F %T')"
1941     podfeed -title "${title}" "$@" | si
1942 }
1943 
1944 # recursively find all files with fewer bytes than the number given
1945 smallfiles() { find "${2:-.}" -size -"$(echo "${1}" | sed 's-_--g')"c; }
1946 
1947 # emit the first line as is, sorting all lines after that, using the
1948 # `sort` command, passing all/any arguments/options to it
1949 sortrest() {
1950     awk -v sort="sort $*" 'NR == 1 { print; fflush() } NR > 1 { print | sort }'
1951 }
1952 
1953 # ignore leading spaces, trailing spaces, even runs of multiple spaces
1954 # in the middle of lines, as well as trailing carriage returns
1955 squeeze() {
1956     awk 'FNR == 1 { gsub(/^\xef\xbb\xbf/, "") } { print; fflush() }' "$@" |
1957         sed -E 's-^ +--; s-  +- -g; s- *\t *-\t-g; s- *\r?$--'
1958 }
1959 
1960 # Show a command, then Run it
1961 sr() { printf "\e[7m%s\e[0m\n" "$*" && "$@"; }
1962 
1963 # turn lines of Space-Separated Values into lines of Tab-Separated Values
1964 ssv2tsv() { awk 1 "$@" | sed -E 's-^ +--; s- *\r?--; s- +-\t-g'; }
1965 
1966 # change color/style of lines, using my script `tl`
1967 style() {
1968     local style="$1"
1969     [ $# -gt 0 ] && shift
1970     tl "${style}(plain(l))" "$@"
1971 }
1972 
1973 # show a random command defined in `clam`, using `wat` from `clam` itself
1974 surprise() {
1975     wat "$(grep -E '^[a-z]+\(' "$(which clam)" | shuf -n 1 | sed -E 's-\(.*--')"
1976 }
1977 
1978 # show a reverse-sorted tally of all lines read, where ties are sorted
1979 # alphabetically
1980 tally() {
1981     printf "value\ttally\n"
1982     awk '
1983         { t[$0]++ }
1984         END { for (k in t) printf "%s\t%d\n", k, t[k] }
1985     ' "$@" | sort -t "$(printf "\t")" -rnk2 -k1d
1986 }
1987 
1988 # Tab AWK: TSV-specific I/O settings for `awk`
1989 tawk() { awk -F "\t" -v OFS="\t" "$@"; }
1990 
1991 # timestamp all lines, as soon as they come from the standard input
1992 timestamp() {
1993     ts '%Y-%m-%d %H:%M:%S' |
1994         sed -u 's-^-\x1b[48;5;255m\x1b[38;5;24m-; s- -\x1b[0m\t-2'
1995 }
1996 
1997 title() {
1998     local title="${1:-no title given}"
1999     [ $# -gt 0 ] && shift
2000     printf "\x1b[7m%s\x1b[0m\n", "${title}"
2001     awk 1 "$@"
2002 }
2003 
2004 # Transform Json, using an interactive Editor for the formula/expression
2005 tje() {
2006     case "$1" in
2007         =|-nil|--nil|-none|--none|-null|--null)
2008             [ $# -gt 0 ] && shift
2009             tj -nil "$(micro -readonly true -filetype python | leak --inv)" "$@"
2010         ;;
2011         *)
2012             tj "$(micro -readonly true -filetype python | leak --inv)" "$@";;
2013     esac
2014 }
2015 
2016 # Transform Json Faster, by using pypy instead of standard python
2017 tjf() { pypy3 "$(which tj)" "$@"; }
2018 
2019 # Transform Json (Node), using an interactive Editor for the expression
2020 tjne() {
2021     case "$1" in
2022         =|-nil|--nil|-none|--none|-null|--null)
2023             [ $# -gt 0 ] && shift
2024             tjn -nil "$(micro -readonly true -filetype js | leak --inv)" "$@"
2025         ;;
2026         *)
2027             tjn "$(micro -readonly true -filetype js | leak --inv)" "$@";;
2028     esac
2029 }
2030 
2031 # Transform Lines, using an interactive Editor for the formula/expression
2032 tle() {
2033     case "$1" in
2034         =|-nil|--nil|-none|--none|-null|--null)
2035             [ $# -gt 0 ] && shift
2036             tl -nil "$(micro -readonly true -filetype python | leak --inv)" "$@"
2037         ;;
2038         *)
2039             tl "$(micro -readonly true -filetype python | leak --inv)" "$@";;
2040     esac
2041 }
2042 
2043 # Transform Lines Faster, by using pypy instead of standard python
2044 tlf() { pypy3 "$(which tl)" "$@"; }
2045 
2046 # Transform Lines (Node), using the Micro editor for the formula/expression
2047 tlne() {
2048     case "$1" in
2049         =|-nil|--nil|-none|--none|-null|--null)
2050             [ $# -gt 0 ] && shift
2051             tln -nil "$(micro -readonly true -filetype js | leak --inv)" "$@"
2052         ;;
2053         *)
2054             tln "$(micro -readonly true -filetype js | leak --inv)" "$@";;
2055     esac
2056 }
2057 
2058 # show current date in a specifc format, which is both people-friendly
2059 # and machine/tool/search/automation-friendly
2060 today() { date +'%Y-%m-%d %a %b %d'; }
2061 
2062 # get the first n lines, or 1 by default
2063 toline() { head -n "${1:-1}" "${2:--}"; }
2064 
2065 # lowercase all ASCII symbols
2066 tolower() { awk '{ print tolower($0); fflush() }' "$@"; }
2067 
2068 # show all files directly in the folder given, without looking any deeper
2069 topfiles() {
2070     local arg
2071     for arg in "${@:-.}"; do
2072         find "${arg}" -maxdepth 1 -type f
2073     done
2074 }
2075 
2076 # show all folders directly in the folder given, without looking any deeper
2077 topfolders() {
2078     local arg
2079     for arg in "${@:-.}"; do
2080         find "${arg}" -maxdepth 1 -type d | awk 'NR > 1'
2081     done
2082 }
2083 
2084 # uppercase all ASCII symbols
2085 toupper() { awk '{ print toupper($0); fflush() }' "$@"; }
2086 
2087 # ignore leading/trailing spaces, as well as trailing carriage returns
2088 trim() { awk 1 "$@" | sed -E 's-^ +--; s- *\r?$--'; }
2089 
2090 # ignore trailing spaces, as well as trailing carriage returns
2091 trimend() { awk 1 "$@" | sed -E 's- *\r?$--'; }
2092 
2093 # ignore trailing spaces, as well as trailing carriage returns
2094 trimends() { awk 1 "$@" | sed -E 's- *\r?$--'; }
2095 
2096 # ignore trailing spaces, as well as trailing carriage returns
2097 trimtrail() { awk 1 "$@" | sed -E 's- *\r?$--'; }
2098 
2099 # ignore trailing spaces, as well as trailing carriage returns
2100 trimtrails() { awk 1 "$@" | sed -E 's- *\r?$--'; }
2101 
2102 # try running a command, emitting an explicit message to standard-error
2103 # if the command given fails
2104 try() {
2105     "$@" || {
2106         printf "\n\e[31mrunning \e[41m\e[97m %s \e[0m\e[31m failed\e[0m\n" \
2107             "$*" >&2
2108         return 255
2109     }
2110 }
2111 
2112 # TimeStamp lines satisfying an AWK condition, ignoring all other lines
2113 tsawk() {
2114     awk -v line="\x1b[48;5;255m\x1b[38;5;24m%s\x1b[0m\t%s\n" \
2115         -v time="%Y-%m-%d %H:%M:%S" \
2116         "${1:-1} { printf line, strftime(time), \$0; fflush() }"
2117 }
2118 
2119 # run my script `realign` using tab as the only field-separator,
2120 # which means the resulting TSV items can have spaces in them
2121 tsv2ssv() { realign --tsv "$@"; }
2122 
2123 # TSV lines DEDUPlicated by the field given, keeping their original order
2124 tsvdedup() {
2125     local n="${1:-0}"
2126     [ $# -gt 0 ] && shift
2127     awk -F "\t" "!c[(\$${n} >= 0) ? \$${n} : \$(NF + ${n} - 1)]++" "$@"
2128 }
2129 
2130 # Unique deduplicates lines, keeping them in their original order
2131 u() { awk '!c[$0]++' "$@"; }
2132 
2133 # UNdo (decode) BASE-64-encoding of data
2134 unbase64() { base64 -d "$@"; }
2135 
2136 # convert away from CSV tables, using my script `decsv`
2137 uncsv() { decsv "$@"; }
2138 
2139 # UNcompress GZip-encoded data
2140 ungz() { zcat "$@"; }
2141 
2142 # UNcompress GZIP-encoded data
2143 ungzip() { zcat "$@"; }
2144 
2145 # deduplicate lines, keeping them in their original order
2146 unique() { awk '!c[$0]++' "$@"; }
2147 
2148 # concatenate all named input sources unix-style: all trailing CRLFs become
2149 # single LFs, each non-empty input will always end in a LF, so lines from
2150 # different sources are accidentally joined; also leading UTF-8 BOMs on the
2151 # first line of each input are ignored, as those are useless at best
2152 unixify() {
2153     awk '
2154         FNR == 1 { gsub(/^\xef\xbb\xbf/, "") }
2155         { print; fflush() }' "$@" | sed -E 's-\r$--'
2156 }
2157 
2158 # turn json lines into a proper json array
2159 unjsonl() { jq -s -M "${@:-.}"; }
2160 
2161 # turn lines of Space-Separated Values into lines of Tab-Separated Values
2162 unssv() { awk 1 "$@" | sed -E 's-^ +--; s- *\r?--; s- +-\t-g'; }
2163 
2164 # convert away from TSV tables, using my script `detsv`
2165 untsv() { detsv "$@"; }
2166 
2167 # turn UTF-16 data into UTF-8
2168 unutf16() { iconv -f utf16 -t utf8 "$@"; }
2169 
2170 # Unify Output by merging stderr into stdout, without any keyboard-dancing
2171 uo() { "$@" 2>&1; }
2172 
2173 # go UP n folders, or go up 1 folder by default
2174 up() {
2175     if [ "${1:-1}" -le 0 ]; then
2176         cd .
2177         return $?
2178     fi
2179 
2180     cd "$(printf "%${1:-1}s" " " | sed 's- -../-g')" || return $?
2181 }
2182 
2183 # UPPERcase all ASCII symbols
2184 upper() { awk '{ print toupper($0); fflush() }' "$@"; }
2185 
2186 # UPPERCASE all ASCII symbols
2187 uppercase() { awk '{ print toupper($0); fflush() }' "$@"; }
2188 
2189 # View with `less`
2190 v() { less -JMKiCRS "$@"; }
2191 
2192 # run a command, showing its success/failure right after
2193 verdict() {
2194     local code
2195 
2196     "$@"
2197     code=$?
2198 
2199     if [ "${code}" -eq 0 ]; then
2200         printf "\n\e[38;5;29m%s \e[48;5;29m\e[97m succeeded \e[0m\n" "$*" >&2
2201     else
2202         printf "\n\e[31m%s \e[41m\e[97m failed with error code %d \e[0m\n" \
2203             "$*" "${code}" >&2
2204     fi
2205 
2206     return "${code}"
2207 }
2208 
2209 # View with Header runs `less` without line numbers, with ANSI styles, with
2210 # no line-wrapping, and using the first line as a sticky-header, so it always
2211 # shows on top
2212 vh() { less --header=1 -JMKiCRS "$@"; }
2213 
2214 # View Nice Hexadecimals; uses my script `nh`
2215 vnh() { nh "$@" | less -JMKiCRS; }
2216 
2217 # View Nice Json / Very Nice Json; uses my scripts `nj` and `nn`
2218 vnj() { nj "$@" | nn | less -JMKiCRS; }
2219 
2220 # View Nice Table / Very Nice Table; uses my scripts `nt` and `nn`
2221 vnt() {
2222     awk 1 "$@" | nl -b a -w 1 -v 0 | nt | nn |
2223         awk '(NR - 1) % 5 == 1 && NR > 1 { print "" } { print; flush() }' |
2224         less -JMKiCRS
2225 }
2226 
2227 # turn UTF-8 into its latin-like subset, where variants of latin letters stay
2228 # as given, and where all other symbols become question marks, one question
2229 # mark for each code-point byte; the name comes from `vulgarization`, which
2230 # refers to the mutation of languages away from latin, during the middle ages
2231 vulgarize() {
2232     iconv -f utf-8 -t latin-1//translit "$@" | iconv -f latin-1 -t utf-8
2233 }
2234 
2235 # View Zoom Json, using my scripts `zj`, `nj`, and `nn`
2236 vzj() { zj "$@" | nj | nn | less -JMKiCRS; }
2237 
2238 # What Are These (?) shows what the names given to it are/do
2239 wat() {
2240     local a
2241     local code=0
2242 
2243     for a in "$@"; do
2244         # printf "\e[48;5;253m\e[38;5;26m%-80s\e[0m\n" "$a"
2245         printf "\e[48;5;253m%-80s\e[0m\n" "$a"
2246         # printf "\e[7m%-80s\e[0m\n" "$a"
2247 
2248         # resolve 1 alias level
2249         if alias "$a" 2> /dev/null > /dev/null; then
2250             a="$(alias "$a" | sed "s-.*=--; s-['\"]--g")"
2251         fi
2252 
2253         if echo "$a" | grep -E '[^ ]+ +[^ ]+' > /dev/null; then
2254             # resolved aliases with args/spaces in them would otherwise fail
2255             echo "$a"
2256         elif type "$a" > /dev/null 2> /dev/null; then
2257             # dash doesn't support `declare`, and `type` in bash/zsh emits
2258             # a redundant first output line, when it's a shell function
2259             type "$a" | awk '
2260                 NR == 1 && /^[a-z0-9_-]+ is a function$/ { skipped = $0; next }
2261                 1
2262                 END { if (NR < 2 && skipped) print skipped }
2263             '
2264         else
2265             printf "\e[31m%s not found\e[0m\n" "$a"
2266             code=1
2267         fi
2268     done
2269 
2270     return "${code}"
2271 }
2272 
2273 # Word-Count with Nice numbers runs `wc` and colors results with my script
2274 # `nn`, alternating styles to make long numbers easier to read
2275 wcn() { wc "$@" | nn; }
2276 
2277 # Word-Count Plus runs `wc` and enriches its output; uses my scripts `nn`
2278 # and `cext`
2279 wcp() {
2280     wc "$@" | sort -rn | nn | cext |
2281         awk '{ printf "%6d  %s\n", NR - 1, $0; fflush() }'
2282 }
2283 
2284 # get weather forecasts, almost filling the terminal's current width
2285 weather() {
2286     finger "${*}~$(($(tput cols) - 2))@graph.no" |
2287     sed -E 's/-/@/g; s/^ +@=/ -=/; s/=@ *$/=-/' |
2288     grep -v '^\['
2289 }
2290 
2291 # WHAT are these (?) shows what the names given to it are/do
2292 what() {
2293     local a
2294     local code=0
2295 
2296     for a in "$@"; do
2297         # printf "\e[48;5;253m\e[38;5;26m%-80s\e[0m\n" "$a"
2298         printf "\e[48;5;253m%-80s\e[0m\n" "$a"
2299         # printf "\e[7m%-80s\e[0m\n" "$a"
2300 
2301         # resolve 1 alias level
2302         if alias "$a" 2> /dev/null > /dev/null; then
2303             a="$(alias "$a" | sed "s-.*=--; s-['\"]--g")"
2304         fi
2305 
2306         if echo "$a" | grep -E '[^ ]+ +[^ ]+' > /dev/null; then
2307             # resolved aliases with args/spaces in them would otherwise fail
2308             echo "$a"
2309         elif type "$a" > /dev/null 2> /dev/null; then
2310             # dash doesn't support `declare`, and `type` in bash/zsh emits
2311             # a redundant first output line, when it's a shell function
2312             type "$a" | awk '
2313                 NR == 1 && /^[a-z0-9_-]+ is a function$/ { skipped = $0; next }
2314                 1
2315                 END { if (NR < 2 && skipped) print skipped }
2316             '
2317         else
2318             printf "\e[31m%s not found\e[0m\n" "$a"
2319             code=1
2320         fi
2321     done
2322 
2323     return "${code}"
2324 }
2325 
2326 # recursively find all files with trailing spaces/CRs
2327 wheretrails() { rg -c '[ \r]+$' "${@:-.}"; }
2328 
2329 # recursively find all files with trailing spaces/CRs
2330 whichtrails() { rg -c '[ \r]+$' "${@:-.}"; }
2331 
2332 # What Is This (?) shows what the names given to it are/do
2333 wit() {
2334     local a
2335     local code=0
2336 
2337     for a in "$@"; do
2338         # printf "\e[48;5;253m\e[38;5;26m%-80s\e[0m\n" "$a"
2339         printf "\e[48;5;253m%-80s\e[0m\n" "$a"
2340         # printf "\e[7m%-80s\e[0m\n" "$a"
2341 
2342         # resolve 1 alias level
2343         if alias "$a" 2> /dev/null > /dev/null; then
2344             a="$(alias "$a" | sed "s-.*=--; s-['\"]--g")"
2345         fi
2346 
2347         if echo "$a" | grep -E '[^ ]+ +[^ ]+' > /dev/null; then
2348             # resolved aliases with args/spaces in them would otherwise fail
2349             echo "$a"
2350         elif type "$a" > /dev/null 2> /dev/null; then
2351             # dash doesn't support `declare`, and `type` in bash/zsh emits
2352             # a redundant first output line, when it's a shell function
2353             type "$a" | awk '
2354                 NR == 1 && /^[a-z0-9_-]+ is a function$/ { skipped = $0; next }
2355                 1
2356                 END { if (NR < 2 && skipped) print skipped }
2357             '
2358         else
2359             printf "\e[31m%s not found\e[0m\n" "$a"
2360             code=1
2361         fi
2362     done
2363 
2364     return "${code}"
2365 }
2366 
2367 # emit each word-like item from each input line on its own line
2368 words() { awk '{ for (i = 1; i <= NF; i++) print $i; fflush() }' "$@"; }
2369 
2370 # Where Trails recursively find all files with trailing spaces/CRs
2371 wt() { rg -c '[ \r]+$' "${@:-.}"; }
2372 
2373 # run `xargs`, using zero/null bytes as the extra-arguments terminator
2374 x0() { xargs -0 "$@"; }
2375 
2376 # run `xargs`, using whole lines as extra arguments
2377 xl() { xargs -d '\n' "$@"; }
2378 
2379 # Youtube Audio Player
2380 yap() {
2381     local url
2382     # some youtube URIs end with extra playlist/tracker parameters
2383     url="$(echo "$1" | sed 's-&.*--')"
2384     # mpv "$(yt-dlp -f 140 --get-url "${url}" 2> /dev/null)"
2385     mpv "$(yt-dlp -x --audio-format aac --get-url "${url}" 2> /dev/null)"
2386 }
2387 
2388 # Youtube Download
2389 yd() { yt-dlp "$@"; }
2390 
2391 # Youtube Download AAC audio
2392 ydaac() { yt-dlp -f 140 "$@"; }
2393 
2394 # Youtube Download MP4 video
2395 ydmp4() { yt-dlp -f 22 "$@"; }
2396 
2397 # show a calendar for the current year, or for the year given
2398 year() {
2399     # debian linux has a different `cal` app which highlights the day
2400     if [ -e "/usr/bin/ncal" ]; then
2401         ncal -C -y "$@"
2402     else
2403         cal -y "$@"
2404     fi
2405 }
2406 
2407 # show the current date in the YYYY-MM-DD format
2408 ymd() { date +'%Y-%m-%d'; }
2409 
2410 # Zoom Json Nice, using my scripts `zj` and `nj`
2411 # zjn() { zj "$@" | nj; }
2412 
2413 # Zoom Json Nice, using my scripts `zj`, `nj`, and `nn`
2414 zjn() { zj "$@" | nj | nn; }