File: clam.sh
   1 #!/bin/sh
   2 
   3 # The MIT License (MIT)
   4 #
   5 # Copyright (c) 2026 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 # To use this script, you're supposed to `source` it, so its definitions
  40 # stay for your whole shell session: for that, you can run `source clam` or
  41 # `. clam` (no quotes either way), either directly or at shell startup.
  42 #
  43 # Almost all commands defined in this script work with `bash`, `zsh`, and
  44 # even `dash`, which is debian linux's default non-interactive shell. Some
  45 # of its commands even seem to work on busybox's shell.
  46 
  47 
  48 case "$1" in
  49     -h|--h|-help|--help)
  50         # show help message, using the info-comment from this very script
  51         awk '
  52             /^case / { exit }
  53             /^# +clam$/, /^$/ { gsub(/^# ?/, ""); print }
  54         ' "$0"
  55         exit 0
  56     ;;
  57 esac
  58 
  59 
  60 # dash doesn't support regex-matching syntax, forcing to use case statements
  61 case "$0" in
  62     -bash|-dash|-sh|bash|dash|sh|/bin/sh)
  63         # script is being sourced with bash, dash, or ash, which is good
  64         :
  65     ;;
  66 
  67     *)
  68         case "$ZSH_EVAL_CONTEXT" in
  69             *:file)
  70                 # script is being sourced with zsh, which is good
  71                 :
  72             ;;
  73 
  74             *)
  75                 # script is being run normally, which is a waste of time
  76         printf "\e[7mDon't run this script directly: instead source it\e[0m\n"
  77         printf "\e[7mby running '. clam' (without the single quotes).\e[0m\n"
  78         printf "\n"
  79         printf "\e[7mBefore doing that, you may want to see the help,\e[0m\n"
  80         printf "\e[7mby running 'clam -h' (without the single quotes).\e[0m\n"
  81                 # exiting during shell-startup may deny shell access, even if
  82                 # the script is being run, instead of being sourced directly
  83             ;;
  84         esac
  85     ;;
  86 esac
  87 
  88 
  89 alias 0='sbs'
  90 
  91 alias 1='bsbs 1'
  92 alias 2='bsbs 2'
  93 alias 3='bsbs 3'
  94 alias 4='bsbs 4'
  95 alias 5='bsbs 5'
  96 alias 6='bsbs 6'
  97 alias 7='bsbs 7'
  98 alias 8='bsbs 8'
  99 alias 9='bsbs 9'
 100 
 101 # Less with Header n runs `less` with line numbers, ANSI styles, without
 102 # line-wraps, and using the first n lines as a sticky-header, so they always
 103 # show on top
 104 alias lh1='less --header=1 -MKNiCRS'
 105 alias lh2='less --header=2 -MKNiCRS'
 106 alias lh3='less --header=3 -MKNiCRS'
 107 alias lh4='less --header=4 -MKNiCRS'
 108 alias lh5='less --header=5 -MKNiCRS'
 109 alias lh6='less --header=6 -MKNiCRS'
 110 alias lh7='less --header=7 -MKNiCRS'
 111 alias lh8='less --header=8 -MKNiCRS'
 112 alias lh9='less --header=9 -MKNiCRS'
 113 
 114 # View with Header n runs `less` without line numbers, ANSI styles, without
 115 # line-wraps, and using the first n lines as a sticky-header, so they always
 116 # show on top
 117 alias vh1='less --header=1 -MKiCRS'
 118 alias vh2='less --header=2 -MKiCRS'
 119 alias vh3='less --header=3 -MKiCRS'
 120 alias vh4='less --header=4 -MKiCRS'
 121 alias vh5='less --header=5 -MKiCRS'
 122 alias vh6='less --header=6 -MKiCRS'
 123 alias vh7='less --header=7 -MKiCRS'
 124 alias vh8='less --header=8 -MKiCRS'
 125 alias vh9='less --header=9 -MKiCRS'
 126 
 127 alias c='cat'
 128 alias e='echo'
 129 alias r='reset'
 130 
 131 # Awk Print
 132 alias ap=abp
 133 
 134 # Book-like MANual, lays out `man` docs as pairs of side-by-side pages; uses
 135 # my tool `bsbs`
 136 alias bman=bookman
 137 
 138 # load/concatenate BYTES from named data sources
 139 # alias bytes='cat'
 140 
 141 # load/concatenate BYTES from named data sources; uses my tool `get`
 142 alias bytes='get'
 143 
 144 # Compile C Optimized
 145 alias cco='cc -Wall -O2 -s -march=native -mtune=native -flto'
 146 
 147 # Color DMESG
 148 alias cdmesg='dmesg --color=always'
 149 
 150 # Colored Json Query runs the `jq` app, allowing an optional filepath as the
 151 # data source, and even an optional transformation formula
 152 alias cjq='jq -C'
 153 
 154 # CLear Screen
 155 alias cls='tput reset 2> /dev/null || reset'
 156 
 157 # Compile C Plus Plus Optimized
 158 alias cppo='c++ -Wall -O2 -s -march=native -mtune=native -flto'
 159 
 160 # CURL Silent spares you the progress bar, but still tells you about errors
 161 alias curls='curl --silent --show-error'
 162 
 163 # dictionary-DEFine the word given, using an online service
 164 alias def=define
 165 
 166 # turn JSON Lines into a proper json array
 167 # alias dejsonl='jq -s -M'
 168 
 169 # turn json lines into a proper json array using the `jq` app
 170 alias dejql='jq -s -M'
 171 
 172 # turn UTF-16 data into UTF-8
 173 alias deutf16='iconv -f utf16 -t utf8'
 174 
 175 # edit plain-text files
 176 # alias edit='micro'
 177 
 178 # ENV with 0/null-terminated lines on stdout
 179 alias env0='env -0'
 180 
 181 # ENV Change folder, runs the command given in the folder given (first)
 182 alias envc='env -C'
 183 
 184 # Extended Plain Interactive Grep
 185 alias epig='ugrep --color=never -Q -E'
 186 
 187 # Editor Read-Only
 188 alias ero='micro -readonly true'
 189 
 190 # Expand 4 turns each tab into up to 4 spaces
 191 alias expand4='expand -t 4'
 192 
 193 # run the Fuzzy Finder (fzf) in multi-choice mode, with custom keybindings
 194 alias ff='fzf -m --bind ctrl-a:select-all,ctrl-space:toggle'
 195 
 196 # get FILE's MIME types
 197 alias filemime='file --mime-type'
 198 
 199 # run `gcc` with all optimizations on and with static analysis on
 200 alias gccmax='gcc -Wall -O2 -s -march=native -mtune=native -flto -fanalyzer'
 201 
 202 # hold stdout if used at the end of a pipe-chain
 203 alias hold='less -MKiCRS'
 204 
 205 # find all hyperlinks inside HREF attributes in the input text
 206 alias hrefs=href
 207 
 208 # make JSON Lines out of JSON data
 209 alias jl=jsonl
 210 
 211 # shrink/compact JSON using the `jq` app, allowing an optional filepath, and
 212 # even an optional transformation formula after that
 213 alias jq0='jq -c -M'
 214 
 215 # show JSON data on multiple lines, using 2 spaces for each indentation level,
 216 # allowing an optional filepath, and even an optional transformation formula
 217 # after that
 218 alias jq2='jq --indent 2 -M'
 219 
 220 # find the LAN (local-area network) IP address for this device
 221 alias lanip='hostname -I'
 222 
 223 # run `less`, showing line numbers, among other settings
 224 alias least='less -MKNiCRS'
 225 
 226 # try to run the command given using line-buffering for its (standard) output
 227 alias livelines='stdbuf -oL'
 228 
 229 # LOAD data from the filename or URI given; uses my `get` tool
 230 alias load=get
 231 
 232 # LOcal SERver webserves files in a folder as localhost, using the port
 233 # number given, or port 8080 by default
 234 alias loser=serve
 235 
 236 # Live RipGrep
 237 alias lrg='rg --line-buffered'
 238 
 239 # run `ls` showing how many 4k pages each file takes
 240 alias lspages='ls -s --block-size=4096'
 241 
 242 # Listen To Youtube
 243 alias lty=yap
 244 
 245 # MAKE IN folder
 246 alias makein=mif
 247 
 248 # Multi-Core MaKe runs `make` using all cores
 249 alias mcmk=mcmake
 250 
 251 # run `less`, showing line numbers, among other settings
 252 alias most='less -MKNiCRS'
 253 
 254 # emit nothing to output and/or discard everything from input
 255 alias nil=null
 256 
 257 # Nice Json Query colors JSON data using the `jq` app
 258 alias njq=cjq
 259 
 260 # Plain Interactive Grep
 261 alias pig='ugrep --color=never -Q -E'
 262 
 263 # Quick Compile C Optimized
 264 alias qcco='cc -Wall -O2 -s -march=native -mtune=native -flto'
 265 
 266 # Quick Compile C Plus Plus Optimized
 267 alias qcppo='c++ -Wall -O2 -s -march=native -mtune=native -flto'
 268 
 269 # Read-Only Editor
 270 alias roe='micro -readonly true'
 271 
 272 # Read-Only Micro (text editor)
 273 alias rom='micro -readonly true'
 274 
 275 # Read-Only Top
 276 alias rot='htop --readonly'
 277 
 278 # RUN IN folder
 279 alias runin='env -C'
 280 
 281 # place lines Side-By-Side
 282 # alias sbs='column'
 283 
 284 # Silent CURL spares you the progress bar, but still tells you about errors
 285 alias scurl='curl --silent --show-error'
 286 
 287 # Stdbuf Output Line-buffered
 288 alias sol='stdbuf -oL'
 289 
 290 # TRY running a command, showing its outcome/error-code on failure
 291 alias try=verdict
 292 
 293 # Time Verbosely the command given
 294 alias tv='/usr/bin/time -v'
 295 
 296 # VERTical REVert emits lines in reverse order of appearance
 297 alias vertrev=tac
 298 
 299 # emit lines in reverse order of appearance
 300 alias upsidedown=tac
 301 
 302 # run `cppcheck` with even stricter options
 303 alias vetc='cppcheck --enable=portability,style --check-level=exhaustive'
 304 
 305 # run `cppcheck` with even stricter options, also checking for c89 compliance
 306 alias vetc89='cppcheck --enable=portability,style --check-level=exhaustive --std=c89'
 307 
 308 # run `cppcheck` with even stricter options
 309 alias vetcpp='cppcheck --enable=portability,style --check-level=exhaustive'
 310 
 311 # VET SHell scripts
 312 alias vetsh=vetshell
 313 
 314 # check shell scripts for common gotchas, avoiding complaints about using
 315 # the `local` keyword, which is widely supported in practice
 316 alias vetshell='shellcheck -e 3043'
 317 
 318 # run a command using an empty environment
 319 alias void='env -i'
 320 
 321 # turn plain-text from latin-1 into UTF-8; the name is from `vulgarization`,
 322 # which is the mutation of languages away from latin during the middle ages
 323 alias vulgarize='iconv -f latin-1 -t utf-8'
 324 
 325 # recursively find all files with trailing spaces/CRs
 326 alias wheretrails=whichtrails
 327 
 328 # run `xargs`, using zero/null bytes as the extra-arguments terminator
 329 alias x0='xargs -0'
 330 
 331 # Xargs Lines, runs `xargs` using whole lines as extra arguments
 332 alias xl=xargsl
 333 
 334 # Awk Begin Print
 335 abp() {
 336     local arg
 337     for arg in "$@"; do
 338         awk "BEGIN { print (${arg}); exit }"
 339     done
 340 }
 341 
 342 # find name from the local `apt` database of installable packages
 343 aptfind() {
 344     local arg
 345     local gap=0
 346     local options='-MKiCRS'
 347 
 348     if [ $# -eq 1 ]; then
 349         options='--header=1 -MKiCRS'
 350     fi
 351 
 352     for arg in "$@"; do
 353         [ "${gap}" -gt 0 ] && printf "\n"
 354         gap=1
 355         printf "\e[7m%-80s\e[0m\n\n" "${arg}"
 356 
 357         # despite warnings, the `search` command has been around for years
 358         apt search "${arg}" 2> /dev/null |
 359             grep -E -A 1 "^[a-z0-9-]*${arg}" | sed 's/^--$//'
 360     done | less ${options}
 361 }
 362 
 363 # APT GET/install packages
 364 aptget() { sudo apt install "$@"; sudo -k; }
 365 
 366 # APT UPdate/grade
 367 aptup() { sudo apt update && sudo apt upgrade "$@"; sudo -k; }
 368 
 369 # emit each argument given as its own line of output
 370 args() { [ $# -eq 0 ] || printf "%s\n" "$@"; }
 371 
 372 # AWK in BLOCKS/paragraphs-input mode
 373 awkblocks() {
 374     if { [ -p /dev/stdout ] || [ -t 1 ]; } && [ -e /usr/bin/stdbuf ]; then
 375         stdbuf -oL awk -F='' -v RS='' "$@"
 376     else
 377         awk -F='' -v RS='' "$@"
 378     fi
 379 }
 380 
 381 # AWK using TABS as input/output field-separators
 382 awktabs() {
 383     if { [ -p /dev/stdout ] || [ -t 1 ]; } && [ -e /usr/bin/stdbuf ]; then
 384         stdbuf -oL awk -F "\t" -v OFS="\t" "$@"
 385     else
 386         awk -F "\t" -v OFS="\t" "$@"
 387     fi
 388 }
 389 
 390 # Breathe lines 3: separate groups of 3 lines with empty lines
 391 b3() {
 392     if { [ -p /dev/stdout ] || [ -t 1 ]; } && [ -e /usr/bin/stdbuf ]; then
 393         stdbuf -oL awk 'NR % 3 == 1 && NR != 1 { print "" } 1' "$@"
 394     else
 395         awk 'NR % 3 == 1 && NR != 1 { print "" } 1' "$@"
 396     fi
 397 }
 398 
 399 # Breathe lines 5: separate groups of 5 lines with empty lines
 400 b5() {
 401     if { [ -p /dev/stdout ] || [ -t 1 ]; } && [ -e /usr/bin/stdbuf ]; then
 402         stdbuf -oL awk 'NR % 5 == 1 && NR != 1 { print "" } 1' "$@"
 403     else
 404         awk 'NR % 5 == 1 && NR != 1 { print "" } 1' "$@"
 405     fi
 406 }
 407 
 408 # show an ansi-styled BANNER-like line
 409 banner() { printf "\e[7m%-$(tput cols)s\e[0m\n" "$*"; }
 410 
 411 # emit a colored bar which can help visually separate different outputs
 412 bar() {
 413     [ "${1:-80}" -gt 0 ] && printf "\e[48;2;218;218;218m%${1:-80}s\e[0m\n" ""
 414 }
 415 
 416 # Breathe Header 3: add an empty line after the first one (the header),
 417 # then separate groups of 3 lines with empty lines between them
 418 bh3() {
 419     if { [ -p /dev/stdout ] || [ -t 1 ]; } && [ -e /usr/bin/stdbuf ]; then
 420         stdbuf -oL awk '(NR - 1) % 3 == 1 { print "" } 1' "$@"
 421     else
 422         awk '(NR - 1) % 3 == 1 { print "" } 1' "$@"
 423     fi
 424 }
 425 
 426 # Breathe Header 5: add an empty line after the first one (the header),
 427 # then separate groups of 5 lines with empty lines between them
 428 bh5() {
 429     if { [ -p /dev/stdout ] || [ -t 1 ]; } && [ -e /usr/bin/stdbuf ]; then
 430         stdbuf -oL awk '(NR - 1) % 5 == 1 { print "" } 1' "$@"
 431     else
 432         awk '(NR - 1) % 5 == 1 { print "" } 1' "$@"
 433     fi
 434 }
 435 
 436 # emit a line with a repeating block-like symbol in it
 437 blocks() { [ "${1:-80}" -gt 0 ] && printf "%${1:-80}s\n" "" | sed 's- -â–ˆ-g'; }
 438 
 439 # BOOK-like MANual, lays out `man` docs as pairs of side-by-side pages; uses
 440 # my tool `bsbs`
 441 bookman() {
 442     local w
 443     w="$(tput cols)"
 444     w="$((w / 2 - 4))"
 445     if [ "$w" -lt 65 ]; then
 446         w=65
 447     fi
 448     MANWIDTH="$w" man "$@" | bsbs 2
 449 }
 450 
 451 # split lines using the separator given, turning them into single-item lines
 452 breakdown() {
 453     local sep="${1:- }"
 454     [ $# -gt 0 ] && shift
 455     local command='awk'
 456     if { [ -p /dev/stdout ] || [ -t 1 ]; } && [ -e /usr/bin/stdbuf ]; then
 457         command='stdbuf -oL awk'
 458     fi
 459 
 460     ${command} -F "${sep}" '{ for (i = 1; i <= NF; i++) print $i }' "$@"
 461 }
 462 
 463 # CAlculator with Nice numbers runs my tool `ca` and colors results with
 464 # my tool `nn`, alternating styles to make long numbers easier to read
 465 can() {
 466     local arg
 467     for arg in "$@"; do
 468         [ $# -ge 2 ] && printf "\e[7m%s\e[0m\n" "${arg}" > /dev/stderr
 469         ca "${arg}" | nn
 470     done
 471 }
 472 
 473 # uppercase the first letter on each line, and lowercase all later letters
 474 capitalize() { sed -E 's-^(.*)-\L\1-; s-^(.)-\u\1-'; }
 475 
 476 # center-align lines of text, using the current screen width
 477 center() {
 478     awk -v width="$(tput cols)" '
 479         {
 480             gsub(/\r$/, "")
 481             lines[NR] = $0
 482             s = $0
 483             gsub(/\x1b\[[0-9;]*[A-Za-z]/, "", s) # ANSI style-changers
 484             l = length(s)
 485             if (maxlen < l) maxlen = l
 486         }
 487 
 488         END {
 489             n = (width - maxlen) / 2
 490             if (n % 1) n = n - (n % 1)
 491             fmt = sprintf("%%%ds%%s\n", (n > 0) ? n : 0)
 492             for (i = 1; i <= NR; i++) printf fmt, "", lines[i]
 493         }
 494     ' "$@"
 495 }
 496 
 497 # Colored Go Test on the folder given; uses my command `gbm`
 498 cgt() { go test "${@:-.}" 2>&1 | gbm '^ok' '^[-]* ?FAIL' '^\?'; }
 499 
 500 # Colored RipGrep ensures app `rg` emits colors when piped
 501 crg() {
 502     if [ -p /dev/stdout ] || [ -t 1 ]; then
 503         rg --line-buffered --color=always "${@:-.}"
 504     else
 505         rg --color=always "${@:-.}"
 506     fi
 507 }
 508 
 509 # Compile Rust Optimized
 510 cro() {
 511     rustc -C lto=true -C codegen-units=1 -C debuginfo=0 -C strip=symbols \
 512         -C opt-level=3 "$@"
 513 }
 514 
 515 # emit a line with a repeating cross-like symbol in it
 516 crosses() { [ "${1:-80}" -gt 0 ] && printf "%${1:-80}s\n" "" | sed 's- -×-g'; }
 517 
 518 # listen to streaming DANCE music
 519 dance() {
 520     printf "streaming \e[7mDance Wave Retro\e[0m\n"
 521     mpv --really-quiet https://retro.dancewave.online/retrodance.mp3
 522 }
 523 
 524 # emit a line with a repeating dash-like symbol in it
 525 dashes() { [ "${1:-80}" -gt 0 ] && printf "%${1:-80}s\n" "" | sed 's- -—-g'; }
 526 
 527 # remove commas in numbers, as well as leading dollar signs in numbers
 528 decomma() {
 529     sed -E 's-([0-9]{3}),-\1-g; s-([0-9]{1,2}),-\1-g; s-\$([0-9\.]+)-\1-g'
 530 }
 531 
 532 # remove indentations from lines
 533 dedent() {
 534     awk '
 535         {
 536             lines[NR] = $0
 537             if (match($0, /^ +/) && (n == 0 || n > RLENGTH)) n = RLENGTH
 538         }
 539 
 540         END {
 541             if (n == 0) {
 542                 for (i = 1; i <= NR; i++) print lines[i]
 543             } else {
 544                 for (i = 1; i <= NR; i++) print substr(lines[i], n + 1)
 545             }
 546         }
 547     ' "$@"
 548 }
 549 
 550 dehtmlify() {
 551     local command='awk'
 552     if { [ -p /dev/stdout ] || [ -t 1 ]; } && [ -e /usr/bin/stdbuf ]; then
 553         command='stdbuf -oL awk'
 554     fi
 555 
 556     ${command} '
 557         {
 558             gsub(/<\/?[^>]+>/, "")
 559             gsub(/&amp;/, "&")
 560             gsub(/&lt;/, "<")
 561             gsub(/&gt;/, ">")
 562             gsub(/^ +| *\r?$/, "")
 563             gsub(/  +/, " ")
 564             print
 565         }
 566     ' "$@"
 567 }
 568 
 569 # expand tabs each into up to the number of space given, or 4 by default
 570 detab() {
 571     local tabstop="${1:-4}"
 572     [ $# -gt 0 ] && shift
 573     if { [ -p /dev/stdout ] || [ -t 1 ]; } && [ -e /usr/bin/stdbuf ]; then
 574         stdbuf -oL expand -t "${tabstop}" "$@"
 575     else
 576         expand -t "${tabstop}" "$@"
 577     fi
 578 }
 579 
 580 # DIVide 2 numbers 3 ways, including the complement
 581 div() {
 582     awk -v a="${1:-1}" -v b="${2:-1}" '
 583         BEGIN {
 584             gsub(/_/, "", a)
 585             gsub(/_/, "", b)
 586             if (a > b) { c = a; a = b; b = c }
 587             c = 1 - a / b
 588             if (0 <= c && c <= 1) printf "%f\n%f\n%f\n", a / b, b / a, c
 589             else printf "%f\n%f\n", a / b, b / a
 590             exit
 591         }'
 592 }
 593 
 594 # emit a line with a repeating dot-like symbol in it
 595 dots() { [ "${1:-80}" -gt 0 ] && printf "%${1:-80}s\n" "" | sed 's- -·-g'; }
 596 
 597 # show the current Date and Time
 598 dt() {
 599     printf "\e[38;2;78;154;6m%s\e[0m  \e[38;2;52;101;164m%s\e[0m\n" \
 600         "$(date +'%a %b %d')" "$(date +%T)"
 601 }
 602 
 603 # show the current Date, Time, and a Calendar with the 3 `current` months
 604 dtc() {
 605     {
 606         # show the current date/time center-aligned
 607         printf "%20s\e[38;2;78;154;6m%s\e[0m  \e[38;2;52;101;164m%s\e[0m\n\n" \
 608             "" "$(date +'%a %b %d')" "$(date +%T)"
 609         # debian linux has a different `cal` app which highlights the day
 610         if [ -e /usr/bin/ncal ]; then
 611             # fix debian/ncal's weird way to highlight the current day
 612             ncal -C -3 | sed -E 's/_\x08(.+)_\x08([^ ]+)/\x1b\[7m\1\2\x1b\[0m/'
 613         else
 614             cal -3
 615         fi
 616     } | less -MKiCRS
 617 }
 618 
 619 # EDit RUN shell commands, using an interactive editor; uses my tool `leak`
 620 edrun() {
 621     # dash doesn't support the process-sub syntax
 622     # . <( micro -readonly true -filetype shell | leak --inv )
 623     micro -readonly true -filetype shell | leak --inv | . /dev/fd/0
 624 }
 625 
 626 # convert EURos into CAnadian Dollars, using the latest official exchange
 627 # rates from the bank of canada; during weekends, the latest rate may be
 628 # from a few days ago; the default amount of euros to convert is 1, when
 629 # not given
 630 eur2cad() {
 631     local url
 632     local site='https://www.bankofcanada.ca/valet/observations/group'
 633     local csv_rates="${site}/FX_RATES_DAILY/csv"
 634     url="${csv_rates}?start_date=$(date -d '3 days ago' +'%Y-%m-%d')"
 635     curl -s "${url}" | awk -F, -v amount="$(echo "${1:-1}" | sed 's-_--g')" '
 636         /EUR/ { for (i = 1; i <= NF; i++) if($i ~ /EUR/) j = i }
 637         END { gsub(/"/, "", $j); if (j != 0) printf "%.2f\n", amount * $j }
 638     '
 639 }
 640 
 641 # Fix Audio Duration on a separate copy of the file given
 642 fad() { ffmpeg -i "${1:-input.m4a}" -acodec copy "${2:-output.dat}"; }
 643 
 644 # get the first n lines, or 1 by default
 645 first() { head -n "${1:-1}" "${2:--}"; }
 646 
 647 # Field-Names AWK remembers field-positions by name, from the first input line
 648 fnawk() {
 649     local code="${1:-1}"
 650     [ $# -gt 0 ] && shift
 651 
 652     local buffering=''
 653     if { [ -p /dev/stdout ] || [ -t 1 ]; } && [ -e /usr/bin/stdbuf ]; then
 654         buffering='stdbuf -oL'
 655     fi
 656 
 657     ${buffering} awk -v OFS="\t" '
 658         NR == 1 {
 659             FS = /\t/ ? "\t" : " "
 660             $0 = $0
 661             for (i in names) delete names[i]
 662             for (i = 1; i <= NF; i++) names[$i] = i
 663             i = ""
 664         }
 665         { low = lower = tolower($0) }
 666         '"${code}"'
 667     ' "$@"
 668 }
 669 
 670 # start from the line number given, skipping all previous ones
 671 fromline() { tail -n +"${1:-1}" "${2:--}"; }
 672 
 673 # convert a mix of FeeT and INches into meters
 674 ftin() {
 675     local ft="${1:-0}"
 676     ft="$(echo "${ft}" | sed 's-_--g')"
 677     local in="${2:-0}"
 678     in="$(echo "${in}" | sed 's-_--g')"
 679     awk "BEGIN { print 0.3048 * ${ft} + 0.0254 * ${in}; exit }"
 680 }
 681 
 682 # Gawk Bignum Print
 683 gbp() { gawk --bignum "BEGIN { print $1; exit }"; }
 684 
 685 # glue/stick together various lines, only emitting a line-feed at the end; an
 686 # optional argument is the output-item-separator, which is empty by default
 687 glue() {
 688     local sep="${1:-}"
 689     [ $# -gt 0 ] && shift
 690     awk -v sep="${sep}" '
 691         NR > 1 { printf "%s", sep }
 692         { gsub(/\r/, ""); printf "%s", $0 }
 693         END { if (NR > 0) print "" }
 694     ' "$@"
 695 }
 696 
 697 # GO Build Stripped: a common use-case for the go compiler
 698 gobs() { go build -ldflags "-s -w" -trimpath "$@"; }
 699 
 700 # GO DEPendencieS: show all dependencies in a go project
 701 godeps() { go list -f '{{ join .Deps "\n" }}' "$@"; }
 702 
 703 # GO IMPortS: show all imports in a go project
 704 goimps() { go list -f '{{ join .Imports "\n" }}' "$@"; }
 705 
 706 # show Help laid out on 2 side-by-side columns; uses my tool `bsbs`
 707 h2() { naman "$@" | bsbs 2; }
 708 
 709 # Highlighted-style ECHO
 710 hecho() { printf "\e[7m%s\e[0m\n" "$*"; }
 711 
 712 # show each byte as a pair of HEXadecimal (base-16) symbols
 713 hexify() {
 714     cat "$@" | od -v -x -A n | awk '
 715         { gsub(/ +/, ""); printf "%s", $0 }
 716         END { print "" }
 717     '
 718 }
 719 
 720 # Help Me Remember my custom shell commands
 721 hmr() {
 722     local cmd="bat"
 723     # debian linux uses a different name for the `bat` app
 724     if [ -e /usr/bin/batcat ]; then
 725         cmd="batcat"
 726     fi
 727 
 728     "$cmd" \
 729         --style=plain,header,numbers --theme='Monokai Extended Light' \
 730         --wrap=never --color=always "$(which clam)" |
 731             sed -e 's-\x1b\[38;5;70m-\x1b[38;5;28m-g' \
 732                 -e 's-\x1b\[38;5;214m-\x1b[38;5;208m-g' \
 733                 -e 's-\x1b\[38;5;243m-\x1b[38;5;103m-g' \
 734                 -e 's-\x1b\[38;5;238m-\x1b[38;5;245m-g' \
 735                 -e 's-\x1b\[38;5;228m-\x1b[48;5;228m-g' |
 736                 less -MKiCRS
 737 }
 738 
 739 # convert seconds into a colon-separated Hours-Minutes-Seconds triple
 740 hms() {
 741     echo "${@:-0}" | sed -E 's-_--g; s- +-\n-g' | awk '
 742         /./ {
 743             x = $0
 744             h = (x - x % 3600) / 3600
 745             m = (x % 3600) / 60
 746             s = x % 60
 747             printf "%02d:%02d:%05.2f\n", h, m, s
 748         }
 749     '
 750 }
 751 
 752 # find all hyperlinks inside HREF attributes in the input text
 753 href() {
 754     local arg
 755     local awk_cmd='awk'
 756     local grep_cmd='grep'
 757     if [ -p /dev/stdout ] || [ -t 1 ]; then
 758         grep_cmd='grep --line-buffered'
 759         if [ -e /usr/bin/stdbuf ]; then
 760             awk_cmd='stdbuf -oL awk'
 761         fi
 762     fi
 763 
 764     for arg in "${@:--}"; do
 765         ${grep_cmd} -i -E -o 'href="[^"]+"' "${arg}"
 766     done | ${awk_cmd} '{ gsub(/^href="|"\r?$/, ""); print }'
 767 }
 768 
 769 # avoid/ignore lines which case-insensitively match any of the regexes given
 770 iavoid() {
 771     local command='awk'
 772     if { [ -p /dev/stdout ] || [ -t 1 ]; } && [ -e /usr/bin/stdbuf ]; then
 773         command='stdbuf -oL awk'
 774     fi
 775 
 776     ${command} '
 777         BEGIN {
 778             if (IGNORECASE == "") {
 779                 m = "this variant of AWK lacks case-insensitive regex-matching"
 780                 print(m) > "/dev/stderr"
 781                 exit 125
 782             }
 783             IGNORECASE = 1
 784 
 785             for (i = 1; i < ARGC; i++) {
 786                 e[i] = ARGV[i]
 787                 delete ARGV[i]
 788             }
 789         }
 790 
 791         {
 792             for (i = 1; i < ARGC; i++) if ($0 ~ e[i]) next
 793             print
 794             got++
 795         }
 796 
 797         END { exit(got == 0) }
 798     ' "${@:-^\r?$}"
 799 }
 800 
 801 # ignore command in a pipe: this allows quick re-editing of pipes, while
 802 # still leaving signs of previously-used steps, as a memo
 803 idem() { cat; }
 804 
 805 # ignore command in a pipe: this allows quick re-editing of pipes, while
 806 # still leaving signs of previously-used steps, as a memo
 807 ignore() { cat; }
 808 
 809 # only keep lines which case-insensitively match any of the regexes given
 810 imatch() {
 811     local command='awk'
 812     if { [ -p /dev/stdout ] || [ -t 1 ]; } && [ -e /usr/bin/stdbuf ]; then
 813         command='stdbuf -oL awk'
 814     fi
 815 
 816     ${command} '
 817         BEGIN {
 818             if (IGNORECASE == "") {
 819                 m = "this variant of AWK lacks case-insensitive regex-matching"
 820                 print(m) > "/dev/stderr"
 821                 exit 125
 822             }
 823             IGNORECASE = 1
 824 
 825             for (i = 1; i < ARGC; i++) {
 826                 e[i] = ARGV[i]
 827                 delete ARGV[i]
 828             }
 829         }
 830 
 831         {
 832             for (i = 1; i < ARGC; i++) {
 833                 if ($0 ~ e[i]) {
 834                     print
 835                     got++
 836                     next
 837                 }
 838             }
 839         }
 840 
 841         END { exit(got == 0) }
 842     ' "${@:-[^\r]}"
 843 }
 844 
 845 # start each non-empty line with extra n spaces
 846 indent() {
 847     local command='awk'
 848     if { [ -p /dev/stdout ] || [ -t 1 ]; } && [ -e /usr/bin/stdbuf ]; then
 849         command='stdbuf -oL awk'
 850     fi
 851 
 852     ${command} '
 853         BEGIN {
 854             n = ARGV[1] + 0
 855             delete ARGV[1]
 856             fmt = sprintf("%%%ds%%s\n", (n > 0) ? n : 0)
 857         }
 858 
 859         /^\r?$/ { print ""; next }
 860         { gsub(/\r$/, ""); printf(fmt, "", $0) }
 861     ' "$@"
 862 }
 863 
 864 # emit each word-like item from each input line on its own line; when a file
 865 # has tabs on its first line, items are split using tabs alone, which allows
 866 # items to have spaces in them
 867 items() {
 868     local command='awk'
 869     if { [ -p /dev/stdout ] || [ -t 1 ]; } && [ -e /usr/bin/stdbuf ]; then
 870         command='stdbuf -oL awk'
 871     fi
 872 
 873     ${command} '
 874         FNR == 1 { FS = /\t/ ? "\t" : " "; $0 = $0 }
 875         { gsub(/\r$/, ""); for (i = 1; i <= NF; i++) print $i }
 876     ' "$@"
 877 }
 878 
 879 # listen to streaming JAZZ music
 880 jazz() {
 881     printf "streaming \e[7mSmooth Jazz Instrumental\e[0m\n"
 882     mpv --quiet https://stream.zeno.fm/00rt0rdm7k8uv
 883 }
 884 
 885 # show a `dad` JOKE from the web, sometimes even a very funny one
 886 joke() {
 887     curl --silent --show-error https://icanhazdadjoke.com | fold -s |
 888         awk '{ gsub(/ *\r?$/, ""); print }'
 889 }
 890 
 891 # JSON Query Lines turns JSON top-level arrays into multiple individually-JSON
 892 # lines using the `jq` app, keeping all other top-level values as single line
 893 # JSON outputs
 894 jql() {
 895     local code="${1:-.}"
 896     [ $# -gt 0 ] && shift
 897     jq -c -M "${code} | .[]" "$@"
 898 }
 899 
 900 # JSON Query Keys runs `jq` to find all unique key-combos from tabular JSON
 901 jqk() {
 902     local code="${1:-.}"
 903     [ $# -gt 0 ] && shift
 904     jq -c -M "${code} | .[] | keys" "$@" | awk '!c[$0]++'
 905 }
 906 
 907 # JSON Keys finds all unique key-combos from tabular JSON data; uses my tools
 908 # `jsonl` and `tjp`
 909 jsonk() {
 910     tjp '[e.keys() for e in v] if isinstance(v, (list, tuple)) else v.keys()' \
 911         "${1:--}" | jsonl | awk '!c[$0]++'
 912 }
 913 
 914 # JSON Table, turns TSV tables into tabular JSON, where valid-JSON values are
 915 # auto-parsed into numbers, booleans, etc...; uses my tools `jsons` and `tjp`
 916 jsont() {
 917     jsons "$@" | tjp \
 918         '[{k: rescue(lambda: loads(v), v) for k, v in e.items()} for e in v]'
 919 }
 920 
 921 # emit the given number of random/junk bytes, or 1024 junk bytes by default
 922 junk() { head -c "$(echo "${1:-1024}" | sed 's-_--g')" /dev/urandom; }
 923 
 924 # get the last n lines, or 1 by default
 925 last() { tail -n "${1:-1}" "${2:--}"; }
 926 
 927 # convert pounds (LB) into kilograms
 928 lb() {
 929     echo "${@:-1}" | sed -E 's-_--g; s- +-\n-g' |
 930         awk '/./ { printf "%.2f\n", 0.45359237 * $0 }'
 931 }
 932 
 933 # convert a mix of pounds (LB) and weight-ounces (OZ) into kilograms
 934 lboz() {
 935     local lb="${1:-0}"
 936     lb="$(echo "${lb}" | sed 's-_--g')"
 937     local oz="${2:-0}"
 938     oz="$(echo "${oz}" | sed 's-_--g')"
 939     awk "BEGIN { print 0.45359237 * ${lb} + 0.028349523 * ${oz}; exit }"
 940 }
 941 
 942 # limit stops at the first n bytes, or 1024 bytes by default
 943 limit() { head -c "$(echo "${1:-1024}" | sed 's-_--g')" "${2:--}"; }
 944 
 945 # ensure LINES are never accidentally joined across files, by always emitting
 946 # a line-feed at the end of each line
 947 lines() {
 948     if { [ -p /dev/stdout ] || [ -t 1 ]; } && [ -e /usr/bin/stdbuf ]; then
 949         stdbuf -oL awk 1 "$@"
 950     else
 951         awk 1 "$@"
 952     fi
 953 }
 954 
 955 # regroup adjacent lines into n-item tab-separated lines
 956 lineup() {
 957     local command='awk'
 958     if { [ -p /dev/stdout ] || [ -t 1 ]; } && [ -e /usr/bin/stdbuf ]; then
 959         command='stdbuf -oL awk'
 960     fi
 961 
 962     local n="${1:-0}"
 963     [ $# -gt 0 ] && shift
 964 
 965     if [ "$n" -le 0 ]; then
 966         ${command} '
 967             NR > 1 { printf "\t" }
 968             { printf "%s", $0 }
 969             END { if (NR > 0) print "" }
 970         ' "$@"
 971         return $?
 972     fi
 973 
 974     ${command} -v n="$n" '
 975         NR % n != 1 && n > 1 { printf "\t" }
 976         { printf "%s", $0 }
 977         NR % n == 0 { print "" }
 978         END { if (NR % n != 0) print "" }
 979     ' "$@"
 980 }
 981 
 982 # LiSt files, showing how many 4K-sized storage blocks they use
 983 ls4k() { ls -s --block-size=4096 "$@"; }
 984 
 985 # LiSt MAN pages
 986 lsman() { man -k "${1:-.}"; }
 987 
 988 # MARK the current tab with the message given, followed by the current folder;
 989 # works only on the `bash` shell
 990 mark() {
 991     if [ $# -eq 0 ]; then
 992         PS1="\[\e[0m\e]0;\w\a\$ "
 993     else
 994         PS1="\[\e[0m\e]0;${*} \w\a\$ "
 995     fi
 996 }
 997 
 998 marklinks() {
 999     local re='https?://[A-Za-z0-9+_.:%-]+(/[A-Za-z0-9+_.%/,#?&=-]*)*'
1000     sed -E 's-('"${re}"')-\x1b]8;;\1\x1b\\\1\x1b]8;;\x1b\\-g' "$@"
1001 }
1002 
1003 # Multi-Core MAKE runs `make` using all cores
1004 mcmake() { make -j "$(nproc)" "$@"; }
1005 
1006 # merge stderr into stdout, which is useful for piped commands
1007 merrge() { "${@:-cat /dev/null}" 2>&1; }
1008 
1009 metajq() {
1010     # https://github.com/stedolan/jq/issues/243#issuecomment-48470943
1011     jq -r -M '
1012         [
1013             path(..) |
1014             map(if type == "number" then "[]" else tostring end) |
1015             join(".") | split(".[]") | join("[]")
1016         ] | unique | map("." + .) | .[]
1017     ' "$@"
1018 }
1019 
1020 # Make In Folder, also showing time and max memory used
1021 mif() {
1022     local f='real %e    user %U    sys %S    mem %M    exit %x'
1023     local folder
1024     folder="${1:-.}"
1025     [ $# -gt 0 ] && shift
1026     env -C "${folder}" /usr/bin/time -f "$f" make "$@"
1027 }
1028 
1029 # MINimize DECimalS ignores all trailing decimal zeros in numbers, and even
1030 # the decimal dots themselves, when decimals in a number are all zeros
1031 # mindecs() {
1032 #     local cmd='sed -E'
1033 #     if { [ -p /dev/stdout ] || [ -t 1 ]; } && [ -e /usr/bin/stdbuf ]; then
1034 #         cmd='sed -E -u'
1035 #     fi
1036 #     ${cmd} 's-([0-9]+)\.0+\W-\1-g; s-([0-9]+\.[0-9]*[1-9])0+\W-\1-g' "$@"
1037 # }
1038 
1039 # Number all lines counting from 0, using a tab right after each line number
1040 n0() {
1041     if { [ -p /dev/stdout ] || [ -t 1 ]; } && [ -e /usr/bin/stdbuf ]; then
1042         stdbuf -oL nl -b a -w 1 -v 0 "$@"
1043     else
1044         nl -b a -w 1 -v 0 "$@"
1045     fi
1046 }
1047 
1048 # Number all lines counting from 1, using a tab right after each line number
1049 n1() {
1050     if { [ -p /dev/stdout ] || [ -t 1 ]; } && [ -e /usr/bin/stdbuf ]; then
1051         stdbuf -oL nl -b a -w 1 -v 1 "$@"
1052     else
1053         nl -b a -w 1 -v 1 "$@"
1054     fi
1055 }
1056 
1057 # NArrow MANual, keeps `man` narrow, even if the window/tab is wide when run
1058 naman() {
1059     local w
1060     w="$(tput cols)"
1061     w="$((w / 2 - 4))"
1062     if [ "$w" -lt 80 ]; then
1063         w=80
1064     fi
1065     MANWIDTH="$w" man "$@"
1066 }
1067 
1068 # Not AND sorts its 2 inputs, then finds lines not in common
1069 nand() {
1070     # comm -3 <(sort "$1") <(sort "$2")
1071     # dash doesn't support the process-sub syntax
1072     (sort "$1" | (sort "$2" | (comm -3 /dev/fd/3 /dev/fd/4) 4<&0) 3<&0)
1073 }
1074 
1075 # Nice DEFine dictionary-defines the words given, using an online service
1076 ndef() {
1077     local arg
1078     local gap=0
1079     local options='-MKiCRS'
1080 
1081     if [ $# -eq 0 ]; then
1082         printf "\e[38;2;204;0;0mndef: no words given\e[0m\n" >&2
1083         return 1
1084     fi
1085 
1086     if [ $# -eq 1 ]; then
1087         options='--header=1 -MKiCRS'
1088     fi
1089 
1090     for arg in "$@"; do
1091         [ "${gap}" -gt 0 ] && printf "\n"
1092         gap=1
1093         printf "\e[7m%-80s\e[0m\n" "${arg}"
1094         curl --silent "dict://dict.org/d:${arg}" | awk '
1095             { gsub(/\r$/, "") }
1096             /^151 / {
1097                 printf "\x1b[38;2;52;101;164m%s\x1b[0m\n", $0
1098                 next
1099             }
1100             /^[1-9][0-9]{2} / {
1101                 printf "\x1b[38;2;128;128;128m%s\x1b[0m\n", $0
1102                 next
1103             }
1104             1
1105         '
1106     done | less ${options}
1107 }
1108 
1109 # listen to streaming NEW WAVE music
1110 newwave() {
1111     printf "streaming \e[7mNew Wave radio\e[0m\n"
1112     mpv --quiet https://puma.streemlion.com:2910/stream
1113 }
1114 
1115 # Nice Json Query Lines colors JSONL data using the `jq` app
1116 njql() {
1117     local code="${1:-.}"
1118     [ $# -gt 0 ] && shift
1119     jq -c -C "${code} | .[]" "$@"
1120 }
1121 
1122 # empty the clipboard
1123 noclip() { wl-copy --clear; }
1124 
1125 # show the current date and time
1126 now() { date +'%Y-%m-%d %H:%M:%S'; }
1127 
1128 # Nice Print Awk result; uses my tool `nn`
1129 npa() {
1130     local arg
1131     for arg in "$@"; do
1132         awk "BEGIN { print(${arg}); exit }"
1133     done | nn
1134 }
1135 
1136 # Nice Print Python result; uses my tool `nn`
1137 npp() {
1138     local arg
1139     for arg in "$@"; do
1140         python -c "print(${arg})"
1141     done | nn
1142 }
1143 
1144 # Nice Size, using my tool `nn`
1145 ns() { wc -c "$@" | nn; }
1146 
1147 # emit nothing to output and/or discard everything from input
1148 null() { [ $# -gt 0 ] && "$@" > /dev/null; }
1149 
1150 # Print Python result
1151 pp() {
1152     local arg
1153     for arg in "$@"; do
1154         python -c "print(${arg})"
1155     done
1156 }
1157 
1158 # PRecede (input) ECHO, prepends a first line to stdin lines
1159 precho() { echo "$@" && cat /dev/stdin; }
1160 
1161 # LABEL/precede data with an ANSI-styled line
1162 prelabel() { printf "\e[7m%-*s\e[0m\n" "$(($(tput cols) - 2))" "$*"; cat -; }
1163 
1164 # PREcede (input) MEMO, prepends a first highlighted line to stdin lines
1165 prememo() { printf "\e[7m%s\e[0m\n" "$*"; cat -; }
1166 
1167 # start by joining all arguments given as a tab-separated-items line of output,
1168 # followed by all lines from stdin verbatim
1169 pretsv() {
1170     awk '
1171         BEGIN {
1172             for (i = 1; i < ARGC; i++) {
1173                 if (i > 1) printf "\t"
1174                 printf "%s", ARGV[i]
1175             }
1176             if (ARGC > 1) print ""
1177             exit
1178         }
1179     ' "$@"
1180     cat -
1181 }
1182 
1183 # Plain RipGrep
1184 prg() {
1185     if [ -p /dev/stdout ] || [ -t 1 ]; then
1186         rg --line-buffered --color=never "${@:-.}"
1187     else
1188         rg --color=never "${@:-.}"
1189     fi
1190 }
1191 
1192 # Quiet MPV
1193 # qmpv() { mpv --quiet "${@:--}"; }
1194 
1195 # Quiet MPV
1196 qmpv() { mpv --really-quiet "${@:--}"; }
1197 
1198 # ignore stderr, without any ugly keyboard-dancing
1199 quiet() { "$@" 2> /dev/null; }
1200 
1201 # keep only lines between the 2 line numbers given, inclusively
1202 rangelines() {
1203     { [ $# -eq 2 ] || [ $# -eq 3 ]; } && [ "${1}" -le "${2}" ] && {
1204         tail -n +"${1}" "${3:--}" | head -n $(("${2}" - "${1}" + 1))
1205     }
1206 }
1207 
1208 # RANdom MANual page
1209 ranman() {
1210     find "/usr/share/man/man${1:-1}" -type f | shuf -n 1 | xargs basename |
1211         sed 's-\.gz$--' | xargs man
1212 }
1213 
1214 # REPeat STRing emits a line with a repeating string in it, given both a
1215 # string and a number in either order
1216 repstr() {
1217     awk '
1218         BEGIN {
1219             if (ARGV[2] ~ /^[+-]?[0-9]+$/) {
1220                 symbol = ARGV[1]
1221                 times = ARGV[2] + 0
1222             } else {
1223                 symbol = ARGV[2]
1224                 times = ARGV[1] + 0
1225             }
1226 
1227             if (times < 0) exit
1228             if (symbol == "") symbol = "-"
1229             s = sprintf("%*s", times, "")
1230             gsub(/ /, symbol, s)
1231             print s
1232             exit
1233         }
1234     ' "$@"
1235 }
1236 
1237 # show a RULER-like width-measuring line
1238 ruler() {
1239     [ "${1:-80}" -gt 0 ] && printf "%${1:-80}s\n" "" | sed -E \
1240         's- {10}-····╵····│-g; s- -·-g; s-·····-····╵-'
1241 }
1242 
1243 # SystemCTL; `sysctl` is already taken for a separate/unrelated app
1244 sctl() { systemctl "$@" 2>&1 | less -MKiCRS; }
1245 
1246 # show a unique-looking SEParator line; useful to run between commands
1247 # which output walls of text
1248 sep() {
1249     [ "${1:-80}" -gt 0 ] &&
1250         printf "\e[48;2;218;218;218m%${1:-80}s\e[0m\n" "" | sed 's- -·-g'
1251 }
1252 
1253 # webSERVE files in a folder as localhost, using the port number given, or
1254 # port 8080 by default
1255 serve() {
1256     if [ -d "$1" ]; then
1257         printf "\e[7mserving files in %s\e[0m\n" "$1" >&2
1258         python3 -m http.server -d "$1" "${2:-8080}"
1259     else
1260         printf "\e[7mserving files in %s\e[0m\n" "${2:-$(pwd)}" >&2
1261         python3 -m http.server -d "${2:-$(pwd)}" "${1:-8080}"
1262     fi
1263 }
1264 
1265 # SET DIFFerence sorts its 2 inputs, then finds lines not in the 2nd input
1266 setdiff() {
1267     # comm -23 <(sort "$1") <(sort "$2")
1268     # dash doesn't support the process-sub syntax
1269     (sort "$1" | (sort "$2" | (comm -23 /dev/fd/3 /dev/fd/4) 4<&0) 3<&0)
1270 }
1271 
1272 # SET INtersection, sorts its 2 inputs, then finds common lines
1273 setin() {
1274     # comm -12 <(sort "$1") <(sort "$2")
1275     # dash doesn't support the process-sub syntax
1276     (sort "$1" | (sort "$2" | (comm -12 /dev/fd/3 /dev/fd/4) 4<&0) 3<&0)
1277 }
1278 
1279 # SET SUBtraction sorts its 2 inputs, then finds lines not in the 2nd input
1280 setsub() {
1281     # comm -23 <(sort "$1") <(sort "$2")
1282     # dash doesn't support the process-sub syntax
1283     (sort "$1" | (sort "$2" | (comm -23 /dev/fd/3 /dev/fd/4) 4<&0) 3<&0)
1284 }
1285 
1286 # Show Files (and folders), coloring folders and links
1287 sf() {
1288     local arg
1289     local gap=0
1290     local options='-MKiCRS'
1291 
1292     if [ $# -le 1 ]; then
1293         options='--header=1 -MKiCRS'
1294     fi
1295 
1296     for arg in "${@:-.}"; do
1297         [ "${gap}" -gt 0 ] && printf "\n"
1298         printf "\e[7m%s\e[0m\n\n" "$(realpath "${arg}")"
1299         gap=1
1300 
1301         ls -al --file-type --color=never --time-style iso "${arg}" | awk '
1302             BEGIN {
1303                 drep = "\x1b[38;2;0;135;255m\x1b[48;2;228;228;228m&\x1b[0m"
1304                 lrep = "\x1b[38;2;0;135;95m\x1b[48;2;228;228;228m&\x1b[0m"
1305             }
1306 
1307             NR < 4 { next }
1308             (NR - 3) % 5 == 1 && (NR - 3) > 1 { print "" }
1309 
1310             {
1311                 gsub(/^(d[rwx-]+)/, drep)
1312                 gsub(/^(l[rwx-]+)/, lrep)
1313                 printf "%6d  %s\n", NR - 3, $0
1314             }
1315         '
1316     done | less ${options}
1317 }
1318 
1319 # run apps in color-mode, using the popular option `--color=always`
1320 shine() {
1321     local cmd="$1"
1322     [ $# -gt 0 ] && shift
1323     "${cmd}" --color=always "$@"
1324 }
1325 
1326 # skip the first n lines, or the 1st line by default
1327 skip() { tail -n +$(("${1:-1}" + 1)) "${2:--}"; }
1328 
1329 # skip the last n lines, or the last line by default
1330 skiplast() { head -n -"${1:-1}" "${2:--}"; }
1331 
1332 # SLOW/delay lines from the standard-input, waiting the number of seconds
1333 # given for each line, or waiting 1 second by default
1334 slow() {
1335     local seconds="${1:-1}"
1336     [ $# -gt 0 ] && shift
1337     (
1338         IFS="$(printf "\n")"
1339         awk 1 "$@" | while read -r line; do
1340             sleep "${seconds}"
1341             printf "%s\n" "${line}"
1342         done
1343     )
1344 }
1345 
1346 # Show Latest Podcasts, using my tools `podfeed` and `si`
1347 slp() {
1348     local title
1349     title="Latest Podcast Episodes as of $(date +'%F %T')"
1350     podfeed -title "${title}" "$@" | si
1351 }
1352 
1353 # emit the first line as is, sorting all lines after that, using the
1354 # `sort` command, passing all/any arguments/options to it
1355 sortrest() {
1356     awk -v sort="sort $*" '
1357         FNR == 1 { gsub(/^\xef\xbb\xbf/, "") }
1358         { gsub(/\r$/, "") }
1359         NR == 1 { print; fflush() }
1360         NR >= 2 { print | sort }
1361     '
1362 }
1363 
1364 # SORt Tab-Separated Values: emit the first line as is, sorting all lines after
1365 # that, using the `sort` command in TSV (tab-separated values) mode, passing
1366 # all/any arguments/options to it
1367 sortsv() {
1368     awk -v sort="sort -t \"$(printf '\t')\" $*" '
1369         FNR == 1 { gsub(/^\xef\xbb\xbf/, "") }
1370         { gsub(/\r$/, "") }
1371         NR == 1 { print; fflush() }
1372         NR >= 2 { print | sort }
1373     '
1374 }
1375 
1376 # emit a line with the number of spaces given in it
1377 spaces() { [ "${1:-80}" -gt 0 ] && printf "%${1:-80}s\n" ""; }
1378 
1379 # SQUeeze horizontal spaces and STOMP vertical gaps
1380 squomp() {
1381     local command='awk'
1382     if { [ -p /dev/stdout ] || [ -t 1 ]; } && [ -e /usr/bin/stdbuf ]; then
1383         command='stdbuf -oL awk'
1384     fi
1385 
1386     ${command} '
1387         FNR == 1 { gsub(/^\xef\xbb\xbf/, "") }
1388         /^\r?$/ { empty = 1; next }
1389         empty { if (n > 0) print ""; empty = 0 }
1390 
1391         {
1392             gsub(/^ +| *\r?$/, "")
1393             gsub(/ *\t */, "\t")
1394             gsub(/  +/, " ")
1395             print; n++
1396         }
1397     ' "$@"
1398 }
1399 
1400 substr() {
1401     local command='awk'
1402     if { [ -p /dev/stdout ] || [ -t 1 ]; } && [ -e /usr/bin/stdbuf ]; then
1403         command='stdbuf -oL awk'
1404     fi
1405     if [ $# -lt 2 ]; then
1406         printf "missing 1-based start index, and substring length\n" >&2
1407         exit 1
1408     fi
1409 
1410     ${command} '{ print substr($0, '"$1"', '"$2"') }'
1411 }
1412 
1413 # TAC Lines outputs input-lines in reverse order, last one first, and so on...
1414 tacl() {
1415     awk '
1416         { gsub(/\r$/, ""); lines[NR] = $0 }
1417         END { for (i = NR; i >= 1; i--) print lines[i] }
1418     ' "$@"
1419 }
1420 
1421 # Simulate the cadence of old-fashioned TELETYPE machines
1422 teletype() {
1423     awk '
1424         {
1425             gsub(/\r$/, "")
1426 
1427             n = length($0)
1428             for (i = 1; i <= n; i++) {
1429                 if (code = system("sleep 0.015")) exit code
1430                 printf "%s", substr($0, i, 1); fflush()
1431             }
1432 
1433             if (code = system("sleep 0.75")) exit code
1434             print ""; fflush()
1435         }
1436 
1437         # END { if (NR > 0 && code != 0) print "" }
1438     ' "$@"
1439 }
1440 
1441 # TINY GO Build Optimized: a common use-case for the tinygo compiler
1442 tinygobo() { tinygo build -no-debug -opt=2 "$@"; }
1443 
1444 # Timed Make, also showing max memory used
1445 tm() {
1446     local f='real %e    user %U    sys %S    mem %M    exit %x'
1447     /usr/bin/time -f "$f" make "$@"
1448 }
1449 
1450 # show current date in a specifc format
1451 today() { date +'%Y-%m-%d %a %b %d'; }
1452 
1453 # get the first n lines, or 1 by default
1454 toline() { head -n "${1:-1}" "${2:--}"; }
1455 
1456 # get the processes currently using the most cpu
1457 topcpu() {
1458     local n="${1:-10}"
1459     [ "$n" -gt 0 ] && ps aux | awk '
1460         NR == 1 { print; fflush() }
1461         NR > 1 { print | "sort -rnk3,3" }
1462     ' | head -n "$(("$n" + 1))"
1463 }
1464 
1465 # get the processes currently using the most memory
1466 topmemory() {
1467     local n="${1:-10}"
1468     [ "$n" -gt 0 ] && ps aux | awk '
1469         NR == 1 { print; fflush() }
1470         NR > 1 { print | "sort -rnk6,6" }
1471     ' | head -n "$(("$n" + 1))"
1472 }
1473 
1474 # only keep UNIQUE lines, keeping them in their original order
1475 unique() {
1476     local command='awk'
1477     if { [ -p /dev/stdout ] || [ -t 1 ]; } && [ -e /usr/bin/stdbuf ]; then
1478         command='stdbuf -oL awk'
1479     fi
1480 
1481     ${command} '
1482         BEGIN { for (i = 1; i < ARGC; i++) if (f[ARGV[i]]++) delete ARGV[i] }
1483         !c[$0]++
1484     ' "$@"
1485 }
1486 
1487 # fix lines, ignoring leading UTF-8_BOMs (byte-order-marks) on each input's
1488 # first line, turning all end-of-line CRLF byte-pairs into single line-feeds,
1489 # and ensuring each input's last line ends with a line-feed; trailing spaces
1490 # are also ignored
1491 unixify() {
1492     local command='awk'
1493     if { [ -p /dev/stdout ] || [ -t 1 ]; } && [ -e /usr/bin/stdbuf ]; then
1494         command='stdbuf -oL awk'
1495     fi
1496 
1497     ${command} '
1498         FNR == 1 { gsub(/^\xef\xbb\xbf/, "") }
1499         { gsub(/ *\r?$/, ""); print }
1500     ' "$@"
1501 }
1502 
1503 # skip the first/leading n bytes
1504 unleaded() { tail -c +$(("$1" + 1)) "${2:--}"; }
1505 
1506 # go UP n folders, or go up 1 folder by default
1507 up() {
1508     if [ "${1:-1}" -le 0 ]; then
1509         cd .
1510     else
1511         cd "$(printf "%${1:-1}s" "" | sed 's- -../-g')" || return $?
1512     fi
1513 }
1514 
1515 # convert United States Dollars into CAnadian Dollars, using the latest
1516 # official exchange rates from the bank of canada; during weekends, the
1517 # latest rate may be from a few days ago; the default amount of usd to
1518 # convert is 1, when not given
1519 usd2cad() {
1520     local url
1521     local site='https://www.bankofcanada.ca/valet/observations/group'
1522     local csv_rates="${site}/FX_RATES_DAILY/csv"
1523     url="${csv_rates}?start_date=$(date -d '3 days ago' +'%Y-%m-%d')"
1524     curl -s "${url}" | awk -F, -v amount="$(echo "${1:-1}" | sed 's-_--g')" '
1525         /USD/ { for (i = 1; i <= NF; i++) if($i ~ /USD/) j = i }
1526         END { gsub(/"/, "", $j); if (j != 0) printf "%.2f\n", amount * $j }
1527     '
1528 }
1529 
1530 # View Nice Table / Very Nice Table; uses my tool `ncol`
1531 # vnt() {
1532 #     ncol "$@" | awk '
1533 #         (NR - 1) % 5 == 1 { print "" }
1534 #         { printf "%6d  %s\n", NR - 1, $0 }
1535 #     ' | { less -MKiCRS --header=1 2> /dev/null || cat; }
1536 # }
1537 
1538 # View Nice Table / Very Nice Table; uses my tool `ncol`
1539 vnt() {
1540     awk '{ printf "%d\t%s\n", NR - 1, $0 }' "$@" | ncol | awk '
1541         NR == 1 || (NR - 1) % 5 == 0 {
1542             gsub(/\x1b\[0m/, "\x1b[0m\x1b[4m")
1543             printf("\x1b[4m%s\x1b[0m\n", $0)
1544             next
1545         }
1546         1
1547     ' | { less -MKiCRS --header=1 2> /dev/null || cat; }
1548 }
1549 
1550 # What Are These (?) shows what the names given to it are/do
1551 wat() {
1552     local arg
1553     local gap=0
1554     local less_options='-MKiCRS'
1555 
1556     if [ $# -eq 0 ]; then
1557         echo "$0"
1558         return 0
1559     fi
1560 
1561     if [ $# -lt 2 ]; then
1562         less_options='-MKiCRS --header=1'
1563     fi
1564 
1565     for arg in "$@"; do
1566         [ "${gap}" -gt 0 ] && printf "\n"
1567         gap=1
1568         printf "\e[7m%-80s\e[0m\n" "${arg}"
1569 
1570         while alias "${arg}" > /dev/null 2> /dev/null; do
1571             arg="$(alias "${arg}" | sed -E "s-^[^=]+=['\"](.+)['\"]\$-\\1-")"
1572         done
1573 
1574         if echo "${arg}" | grep -q ' '; then
1575             printf "%s\n" "${arg}"
1576             continue
1577         fi
1578 
1579         if declare -f "${arg}"; then
1580             continue
1581         fi
1582 
1583         if which "${arg}" > /dev/null 2> /dev/null; then
1584             which "${arg}"
1585             continue
1586         fi
1587 
1588         printf "\e[38;2;204;0;0m%s not found\e[0m\n" "${arg}"
1589     done | { less -MKiCRS ${less_options} 2> /dev/null || cat; }
1590 }
1591 
1592 # find all WEB/hyperLINKS (https:// and http://) in the input text
1593 weblinks() {
1594     local arg
1595     local re='https?://[A-Za-z0-9+_.:%-]+(/[A-Za-z0-9+_.%/,#?&=-]*)*'
1596     local grep_cmd='grep'
1597     if [ -p /dev/stdout ] || [ -t 1 ]; then
1598         grep_cmd='grep --line-buffered'
1599     fi
1600 
1601     for arg in "${@:--}"; do
1602         ${grep_cmd} -i -E -o "${re}" "${arg}"
1603     done
1604 }
1605 
1606 # recursively find all files with trailing spaces/CRs
1607 whichtrails() {
1608     if [ -p /dev/stdout ] || [ -t 1 ]; then
1609         rg --line-buffered  -c '[ \r]+$' "${@:-.}"
1610     else
1611         rg -c '[ \r]+$' "${@:-.}"
1612     fi
1613 }
1614 
1615 # turn all wsl/unix-style full-paths into WINdows-style full-PATHS
1616 winpaths() { sed -E 's-/mnt/(.)/-\u\1:/-' "$@"; }
1617 
1618 # XARGS Lines, runs `xargs` using whole lines as extra arguments
1619 xargsl() {
1620     if { [ -p /dev/stdout ] || [ -t 1 ]; } && [ -e /usr/bin/stdbuf ]; then
1621         stdbuf -oL awk -v ORS='\000' '
1622             FNR == 1 { gsub(/^\xef\xbb\xbf/, "") }
1623             { gsub(/\r$/, ""); print }
1624         ' | stdbuf -oL xargs -0 "$@"
1625     else
1626         awk -v ORS='\000' '
1627             FNR == 1 { gsub(/^\xef\xbb\xbf/, "") }
1628             { gsub(/\r$/, ""); print }
1629         ' | xargs -0 "$@"
1630     fi
1631 }
1632 
1633 # Youtube Audio Player
1634 yap() {
1635     local url
1636     # some youtube URIs end with extra playlist/tracker parameters
1637     url="$(echo "$1" | sed 's-&.*--')"
1638     mpv "$(yt-dlp -x --audio-format best --get-url "${url}" 2> /dev/null)"
1639 }
1640 
1641 # show a calendar for the current YEAR, or for the year given
1642 year() {
1643     {
1644         # show the current date/time center-aligned
1645         printf \
1646             "%21s\e[38;2;78;154;6m%s\e[0m  \e[38;2;52;101;164m%s\e[0m\n\n" \
1647             "" "$(date +'%a %b %d %Y')" "$(date +'%H:%M')"
1648         # debian linux has a different `cal` app which highlights the day
1649         if [ -e /usr/bin/ncal ]; then
1650             # fix debian/ncal's weird way to highlight the current day
1651             ncal -C -y "$@" | sed -E \
1652                 's/_\x08(.+)_\x08([^ ]+)/\x1b\[7m\1\2\x1b\[0m/'
1653         else
1654             cal -y "$@"
1655         fi
1656     } | { less -MKiCRS 2> /dev/null || cat; }
1657 }
1658 
1659 # show the current date in the YYYY-MM-DD format
1660 ymd() { date +'%Y-%m-%d'; }