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