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