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