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