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