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