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