File: clam.sh
   1 #!/bin/sh
   2 
   3 # The MIT License (MIT)
   4 #
   5 # Copyright © 2024 pacman64
   6 #
   7 # Permission is hereby granted, free of charge, to any person obtaining a copy
   8 # of this software and associated documentation files (the “Software”), to deal
   9 # in the Software without restriction, including without limitation the rights
  10 # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  11 # copies of the Software, and to permit persons to whom the Software is
  12 # furnished to do so, subject to the following conditions:
  13 #
  14 # The above copyright notice and this permission notice shall be included in
  15 # all copies or substantial portions of the Software.
  16 #
  17 # THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  18 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  19 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  20 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  21 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  22 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  23 # SOFTWARE.
  24 
  25 
  26 # clam
  27 # Command-Line Augmentation Module: get the best out of your shell
  28 #
  29 # This is a collection of arguably useful shell functions and shortcuts,
  30 # beyond the ones defined in `shshsh` (SHort SHell SHortcuts): some of
  31 # these extra commands can be real time/effort savers, ideally letting
  32 # you concentrate on getting things done.
  33 #
  34 # Some of these commands depend on my other scripts from the `pac-tools`,
  35 # others either rely on widely-preinstalled command-line apps, or ones
  36 # which are available on most command-line `package` managers.
  37 #
  38 # You're supposed to `source` this script, so its definitions stay for
  39 # your whole shell session: for that, you can run `source clam` or
  40 # `. clam` (no quotes either way), either directly or at shell startup.
  41 #
  42 #
  43 # Partial list of funcs/commands added
  44 #
  45 # args      emit each argument given to it on its own line
  46 # blawk     process BLocks of non-empty lines with AWK
  47 # can       CAlculate with Nice numbers, using my scripts `ca` and `nn`
  48 # coco      COunt COndition, uses an AWK condition as its first arg
  49 # cu        Change Units, using my scripts `bu` and `nn`
  50 # hawk      Highlight lines matching the AWK condition given
  51 # lab       Like A Book, shows lines the way books do; uses my script `book`
  52 # largs     run `xargs` taking the extra arguments from whole stdin lines
  53 # loco      LOwercase line, check (awk) COndition
  54 # loser     LOcal SERver webserves files in a folder as localhost
  55 # lineup    regroup adjacent lines into n-item tab-separated lines
  56 # merrge    merge stderr into stdout, without any ugly keyboard-dancing
  57 # n         Number all lines, starting from the number given, or 1 by default
  58 # nfs       Nice File Sizes, using my scripts `nn` and `cext`
  59 # noerr     ignore stderr, without any ugly finger-dancing
  60 # plain     ignore all ANSI styles
  61 # restyle   change color/style of lines, using my script `ecoli`
  62 # sf        Show Files (and folders), using colors
  63 # skip      skip the first n lines, or the first 1 by default
  64 # skiplast  skip the last n lines, or the last 1 by default
  65 # slp       Show Latest Podcasts, using my scripts `podfeed` and `si`
  66 # style     change color/style of lines, using my script `ecoli`
  67 # surprise  show a random command defined in `shshsh` or `clam`, using `wat`
  68 # tawk      Tab AWK, runs AWK using tab as its IO item-separator
  69 # today     show current date in a way friendly both to people and tools
  70 # tsawk     TimeStamp lines satisfying AWK condition, ignoring the rest
  71 # tsv2ssv   run my script `realign`, using TSV-input settings
  72 # unixify   ensure plain-text lines are unix-like
  73 # verdict   run a command, showing its success/failure right after
  74 # wcp       Word-Count Plus runs `wc`, enriching its output using my tools
  75 # year      show a calendar for the current year, or for the year given
  76 
  77 
  78 # handle help options
  79 case "$1" in
  80     -h|--h|-help|--help)
  81         # show help message, extracting the info-comment at the start
  82         # of this file, and quit
  83         awk '/^# +clam/, /^$/ { gsub(/^# ?/, ""); print }' "$0"
  84         exit 0
  85     ;;
  86 esac
  87 
  88 
  89 # use a simple shell prompt
  90 # PS1="\$ "
  91 # PS2="> "
  92 
  93 # use a simple shell prompt, showing the current folder in the title
  94 # PS1="\[\e]0;\w\a\]\$ "
  95 # PS2="> "
  96 
  97 # use a simple shell prompt, showing the current folder both in the prompt
  98 # itself, and in the title
  99 # PS1="\[\e]0;\w\a\]\w\$ "
 100 # PS2="> "
 101 
 102 # prevent `less` from saving commands
 103 # LESSHISTFILE="-"
 104 # LESSSECURE=1
 105 
 106 # prevent the shell from saving commands
 107 # unset HISTFILE
 108 
 109 
 110 # aliases for external scripts from my `pac-tools`
 111 alias json0='j0'
 112 alias uncsv='decsv'
 113 alias untsv='detsv'
 114 
 115 
 116 # column-layout shortcuts, using my script `sbs` (Side By Side); the `c` in
 117 # the names stands for `columns`
 118 # 2c() { sbs 2 "$@"; }
 119 # 3c() { sbs 3 "$@"; }
 120 # 4c() { sbs 4 "$@"; }
 121 # 5c() { sbs 5 "$@"; }
 122 # 6c() { sbs 6 "$@"; }
 123 # 7c() { sbs 7 "$@"; }
 124 # 8c() { sbs 8 "$@"; }
 125 # 9c() { sbs 9 "$@"; }
 126 
 127 # shortcuts for my script `ca`, an arbitrary-precision calculator, using
 128 # various decimal precisions
 129 ca10() { ca "scale=10; $*"; }
 130 ca20() { ca "scale=20; $*"; }
 131 ca30() { ca "scale=30; $*"; }
 132 ca40() { ca "scale=40; $*"; }
 133 ca50() { ca "scale=50; $*"; }
 134 ca60() { ca "scale=60; $*"; }
 135 ca70() { ca "scale=70; $*"; }
 136 ca80() { ca "scale=80; $*"; }
 137 ca90() { ca "scale=90; $*"; }
 138 
 139 # run `awk`
 140 a() {
 141     awk "$@"
 142 }
 143 
 144 # emit each argument given as its own line of output
 145 args() {
 146     awk 'BEGIN { for (i = 1; i < ARGC; i++) print ARGV[i]; exit }' "$@"
 147 }
 148 
 149 # emit a colored bar which can help visually separate different outputs
 150 bar() {
 151     printf "\x1b[48;5;253m%${1:-80}s\x1b[0m\n" " "
 152 }
 153 
 154 # Breathe Header: add an empty line after the first one (the header), then
 155 # separate groups of 5 lines (by default) with empty lines between them
 156 bh() {
 157     local n="${1:-5}"
 158     shift
 159     awk -v n="$n" '(NR - 1) % n == 1 && NR > 1 { print "" } 1' "$@"
 160 }
 161 
 162 # Breathe Lines: separate groups of 5 lines (by default) with empty lines
 163 bl() {
 164     local n="${1:-5}"
 165     shift
 166     awk -v n="$n" 'NR % n == 1 && NR != 1 { print "" } 1' "$@"
 167 }
 168 
 169 # process BLocks of non-empty lines with AWK
 170 blawk() {
 171     awk -F='' -v RS='' "$@"
 172 }
 173 
 174 # show a reverse-sorted tally of all lines read, where ties are sorted
 175 # alphabetically, and where trailing bullets are added to quickly make
 176 # the tally counts comparable at a glance
 177 bully() {
 178     printf "value\ttally\tbullets\n"
 179     awk '
 180         { tally[$0]++ }
 181 
 182         END {
 183             # find the max tally, which is needed to build the bullets-string
 184             max = 0
 185             for (k in tally) {
 186                 if (max < tally[k]) max = tally[k]
 187             }
 188 
 189             # make enough bullets for all tallies: this loop makes growing the
 190             # string a task with complexity O(n * log n), instead of a naive
 191             # O(n**2), which can slow-down things when tallies are high enough
 192             bullets = "•"
 193             for (n = max; n > 1; n /= 2) {
 194                 bullets = bullets bullets
 195             }
 196 
 197             # emit unsorted output lines to the sort cmd, which will emit the
 198             # final reverse-sorted tally lines
 199             for (k in tally) {
 200                 s = substr(bullets, 1, tally[k])
 201                 printf "%s\t%d\t%s\n", k, tally[k], s
 202             }
 203         }
 204     ' "$@" | sort -t '  ' -rnk2 -k1d
 205 }
 206 
 207 # Cat
 208 c() {
 209     cat "$@"
 210 }
 211 
 212 # CAlculator with Nice numbers runs my script `ca` and colors results with
 213 # my script `nn`, alternating styles to make long numbers easier to read
 214 can() {
 215     ca "$@" | nn
 216 }
 217 
 218 # CHOP DECimalS ignores all trailing decimal zeros in numbers, even the
 219 # decimal dots themselves, when decimals in a number are all zeros
 220 chopdecs() {
 221     awk 1 "$@" | sed -E 's-(\.[0-9]+[1-9]+)0+$-\1-g; s-([0-9]+)\.0*$-\1-g'
 222 }
 223 
 224 # ignore final life-feed from text, if it's the very last byte; also, all
 225 # trailing carriage-returns are ignored
 226 choplf() {
 227     awk '
 228         FNR == 1 { gsub(/^\xef\xbb\xbf/, "") }
 229         NR > 1 { print "" }
 230         { gsub(/\r$/, ""); printf "%s", $0 }
 231     ' "$@"
 232 }
 233 
 234 # CLear Screen
 235 cls() {
 236     clear
 237 }
 238 
 239 # COunt COndition: count how many times the AWK expression given is true
 240 coco() {
 241     local cond="${1:-1}"
 242     shift
 243     awk "${cond} { c++ } END { print c }" "$@"
 244 }
 245 
 246 # Colored RipGrep: ensures app `rg` emits colors when piped
 247 crg() {
 248     rg --color=always "$@"
 249 }
 250 
 251 # Color Syntax: run syntax-coloring app `bat` without line-wrapping
 252 cs() {
 253     local cmd="bat"
 254     # debian linux uses a different name for the `bat` app
 255     if [ -e "/usr/bin/batcat" ]; then
 256         cmd="batcat"
 257     fi
 258 
 259     "$cmd" --style=plain,header,numbers --theme='Monokai Extended Light' \
 260         --wrap=never --color=always "$@" |
 261     sed 's-\x1b\[38;5;70m-\x1b\[38;5;28m-g' | less -JMKiCRS
 262 }
 263 
 264 # Color Syntax of all files in a Folder, showing line numbers
 265 csf() {
 266     local cmd="bat"
 267     # debian linux uses a different name for the `bat` app
 268     if [ -e "/usr/bin/batcat" ]; then
 269         cmd="batcat"
 270     fi
 271 
 272     find "${1:-.}" -type f -print0 | xargs --null "$cmd" \
 273         --style=plain,header,numbers --theme='Monokai Extended Light' \
 274         --wrap=never --color=always |
 275     sed 's-\x1b\[38;5;70m-\x1b\[38;5;28m-g' | less -JMKiCRS
 276 }
 277 
 278 # Change Units turns common US units into international ones; uses my
 279 # scripts `bu` (Better Units) and `nn` (Nice Numbers)
 280 cu() {
 281     bu "$@" | awk '
 282         NF == 5 { print $(NF-1), $NF }
 283         NF == 4 && $NF == "s" { print $(NF-1), $NF }
 284         NF == 4 && $NF != "s" { print $NF }
 285     ' | nn
 286 }
 287 
 288 # DEDUPlicate prevents lines from appearing more than once
 289 dedup() {
 290     awk '!c[$0]++' "$@"
 291 }
 292 
 293 # DEFine the word given, using an online service
 294 def() {
 295     curl -s "dict://dict.org/d:$*" | awk '
 296         /^151 / { printf "\x1b[38;5;4m%s\x1b[0m\n", $0; next }
 297         /^[1-9][0-9]{2} / { printf "\x1b[38;5;244m%s\x1b[0m\n", $0; next }
 298         1'
 299 }
 300 
 301 # DEcompress GZip data
 302 degz() {
 303     gzip -d
 304 }
 305 
 306 # DEcompress GZIP data
 307 degzip() {
 308     gzip -d
 309 }
 310 
 311 # turn lines of Space-Separated Values into lines of Tab-Separated Values
 312 dessv() {
 313     awk 1 "$@" | sed -E 's-^ +--; s- *\r?--; s- +-\t-g'
 314 }
 315 
 316 # DICtionary definitions, using an online service
 317 dic() {
 318     curl -s "dict://dict.org/d:$*" | awk '
 319         /^151 / { printf "\x1b[38;5;4m%s\x1b[0m\n", $0; next }
 320         /^[1-9][0-9]{2} / { printf "\x1b[38;5;244m%s\x1b[0m\n", $0; next }
 321         1'
 322 }
 323 
 324 # DIVide 2 numbers 3 ways, including the complement
 325 div() {
 326     awk -v a="${1:-1}" -v b="${2:-1}" '
 327     BEGIN {
 328         gsub(/_/, "", a)
 329         gsub(/_/, "", b)
 330         if (a > b) { c = a; a = b; b = c; }
 331         print a / b
 332         print b / a
 333         print 1 - a / b
 334         exit
 335     }'
 336 }
 337 
 338 # show the current Date and Time, and the 3 `current` months
 339 dt() {
 340     # debian linux has a different `cal` app which highlights the day
 341     if [ -e "/usr/bin/ncal" ]; then
 342         ncal -C -3
 343     else
 344         cal -3
 345     fi
 346 
 347     # show the current time center-aligned
 348     # printf "%28s\x1b[34m%s\x1b[0m\n" " " "$(date +'%T')"
 349     printf "%22s\x1b[32m%s\x1b[0m  \x1b[34m%s\x1b[0m\n" " " \
 350         "$(date +'%a %b %d')" "$(date +'%T')"
 351 }
 352 
 353 # show all files in a folder, digging recursively
 354 files() {
 355     local arg
 356     for arg in "${@:-.}"; do
 357         find "${arg}" -type f
 358     done
 359 }
 360 
 361 # get the first n lines, or 1 by default
 362 first() {
 363     head -n "${1:-1}" "${2:--}"
 364 }
 365 
 366 # fix lines, ignoring leading UTF-8_BOMs (byte-order-marks) on each input's
 367 # first line, turning all end-of-line CRLF byte-pairs into single line-feeds,
 368 # and ensuring each input's last line ends with a line-feed
 369 fixlines() {
 370     awk 'FNR == 1 { gsub(/^\xef\xbb\xbf/, "") } 1' "$@" | sed -E 's-\r$--'
 371 }
 372 
 373 # show all folders in a folder, digging recursively
 374 folders() {
 375     local arg
 376     for arg in "${@:-.}"; do
 377         find "${arg}" -type d | awk 'NR > 1'
 378     done
 379 }
 380 
 381 # run the FuZzy finder (fzf) in multi-choice mode, with custom keybindings
 382 fz() {
 383     fzf -m --bind ctrl-a:select-all,ctrl-space:toggle "$@"
 384 }
 385 
 386 # run `grep` in extended mode
 387 g() {
 388     grep -E "$@"
 389 }
 390 
 391 # GET/fetch data from the filename or URI given
 392 get() {
 393     case "$1" in
 394         http://*|https://*|file://*|ftp://*|ftps://*|sftp://*|dict://*)
 395             # curl -s "$1"
 396             curl -s "$1" || (
 397                 printf "\x1b[31mcan't get %s\x1b[0m\n" "$1" >&2
 398                 return 1
 399             )
 400         ;;
 401         *)
 402             cat "$1"
 403         ;;
 404     esac
 405 }
 406 
 407 # Good, Bad, Meh colors lines using 1..3 awk conditions
 408 gbm() {
 409     local good="${1:-0}"
 410     shift
 411     local bad="${1:-0}"
 412     shift
 413     local meh="${1:-0}"
 414     shift
 415 
 416     awk "
 417         { lower = tolower(\$0) }
 418 
 419         ${good} {
 420             # code to use a color-blind-friendlier blue, instead of green
 421             # gsub(/\\x1b\\[0m/, \"\\x1b[0m\\x1b[38;5;26m\")
 422             # printf \"\\x1b[38;5;26m%s\\x1b[0m\\n\", \$0
 423             gsub(/\\x1b\\[0m/, \"\\x1b[0m\\x1b[38;5;29m\")
 424             printf \"\\x1b[38;5;29m%s\\x1b[0m\\n\", \$0
 425             fflush()
 426             next
 427         }
 428 
 429         ${bad} {
 430             gsub(/\\x1b\\[0m/, \"\\x1b[0m\\x1b[38;5;1m\")
 431             printf \"\\x1b[38;5;1m%s\\x1b[0m\\n\", \$0
 432             fflush()
 433             next
 434         }
 435 
 436         ${meh} {
 437             gsub(/\\x1b\\[0m/, \"\\x1b[0m\\x1b[38;5;248m\")
 438             printf \"\\x1b[38;5;248m%s\\x1b[0m\\n\", \$0
 439             fflush()
 440             next
 441         }
 442 
 443         {
 444             print
 445             fflush()
 446         }
 447     " "$@"
 448 }
 449 
 450 # Highlight (lines) with AWK
 451 hawk() {
 452     local cond="${1:-1}"
 453     shift
 454 
 455     awk "
 456         ${cond} {
 457             gsub(/\\x1b\\[0m/, \"\x1b[0m\\x1b[7m\")
 458             printf \"\\x1b[7m%s\\x1b[0m\\n\", \$0
 459             fflush()
 460             next
 461         }
 462 
 463         { print; fflush() }" "$@"
 464 }
 465 
 466 highlight() {
 467     awk '{
 468         gsub(/\x1b\[[0-9;]*[A-Za-z]/, "")
 469         printf "\x1b[7m%s\x1b[0m\n", $0
 470     }' "$@"
 471 }
 472 
 473 hilite() {
 474     awk '{
 475         gsub(/\x1b\[[0-9;]*[A-Za-z]/, "")
 476         printf "\x1b[7m%s\x1b[0m\n", $0
 477     }' "$@"
 478 }
 479 
 480 # Header Less runs `less` with line numbers, ANSI styles, no line-wrapping,
 481 # and using the first line as a sticky-header, so it always shows on top
 482 hl() {
 483     less --header=1 -JMKNiCRS "$@"
 484 }
 485 
 486 # Help Me Remember my custom shell commands
 487 hmr() {
 488     local cmd
 489     cmd="bat"
 490     # debian linux uses a different name for the `bat` app
 491     if [ -e "/usr/bin/batcat" ]; then
 492         cmd="batcat"
 493     fi
 494 
 495     "$cmd" \
 496         --style=plain,header,numbers --theme='Monokai Extended Light' \
 497         --wrap=never --color=always "$(which shshsh)" "$(which clam)" |
 498             sed 's-\x1b\[38;5;70m-\x1b\[38;5;28m-g' | less -JMKiCRS
 499 }
 500 
 501 # Header View runs `less` without line numbers, with ANSI styles, with no
 502 # line-wrapping, and using the first line as a sticky-header, so it always
 503 # shows on top
 504 hv() {
 505     less --header=1 -JMKiCRS "$@"
 506 }
 507 
 508 # show a `dad` JOKE from the web, sometimes even a very funny one
 509 joke() {
 510     curl -s https://icanhazdadjoke.com | fold -s | sed -E 's- *\r?$--'
 511     # plain-text output from previous cmd doesn't end with a line-feed
 512     printf "\n"
 513 }
 514 
 515 # run `less`, showing line numbers, among other settings
 516 l() {
 517     less -JMKNiCRS "$@"
 518 }
 519 
 520 # Like A Book groups lines as 2 side-by-side pages, the same way books
 521 # do it; uses my script `book`
 522 lab() {
 523     book "$(($(tput lines) - 1))" "$@" | less -JMKiCRS
 524 }
 525 
 526 # Line xARGS: `xargs` using line separators, which handles filepaths
 527 # with spaces, as long as the standard input has 1 path per line
 528 largs() {
 529     xargs -d "\n" "$@"
 530 }
 531 
 532 # get the last n lines, or 1 by default
 533 last() {
 534     tail -n "${1:-1}" "${2:--}"
 535 }
 536 
 537 # limit stops at the first n bytes, or 1024 bytes by default
 538 limit() {
 539     head -c "${1:-1024}" "${2:--}"
 540 }
 541 
 542 # list files, coloring folders and links
 543 lf() {
 544     ls -al --file-type --color=never --time-style iso "$@" | awk '
 545         /^d/ { printf "\x1b[38;5;33m%s\x1b[0m\n", $0; next }
 546         /^l/ { printf "\x1b[38;5;29m%s\x1b[0m\n", $0; next }
 547         1'
 548 }
 549 
 550 # Less with Header runs `less` with line numbers, ANSI styles, no line-wraps,
 551 # and using the first line as a sticky-header, so it always shows on top
 552 lh() {
 553     less --header=1 -JMKNiCRS "$@"
 554 }
 555 
 556 # regroup adjacent lines into n-item tab-separated lines
 557 lineup() {
 558     local n="${1:-0}"
 559     shift
 560 
 561     if [ "${n}" -le 0 ]; then
 562         awk '
 563             NR > 1 { printf "\t" }
 564             { printf "%s", $0 }
 565             END { if (NR > 0) print "" }' "$@"
 566         return "$?"
 567     fi
 568 
 569     awk -v n="${n}" '
 570         NR % n != 1 { printf "\t" }
 571         { printf "%s", $0 }
 572         NR % n == 0 { print "" }
 573         END { if (NR % n != 0) print "" }' "$@"
 574 }
 575 
 576 # LOwercase line, check (awk) COndition
 577 loco() {
 578     local cond="${1:-1}"
 579     shift
 580     # awk "{ \$0 = tolower(\$0) } ${cond}" "$@"
 581     awk "{ line = \$0; l = line; \$0 = tolower(\$0) }
 582         ${cond} { print line }" "$@"
 583 }
 584 
 585 # LOcal SERver webserves files in a folder as localhost, using the port
 586 # number given, or port 8080 by default
 587 loser() {
 588     printf "\x1b[38;5;26mserving files in %s\x1b[0m\n" "${2:-$(pwd)}" >&2
 589     python3 -m http.server "${1:-8080}" -d "${2:-.}"
 590 }
 591 
 592 # merge stderr into stdout, without any ugly keyboard-dancing
 593 merrge() {
 594     "$@" 2>&1
 595 }
 596 
 597 # Number all lines, starting from the number given, or 1 by default
 598 n() {
 599     local n="${1:-1}"
 600     shift
 601     awk -v n="$n" '{ printf "%d\t%s\n", NR - 1 + n, $0; fflush() }' "$@"
 602 }
 603 
 604 # emit nothing to output and/or discard everything from input
 605 nil() {
 606     if [ -p /dev/stdin ]; then
 607         cat > /dev/null
 608     else
 609         head -c 0
 610     fi
 611 }
 612 
 613 # Nice File Sizes, using my scripts `nn` and `cext`
 614 nfs() {
 615     # turn arg-list into single-item lines
 616     awk 'BEGIN { for (i = 1; i < ARGC; i++) print ARGV[i]; exit }' "$@" |
 617     # calculate file-sizes, and reverse-sort results
 618     xargs -d '\n' wc -c | sort -rn |
 619     # start output with a header-like line, and add a MiB field
 620     awk 'BEGIN { printf "%5s  %9s  %8s  name\n", "n", "bytes", "MiB" }
 621     { printf "%6d  %9d  %8.2f  %s\n", NR - 1, $1, $1 / 1048576, $2 }' |
 622     # make zeros in the MiB field stand out with a special color
 623     awk '{ gsub(/ 0.00 /, "\x1b[38;5;103m 0.00 \x1b[0m"); print }' |
 624     # make numbers nice, alternating styles along 3-digit groups
 625     nn |
 626     # color-code file extensions
 627     cext |
 628     # make table breathe with empty lines, so tall outputs are readable
 629     awk '(NR - 2) % 5 == 1 && NR > 1 { print "" } 1'
 630 }
 631 
 632 # ignore stderr, without any ugly keyboard-dancing
 633 noerr() {
 634     "$@" 2> /dev/null
 635 }
 636 
 637 # show the current date and time
 638 now() {
 639     date +'%Y-%m-%d %H:%M:%S'
 640 }
 641 
 642 # ignore ANSI terminal styling
 643 plain() {
 644     awk '
 645     {
 646         # ignore notifications (code 9) and hyperlinks (code 8)
 647         gsub(/\x1b\](8|9);[^\x07]*\x07/, "")
 648         # ignore cursor-movers and style-changers
 649         gsub(/\x1b\[([0-9]*[A-HJKST]|[0-9;]*m)/, "")
 650 
 651         print
 652         fflush()
 653     }' "$@"
 654 }
 655 
 656 # Paused MPV
 657 pmpv() {
 658     mpv --pause "$@"
 659 }
 660 
 661 # reflow/trim lines of prose (text) to improve its legibility: it
 662 # seems especially useful when the text is pasted from web-pages
 663 # being viewed in reader mode
 664 reprose() {
 665     local w="${1:-80}"
 666     shift
 667     awk 'FNR == 1 && NR > 1 { print "" } 1' "$@" | sed -E 's- *\r?$--' |
 668         fold -s -w="$w" | sed -E 's- +$--'
 669 }
 670 
 671 # change color/style of lines, using my script `ecoli`
 672 restyle() {
 673     local style="${1:-plain}"
 674     shift
 675     awk 1 "$@" | ecoli . "${style}"
 676 }
 677 
 678 # Read-Only Micro (text editor)
 679 rom() {
 680     micro --readonly true "$@"
 681 }
 682 
 683 # run `sed` in extended mode
 684 s() {
 685     sed -E "$@"
 686 }
 687 
 688 # show a unique-looking SEParator line; useful to run between commands
 689 # which output walls of text
 690 sep() {
 691     printf "\x1b[48;5;253m"
 692     printf "·························································"
 693     printf "·······················"
 694     printf "\x1b[0m\n"
 695 }
 696 
 697 # webserves files in a folder as localhost, using the port number given, or
 698 # port 8080 by default
 699 serve() {
 700     printf "\x1b[38;5;26mserving files in %s\x1b[0m\n" "${2:-$(pwd)}" >&2
 701     python3 -m http.server "${1:-8080}" -d "${2:-.}"
 702 }
 703 
 704 # SET DIFFerence sorts its 2 inputs, then finds lines not in the 2nd input
 705 setdiff() {
 706     # comm -23 <(sort "$1") <(sort "$2")
 707     # dash doesn't support the process-sub syntax
 708     (sort "$1" | (sort "$2" | (comm -23 /dev/fd/3 /dev/fd/4) 4<&0) 3<&0)
 709 }
 710 
 711 # SET INtersection, sorts its 2 inputs, then finds common lines
 712 setin() {
 713     # comm -12 <(sort "$1") <(sort "$2")
 714     # dash doesn't support the process-sub syntax
 715     (sort "$1" | (sort "$2" | (comm -12 /dev/fd/3 /dev/fd/4) 4<&0) 3<&0)
 716 }
 717 
 718 # SET SUBtraction sorts its 2 inputs, then finds lines not in the 2nd input
 719 setsub() {
 720     # comm -23 <(sort "$1") <(sort "$2")
 721     # dash doesn't support the process-sub syntax
 722     (sort "$1" | (sort "$2" | (comm -23 /dev/fd/3 /dev/fd/4) 4<&0) 3<&0)
 723 }
 724 
 725 # Show Files (and folders), using colors
 726 sf() {
 727     ls -al --file-type --color=never --time-style iso "$@" |
 728         nn | cext | awk '
 729             /^d/ { gsub(/\x1b\[0m/, "\x1b[38;5;33m") }
 730             /^d/ { printf "\x1b[38;5;33m%s\x1b[0m\n", $0; next }
 731             /^l/ { gsub(/\x1b\[0m/, "\x1b[38;5;29m") }
 732             /^l/ { printf "\x1b[38;5;29m%s\x1b[0m\n", $0; next }
 733             1'
 734 }
 735 
 736 # show a command, then run it
 737 showrun() {
 738     printf "\x1b[7m%s\x1b[0m\n" "$*" && "$@"
 739 }
 740 
 741 # SKIP the first n lines, or the 1st line by default
 742 skip() {
 743     tail -n +$(("${1:-1}" + 1)) "${2:--}"
 744 }
 745 
 746 # SKIP the LAST n lines, or the last line by default
 747 skiplast() {
 748     head -n -"${1:-1}" "${2:--}"
 749 }
 750 
 751 # Styled LEAK, runs my script `leak`
 752 sleak() {
 753     leak "$@"
 754 }
 755 
 756 # Show Latest Podcasts, using my scripts `podfeed` and `si`
 757 slp() {
 758     local title
 759     title="Latest Podcast Episodes as of $(date +'%F %T')"
 760     podfeed -title "${title}" "$@" | si
 761 }
 762 
 763 # turn lines of Space-Separated Values into lines of Tab-Separated Values
 764 ssv2tsv() {
 765     awk 1 "$@" | sed -E 's-^ +--; s- *\r?--; s- +-\t-g'
 766 }
 767 
 768 # change color/style of lines, using my script `ecoli`
 769 style() {
 770     local style="${1:-plain}"
 771     shift
 772     awk 1 "$@" | ecoli . "${style}"
 773 }
 774 
 775 # show a random command defined in `shshsh` or `clam`, using `wat`
 776 # surprise() {
 777 #     wat "$(
 778 #         cat "$(which shshsh)" "$(which clam)" | grep -E '^[a-z]+\(' |
 779 #             shuf -n 1 | sed -E 's-\(.*--'
 780 #     )"
 781 # }
 782 
 783 # show a random command defined in `clam`, using `wat`
 784 surprise() {
 785     wat "$(
 786         grep -E '^[a-z]+\(' "$(which clam)" | shuf -n 1 | sed -E 's-\(.*--'
 787     )"
 788 }
 789 
 790 # show a reverse-sorted tally of all lines read, where ties are sorted
 791 # alphabetically
 792 tally() {
 793     printf "value\ttally\n"
 794     awk '
 795         { tally[$0]++ }
 796         END { for (k in tally) { printf "%s\t%d\n", k, tally[k] } }
 797     ' "$@" | sort -t '  ' -rnk2 -k1d
 798 }
 799 
 800 # Tab AWK: TSV-specific I/O settings for `awk`
 801 tawk() {
 802     awk -F "\t" -v OFS="\t" "$@"
 803 }
 804 
 805 title() {
 806     local title="${1:-no title given}"
 807     shift
 808     awk -v t="${title}" 'BEGIN { printf "\x1b[7m%s\x1b[0m\n", t } 1' "$@"
 809 }
 810 
 811 # Transform Json, using an interactive Editor for the formula/expression
 812 tje() {
 813     if [ "$1" = "=" ]; then
 814         shift
 815         tj '=' "$(micro --readonly true -filetype python | leak --inv)" "$@"
 816     else
 817         tj "$(micro --readonly true -filetype python | leak --inv)" "$@"
 818     fi
 819 }
 820 
 821 # Transform Json, using the Micro editor for the formula/expression
 822 tjm() {
 823     if [ "$1" = "=" ]; then
 824         shift
 825         tj '=' "$(micro --readonly true -filetype python | leak --inv)" "$@"
 826     else
 827         tj "$(micro --readonly true -filetype python | leak --inv)" "$@"
 828     fi
 829 }
 830 
 831 # Transform Json (Node), using an interactive Editor for the expression
 832 tjne() {
 833     if [ "$1" = "=" ]; then
 834         shift
 835         tjn '=' "$(micro --readonly true -filetype js | leak --inv)" "$@"
 836     else
 837         tjn "$(micro --readonly true -filetype js | leak --inv)" "$@"
 838     fi
 839 }
 840 
 841 # Transform Json (Node), using the Micro editor for the formula/expression
 842 tjnm() {
 843     if [ "$1" = "=" ]; then
 844         shift
 845         tjn '=' "$(micro --readonly true -filetype js | leak --inv)" "$@"
 846     else
 847         tjn "$(micro --readonly true -filetype js | leak --inv)" "$@"
 848     fi
 849 }
 850 
 851 # Transform Lines, using an interactive Editor for the formula/expression
 852 tle() {
 853     if [ "$1" = "=" ]; then
 854         shift
 855         tl '=' "$(micro --readonly true -filetype python | leak --inv)" "$@"
 856     else
 857         tl "$(micro --readonly true -filetype python | leak --inv)" "$@"
 858     fi
 859 }
 860 
 861 # Transform Lines, using the Micro editor for the formula/expression
 862 tlm() {
 863     if [ "$1" = "=" ]; then
 864         shift
 865         tl '=' "$(micro --readonly true -filetype python | leak --inv)" "$@"
 866     else
 867         tl "$(micro --readonly true -filetype python | leak --inv)" "$@"
 868     fi
 869 }
 870 
 871 # Transform Lines (Node), using the Micro editor for the formula/expression
 872 tlne() {
 873     if [ "$1" = "=" ]; then
 874         shift
 875         tln '=' "$(micro --readonly true -filetype js | leak --inv)" "$@"
 876     else
 877         tln "$(micro --readonly true -filetype js | leak --inv)" "$@"
 878     fi
 879 }
 880 
 881 # Transform Lines (Node), using the Micro editor for the formula/expression
 882 tlnm() {
 883     if [ "$1" = "=" ]; then
 884         shift
 885         tln '=' "$(micro --readonly true -filetype python | leak --inv)" "$@"
 886     else
 887         tln "$(micro --readonly true -filetype python | leak --inv)" "$@"
 888     fi
 889 }
 890 
 891 # show current date in a specifc format, which is both people-friendly
 892 # and machine/tool/search/automation-friendly
 893 today() {
 894     date +'%Y-%m-%d %a %b %d'
 895 }
 896 
 897 # show all files directly in the folder given, without looking any deeper
 898 topfiles() {
 899     local arg
 900     for arg in "${@:-.}"; do
 901         find "${arg}" -maxdepth 1 -type f
 902     done
 903 }
 904 
 905 # show all folders directly in the folder given, without looking any deeper
 906 topfolders() {
 907     local arg
 908     for arg in "${@:-.}"; do
 909         find "${arg}" -maxdepth 1 -type d | awk 'NR > 1'
 910     done
 911 }
 912 
 913 # try running a command, emitting an explicit message to standard-error
 914 # if the command given fails
 915 try() {
 916     "$@" || (
 917         # printf "%s: failure running %s\n" "$0" "$*" >&2
 918         printf "\n\x1b[31mrunning \x1b[41m\x1b[97m %s \x1b[0m\x1b[31m failed\x1b[0m\n" "$*" >&2
 919         return 255
 920     )
 921 }
 922 
 923 # TimeStamp lines satisfying an AWK condition, ignoring all other lines
 924 tsawk() {
 925     # -v line="\x1b[38;5;27m%s\x1b[0m %s\n"
 926     awk \
 927         -v line="\x1b[48;5;255m\x1b[38;5;24m%s\x1b[0m %s\n" \
 928         -v time="%Y-%m-%d %H:%M:%S" \
 929         "${1:-1} { printf line, strftime(time), \$0; fflush() }"
 930 }
 931 
 932 # run my script `realign` using tab as the only field-separator,
 933 # which means the resulting TSV items can have spaces in them
 934 tsv2ssv() {
 935     realign --tsv "$@"
 936 }
 937 
 938 # UNcompress GZip data
 939 ungz() {
 940     gzip -d
 941 }
 942 
 943 # UNcompress GZIP data
 944 ungzip() {
 945     gzip -d
 946 }
 947 
 948 # deduplicate lines, keeping them in their original order
 949 unique() {
 950     awk '!c[$0]++' "$@"
 951 }
 952 
 953 # concatenate all named input sources, ignoring trailing CRLFs into LFs,
 954 # and guaranteeing lines from different sources are accidentally joined,
 955 # by adding a line-feed when an input's last line doesn't end with one;
 956 # also, ignore leading UTF-8 BOMs on the first line of each input, as
 957 # those are useless at best
 958 unixify() {
 959     awk 'FNR == 1 { gsub(/^\xef\xbb\xbf/, "") } 1' "$@" | sed -E 's-\r$--'
 960 }
 961 
 962 # turn lines of Space-Separated Values into lines of Tab-Separated Values
 963 unssv() {
 964     awk 1 "$@" | sed -E 's-^ +--; s- *\r?--; s- +-\t-g'
 965 }
 966 
 967 # go UP n folders, or go up 1 folder by default
 968 up() {
 969     if [ "${1:-1}" -le 0 ]; then
 970         cd .
 971         return "$?"
 972     fi
 973 
 974     cd "$(printf "%${1:-1}s" "" | sed 's- -../-g')" || return $?
 975 }
 976 
 977 # View with `less`
 978 v() {
 979     less -JMKiCRS "$@"
 980 }
 981 
 982 # run a command, showing its success/failure right after
 983 verdict() {
 984     local code
 985 
 986     "$@"
 987     code="$?"
 988 
 989     if [ "${code}" -eq 0 ]; then
 990         printf "\n\x1b[38;5;29m%s \x1b[48;5;29m\x1b[97m succeeded \x1b[0m\n" "$*" >&2
 991         return 0
 992     fi
 993 
 994     printf "\n\x1b[31m%s \x1b[41m\x1b[97m failed with error code ${code} \x1b[0m\n" "$*" >&2
 995     return "${code}"
 996 }
 997 
 998 # View with Header runs `less` without line numbers, with ANSI styles, with
 999 # no line-wrapping, and using the first line as a sticky-header, so it always
1000 # shows on top
1001 vh() {
1002     less --header=1 -JMKiCRS "$@"
1003 }
1004 
1005 # What Are These (?) shows what the names given to it are/do
1006 wat() {
1007     local a
1008     local res
1009     local code=0
1010 
1011     for a in "$@"; do
1012         printf "\x1b[48;5;253m\x1b[38;5;26m%-80s\x1b[0m\n" "${a}"
1013         (
1014             alias "${a}" || declare -f "${a}" || which "${a}" || type "${a}"
1015         ) 2> /dev/null
1016         res="$?"
1017 
1018         if [ "${res}" -ne 0 ]; then
1019             code="${res}"
1020             printf "\x1b[31m%s not found\x1b[0m\n" "${a}"
1021         fi
1022     done
1023 
1024     return "${code}"
1025 }
1026 
1027 # Word-Count Plus runs `wc` and enriches its output; uses my scripts `nn`
1028 # and `cext`
1029 wcp() {
1030     wc "$@" | sort -rn | nn | cext | awk '{ printf "%6d  %s\n", NR - 1, $0 }'
1031 }
1032 
1033 # find all files which have at least 1 line with trailing spaces/CRs, with
1034 # the option to limit the (fully-recursive) search to the files/folders given
1035 wheretrails() {
1036     rg -c '[ \r]+$' "${@:-.}"
1037 }
1038 
1039 # find all files which have at least 1 line with trailing spaces/CRs, with
1040 # the option to limit the (fully-recursive) search to the files/folders given
1041 whichtrails() {
1042     rg -c '[ \r]+$' "${@:-.}"
1043 }
1044 
1045 # emit each word-like item from each input line on its own line
1046 words() {
1047     awk '{ for (i = 1; i <= NF; i++) print $i }' "$@"
1048 }
1049 
1050 # run `xargs`, using whole lines as extra arguments
1051 x() {
1052     xargs -d '\n' "$@"
1053 }
1054 
1055 # Youtube Audio Player
1056 yap() {
1057     local url="$(echo "${1}" | sed 's-&.*--')"
1058     mpv "$(yt-dlp -f 140 --get-url "${url}" 2> /dev/null)"
1059 }
1060 
1061 # Youtube Download
1062 yd() {
1063     yt-dlp "$@"
1064 }
1065 
1066 # Youtube Download AAC audio
1067 ydaac() {
1068     yt-dlp -f 140 "$@"
1069 }
1070 
1071 # Youtube Download MP4 video
1072 ydmp4() {
1073     yt-dlp -f 22 "$@"
1074 }
1075 
1076 # show a calendar for the current year, or for the year given
1077 year() {
1078     # debian linux has a different `cal` app which highlights the day
1079     if [ -e "/usr/bin/ncal" ]; then
1080         ncal -C -y "$@"
1081     else
1082         cal -y "$@"
1083     fi
1084 }
1085 
1086 # show the current date in the YYYY-MM-DD format
1087 ymd() {
1088     date +'%Y-%m-%d'
1089 }