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