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