File: clam.sh
   1 #!/bin/sh
   2 
   3 # The MIT License (MIT)
   4 #
   5 # Copyright (c) 2026 pacman64
   6 #
   7 # Permission is hereby granted, free of charge, to any person obtaining a copy
   8 # of this software and associated documentation files (the "Software"), to deal
   9 # in the Software without restriction, including without limitation the rights
  10 # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  11 # copies of the Software, and to permit persons to whom the Software is
  12 # furnished to do so, subject to the following conditions:
  13 #
  14 # The above copyright notice and this permission notice shall be included in
  15 # all copies or substantial portions of the Software.
  16 #
  17 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  18 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  19 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  20 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  21 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  22 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  23 # SOFTWARE.
  24 
  25 
  26 # clam
  27 #
  28 # Command-Line Augmentation Module (clam): get the best out of your shell.
  29 #
  30 #
  31 # This is a collection of arguably useful shell functions and shortcuts:
  32 # some of these extra commands can be real time/effort savers, ideally
  33 # letting you concentrate on getting things done.
  34 #
  35 # Some of these commands depend on my other scripts from the `pac-tools`,
  36 # others either rely on widely-preinstalled command-line apps, or ones
  37 # which are available on most of the major command-line `package` managers.
  38 #
  39 # To use this script, you're supposed to `source` it, so its definitions
  40 # stay for your whole shell session: for that, you can run `source clam` or
  41 # `. clam` (no quotes either way), either directly or at shell startup.
  42 #
  43 # Almost all commands defined in this script work with `bash`, `zsh`, and
  44 # even `dash`, which is debian linux's default non-interactive shell. Some
  45 # of its commands even 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|/bin/sh)
  63         # script is being sourced with bash, dash, or ash, 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         printf "\n"
  77         printf "\e[7mBefore doing that, you may want to see the help,\e[0m\n"
  78         printf "\e[7mby running 'clam -h' (without the single quotes).\e[0m\n"
  79                 # failing during shell-startup may deny shell access, so exit
  80                 # with a 0 error-code to declare success
  81                 # exit 0
  82             ;;
  83         esac
  84     ;;
  85 esac
  86 
  87 
  88 alias 0=sbs
  89 
  90 alias 1='bsbs 1'
  91 alias 2='bsbs 2'
  92 alias 3='bsbs 3'
  93 alias 4='bsbs 4'
  94 alias 5='bsbs 5'
  95 alias 6='bsbs 6'
  96 alias 7='bsbs 7'
  97 alias 8='bsbs 8'
  98 alias 9='bsbs 9'
  99 
 100 # Less with Header n runs `less` with line numbers, ANSI styles, without
 101 # line-wraps, and using the first n lines as a sticky-header, so they always
 102 # show on top
 103 alias lh1='less --header=1 -MKNiCRS'
 104 alias lh2='less --header=2 -MKNiCRS'
 105 alias lh3='less --header=3 -MKNiCRS'
 106 alias lh4='less --header=4 -MKNiCRS'
 107 alias lh5='less --header=5 -MKNiCRS'
 108 alias lh6='less --header=6 -MKNiCRS'
 109 alias lh7='less --header=7 -MKNiCRS'
 110 alias lh8='less --header=8 -MKNiCRS'
 111 alias lh9='less --header=9 -MKNiCRS'
 112 
 113 # View with Header n runs `less` without line numbers, ANSI styles, without
 114 # line-wraps, and using the first n lines as a sticky-header, so they always
 115 # show on top
 116 alias vh1='less --header=1 -MKiCRS'
 117 alias vh2='less --header=2 -MKiCRS'
 118 alias vh3='less --header=3 -MKiCRS'
 119 alias vh4='less --header=4 -MKiCRS'
 120 alias vh5='less --header=5 -MKiCRS'
 121 alias vh6='less --header=6 -MKiCRS'
 122 alias vh7='less --header=7 -MKiCRS'
 123 alias vh8='less --header=8 -MKiCRS'
 124 alias vh9='less --header=9 -MKiCRS'
 125 
 126 alias c=cat
 127 alias e=echo
 128 alias r='tput reset 2> /dev/null || reset'
 129 
 130 # Awk Print
 131 alias ap=abp
 132 
 133 # AWK in PARagraph-input mode
 134 alias awkpar=awkblock
 135 
 136 # Better LESS runs `less`, showing line numbers, among other settings
 137 alias bless='less -MKNiCRS'
 138 
 139 # Book-like MANual, lays out `man` docs as pairs of side-by-side pages; uses
 140 # my tool `bsbs`
 141 alias bman=bookman
 142 
 143 # Better Units
 144 alias bu=bunits
 145 
 146 # load/concatenate BYTES from named data sources; uses my tool `get`
 147 alias bytes=get
 148 
 149 # Compile C Optimized
 150 alias cco='cc -Wall -O2 -s -march=native -mtune=native -flto'
 151 
 152 # Color DMESG
 153 alias cdmesg='dmesg --color=always'
 154 
 155 # Colored Json Query runs the `jq` app, allowing an optional filepath as the
 156 # data source, and even an optional transformation formula
 157 alias cjq='jq -C'
 158 
 159 # CLear Screen
 160 alias cls='tput reset 2> /dev/null || reset'
 161 
 162 # Compile C Plus Plus Optimized
 163 alias cppo='c++ -Wall -O2 -s -march=native -mtune=native -flto'
 164 
 165 # CURL Silent spares you the progress bar, but still tells you about errors
 166 alias curls='curl --silent --show-error'
 167 
 168 # dictionary-DEFine the word given, using an online service
 169 alias def=define
 170 
 171 # turn JSON Lines into a proper json array
 172 # alias dejsonl='jq -s -M'
 173 
 174 # turn json lines into a proper json array using the `jq` app
 175 alias dejql='jq -s -M'
 176 
 177 # turn UTF-16 data into UTF-8
 178 alias deutf16='iconv -f utf16 -t utf8'
 179 
 180 # edit plain-text files
 181 # alias edit=micro
 182 
 183 # ENV with 0/null-terminated lines on stdout
 184 alias env0='env -0'
 185 
 186 # ENV Change folder, runs the command given in the folder given (first)
 187 alias envc='env -C'
 188 
 189 # Extended Plain Interactive Grep
 190 alias epig='ugrep --color=never -Q -E'
 191 
 192 # Editor Read-Only
 193 alias ero='micro -readonly true'
 194 
 195 # Expand 4 turns each tab into up to 4 spaces
 196 alias expand4='expand -t 4'
 197 
 198 # run the Fuzzy Finder (fzf) in multi-choice mode, with custom keybindings
 199 alias ff='fzf -m --bind ctrl-a:select-all,ctrl-space:toggle'
 200 
 201 # get FILE's MIME types
 202 alias filemime='file --mime-type'
 203 
 204 # APT GET/install packages
 205 alias getapt=aptget
 206 
 207 # run `gcc` with all optimizations on and with static analysis on
 208 alias gccmax='gcc -Wall -O2 -s -march=native -mtune=native -flto -fanalyzer'
 209 
 210 # hold stdout if used at the end of a pipe-chain
 211 alias hold='less -MKiCRS'
 212 
 213 # find all hyperlinks inside HREF attributes in the input text
 214 alias hrefs=href
 215 
 216 # make JSON Lines out of JSON data
 217 alias jl=jsonl
 218 
 219 # shrink/compact JSON using the `jq` app, allowing an optional filepath, and
 220 # even an optional transformation formula after that
 221 alias jq0='jq -c -M'
 222 
 223 # show JSON data on multiple lines, using 2 spaces for each indentation level,
 224 # allowing an optional filepath, and even an optional transformation formula
 225 # after that
 226 alias jq2='jq --indent 2 -M'
 227 
 228 # find the LAN (local-area network) IP address for this device
 229 alias lanip='hostname -I'
 230 
 231 # run `less`, showing line numbers, among other settings
 232 alias least='less -MKNiCRS'
 233 
 234 # try to run the command given using line-buffering for its (standard) output
 235 alias livelines='stdbuf -oL'
 236 
 237 # LOAD data from the filename or URI given; uses my `get` tool
 238 alias load=get
 239 
 240 # LOcal SERver webserves files in a folder as localhost, using the port
 241 # number given, or port 8080 by default
 242 alias loser=serve
 243 
 244 # LOWercase all ASCII symbols
 245 alias low=tolower
 246 
 247 # LOWERcase all ASCII symbols
 248 alias lower=tolower
 249 
 250 # run `ls` showing how many 4k pages each file takes
 251 alias lspages='ls -s --block-size=4096'
 252 
 253 # Listen To Youtube
 254 alias lty=yap
 255 
 256 # MAKE IN folder
 257 alias makein=mif
 258 
 259 # Multi-Core MaKe runs `make` using all cores
 260 alias mcmk=mcmake
 261 
 262 # run `less`, showing line numbers, among other settings
 263 alias most='less -MKNiCRS'
 264 
 265 # emit nothing to output and/or discard everything from input
 266 alias nil=null
 267 
 268 # Nice Json Query colors JSON data using the `jq` app
 269 alias njq=cjq
 270 
 271 # Plain Interactive Grep
 272 alias pig='ugrep --color=never -Q -E'
 273 
 274 # Quick Compile C Optimized
 275 alias qcco='cc -Wall -O2 -s -march=native -mtune=native -flto'
 276 
 277 # Quick Compile C Plus Plus Optimized
 278 alias qcppo='c++ -Wall -O2 -s -march=native -mtune=native -flto'
 279 
 280 # Read-Only Editor
 281 alias roe='micro -readonly true'
 282 
 283 # Read-Only Micro (text editor)
 284 alias rom='micro -readonly true'
 285 
 286 # Read-Only Top
 287 alias rot='htop --readonly'
 288 
 289 # RUN IN folder
 290 alias runin='env -C'
 291 
 292 # place lines Side-By-Side
 293 # alias sbs=column
 294 
 295 # Silent CURL spares you the progress bar, but still tells you about errors
 296 alias scurl='curl --silent --show-error'
 297 
 298 # Stdbuf Output Line-buffered
 299 alias sol='stdbuf -oL'
 300 
 301 # TRY running a command, showing its outcome/error-code on failure
 302 alias try=verdict
 303 
 304 # Time Verbosely the command given
 305 alias tv='/usr/bin/time -v'
 306 
 307 # VERTical REVert emits lines in reverse order of appearance
 308 alias vertrev=tac
 309 
 310 # emit lines in reverse order of appearance
 311 alias upsidedown=tac
 312 
 313 # run `cppcheck` with even stricter options
 314 alias vetc='cppcheck --enable=portability,style --check-level=exhaustive'
 315 
 316 # run `cppcheck` with even stricter options, also checking for c89 compliance
 317 alias vetc89='cppcheck --enable=portability,style --check-level=exhaustive --std=c89'
 318 
 319 # run `cppcheck` with even stricter options
 320 alias vetcpp='cppcheck --enable=portability,style --check-level=exhaustive'
 321 
 322 # VET SHell scripts
 323 alias vetsh=vetshell
 324 
 325 # check shell scripts for common gotchas, avoiding complaints about using
 326 # the `local` keyword, which is widely supported in practice
 327 alias vetshell='shellcheck -e 3043'
 328 
 329 # run a command using an empty environment
 330 alias void='env -i'
 331 
 332 # turn plain-text from latin-1 into UTF-8; the name is from `vulgarization`,
 333 # which is the mutation of languages away from latin during the middle ages
 334 alias vulgarize='iconv -f latin-1 -t utf-8'
 335 
 336 # recursively find all files with trailing spaces/CRs
 337 alias wheretrails=whichtrails
 338 
 339 # run `xargs`, using zero/null bytes as the extra-arguments terminator
 340 alias x0='xargs -0'
 341 
 342 # Xargs Lines, runs `xargs` using whole lines as extra arguments
 343 alias xl=xargsl
 344 
 345 # Awk Begin Print
 346 abp() {
 347     local arg
 348     for arg in "$@"; do
 349         awk "BEGIN { print (${arg}); exit }"
 350     done
 351 }
 352 
 353 # find name from the local `apt` database of installable packages
 354 aptfind() {
 355     local arg
 356     local gap=0
 357     local options='-MKiCRS'
 358 
 359     if [ $# -eq 1 ]; then
 360         options='--header=1 -MKiCRS'
 361     fi
 362 
 363     for arg in "$@"; do
 364         [ "${gap}" -gt 0 ] && printf "\n"
 365         gap=1
 366         printf "\e[7m%-80s\e[0m\n\n" "${arg}"
 367 
 368         # despite warnings, the `search` command has been around for years
 369         apt search "${arg}" 2> /dev/null |
 370             grep -E -A 1 "^[a-z0-9-]*${arg}" | sed 's/^--$//'
 371     done | less ${options}
 372 }
 373 
 374 # APT GET/install packages
 375 aptget() { sudo apt install "$@"; sudo -k; }
 376 
 377 # APT UPdate/grade
 378 aptup() { sudo apt update && sudo apt upgrade "$@"; sudo -k; }
 379 
 380 # emit each argument given as its own line of output
 381 args() { printf "%s\n" "$@"; }
 382 
 383 # AWK in BLOCKS/paragraphs-input mode
 384 awkblocks() {
 385     if { [ -p /dev/stdout ] || [ -t 1 ]; } && [ -e /usr/bin/stdbuf ]; then
 386         stdbuf -oL awk -F='' -v RS='' "$@"
 387     else
 388         awk -F='' -v RS='' "$@"
 389     fi
 390 }
 391 
 392 # AWK using TABS as input/output field-separators
 393 awktabs() {
 394     if { [ -p /dev/stdout ] || [ -t 1 ]; } && [ -e /usr/bin/stdbuf ]; then
 395         stdbuf -oL awk -F "\t" -v OFS="\t" "$@"
 396     else
 397         awk -F "\t" -v OFS="\t" "$@"
 398     fi
 399 }
 400 
 401 # Breathe lines 3: separate groups of 3 lines with empty lines
 402 b3() {
 403     if { [ -p /dev/stdout ] || [ -t 1 ]; } && [ -e /usr/bin/stdbuf ]; then
 404         stdbuf -oL awk 'NR % 3 == 1 && NR != 1 { print "" } 1' "$@"
 405     else
 406         awk 'NR % 3 == 1 && NR != 1 { print "" } 1' "$@"
 407     fi
 408 }
 409 
 410 # Breathe lines 5: separate groups of 5 lines with empty lines
 411 b5() {
 412     if { [ -p /dev/stdout ] || [ -t 1 ]; } && [ -e /usr/bin/stdbuf ]; then
 413         stdbuf -oL awk 'NR % 5 == 1 && NR != 1 { print "" } 1' "$@"
 414     else
 415         awk 'NR % 5 == 1 && NR != 1 { print "" } 1' "$@"
 416     fi
 417 }
 418 
 419 # show an ansi-styled BANNER-like line
 420 banner() { printf "\e[7m%-$(tput cols)s\e[0m\n" "$*"; }
 421 
 422 # emit a colored bar which can help visually separate different outputs
 423 bar() {
 424     [ "${1:-80}" -gt 0 ] && printf "\e[48;2;218;218;218m%${1:-80}s\e[0m\n" ""
 425 }
 426 
 427 # Breathe Header 3: add an empty line after the first one (the header),
 428 # then separate groups of 3 lines with empty lines between them
 429 bh3() {
 430     if { [ -p /dev/stdout ] || [ -t 1 ]; } && [ -e /usr/bin/stdbuf ]; then
 431         stdbuf -oL awk '(NR - 1) % 3 == 1 { print "" } 1' "$@"
 432     else
 433         awk '(NR - 1) % 3 == 1 { print "" } 1' "$@"
 434     fi
 435 }
 436 
 437 # Breathe Header 5: add an empty line after the first one (the header),
 438 # then separate groups of 5 lines with empty lines between them
 439 bh5() {
 440     if { [ -p /dev/stdout ] || [ -t 1 ]; } && [ -e /usr/bin/stdbuf ]; then
 441         stdbuf -oL awk '(NR - 1) % 5 == 1 { print "" } 1' "$@"
 442     else
 443         awk '(NR - 1) % 5 == 1 { print "" } 1' "$@"
 444     fi
 445 }
 446 
 447 # emit a line with a repeating block-like symbol in it
 448 blocks() { [ "${1:-80}" -gt 0 ] && printf "%${1:-80}s\n" "" | sed 's- -â–ˆ-g'; }
 449 
 450 # BOOK-like MANual, lays out `man` docs as pairs of side-by-side pages; uses
 451 # my tool `bsbs`
 452 bookman() {
 453     local w
 454     w="$(tput cols)"
 455     w="$((w / 2 - 4))"
 456     if [ "$w" -lt 65 ]; then
 457         w=65
 458     fi
 459     MANWIDTH="$w" man "$@" | bsbs 2
 460 }
 461 
 462 # split lines using the separator given, turning them into single-item lines
 463 breakdown() {
 464     local sep="${1:- }"
 465     [ $# -gt 0 ] && shift
 466     local command='awk'
 467     if { [ -p /dev/stdout ] || [ -t 1 ]; } && [ -e /usr/bin/stdbuf ]; then
 468         command='stdbuf -oL awk'
 469     fi
 470 
 471     ${command} -F "${sep}" '{ for (i = 1; i <= NF; i++) print $i }' "$@"
 472 }
 473 
 474 # Better UNITS
 475 bunits() {
 476     local unit
 477     local conv="units --terse -v -H ''"
 478     unit="$(echo "$2" | awk '{ print tolower($0) }')"
 479 
 480     case "${unit}" in
 481         ac|acre|acres) ${conv} "$1 acres" kilometers^2;;
 482         cup|cups) ${conv} "$1 cups" liters;;
 483         day|days) ${conv} "$1 days" seconds;;
 484         deg|degs|degree|degrees) ${conv} "$1 degrees" radians;;
 485         f|fahr|fahrenheit|fahrenheits) ${conv} "tempF($1)" tempC;;
 486         floz) ${conv} "$1 floz" milliliters;;
 487         ft|feet|foot) ${conv} "$1 feet" meters;;
 488         ft2|ft^2|sqft|sqfeet) ${conv} "$1 ft^2" meters^2;;
 489         ft3|ft^3|cuft|cufeet) ${conv} "$1 ft^3" meters^3;;
 490         gal|gallon|gals|gallons) ${conv} "$1 gallons" liters;;
 491         gb|gib|gibi|gibibytes) ${conv} "$1 gibibytes" bytes;;
 492         hr|hour|hours) ${conv} "$1 hours" seconds;;
 493         in|inch|inches) ${conv} "$1 inches" centimeters;;
 494         kb|kib|kibi|kibibytes) ${conv} "$1 kibibytes" bytes;;
 495         lb|lbs|pound|pounds) ${conv} "$1 pounds" kilograms;;
 496         lbt|lbts|troypound|troypounds) ${conv} "$1 troypound" kilograms;;
 497         mb|mib|mibi|mibibytes) ${conv} "$1 mibibytes" bytes;;
 498         mi|mile|miles) ${conv} "$1 miles" kilometers;;
 499         min|minute|minutes) ${conv} "$1 minutes" seconds;;
 500         mi2|mi^2|miles^2) ${conv} "$1 mi^2" kilometers^2;;
 501         mi3|mi^3|miles^3) ${conv} "$1 mi^3" kilometers^3;;
 502         mph) ${conv} "$1 mph" kph;;
 503         nmi|nmile|nmiles) ${conv} "$1 nmi" kilometers;;
 504         nmi2|nmi^2|nmile^2|nmiles^2) ${conv} "$1 nmi^2" kilometers^2;;
 505         oz|ozs|ounce|ounces) ${conv} "$1 ounces" grams;;
 506         ozt|ozys|troyounce|troyounces) ${conv} "$1 troyounce" grams;;
 507         pt|pts|pint|pints|uspint|uspints) ${conv} "$1 uspints" liters;;
 508         tb|tib|tibi|tibibytes) ${conv} "$1 tibibytes" bytes;;
 509         yd|yds|yard|yards) ${conv} "$1 yards" meters;;
 510         yd^2|yds^2|yard^2|yards^2) ${conv} "$1 yards^2" meters^2;;
 511         wk|week|weeks) ${conv} "$1 weeks" seconds;;
 512         *) ${conv} "$@";;
 513     esac
 514 }
 515 
 516 # CAlculator with Nice numbers runs my tool `ca` and colors results with
 517 # my tool `nn`, alternating styles to make long numbers easier to read
 518 can() {
 519     local arg
 520     for arg in "$@"; do
 521         [ $# -ge 2 ] && printf "\e[7m%s\e[0m\n" "${arg}" > /dev/stderr
 522         ca "${arg}" | nn
 523     done
 524 }
 525 
 526 # uppercase the first letter on each line, and lowercase all later letters
 527 capitalize() { sed -E 's-^(.*)-\L\1-; s-^(.)-\u\1-'; }
 528 
 529 # Count with AWK: count the times the AWK expression/condition given is true
 530 cawk() {
 531     local cond="${1:-1}"
 532     [ $# -gt 0 ] && shift
 533     awk '
 534         BEGIN { count = c = 0 }
 535         FNR == 1 { FS = /\t/ ? "\t" : " "; $0 = $0 }
 536         { low = lower = tolower($0) }
 537         '"${cond}"' { count++; c = count }
 538         END { print count }
 539     ' "$@"
 540 }
 541 
 542 # center-align lines of text, using the current screen width
 543 center() {
 544     local command='awk'
 545     if [ -e /usr/bin/gawk ]; then
 546         command='gawk'
 547     fi
 548 
 549     ${command} -v width="$(tput cols)" '
 550         {
 551             gsub(/\r$/, "")
 552             lines[NR] = $0
 553             s = $0
 554             gsub(/\x1b\[[0-9;]*[A-Za-z]/, "", s) # ANSI style-changers
 555             l = length(s)
 556             if (maxlen < l) maxlen = l
 557         }
 558 
 559         END {
 560             n = (width - maxlen) / 2
 561             if (n % 1) n = n - (n % 1)
 562             fmt = sprintf("%%%ds%%s\n", (n > 0) ? n : 0)
 563             for (i = 1; i <= NR; i++) printf fmt, "", lines[i]
 564         }
 565     ' "$@"
 566 }
 567 
 568 # Colored Go Test on the folder given; uses my command `gbm`
 569 cgt() { go test "${@:-.}" 2>&1 | gbm '^ok' '^[-]* ?FAIL' '^\?'; }
 570 
 571 # Colored RipGrep ensures app `rg` emits colors when piped
 572 crg() {
 573     if [ -p /dev/stdout ] || [ -t 1 ]; then
 574         rg --line-buffered --color=always "${@:-.}"
 575     else
 576         rg --color=always "${@:-.}"
 577     fi
 578 }
 579 
 580 # Compile Rust Optimized
 581 cro() {
 582     rustc -C lto=true -C codegen-units=1 -C debuginfo=0 -C strip=symbols \
 583         -C opt-level=3 "$@"
 584 }
 585 
 586 # emit a line with a repeating cross-like symbol in it
 587 crosses() { [ "${1:-80}" -gt 0 ] && printf "%${1:-80}s\n" "" | sed 's- -×-g'; }
 588 
 589 # listen to streaming DANCE music
 590 dance() {
 591     printf "streaming \e[7mDance Wave Retro\e[0m\n"
 592     mpv --really-quiet https://retro.dancewave.online/retrodance.mp3
 593 }
 594 
 595 # emit a line with a repeating dash-like symbol in it
 596 dashes() { [ "${1:-80}" -gt 0 ] && printf "%${1:-80}s\n" "" | sed 's- -—-g'; }
 597 
 598 # remove commas in numbers, as well as leading dollar signs in numbers
 599 decomma() {
 600     sed -E 's-([0-9]{3}),-\1-g; s-([0-9]{1,2}),-\1-g; s-\$([0-9\.]+)-\1-g'
 601 }
 602 
 603 # remove indentations from lines
 604 dedent() {
 605     awk '
 606         { lines[NR] = $0 }
 607         { if (match($0, /^ +/) && (n == 0 || n > RLENGTH)) n = RLENGTH }
 608         END { for (i = 1; i <= NR; i++) print substr(lines[i], n + 1) }
 609     ' "$@"
 610 }
 611 
 612 dehtmlify() {
 613     local command='awk'
 614     if { [ -p /dev/stdout ] || [ -t 1 ]; } && [ -e /usr/bin/stdbuf ]; then
 615         command='stdbuf -oL awk'
 616     fi
 617 
 618     ${command} '
 619         {
 620             gsub(/<\/?[^>]+>/, "")
 621             gsub(/&amp;/, "&")
 622             gsub(/&lt;/, "<")
 623             gsub(/&gt;/, ">")
 624             gsub(/^ +| *\r?$/, "")
 625             gsub(/  +/, " ")
 626             print
 627         }
 628     ' "$@"
 629 }
 630 
 631 # expand tabs each into up to the number of space given, or 4 by default
 632 detab() {
 633     local tabstop="${1:-4}"
 634     [ $# -gt 0 ] && shift
 635     if { [ -p /dev/stdout ] || [ -t 1 ]; } && [ -e /usr/bin/stdbuf ]; then
 636         stdbuf -oL expand -t "${tabstop}" "$@"
 637     else
 638         expand -t "${tabstop}" "$@"
 639     fi
 640 }
 641 
 642 # DIVide 2 numbers 3 ways, including the complement
 643 div() {
 644     awk -v a="${1:-1}" -v b="${2:-1}" '
 645         BEGIN {
 646             gsub(/_/, "", a)
 647             gsub(/_/, "", b)
 648             if (a > b) { c = a; a = b; b = c }
 649             c = 1 - a / b
 650             if (0 <= c && c <= 1) printf "%f\n%f\n%f\n", a / b, b / a, c
 651             else printf "%f\n%f\n", a / b, b / a
 652             exit
 653         }'
 654 }
 655 
 656 # emit a line with a repeating dot-like symbol in it
 657 dots() { [ "${1:-80}" -gt 0 ] && printf "%${1:-80}s\n" "" | sed 's- -·-g'; }
 658 
 659 # show the current Date and Time
 660 dt() {
 661     printf "\e[38;2;78;154;6m%s\e[0m  \e[38;2;52;101;164m%s\e[0m\n" \
 662         "$(date +'%a %b %d')" "$(date +%T)"
 663 }
 664 
 665 # show the current Date, Time, and a Calendar with the 3 `current` months
 666 dtc() {
 667     {
 668         # show the current date/time center-aligned
 669         printf "%20s\e[38;2;78;154;6m%s\e[0m  \e[38;2;52;101;164m%s\e[0m\n\n" \
 670             "" "$(date +'%a %b %d')" "$(date +%T)"
 671         # debian linux has a different `cal` app which highlights the day
 672         if [ -e /usr/bin/ncal ]; then
 673             # fix debian/ncal's weird way to highlight the current day
 674             ncal -C -3 | sed -E 's/_\x08(.+)_\x08([^ ]+)/\x1b\[7m\1\2\x1b\[0m/'
 675         else
 676             cal -3
 677         fi
 678     } | less -MKiCRS
 679 }
 680 
 681 # EDit RUN shell commands, using an interactive editor; uses my tool `leak`
 682 edrun() {
 683     # dash doesn't support the process-sub syntax
 684     # . <( micro -readonly true -filetype shell | leak --inv )
 685     micro -readonly true -filetype shell | leak --inv | . /dev/fd/0
 686 }
 687 
 688 # convert EURos into CAnadian Dollars, using the latest official exchange
 689 # rates from the bank of canada; during weekends, the latest rate may be
 690 # from a few days ago; the default amount of euros to convert is 1, when
 691 # not given
 692 eur2cad() {
 693     local site='https://www.bankofcanada.ca/valet/observations/group'
 694     local csv_rates="${site}/FX_RATES_DAILY/csv"
 695     local url="${csv_rates}?start_date=$(date -d '3 days ago' +'%Y-%m-%d')"
 696     curl -s "${url}" | awk -F, -v amount="$(echo "${1:-1}" | sed 's-_--g')" '
 697         /EUR/ { for (i = 1; i <= NF; i++) if($i ~ /EUR/) j = i }
 698         END { gsub(/"/, "", $j); if (j != 0) printf "%.2f\n", amount * $j }
 699     '
 700 }
 701 
 702 # Fix Audio Duration on a separate copy of the file given
 703 fad() { ffmpeg -i "${1:-input.m4a}" -acodec copy "${2:-output.dat}"; }
 704 
 705 # get the first n lines, or 1 by default
 706 first() { head -n "${1:-1}" "${2:--}"; }
 707 
 708 # Field-Names AWK remembers field-positions by name, from the first input line
 709 fnawk() {
 710     local code="${1:-1}"
 711     [ $# -gt 0 ] && shift
 712 
 713     local buffering=''
 714     if { [ -p /dev/stdout ] || [ -t 1 ]; } && [ -e /usr/bin/stdbuf ]; then
 715         buffering='stdbuf -oL'
 716     fi
 717 
 718     ${buffering} awk -v OFS="\t" '
 719         NR == 1 {
 720             FS = /\t/ ? "\t" : " "
 721             $0 = $0
 722             for (i = 1; i <= NF; i++) names[$i] = i
 723             i = ""
 724         }
 725         { low = lower = tolower($0) }
 726         '"${code}"'
 727     ' "$@"
 728 }
 729 
 730 # start from the line number given, skipping all previous ones
 731 fromline() { tail -n +"${1:-1}" "${2:--}"; }
 732 
 733 # convert a mix of FeeT and INches into meters
 734 ftin() {
 735     local ft="${1:-0}"
 736     ft="$(echo "${ft}" | sed 's-_--g')"
 737     local in="${2:-0}"
 738     in="$(echo "${in}" | sed 's-_--g')"
 739     awk "BEGIN { print 0.3048 * ${ft} + 0.0254 * ${in}; exit }"
 740 }
 741 
 742 # Gawk Bignum Print
 743 gbp() { gawk --bignum "BEGIN { print $1; exit }"; }
 744 
 745 # glue/stick together various lines, only emitting a line-feed at the end; an
 746 # optional argument is the output-item-separator, which is empty by default
 747 glue() {
 748     local sep="${1:-}"
 749     [ $# -gt 0 ] && shift
 750     awk -v sep="${sep}" '
 751         NR > 1 { printf "%s", sep }
 752         { gsub(/\r/, ""); printf "%s", $0 }
 753         END { if (NR > 0) print "" }
 754     ' "$@"
 755 }
 756 
 757 # GO Build Stripped: a common use-case for the go compiler
 758 gobs() { go build -ldflags "-s -w" -trimpath "$@"; }
 759 
 760 # GO DEPendencieS: show all dependencies in a go project
 761 godeps() { go list -f '{{ join .Deps "\n" }}' "$@"; }
 762 
 763 # GO IMPortS: show all imports in a go project
 764 goimps() { go list -f '{{ join .Imports "\n" }}' "$@"; }
 765 
 766 # Global extended regex SUBstitute, using the AWK function of the same name:
 767 # arguments are used as regex/replacement pairs, in that order
 768 gsub() {
 769     local command='awk'
 770     if { [ -p /dev/stdout ] || [ -t 1 ]; } && [ -e /usr/bin/stdbuf ]; then
 771         command='stdbuf -oL awk'
 772     fi
 773 
 774     ${command} '
 775         BEGIN {
 776             for (i = 1; i < ARGC; i++) {
 777                 args[++n] = ARGV[i]
 778                 delete ARGV[i]
 779             }
 780         }
 781 
 782         {
 783             for (i = 1; i <= n; i += 2) gsub(args[i], args[i + 1])
 784             print
 785         }
 786     ' "$@"
 787 }
 788 
 789 # show Help laid out on 2 side-by-side columns; uses my tool `bsbs`
 790 h2() { naman "$@" | bsbs 2; }
 791 
 792 # Highlighted-style ECHO
 793 hecho() { printf "\e[7m%s\e[0m\n" "$*"; }
 794 
 795 # show each byte as a pair of HEXadecimal (base-16) symbols
 796 hexify() {
 797     cat "$@" | od -x -A n |
 798         awk '{ gsub(/ +/, ""); printf "%s", $0 } END { printf "\n" }'
 799 }
 800 
 801 # Help Me Remember my custom shell commands
 802 hmr() {
 803     local cmd="bat"
 804     # debian linux uses a different name for the `bat` app
 805     if [ -e /usr/bin/batcat ]; then
 806         cmd="batcat"
 807     fi
 808 
 809     "$cmd" \
 810         --style=plain,header,numbers --theme='Monokai Extended Light' \
 811         --wrap=never --color=always "$(which clam)" |
 812             sed -e 's-\x1b\[38;5;70m-\x1b[38;5;28m-g' \
 813                 -e 's-\x1b\[38;5;214m-\x1b[38;5;208m-g' \
 814                 -e 's-\x1b\[38;5;243m-\x1b[38;5;103m-g' \
 815                 -e 's-\x1b\[38;5;238m-\x1b[38;5;245m-g' \
 816                 -e 's-\x1b\[38;5;228m-\x1b[48;5;228m-g' |
 817                 less -MKiCRS
 818 }
 819 
 820 # convert seconds into a colon-separated Hours-Minutes-Seconds triple
 821 hms() {
 822     echo "${@:-0}" | sed -E 's-_--g; s- +-\n-g' | awk '/./ {
 823         x = $0
 824         h = (x - x % 3600) / 3600
 825         m = (x % 3600) / 60
 826         s = x % 60
 827         printf "%02d:%02d:%05.2f\n", h, m, s
 828     }'
 829 }
 830 
 831 # find all hyperlinks inside HREF attributes in the input text
 832 href() {
 833     local arg
 834     local awk_cmd='awk'
 835     local grep_cmd='grep'
 836     if [ -p /dev/stdout ] || [ -t 1 ]; then
 837         grep_cmd='grep --line-buffered'
 838         if [ -e /usr/bin/stdbuf ]; then
 839             awk_cmd='stdbuf -oL awk'
 840         fi
 841     fi
 842 
 843     for arg in "${@:--}"; do
 844         ${grep_cmd} -i -E -o 'href="[^"]+"' "${arg}"
 845     done | ${awk_cmd} '{ gsub(/^href="|"\r?$/, ""); print }'
 846 }
 847 
 848 # avoid/ignore lines which case-insensitively match any of the regexes given
 849 iavoid() {
 850     local command='awk'
 851     if { [ -p /dev/stdout ] || [ -t 1 ]; } && [ -e /usr/bin/stdbuf ]; then
 852         command='stdbuf -oL awk'
 853     fi
 854 
 855     ${command} '
 856         BEGIN {
 857             if (IGNORECASE == "") {
 858                 m = "this variant of AWK lacks case-insensitive regex-matching"
 859                 print(m) > "/dev/stderr"
 860                 exit 125
 861             }
 862             IGNORECASE = 1
 863 
 864             for (i = 1; i < ARGC; i++) {
 865                 e[i] = ARGV[i]
 866                 delete ARGV[i]
 867             }
 868         }
 869 
 870         {
 871             for (i = 1; i < ARGC; i++) if ($0 ~ e[i]) next
 872             print
 873             got++
 874         }
 875 
 876         END { exit(got == 0) }
 877     ' "${@:-^\r?$}"
 878 }
 879 
 880 # ignore command in a pipe: this allows quick re-editing of pipes, while
 881 # still leaving signs of previously-used steps, as a memo
 882 idem() { cat; }
 883 
 884 # ignore command in a pipe: this allows quick re-editing of pipes, while
 885 # still leaving signs of previously-used steps, as a memo
 886 ignore() { cat; }
 887 
 888 # only keep lines which case-insensitively match any of the regexes given
 889 imatch() {
 890     local command='awk'
 891     if { [ -p /dev/stdout ] || [ -t 1 ]; } && [ -e /usr/bin/stdbuf ]; then
 892         command='stdbuf -oL awk'
 893     fi
 894 
 895     ${command} '
 896         BEGIN {
 897             if (IGNORECASE == "") {
 898                 m = "this variant of AWK lacks case-insensitive regex-matching"
 899                 print(m) > "/dev/stderr"
 900                 exit 125
 901             }
 902             IGNORECASE = 1
 903 
 904             for (i = 1; i < ARGC; i++) {
 905                 e[i] = ARGV[i]
 906                 delete ARGV[i]
 907             }
 908         }
 909 
 910         {
 911             for (i = 1; i < ARGC; i++) {
 912                 if ($0 ~ e[i]) {
 913                     print
 914                     got++
 915                     next
 916                 }
 917             }
 918         }
 919 
 920         END { exit(got == 0) }
 921     ' "${@:-[^\r]}"
 922 }
 923 
 924 # start each non-empty line with extra n spaces
 925 indent() {
 926     local command='awk'
 927     if { [ -p /dev/stdout ] || [ -t 1 ]; } && [ -e /usr/bin/stdbuf ]; then
 928         command='stdbuf -oL awk'
 929     fi
 930 
 931     ${command} '
 932         BEGIN {
 933             n = ARGV[1] + 0
 934             delete ARGV[1]
 935             fmt = sprintf("%%%ds%%s\n", (n > 0) ? n : 0)
 936         }
 937 
 938         /^\r?$/ { print ""; next }
 939         { gsub(/\r$/, ""); printf(fmt, "", $0) }
 940     ' "$@"
 941 }
 942 
 943 # emit each word-like item from each input line on its own line; when a file
 944 # has tabs on its first line, items are split using tabs alone, which allows
 945 # items to have spaces in them
 946 items() {
 947     local command='awk'
 948     if { [ -p /dev/stdout ] || [ -t 1 ]; } && [ -e /usr/bin/stdbuf ]; then
 949         command='stdbuf -oL awk'
 950     fi
 951 
 952     ${command} '
 953         FNR == 1 { FS = /\t/ ? "\t" : " "; $0 = $0 }
 954         { gsub(/\r$/, ""); for (i = 1; i <= NF; i++) print $i }
 955     ' "$@"
 956 }
 957 
 958 # Judge with AWK colors lines using up to 3 (optional) AWK conditions, namely
 959 # `good` (green), `bad` (red), and `meh` (gray)
 960 jawk() {
 961     local code="${1:-\$0}"
 962     [ $# -gt 0 ] && shift
 963 
 964     local command='awk'
 965     if { [ -p /dev/stdout ] || [ -t 1 ]; } && [ -e /usr/bin/stdbuf ]; then
 966         command='stdbuf -oL awk'
 967     fi
 968 
 969     local good="${1:-0}"
 970     local bad="${2:-0}"
 971     local meh="${3:-0}"
 972 
 973     [ $# -gt 0 ] && shift
 974     [ $# -gt 0 ] && shift
 975     [ $# -gt 0 ] && shift
 976 
 977     ${command} '
 978         BEGIN {
 979             # normal good-style is green, colorblind-friendly good-style is blue
 980             cb = ENVIRON["COLORBLIND"] != 0 || ENVIRON["COLOR_BLIND"] != 0
 981             good_style = cb ? "\x1b[38;2;0;95;215m" : "\x1b[38;2;0;135;95m"
 982             good_fmt = good_style "%s\x1b[0m\n"
 983             good_reset = "\x1b[0m" good_style
 984         }
 985 
 986         FNR == 1 { FS = /\t/ ? "\t" : " "; $0 = $0 }
 987         { low = lower = tolower($0) }
 988 
 989         '"${good}"' {
 990             gsub(/\x1b\[0m/, good_reset)
 991             printf good_fmt, $0
 992             next
 993         }
 994 
 995         '"${bad}"' {
 996             gsub(/\x1b\[0m/, "\x1b[0m\x1b[38;2;204;0;0m")
 997             printf "\x1b[38;2;204;0;0m%s\x1b[0m\n", $0
 998             next
 999         }
1000 
1001         '"${meh}"' {
1002             gsub(/\x1b\[0m/, "\x1b[0m\x1b[38;2;168;168;168m")
1003             printf "\x1b[38;2;168;168;168m%s\x1b[0m\n", $0
1004             next
1005         }
1006 
1007         1
1008     ' "$@"
1009 }
1010 
1011 # listen to streaming JAZZ music
1012 jazz() {
1013     printf "streaming \e[7mSmooth Jazz Instrumental\e[0m\n"
1014     mpv --quiet https://stream.zeno.fm/00rt0rdm7k8uv
1015 }
1016 
1017 # show a `dad` JOKE from the web, sometimes even a very funny one
1018 joke() {
1019     curl --silent --show-error https://icanhazdadjoke.com | fold -s |
1020         awk '{ gsub(/ *\r?$/, ""); print }'
1021 }
1022 
1023 # JSON Query Lines turns JSON top-level arrays into multiple individually-JSON
1024 # lines using the `jq` app, keeping all other top-level values as single line
1025 # JSON outputs
1026 jql() {
1027     local code="${1:-.}"
1028     [ $# -gt 0 ] && shift
1029     jq -c -M "${code} | .[]" "$@"
1030 }
1031 
1032 # JSON Query Keys runs `jq` to find all unique key-combos from tabular JSON
1033 jqk() {
1034     local code="${1:-.}"
1035     [ $# -gt 0 ] && shift
1036     jq -c -M "${code} | .[] | keys" "$@" | awk '!c[$0]++'
1037 }
1038 
1039 # JSON Keys finds all unique key-combos from tabular JSON data; uses my tools
1040 # `jsonl` and `zj`
1041 # jsonk() { cat "${1:--}" | zj . .keys | jsonl | awk '!c[$0]++'; }
1042 
1043 # JSON Keys finds all unique key-combos from tabular JSON data; uses my tools
1044 # `jsonl` and `tjp`
1045 jsonk() {
1046     tjp '[e.keys() for e in v] if isinstance(v, (list, tuple)) else v.keys()' \
1047         "${1:--}" | jsonl | awk '!c[$0]++'
1048 }
1049 
1050 # JSON Table, turns TSV tables into tabular JSON, where valid-JSON values are
1051 # auto-parsed into numbers, booleans, etc...; uses my tools `jsons` and `tjp`
1052 jsont() {
1053     jsons "$@" | tjp \
1054         '[{k: rescue(lambda: loads(v), v) for k, v in e.items()} for e in v]'
1055 }
1056 
1057 # emit the given number of random/junk bytes, or 1024 junk bytes by default
1058 junk() { head -c "$(echo "${1:-1024}" | sed 's-_--g')" /dev/urandom; }
1059 
1060 # get the last n lines, or 1 by default
1061 last() { tail -n "${1:-1}" "${2:--}"; }
1062 
1063 # convert a mix of pounds (LB) and weight-ounces (OZ) into kilograms
1064 lboz() {
1065     local lb="${1:-0}"
1066     lb="$(echo "${lb}" | sed 's-_--g')"
1067     local oz="${2:-0}"
1068     oz="$(echo "${oz}" | sed 's-_--g')"
1069     awk "BEGIN { print 0.45359237 * ${lb} + 0.028349523 * ${oz}; exit }"
1070 }
1071 
1072 # limit stops at the first n bytes, or 1024 bytes by default
1073 limit() { head -c "$(echo "${1:-1024}" | sed 's-_--g')" "${2:--}"; }
1074 
1075 # ensure LINES are never accidentally joined across files, by always emitting
1076 # a line-feed at the end of each line
1077 lines() {
1078     if { [ -p /dev/stdout ] || [ -t 1 ]; } && [ -e /usr/bin/stdbuf ]; then
1079         stdbuf -oL awk 1 "$@"
1080     else
1081         awk 1 "$@"
1082     fi
1083 }
1084 
1085 # regroup adjacent lines into n-item tab-separated lines
1086 lineup() {
1087     local command='awk'
1088     if { [ -p /dev/stdout ] || [ -t 1 ]; } && [ -e /usr/bin/stdbuf ]; then
1089         command='stdbuf -oL awk'
1090     fi
1091 
1092     local n="${1:-0}"
1093     [ $# -gt 0 ] && shift
1094 
1095     if [ "$n" -le 0 ]; then
1096         ${command} '
1097             NR > 1 { printf "\t" }
1098             { printf "%s", $0 }
1099             END { if (NR > 0) print "" }
1100         ' "$@"
1101         return $?
1102     fi
1103 
1104     ${command} -v n="$n" '
1105         NR % n != 1 && n > 1 { printf "\t" }
1106         { printf "%s", $0 }
1107         NR % n == 0 { print "" }
1108         END { if (NR % n != 0) print "" }
1109     ' "$@"
1110 }
1111 
1112 # LiSt MAN pages
1113 lsman() { man -k "${1:-.}"; }
1114 
1115 # MARK the current tab with the message given, followed by the current folder;
1116 # works only on the `bash` shell
1117 mark() {
1118     if [ $# -eq 0 ]; then
1119         PS1="\[\e[0m\e]0;\w\a$ "
1120     else
1121         PS1="\[\e[0m\e]0;${*} \w\a$ "
1122     fi
1123 }
1124 
1125 marklinks() {
1126     local command='sed -E'
1127     # if { [ -p /dev/stdout ] || [ -t 1 ]; } && [ ! -L /bin/sed ]; then
1128     #     command='sed -E -u'
1129     # fi
1130     local re='https?://[A-Za-z0-9+_.:%-]+(/[A-Za-z0-9+_.%/,#?&=-]*)*'
1131     ${command} 's-('"${re}"')-\x1b]8;;\1\x1b\\\1\x1b]8;;\x1b\\-g' "$@"
1132 }
1133 
1134 # Multi-Core MAKE runs `make` using all cores
1135 mcmake() { make -j "$(nproc)" "$@"; }
1136 
1137 # merge stderr into stdout, which is useful for piped commands
1138 merrge() { "${@:-cat /dev/null}" 2>&1; }
1139 
1140 metajq() {
1141     # https://github.com/stedolan/jq/issues/243#issuecomment-48470943
1142     jq -r -M '
1143         [
1144             path(..) |
1145             map(if type == "number" then "[]" else tostring end) |
1146             join(".") | split(".[]") | join("[]")
1147         ] | unique | map("." + .) | .[]
1148     ' "$@"
1149 }
1150 
1151 # Make In Folder, also showing time and max memory used
1152 mif() {
1153     local folder
1154     folder="${1:-.}"
1155     [ $# -gt 0 ] && shift
1156     env -C "${folder}" \
1157         /usr/bin/time -f 'real %e    user %U    sys %S    mem %M' make "$@"
1158 }
1159 
1160 # MINimize DECimalS ignores all trailing decimal zeros in numbers, and even
1161 # the decimal dots themselves, when decimals in a number are all zeros
1162 # mindecs() {
1163 #     local cmd='sed -E'
1164 #     if { [ -p /dev/stdout ] || [ -t 1 ]; } && [ -e /usr/bin/stdbuf ]; then
1165 #         cmd='sed -E -u'
1166 #     fi
1167 #     ${cmd} 's-([0-9]+)\.0+\W-\1-g; s-([0-9]+\.[0-9]*[1-9])0+\W-\1-g' "$@"
1168 # }
1169 
1170 # Number all lines counting from 0, using a tab right after each line number
1171 n0() {
1172     if { [ -p /dev/stdout ] || [ -t 1 ]; } && [ -e /usr/bin/stdbuf ]; then
1173         stdbuf -oL nl -b a -w 1 -v 0 "$@"
1174     else
1175         nl -b a -w 1 -v 0 "$@"
1176     fi
1177 }
1178 
1179 # Number all lines counting from 1, using a tab right after each line number
1180 n1() {
1181     if { [ -p /dev/stdout ] || [ -t 1 ]; } && [ -e /usr/bin/stdbuf ]; then
1182         stdbuf -oL nl -b a -w 1 -v 1 "$@"
1183     else
1184         nl -b a -w 1 -v 1 "$@"
1185     fi
1186 }
1187 
1188 # NArrow MANual, keeps `man` narrow, even if the window/tab is wide when run
1189 naman() {
1190     local w
1191     w="$(tput cols)"
1192     w="$((w / 2 - 4))"
1193     if [ "$w" -lt 80 ]; then
1194         w=80
1195     fi
1196     MANWIDTH="$w" man "$@"
1197 }
1198 
1199 # Not AND sorts its 2 inputs, then finds lines not in common
1200 nand() {
1201     # comm -3 <(sort "$1") <(sort "$2")
1202     # dash doesn't support the process-sub syntax
1203     (sort "$1" | (sort "$2" | (comm -3 /dev/fd/3 /dev/fd/4) 4<&0) 3<&0)
1204 }
1205 
1206 # Nice DEFine dictionary-defines the words given, using an online service
1207 ndef() {
1208     local arg
1209     local gap=0
1210     local options='-MKiCRS'
1211 
1212     if [ $# -eq 0 ]; then
1213         printf "\e[38;2;204;0;0mndef: no words given\e[0m\n" >&2
1214         return 1
1215     fi
1216 
1217     if [ $# -eq 1 ]; then
1218         options='--header=1 -MKiCRS'
1219     fi
1220 
1221     for arg in "$@"; do
1222         [ "${gap}" -gt 0 ] && printf "\n"
1223         gap=1
1224         printf "\e[7m%-80s\e[0m\n" "${arg}"
1225         curl --silent "dict://dict.org/d:${arg}" | awk '
1226             { gsub(/\r$/, "") }
1227             /^151 / {
1228                 printf "\x1b[38;2;52;101;164m%s\x1b[0m\n", $0
1229                 next
1230             }
1231             /^[1-9][0-9]{2} / {
1232                 printf "\x1b[38;2;128;128;128m%s\x1b[0m\n", $0
1233                 next
1234             }
1235             1
1236         '
1237     done | less ${options}
1238 }
1239 
1240 # listen to streaming NEW WAVE music
1241 newwave() {
1242     printf "streaming \e[7mNew Wave radio\e[0m\n"
1243     mpv --quiet https://puma.streemlion.com:2910/stream
1244 }
1245 
1246 # Nice Json Query Lines colors JSONL data using the `jq` app
1247 njql() {
1248     local code="${1:-.}"
1249     [ $# -gt 0 ] && shift
1250     jq -c -C "${code} | .[]" "$@"
1251 }
1252 
1253 # empty the clipboard
1254 noclip() { wl-copy --clear; }
1255 
1256 # show the current date and time
1257 now() { date +'%Y-%m-%d %H:%M:%S'; }
1258 
1259 # Nice Print Awk result; uses my tool `nn`
1260 npa() {
1261     local arg
1262     for arg in "$@"; do
1263         awk "BEGIN { print(${arg}); exit }"
1264     done | nn
1265 }
1266 
1267 # Nice Print Python result; uses my tool `nn`
1268 npp() {
1269     local arg
1270     for arg in "$@"; do
1271         python -c "print(${arg})"
1272     done | nn
1273 }
1274 
1275 # Nice Size, using my tool `nn`
1276 ns() { wc -c "$@" | nn; }
1277 
1278 # Nice TimeStamp; avoid using `ts`, since busybox's version doesn't flush its
1279 # output every line, which can hide several lines for a long while
1280 nts() {
1281     awk '
1282         {
1283             now = strftime("%Y-%m-%d %H:%M:%S")
1284             printf "\x1b[48;2;218;218;218m\x1b[38;2;0;95;153m%s\x1b[0m\t", now
1285             print
1286             fflush()
1287         }
1288     ' "$@"
1289 }
1290 
1291 # emit nothing to output and/or discard everything from input
1292 null() { [ $# -gt 0 ] && "$@" > /dev/null; }
1293 
1294 # Print AWK expression for each input line
1295 pawk() {
1296     local command='awk'
1297     if { [ -p /dev/stdout ] || [ -t 1 ]; } && [ -e /usr/bin/stdbuf ]; then
1298         command='stdbuf -oL awk'
1299     fi
1300 
1301     local code="${1:-\$0}"
1302     [ $# -gt 0 ] && shift
1303 
1304     ${command} '
1305         FNR == 1 { FS = /\t/ ? "\t" : " "; $0 = $0 }
1306         { low = lower = tolower($0) }
1307         { print('"${code}"') }
1308     ' "$@"
1309 }
1310 
1311 # Print Python result
1312 pp() {
1313     local arg
1314     for arg in "$@"; do
1315         python -c "print(${arg})"
1316     done
1317 }
1318 
1319 # PRecede (input) ECHO, prepends a first line to stdin lines
1320 precho() { echo "$@" && cat /dev/stdin; }
1321 
1322 # LABEL/precede data with an ANSI-styled line
1323 prelabel() { printf "\e[7m%-*s\e[0m\n" "$(($(tput cols) - 2))" "$*"; cat -; }
1324 
1325 # PREcede (input) MEMO, prepends a first highlighted line to stdin lines
1326 prememo() { printf "\e[7m%s\e[0m\n" "$*"; cat -; }
1327 
1328 # start by joining all arguments given as a tab-separated-items line of output,
1329 # followed by all lines from stdin verbatim
1330 pretsv() {
1331     awk '
1332         BEGIN {
1333             for (i = 1; i < ARGC; i++) {
1334                 if (i > 1) printf "\t"
1335                 printf "%s", ARGV[i]
1336             }
1337             if (ARGC > 1) printf "\n"
1338             exit
1339         }
1340     ' "$@"
1341     cat -
1342 }
1343 
1344 # Plain RipGrep
1345 prg() {
1346     if [ -p /dev/stdout ] || [ -t 1 ]; then
1347         rg --line-buffered --color=never "${@:-.}"
1348     else
1349         rg --color=never "${@:-.}"
1350     fi
1351 }
1352 
1353 # Quiet MPV
1354 # qmpv() { mpv --quiet "${@:--}"; }
1355 
1356 # Quiet MPV
1357 qmpv() { mpv --really-quiet "${@:--}"; }
1358 
1359 # ignore stderr, without any ugly keyboard-dancing
1360 quiet() { "$@" 2> /dev/null; }
1361 
1362 # keep only lines between the 2 line numbers given, inclusively
1363 rangelines() {
1364     { [ $# -eq 2 ] || [ $# -eq 3 ]; } && [ "${1}" -le "${2}" ] && {
1365         tail -n +"${1}" "${3:--}" | head -n $(("${2}" - "${1}" + 1))
1366     }
1367 }
1368 
1369 # RANdom MANual page
1370 ranman() {
1371     find "/usr/share/man/man${1:-1}" -type f | shuf -n 1 | xargs basename |
1372         sed 's-\.gz$--' | xargs man
1373 }
1374 
1375 # REPeat STRing emits a line with a repeating string in it, given both a
1376 # string and a number in either order
1377 repstr() {
1378     awk '
1379         BEGIN {
1380             if (ARGV[2] ~ /^[+-]?[0-9]+$/) {
1381                 symbol = ARGV[1]
1382                 times = ARGV[2] + 0
1383             } else {
1384                 symbol = ARGV[2]
1385                 times = ARGV[1] + 0
1386             }
1387 
1388             if (times < 0) exit
1389             if (symbol == "") symbol = "-"
1390             s = sprintf("%*s", times, "")
1391             gsub(/ /, symbol, s)
1392             print s
1393             exit
1394         }
1395     ' "$@"
1396 }
1397 
1398 # show a RULER-like width-measuring line
1399 ruler() {
1400     [ "${1:-80}" -gt 0 ] && printf "%${1:-80}s\n" "" | sed -E \
1401         's- {10}-····╵····│-g; s- -·-g; s-·····-····╵-'
1402 }
1403 
1404 # Summarize via AWK calculates some numeric statistics from an AWK expression
1405 sawk() {
1406     local code="${1:-\$0}"
1407     [ $# -gt 0 ] && shift
1408 
1409     awk '
1410         BEGIN {
1411             numeric = ints = pos = zero = neg = 0
1412 
1413             inf = "+inf" + 0
1414 
1415             min = inf
1416             max = -inf
1417             sum = 0
1418             mean = 0
1419             prod = 1
1420         }
1421 
1422         FNR == 1 { FS = /\t/ ? "\t" : " "; $0 = $0 }
1423         { low = lower = tolower($0) }
1424 
1425         {
1426             v = ('"${code}"')
1427             if (v !~ /^ *(0|[0-9]+|[0-9]*\.[0-9]+) *$/) next
1428             v = v + 0
1429 
1430             numeric++
1431             ints += v % 1 == 0
1432             if (v > 0) pos++
1433             else if (v < 0) neg++
1434             else if (v == 0) zero++
1435 
1436             min = min < v ? min : v
1437             max = max > v ? max : v
1438             sum += v
1439             prod *= v
1440             lnSum += v <= 0 ? -inf : log(v)
1441 
1442             # advance welford`s algorithm
1443             d1 = v - mean
1444             mean += d1 / numeric
1445             d2 = v - mean
1446             meanSq += d1 * d2
1447         }
1448 
1449         END {
1450             sum = mean * numeric
1451             if (numeric == 0) lnSum = -inf
1452 
1453             # separate name-value pairs using tabs, and prepare a
1454             # pipeable command which ignores all-zero decimals
1455             OFS = "\t"
1456 
1457             print "numeric", numeric
1458             if (numeric > 0) {
1459                 print "min", sprintf("%f", min)
1460                 print "max", sprintf("%f", max)
1461                 print "sum", sprintf("%f", sum)
1462                 print "mean", sprintf("%f", mean)
1463                 print "geomean", (zero == 0 && neg == 0) ?
1464                     sprintf("%f", exp(lnSum / numeric)) :
1465                     ""
1466                 print "sd", sprintf("%f", sqrt(meanSq / numeric))
1467                 print "product", sprintf("%g", prod)
1468             } else {
1469                 print "min", ""
1470                 print "max", ""
1471                 print "sum", ""
1472                 print "mean", ""
1473                 print "geomean", ""
1474                 print "sd", ""
1475                 print "product", ""
1476             }
1477             print "integer", ints
1478             print "positive", pos
1479             print "zero", zero
1480             print "negative", neg
1481         }
1482     ' "$@" | sed -E 's-([0-9]+)\.0+$-\1-g; s-([0-9]+\.[0-9]*[1-9])0+$-\1-g'
1483 }
1484 
1485 # SystemCTL; `sysctl` is already taken for a separate/unrelated app
1486 sctl() { systemctl "$@" 2>&1 | less -MKiCRS; }
1487 
1488 # show a unique-looking SEParator line; useful to run between commands
1489 # which output walls of text
1490 sep() {
1491     [ "${1:-80}" -gt 0 ] &&
1492         printf "\e[48;2;218;218;218m%${1:-80}s\e[0m\n" "" | sed 's- -·-g'
1493 }
1494 
1495 # webSERVE files in a folder as localhost, using the port number given, or
1496 # port 8080 by default
1497 serve() {
1498     if [ -d "$1" ]; then
1499         printf "\e[7mserving files in %s\e[0m\n" "$1" >&2
1500         python3 -m http.server -d "$1" "${2:-8080}"
1501     else
1502         printf "\e[7mserving files in %s\e[0m\n" "${2:-$(pwd)}" >&2
1503         python3 -m http.server -d "${2:-$(pwd)}" "${1:-8080}"
1504     fi
1505 }
1506 
1507 # SET DIFFerence sorts its 2 inputs, then finds lines not in the 2nd input
1508 setdiff() {
1509     # comm -23 <(sort "$1") <(sort "$2")
1510     # dash doesn't support the process-sub syntax
1511     (sort "$1" | (sort "$2" | (comm -23 /dev/fd/3 /dev/fd/4) 4<&0) 3<&0)
1512 }
1513 
1514 # SET INtersection, sorts its 2 inputs, then finds common lines
1515 setin() {
1516     # comm -12 <(sort "$1") <(sort "$2")
1517     # dash doesn't support the process-sub syntax
1518     (sort "$1" | (sort "$2" | (comm -12 /dev/fd/3 /dev/fd/4) 4<&0) 3<&0)
1519 }
1520 
1521 # SET SUBtraction sorts its 2 inputs, then finds lines not in the 2nd input
1522 setsub() {
1523     # comm -23 <(sort "$1") <(sort "$2")
1524     # dash doesn't support the process-sub syntax
1525     (sort "$1" | (sort "$2" | (comm -23 /dev/fd/3 /dev/fd/4) 4<&0) 3<&0)
1526 }
1527 
1528 # Show Files (and folders), coloring folders and links
1529 sf() {
1530     local arg
1531     local gap=0
1532     local options='-MKiCRS'
1533 
1534     if [ $# -le 1 ]; then
1535         options='--header=1 -MKiCRS'
1536     fi
1537 
1538     for arg in "${@:-.}"; do
1539         [ "${gap}" -gt 0 ] && printf "\n"
1540         printf "\e[7m%s\e[0m\n\n" "$(realpath "${arg}")"
1541         gap=1
1542 
1543         ls -al --file-type --color=never --time-style iso "${arg}" | awk '
1544             BEGIN {
1545                 drep = "\x1b[38;2;0;135;255m\x1b[48;2;228;228;228m&\x1b[0m"
1546                 lrep = "\x1b[38;2;0;135;95m\x1b[48;2;228;228;228m&\x1b[0m"
1547             }
1548 
1549             NR < 4 { next }
1550             (NR - 3) % 5 == 1 && (NR - 3) > 1 { print "" }
1551 
1552             {
1553                 gsub(/^(d[rwx-]+)/, drep)
1554                 gsub(/^(l[rwx-]+)/, lrep)
1555                 printf "%6d  %s\n", NR - 3, $0
1556             }
1557         '
1558     done | less ${options}
1559 }
1560 
1561 # run apps in color-mode, using the popular option `--color=always`
1562 shine() {
1563     local cmd="$1"
1564     [ $# -gt 0 ] && shift
1565     "${cmd}" --color=always "$@"
1566 }
1567 
1568 # skip the first n lines, or the 1st line by default
1569 skip() { tail -n +$(("${1:-1}" + 1)) "${2:--}"; }
1570 
1571 # skip the last n lines, or the last line by default
1572 skiplast() { head -n -"${1:-1}" "${2:--}"; }
1573 
1574 # SLOW/delay lines from the standard-input, waiting the number of seconds
1575 # given for each line, or waiting 1 second by default
1576 slow() {
1577     local seconds="${1:-1}"
1578     [ $# -gt 0 ] && shift
1579     (
1580         IFS="$(printf "\n")"
1581         awk 1 "$@" | while read -r line; do
1582             sleep "${seconds}"
1583             printf "%s\n" "${line}"
1584         done
1585     )
1586 }
1587 
1588 # Show Latest Podcasts, using my tools `podfeed` and `si`
1589 slp() {
1590     local title
1591     title="Latest Podcast Episodes as of $(date +'%F %T')"
1592     podfeed -title "${title}" "$@" | si
1593 }
1594 
1595 # emit the first line as is, sorting all lines after that, using the
1596 # `sort` command, passing all/any arguments/options to it
1597 sortrest() {
1598     awk -v sort="sort $*" '
1599         FNR == 1 { gsub(/^\xef\xbb\xbf/, "") }
1600         { gsub(/\r$/, "") }
1601         NR == 1 { print; fflush() }
1602         NR >= 2 { print | sort }
1603     '
1604 }
1605 
1606 # SORt Tab-Separated Values: emit the first line as is, sorting all lines after
1607 # that, using the `sort` command in TSV (tab-separated values) mode, passing
1608 # all/any arguments/options to it
1609 sortsv() {
1610     awk -v sort="sort -t \"$(printf '\t')\" $*" '
1611         FNR == 1 { gsub(/^\xef\xbb\xbf/, "") }
1612         { gsub(/\r$/, "") }
1613         NR == 1 { print; fflush() }
1614         NR >= 2 { print | sort }
1615     '
1616 }
1617 
1618 # emit a line with the number of spaces given in it
1619 spaces() { [ "${1:-80}" -gt 0 ] && printf "%${1:-80}s\n" ""; }
1620 
1621 # SQUeeze horizontal spaces and STOMP vertical gaps
1622 squomp() {
1623     local command='awk'
1624     if { [ -p /dev/stdout ] || [ -t 1 ]; } && [ -e /usr/bin/stdbuf ]; then
1625         command='stdbuf -oL awk'
1626     fi
1627 
1628     ${command} '
1629         FNR == 1 { gsub(/^\xef\xbb\xbf/, "") }
1630         /^\r?$/ { empty = 1; next }
1631         empty { if (n > 0) print ""; empty = 0 }
1632 
1633         {
1634             gsub(/^ +| *\r?$/, "")
1635             gsub(/ *\t */, "\t")
1636             gsub(/  +/, " ")
1637             print; n++
1638         }
1639     ' "$@"
1640 }
1641 
1642 substr() {
1643     local command='awk'
1644     if { [ -p /dev/stdout ] || [ -t 1 ]; } && [ -e /usr/bin/stdbuf ]; then
1645         command='stdbuf -oL awk'
1646     fi
1647     if [ $# -lt 2 ]; then
1648         printf "missing 1-based start index, and substring length\n" >&2
1649         exit 1
1650     fi
1651 
1652     ${command} '{ print substr($0, '"$1"', '"$2"') }'
1653 }
1654 
1655 # add a final sums row after all input lines
1656 sums() {
1657     local command='awk'
1658     if { [ -p /dev/stdout ] || [ -t 1 ]; } && [ -e /usr/bin/stdbuf ]; then
1659         command='stdbuf -oL awk'
1660     fi
1661 
1662     ${command} '
1663         { gsub(/\r$/, "") }
1664 
1665         NR == 1 { FS = /\t/ ? "\t" : " "; $0 = $0 }
1666 
1667         {
1668             if (n < NF) n = NF
1669             for (i = 1; i <= NF; i++) sums[i] += $i
1670             print
1671         }
1672 
1673         END {
1674             for (i = 1; i <= n; i++) {
1675                 if (i > 1) printf(FS)
1676                 printf("%s", sums[i])
1677             }
1678             if (n > 0) printf "\n"
1679         }
1680     ' "$@"
1681 }
1682 
1683 # TAC Lines outputs input-lines in reverse order, last one first, and so on...
1684 tacl() {
1685     awk '
1686         { gsub(/\r$/, ""); lines[NR] = $0 }
1687         END { for (i = NR; i >= 1; i--) print lines[i] }
1688     ' "$@"
1689 }
1690 
1691 # Simulate the cadence of old-fashioned TELETYPE machines
1692 teletype() {
1693     awk '
1694         {
1695             gsub(/\r$/, "")
1696 
1697             n = length($0)
1698             for (i = 1; i <= n; i++) {
1699                 if (code = system("sleep 0.015")) exit code
1700                 printf "%s", substr($0, i, 1); fflush()
1701             }
1702 
1703             if (code = system("sleep 0.75")) exit code
1704             printf "\n"; fflush()
1705         }
1706 
1707         # END { if (NR > 0 && code != 0) printf "\n" }
1708     ' "$@"
1709 }
1710 
1711 # TINY GO Build Optimized: a common use-case for the tinygo compiler
1712 tinygobo() { tinygo build -no-debug -opt=2 "$@"; }
1713 
1714 # Timed Make, also showing max memory used
1715 tm() { /usr/bin/time -f 'real %e    user %U    sys %S    mem %M' make "$@"; }
1716 
1717 # show current date in a specifc format
1718 today() { date +'%Y-%m-%d %a %b %d'; }
1719 
1720 # get the first n lines, or 1 by default
1721 toline() { head -n "${1:-1}" "${2:--}"; }
1722 
1723 # lowercase all ASCII symbols
1724 tolower() {
1725     if { [ -p /dev/stdout ] || [ -t 1 ]; } && [ -e /usr/bin/stdbuf ]; then
1726         stdbuf -oL awk '{ print tolower($0) }' "$@"
1727     else
1728         awk '{ print tolower($0) }' "$@"
1729     fi
1730 }
1731 
1732 # get the processes currently using the most cpu
1733 topcpu() {
1734     local n="${1:-10}"
1735     [ "$n" -gt 0 ] && ps aux | awk '
1736         NR == 1 { print; fflush() }
1737         NR > 1 { print | "sort -rnk3,3" }
1738     ' | head -n "$(("$n" + 1))"
1739 }
1740 
1741 # get the processes currently using the most memory
1742 topmemory() {
1743     local n="${1:-10}"
1744     [ "$n" -gt 0 ] && ps aux | awk '
1745         NR == 1 { print; fflush() }
1746         NR > 1 { print | "sort -rnk6,6" }
1747     ' | head -n "$(("$n" + 1))"
1748 }
1749 
1750 # transpose (switch) rows and columns from tables
1751 transpose() {
1752     awk '
1753         { gsub(/\r$/, "") }
1754 
1755         FNR == 1 { FS = /\t/ ? "\t" : " "; $0 = $0 }
1756 
1757         {
1758             for (i = 1; i <= NF; i++) lines[i][NR] = $i
1759             if (maxitems < NF) maxitems = NF
1760         }
1761 
1762         END {
1763             for (j = 1; j <= maxitems; j++) {
1764                 for (i = 1; i <= NR; i++) {
1765                     if (i > 1) printf "\t"
1766                     printf "%s", lines[j][i]
1767                 }
1768                 printf "\n"
1769             }
1770         }
1771     ' "$@"
1772 }
1773 
1774 # Underline every 3 lines: make groups of 3 lines stand out by underlining
1775 # the last line of each such group
1776 u3() {
1777     local command='awk'
1778     if { [ -p /dev/stdout ] || [ -t 1 ]; } && [ -e /usr/bin/stdbuf ]; then
1779         command='stdbuf -oL awk'
1780     fi
1781 
1782     ${command} '
1783         NR % 3 == 0 && NR != 1 {
1784             gsub(/\x1b\[0m/, "\x1b[0m\x1b[4m")
1785             printf("\x1b[4m%s\x1b[0m\n", $0)
1786             next
1787         }
1788         1
1789     ' "$@"
1790 }
1791 
1792 # Underline every 5 lines: make groups of 5 lines stand out by underlining
1793 # the last line of each such group
1794 u5() {
1795     local command='awk'
1796     if { [ -p /dev/stdout ] || [ -t 1 ]; } && [ -e /usr/bin/stdbuf ]; then
1797         command='stdbuf -oL awk'
1798     fi
1799 
1800     ${command} '
1801         NR % 5 == 0 && NR != 1 {
1802             gsub(/\x1b\[0m/, "\x1b[0m\x1b[4m")
1803             printf("\x1b[4m%s\x1b[0m\n", $0)
1804             next
1805         }
1806         1
1807     ' "$@"
1808 }
1809 
1810 # Underline Header 3: underline the first line (the header), then underline
1811 # every 3rd line after that
1812 uh3() {
1813     local command='awk'
1814     if { [ -p /dev/stdout ] || [ -t 1 ]; } && [ -e /usr/bin/stdbuf ]; then
1815         command='stdbuf -oL awk'
1816     fi
1817 
1818     ${command} '
1819         NR == 1 || (NR - 1) % 3 == 0 {
1820             gsub(/\x1b\[0m/, "\x1b[0m\x1b[4m")
1821             printf("\x1b[4m%s\x1b[0m\n", $0)
1822             next
1823         }
1824         1
1825     ' "$@"
1826 }
1827 
1828 # Underline Header 5: underline the first line (the header), then underline
1829 # every 5th line after that
1830 uh5() {
1831     local command='awk'
1832     if { [ -p /dev/stdout ] || [ -t 1 ]; } && [ -e /usr/bin/stdbuf ]; then
1833         command='stdbuf -oL awk'
1834     fi
1835 
1836     ${command} '
1837         NR == 1 || (NR - 1) % 5 == 0 {
1838             gsub(/\x1b\[0m/, "\x1b[0m\x1b[4m")
1839             printf("\x1b[4m%s\x1b[0m\n", $0)
1840             next
1841         }
1842         1
1843     ' "$@"
1844 }
1845 
1846 # only keep UNIQUE lines, keeping them in their original order
1847 unique() {
1848     local command='awk'
1849     if { [ -p /dev/stdout ] || [ -t 1 ]; } && [ -e /usr/bin/stdbuf ]; then
1850         command='stdbuf -oL awk'
1851     fi
1852 
1853     ${command} '
1854         BEGIN { for (i = 1; i < ARGC; i++) if (f[ARGV[i]]++) delete ARGV[i] }
1855         !c[$0]++
1856     ' "$@"
1857 }
1858 
1859 # fix lines, ignoring leading UTF-8_BOMs (byte-order-marks) on each input's
1860 # first line, turning all end-of-line CRLF byte-pairs into single line-feeds,
1861 # and ensuring each input's last line ends with a line-feed; trailing spaces
1862 # are also ignored
1863 unixify() {
1864     local command='awk'
1865     if { [ -p /dev/stdout ] || [ -t 1 ]; } && [ -e /usr/bin/stdbuf ]; then
1866         command='stdbuf -oL awk'
1867     fi
1868 
1869     ${command} '
1870         FNR == 1 { gsub(/^\xef\xbb\xbf/, "") }
1871         { gsub(/ *\r?$/, ""); print }
1872     ' "$@"
1873 }
1874 
1875 # skip the first/leading n bytes
1876 unleaded() { tail -c +$(("$1" + 1)) "${2:--}"; }
1877 
1878 # go UP n folders, or go up 1 folder by default
1879 up() {
1880     if [ "${1:-1}" -le 0 ]; then
1881         cd .
1882     else
1883         cd "$(printf "%${1:-1}s" "" | sed 's- -../-g')" || return $?
1884     fi
1885 }
1886 
1887 # convert United States Dollars into CAnadian Dollars, using the latest
1888 # official exchange rates from the bank of canada; during weekends, the
1889 # latest rate may be from a few days ago; the default amount of usd to
1890 # convert is 1, when not given
1891 usd2cad() {
1892     local site='https://www.bankofcanada.ca/valet/observations/group'
1893     local csv_rates="${site}/FX_RATES_DAILY/csv"
1894     local url="${csv_rates}?start_date=$(date -d '3 days ago' +'%Y-%m-%d')"
1895     curl -s "${url}" | awk -F, -v amount="$(echo "${1:-1}" | sed 's-_--g')" '
1896         /USD/ { for (i = 1; i <= NF; i++) if($i ~ /USD/) j = i }
1897         END { gsub(/"/, "", $j); if (j != 0) printf "%.2f\n", amount * $j }
1898     '
1899 }
1900 
1901 # View Nice Table / Very Nice Table; uses my tool `ncol`
1902 vnt() {
1903     ncol "$@" | awk '
1904         (NR - 1) % 5 == 1 { print "" }
1905         { printf "%6d  %s\n", NR - 1, $0 }
1906     ' | { less -MKiCRS --header=1 2> /dev/null || cat; }
1907 }
1908 
1909 # View Text, turning documents into plain-text if needed; uses `pandoc`
1910 vt() {
1911     local arg
1912     local gap=0
1913     local options='-MKiCRS'
1914 
1915     if [ $# -eq 1 ]; then
1916         options='--header=1 -MKiCRS'
1917     fi
1918 
1919     if [ $# -eq 0 ]; then
1920         pandoc -s -t plain - 2>&1 | less -MKiCRS
1921     else
1922         for arg in "$@"; do
1923             [ "${gap}" -eq 1 ] && printf "\n"
1924             gap=1
1925             printf "\e[7m%-80s\e[0m\n" "${arg}"
1926             pandoc -s -t plain "${arg}" 2>&1 | awk 1
1927         done | less ${options}
1928     fi
1929 }
1930 
1931 # What Are These (?) shows what the names given to it are/do
1932 wat() {
1933     local arg
1934     local gap=0
1935     local less_options='-MKiCRS'
1936 
1937     if [ $# -eq 0 ]; then
1938         echo "$0"
1939         return 0
1940     fi
1941 
1942     if [ $# -lt 2 ]; then
1943         less_options='-MKiCRS --header=1'
1944     fi
1945 
1946     for arg in "$@"; do
1947         [ "${gap}" -gt 0 ] && printf "\n"
1948         gap=1
1949         printf "\e[7m%-80s\e[0m\n" "${arg}"
1950 
1951         while alias "${arg}" > /dev/null 2> /dev/null; do
1952             arg="$(alias "${arg}" | sed -E "s-^[^=]+=['\"](.+)['\"]\$-\\1-")"
1953         done
1954 
1955         if echo "${arg}" | grep -q ' '; then
1956             printf "%s\n" "${arg}"
1957             continue
1958         fi
1959 
1960         if declare -f "${arg}"; then
1961             continue
1962         fi
1963 
1964         if which "${arg}" > /dev/null 2> /dev/null; then
1965             which "${arg}"
1966             continue
1967         fi
1968 
1969         printf "\e[38;2;204;0;0m%s not found\e[0m\n" "${arg}"
1970     done | { less -MKiCRS ${less_options} 2> /dev/null || cat; }
1971 }
1972 
1973 # find all WEB/hyperLINKS (https:// and http://) in the input text
1974 weblinks() {
1975     local arg
1976     local re='https?://[A-Za-z0-9+_.:%-]+(/[A-Za-z0-9+_.%/,#?&=-]*)*'
1977     local grep_cmd='grep'
1978     if [ -p /dev/stdout ] || [ -t 1 ]; then
1979         grep_cmd='grep --line-buffered'
1980     fi
1981 
1982     for arg in "${@:--}"; do
1983         ${grep_cmd} -i -E -o "${re}" "${arg}"
1984     done
1985 }
1986 
1987 # recursively find all files with trailing spaces/CRs
1988 whichtrails() {
1989     if [ -p /dev/stdout ] || [ -t 1 ]; then
1990         rg --line-buffered  -c '[ \r]+$' "${@:-.}"
1991     else
1992         rg -c '[ \r]+$' "${@:-.}"
1993     fi
1994 }
1995 
1996 # turn all wsl/unix-style full-paths into windows-style full-paths
1997 # winpaths() { sed -E 's-/mnt/(.)/-\u\1:/-' "$@"; }
1998 
1999 # XARGS Lines, runs `xargs` using whole lines as extra arguments
2000 xargsl() {
2001     if { [ -p /dev/stdout ] || [ -t 1 ]; } && [ -e /usr/bin/stdbuf ]; then
2002         stdbuf -oL awk -v ORS='\000' '
2003             FNR == 1 { gsub(/^\xef\xbb\xbf/, "") }
2004             { gsub(/\r$/, ""); print }
2005         ' | stdbuf -oL xargs -0 "$@"
2006     else
2007         awk -v ORS='\000' '
2008             FNR == 1 { gsub(/^\xef\xbb\xbf/, "") }
2009             { gsub(/\r$/, ""); print }
2010         ' | xargs -0 "$@"
2011     fi
2012 }
2013 
2014 # Youtube Audio Player
2015 yap() {
2016     # some youtube URIs end with extra playlist/tracker parameters
2017     local url="$(echo "$1" | sed 's-&.*--')"
2018     mpv "$(yt-dlp -x --audio-format best --get-url "${url}" 2> /dev/null)"
2019 }
2020 
2021 # show a calendar for the current YEAR, or for the year given
2022 year() {
2023     {
2024         # show the current date/time center-aligned
2025         printf \
2026             "%21s\e[38;2;78;154;6m%s\e[0m  \e[38;2;52;101;164m%s\e[0m\n\n" \
2027             "" "$(date +'%a %b %d %Y')" "$(date +'%H:%M')"
2028         # debian linux has a different `cal` app which highlights the day
2029         if [ -e /usr/bin/ncal ]; then
2030             # fix debian/ncal's weird way to highlight the current day
2031             ncal -C -y "$@" | sed -E \
2032                 's/_\x08(.+)_\x08([^ ]+)/\x1b\[7m\1\2\x1b\[0m/'
2033         else
2034             cal -y "$@"
2035         fi
2036     } | { less -MKiCRS 2> /dev/null || cat; }
2037 }
2038 
2039 # show the current date in the YYYY-MM-DD format
2040 ymd() { date +'%Y-%m-%d'; }