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