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