File: clam.sh
   1 #!/bin/sh
   2 
   3 # The MIT License (MIT)
   4 #
   5 # Copyright © 2024 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 # Command-Line Augmentation Module: get the best out of your shell
  28 #
  29 # This is a collection of arguably useful shell functions and shortcuts:
  30 # some of these can be real time/effort savers, letting you concentrate
  31 # on getting things done.
  32 #
  33 # You're supposed to `source` this script, so its definitions stay for
  34 # your whole shell session: for that, you can run `source clam` or
  35 # `. clam` (no quotes either way), either directly or at shell startup.
  36 #
  37 # Most of these shell functions rely on tools which are almost always
  38 # available. The functions depending on tools which are least-likely
  39 # pre-installed are
  40 #
  41 #   cancur       curl
  42 #   cgt          go
  43 #   crg          rg
  44 #   cs           bat
  45 #   csf          bat
  46 #   dic          curl
  47 #   dict         curl
  48 #   fetch        curl
  49 #   fetchjson    curl
  50 #   get          curl
  51 #   getjson      curl
  52 #   gobs         go
  53 #   gobwingui    go
  54 #   godep        go
  55 #   goimp        go
  56 #   h            hat         my own `HAndy Tools` app; commented out
  57 #   hmr          bat
  58 #   j2           python      commented out
  59 #   jc           python      uses non-built-in package; commented out
  60 #   joke         curl
  61 #   lab          book        my own script; commented out
  62 #   plin         mpv
  63 #   qurl         curl
  64 #   serve        python
  65 #   wheretrails  rg
  66 #   whichtrails  rg
  67 #   yap          mpv, yt-dlp
  68 #   yd           yt-dlp
  69 #   ydaac        yt-dlp
  70 #   ydmp4        yt-dlp
  71 #
  72 #
  73 # Full list of funcs/commands added
  74 #
  75 # a            run Awk
  76 # allfiles     show all files in a folder, digging recursively
  77 # allfolders   show all folders in a folder, digging recursively
  78 # args         emit each argument given to it on its own line
  79 # avoid        ignore lines matching the regex given
  80 # banner       show words/arguments given as a styled line
  81 # bar          emit a styled bar to visually separate different outputs
  82 # begincsv     emit a line with comma-separated values, then emit stdin lines
  83 # begintsv     emit a line with tab-separated values, then emit stdin lines
  84 # bh           Breathe Header makes lines breathe starting from the 2nd one
  85 # bigfiles     dig into a folder recursively for files of at least n bytes
  86 # bl           Breathe Lines regularly adds extra empty lines every few
  87 # blawk        process BLocks of non-empty lines with AWK
  88 # bleak        Blue LEAK helps you debug pipes, via colored stderr lines
  89 # blow         expand tabs into spaces, using the tabstop-width given
  90 # blowtabs     expand tabs into spaces, using the tabstop-width given
  91 # breathe      Breathe regularly adds extra empty lines every few
  92 # c            run `cat`, which is useful, despite claims to the contrary
  93 # cancur       get the latest exchange rates from the bank of canada as TSV
  94 # cap          limit lines up to their first n bytes, line-feed excluded
  95 # cawk         Comma AWK runs AWK in CSV mode
  96 # cgt          Colored Go Test runs `go test`, making problems stand out
  97 # chopdecs     ignore trailing decimal zeros in numbers
  98 # choplf       ignore last byte if it's a line-feed
  99 # cls          CLear Screen
 100 # coco         COunt COndition, uses an AWK condition as its first arg
 101 # crep         Colored Regular Expression Printer runs `grep` in color-mode
 102 # crg          Colored RipGrep
 103 # cs           Color Syntax
 104 # csf          Color Syntax for a whole Folder, even its subfolders
 105 # debase64     DEcode BASE64 bytes
 106 # debom        ignore leading utf-8 BOM markers for each input, when present
 107 # decap        DECAPitate emits the first n lines to stderr
 108 # decrlf       turn all CRLF byte-pairs into single line-feed bytes
 109 # dedent       ignore the first n leading spaces per line, or 4 by default
 110 # dedup        deduplicate lines, keeping them in their original order
 111 # delay        delay each input line, waiting the number of seconds given
 112 # detab        expand tabs into spaces, using the tabstop-width given
 113 # dic          lookup words using an online dictionary
 114 # dict         lookup words using an online dictionary
 115 # div          divide 2 numbers both ways/orders
 116 # dt           show the current Date and Time, and the 3 `current` months
 117 # each         run command using each stdin line as a stdin-redirected file
 118 # eg           Extended Grep
 119 # emptyfiles   dig into a folder recursively to show all empty files in it
 120 # emptyfolders  dig into a folder recursively to show all empty folders in it
 121 # enum         enumerate all lines, starting from 1
 122 # enum1        enumerate all lines, starting from 1
 123 # enum0        enumerate all lines, starting from 0
 124 # except       ignore lines matching the regex given
 125 # f            show all Files in a folder, digging recursively
 126 # fail         show an error message and fail
 127 # fetch        get/fetch bytes from the URI given
 128 # fetchjson    asks webserver at URI given to respond with a JSON payload
 129 # findtargets  show/find all targets from makefiles
 130 # first        keep only the first n lines, or just the first 1 by default
 131 # flawk        First Line AWK, and lines satisfying the optional condition
 132 # forever      keep (re)running the command given, until forced to quit
 133 # g            run Grep (extended)
 134 # get          get/fetch bytes from the URI given
 135 # gethelp      show help for the command given, using common help options
 136 # getjson      asks webserver at URI given to respond with a JSON payload
 137 # get          try to get/fetch JSON data from the URI given
 138 # gleak        Green LEAK helps you debug pipes, via colored stderr lines
 139 # glue         join all input lines using the separator/joiner string given
 140 # gobs         GO Build Stripped
 141 # gobwingui    GO Build WINdows GUI
 142 # godep        GO DEPendencies from the folder given
 143 # goimp        GO IMPorts from the folder given
 144 # gsub         run the awk function of the same name
 145 # h            run my own `hat` (HAndy Tools) cmd-line app; disabled
 146 # hawk         highlight lines matching the awk condition given
 147 # helpless     view most apps' help messages using `less`
 148 # hl           Header Less runs `less` and always shows the 1st line on top
 149 # hmr          Help Me Remember one of these commands
 150 # hv           Header View runs `less` and always shows the 1st line on top
 151 # indent       indent text the number of spaces given, or 4 by default
 152 # j2           reformat JSON into multiple lines with 2-space indent levels
 153 # jc           shortcut to run `jc`, a useful data-to-JSON python module
 154 # joke         show a `Dad Joke` from the web, sometimes even a funny one
 155 # l            run `less`, enabling line numbers, scrolling, and ANSI styles
 156 # lab          layout content like a book; disabled; uses my own `book` app
 157 # largs        run `xargs` taking the extra arguments from whole stdin lines
 158 # last         keep only the last n lines, or just the last 1 by default
 159 # leak         emit input lines both to stdout and stderr, to debug pipes
 160 # leako        LEAK Orange helps you debug pipes, via colored stderr lines
 161 # least        run `less`, enabling line numbers, scrolling, and ANSI styles
 162 # lf           List Files, coloring folders and links
 163 # lh           Less with Header runs `less` and always shows the 1st line
 164 # lineup       regroup adjacent lines into n-item tab-separated lines
 165 # listfiles    list files, coloring folders and links
 166 # loser        LOcal SERver webserves files in a folder as localhost
 167 # lower        lowercase all lines
 168 # m            case-sensitively match the extended regex given
 169 # match        case-sensitively match the extended regex given
 170 # merrge       redirect stderr into stdout, without any ugly finger-dancing
 171 # n            Number all lines, starting from 1
 172 # n0           Number all lines, starting from 0, instead of 1
 173 # n1           Number all lines, starting from 1
 174 # narrow       try to limit lines up to n symbols per line, or 80 by default
 175 # nfs          Nice File Sizes
 176 # nil          emit nothing to stdout and/or discard everything from stdin
 177 # noerr        ignore stderr, without any ugly finger-dancing
 178 # now          show the current date and time
 179 # nth          keep only the n-th line from the input, if it has enough lines
 180 # oleak        Orange LEAK helps you debug pipes, via colored stderr lines
 181 # p            Plain ignores ANSI terminal styling
 182 # pawk         Print AWK expressions
 183 # plain        ignore all ANSI styles
 184 # plainend     reset ANSI styles at the end of each line
 185 # plin         PLay INput handles playable/multimedia streams from stdin
 186 # qurl         Quiet cURL
 187 # reprose      reflow/trim lines of prose (text) for improved legibility
 188 # repstr       REPeat a STRing n times, or 80 times by default
 189 # runin        run a command in the folder given
 190 # s            run Sed (extended)
 191 # sep          emit a unique-looking separator line
 192 # serve        start a local webserver from the current folder
 193 # setdiff      find the SET DIFFerence, after sorting its 2 inputs
 194 # setin        SET INtersection, sorts its 2 inputs, then finds common lines
 195 # setsub       find the SET SUBtraction, after sorting its 2 inputs
 196 # showrun      show a command, then run it
 197 # skip         skip the first n lines, or the first 1 by default
 198 # skipfirst    skip the first n lines, or the first 1 by default
 199 # skiplast     ignore the last n lines, or the very last 1 by default
 200 # smallfiles   dig into a folder recursively for files under n bytes
 201 # sosi         reverse-SOrted SIzes of the files given
 202 # squeeze      aggressively get rid of extra spaces on every line
 203 # ssv2tsv      turn each run of 1+ spaces into a single tab
 204 # stripe       underline every 5th line
 205 # t            Trim trailing spaces and carriage-returns
 206 # tawk         Tab AWK, runs AWK using tab as its IO item-separator
 207 # today        show current date in a way friendly both to people and tools
 208 # topfiles     show all files directly in a folder, without recursion
 209 # topfolders   show all folders directly in a folder, without recursion
 210 # trim         get rid of leading and trailing spaces on lines
 211 # trimprefix   ignore the prefix given from input lines which start with it
 212 # trimsuffix   ignore the suffix given from input lines which end with it
 213 # trimtrail    get rid of trailing spaces on lines
 214 # trimtrails   get rid of trailing spaces on lines
 215 # try          try running the command given
 216 # tsawk        TimeStamp lines satisfying AWK condition, ignoring the rest
 217 # u            Unixify ensures plain-text lines are unix-like
 218 # unbase64     decode base64 bytes
 219 # unique       deduplicate lines, keeping them in their original order
 220 # unixify      ensure plain-text lines are unix-like
 221 # uncrlf       turn all CRLF byte-pairs into single line-feed bytes
 222 # untab        expand tabs into spaces, using the tabstop-width given
 223 # up           go UP n folders, or go up 1 folder by default
 224 # upsidedown   emit input lines in reverse order, or last to first
 225 # ut           Underline Table underlines the 1st line, then 1 line every 5
 226 # v            View runs `less`, enabling scrolling and ANSI styles
 227 # verdict      run a command, showing its success/failure right after
 228 # vh           View with Header runs `less` and always shows the 1st line
 229 # wat          What Are These (?) shows what the names given to it do
 230 # wheretrails  find all files which have trailing spaces/CRs on their lines
 231 # whichtrails  find all files which have trailing spaces/CRs on their lines
 232 # wit          What Is This (?) shows what the name given to it does
 233 # yap          Youtube Audio Player
 234 # yd           Youtube Download
 235 # ydaac        Youtube Download AAC audio
 236 # ydmp4        Youtube Download MP4 video
 237 # year         show a full calendar for the current year, or the year given
 238 # ymd          show the current date in the YYYY-MM-DD format
 239 
 240 
 241 # handle help options
 242 case "$1" in
 243     -h|--h|-help|--help)
 244         # show help message, extracting the info-comment at the start
 245         # of this file, and quit
 246         awk '/^# +clam/, /^$/ { gsub(/^# ?/, ""); print }' "$0"
 247         exit 0
 248     ;;
 249 esac
 250 
 251 
 252 # use a simple shell prompt
 253 # PS1="\$ "
 254 # PS2="> "
 255 
 256 # use a simple shell prompt, showing the current folder in the title
 257 # PS1="\[\e]0;\w\a\]\$ "
 258 # PS2="> "
 259 
 260 # prevent `less` from saving searches/commands
 261 # LESSHISTFILE="-"
 262 # LESSSECURE=1
 263 
 264 # prevent the shell from saving commands
 265 # unset HISTFILE
 266 
 267 
 268 # dashed aliases of multi-word commands defined later
 269 alias all-files='allfiles'
 270 alias all-folders='allfolders'
 271 alias begin-csv='begincsv'
 272 alias begin-tsv='begintsv'
 273 alias blow-tabs='blowtabs'
 274 alias chop-decs='chopdecs'
 275 alias chop-lf='choplf'
 276 alias empty-files='emptyfiles'
 277 alias empty-folders='emptyfolders'
 278 alias fetch-json='fetchjson'
 279 alias get-help='gethelp'
 280 alias get-json='getjson'
 281 alias help-for='gethelp'
 282 alias help-less='helpless'
 283 alias line-up='lineup'
 284 alias list-files='listfiles'
 285 alias plain-end='plainend'
 286 alias rep-str='repstr'
 287 alias run-in='runin'
 288 alias set-diff='setdiff'
 289 alias set-in='setin'
 290 alias set-sub='setsub'
 291 alias show-run='showrun'
 292 alias skip-first='skipfirst'
 293 alias skip-last='skiplast'
 294 alias small-files='smallfiles'
 295 alias ssv-to-tsv='ssv2tsv'
 296 alias top-files='topfiles'
 297 alias top-folders='topfolders'
 298 alias trim-prefix='trimprefix'
 299 alias trim-suffix='trimsuffix'
 300 alias trim-trail='trimtrail'
 301 alias trim-trails='trimtrails'
 302 alias ts-awk='tsawk'
 303 alias upside-down='upsidedown'
 304 alias where-trails='wheretrails'
 305 alias which-trails='whichtrails'
 306 alias ydaac='ydaac'
 307 alias ydmp4='ydmp4'
 308 
 309 # undashed aliases for commands defined later
 310 alias bocler='cancur'
 311 
 312 
 313 # run Awk
 314 a() {
 315     awk "$@"
 316 }
 317 
 318 # show all files in a folder, digging recursively
 319 allfiles() {
 320     local arg
 321     for arg in "${@:-.}"; do
 322         find "${arg}" -type f
 323     done
 324 }
 325 
 326 # show all folders in a folder, digging recursively
 327 allfolders() {
 328     local arg
 329     for arg in "${@:-.}"; do
 330         find "${arg}" -type d | awk 'NR > 1'
 331     done
 332 }
 333 
 334 # emit each argument given as its own line of output
 335 args() {
 336     awk 'BEGIN { for (i = 1; i < ARGC; i++) print ARGV[i]; exit }' "$@"
 337 }
 338 
 339 # avoid lines matching the regex given, or avoid empty(ish) lines by default
 340 avoid() {
 341     regex="${1:-[^ *]\r?$}"
 342     shift
 343     grep -E -v "${regex}" "$@"
 344 }
 345 
 346 # show a line which clearly labels part of a shell session
 347 banner() {
 348     # printf "\x1b[7m%-80s\x1b[0m\n" "$*"
 349     printf "\x1b[48;5;253m%-80s\x1b[0m\n" "$*"
 350 }
 351 
 352 # emit a colored bar which can help visually separate different outputs
 353 bar() {
 354     printf "\x1b[48;5;253m%${1:-80}s\x1b[0m\n" " "
 355 }
 356 
 357 # emit a line with comma-separated values, then emit all stdin lines
 358 begincsv() {
 359     awk 'BEGIN {
 360         for (i = 1; i < ARGC; i++) {
 361             if (i > 1) printf ","
 362             printf "%s", ARGV[i]
 363             delete ARGV[i]
 364         }
 365         if (ARGC > 0) printf "\n"
 366     }
 367     1' "$@"
 368 }
 369 
 370 # emit a line with tab-separated values, then emit all stdin lines
 371 begintsv() {
 372     awk 'BEGIN {
 373         for (i = 1; i < ARGC; i++) {
 374             if (i > 1) printf "\t"
 375             printf "%s", ARGV[i]
 376             delete ARGV[i]
 377         }
 378         if (ARGC > 0) printf "\n"
 379     }
 380     1' "$@"
 381 }
 382 
 383 # Breathe Header: add an empty line after the first one (the header),
 384 # then separate groups of 5 lines with empty lines between them
 385 bh() {
 386     awk '(NR - 1) % 5 == 1 && NR > 1 { print "" } 1' "$@"
 387 }
 388 
 389 # dig into a folder recursively for files of at least n bytes
 390 bigfiles() {
 391     find "${2:-.}" -type f -size "${1:-1000000}"c -o -size +"${1:-1000000}"c
 392 }
 393 
 394 # Breathe Lines: separate groups of 5 lines with empty lines
 395 bl() {
 396     awk 'NR % 5 == 1 && NR != 1 { print "" } 1' "$@"
 397 }
 398 
 399 # process BLocks of non-empty lines with AWK
 400 blawk() {
 401     awk -F='' -v RS='' "$@"
 402 }
 403 
 404 # Blue LEAK emits/tees input both to stdout and stderr, coloring blue what
 405 # it emits to stderr using an ANSI-style; this cmd is useful to `debug`
 406 # pipes involving several steps
 407 bleak() {
 408     awk '
 409     {
 410         print
 411         fflush()
 412         gsub(/\x1b\[0m/, "\x1b[0m\x1b[38;5;26m")
 413         printf "\x1b[38;5;26m%s\x1b[0m\n", $0 > "/dev/stderr"
 414         fflush("/dev/stderr")
 415     }'
 416 }
 417 
 418 # expand tabs into spaces using the tabstop given
 419 blow() {
 420     local tabstop
 421     tabstop="${1:-4}"
 422     shift
 423     expand -t "${tabstop}" "$@"
 424 }
 425 
 426 # expand tabs into spaces using the tabstop given
 427 blowtabs() {
 428     local tabstop
 429     tabstop="${1:-4}"
 430     shift
 431     expand -t "${tabstop}" "$@"
 432 }
 433 
 434 # separate groups of 5 lines with empty lines, making text-rows much
 435 # easier to follow/eye-scan along, especially with tall walls of text
 436 breathe() {
 437     awk 'NR % 5 == 1 && NR != 1 { print "" } 1' "$@"
 438 }
 439 
 440 # `cat` can be useful, despite claims to the contrary
 441 c() {
 442     cat "$@"
 443 }
 444 
 445 # CANadian CURrencies emits the Bank Of Canada's Latest Exchange Rates as a
 446 # 2-line table of tab-separated values, where the first line is the header
 447 cancur() {
 448     local b
 449     b='https://www.bankofcanada.ca/valet/observations/group/FX_RATES_DAILY/'
 450     # starting `3 days ago` ensures this works even on weekends, while
 451     # minimizing the data transmitted
 452     curl -s "${b}csv?start_date=$(date -d '3 days ago' +'%Y-%m-%d')" |
 453     # pick the header line, along with the last one, turning CSV into TSV
 454     awk '/^"date"/; END { print }' | tr -d '"' | tr ',' '\t' |
 455     # simplify/change most column names
 456     sed 's-FX--g; s-CAD--g'
 457 }
 458 
 459 # limit lines up to their first n bytes (80 by default), line-feed excluded
 460 cap() {
 461     local n
 462     n="${1:-80}"
 463     shift
 464     awk -v n="${n}" '{ print substr($0, 1, n) }' "$@"
 465 }
 466 
 467 # Comma AWK: run awk in CSV mode
 468 cawk() {
 469     awk --csv "$@"
 470 }
 471 
 472 # Colored Go Test on the folder given
 473 cgt() {
 474     go test "${1:-.}" 2>&1 | awk '
 475         /^ok/ { printf "\x1b[38;5;29m%s\x1b[0m\n", $0; fflush() }
 476         /^[-]* ?FAIL/ { printf "\x1b[38;5;1m%s\x1b[0m\n", $0; fflush() }
 477         /^\?/ { printf "\x1b[38;5;249m%s\x1b[0m\n", $0; fflush() }'
 478 }
 479 
 480 # ignore trailing decimal zeros in numbers
 481 chopdecs() {
 482     awk '{
 483         for (i = 1; i <= NF; i++) {
 484             gsub(/(\.[0-9]+[1-9]+)0+$/, "&1")
 485             gsub(/([0-9]+)\.0*$/, "&1")
 486         }
 487         print
 488     }' "$@"
 489 }
 490 
 491 # ignore final life-feed from text, if it's the very last byte; all
 492 # carriage-returns in CRLF byte-pairs are also ignored
 493 choplf() {
 494     awk 'NR > 1 { print "" } { printf "%s", $0 }' "$@"
 495 }
 496 
 497 # CLear Screen
 498 cls() {
 499     clear
 500 }
 501 
 502 # COunt COndition: count how many times the AWK expression given is true
 503 coco() {
 504     local cond
 505     cond="${1:-1}"
 506     shift
 507     awk "${cond} { c++ } END { print c }" "$@"
 508 }
 509 
 510 # Colored Regular Expression Printer runs `grep` to shows colored matches
 511 crep() {
 512     grep --color=always "$@"
 513 }
 514 
 515 # Colored RipGrep: ensures app `rg` emits colors when piped
 516 crg() {
 517     rg --color=always "$@"
 518 }
 519 
 520 # Color Syntax: run syntax-coloring app `bat` without line-wrapping
 521 cs() {
 522     local cmd
 523     cmd="bat"
 524     # debian linux uses a different name for the `bat` app
 525     if [ -e "/usr/bin/batcat" ]; then
 526         cmd="batcat"
 527     fi
 528 
 529     "$cmd" --style=plain,header,numbers --theme='Monokai Extended Light' \
 530         --wrap=never --color=always "$@" |
 531     sed 's-\x1b\[38;5;70m-\x1b\[38;5;28m-g' | less -KiCRS
 532 }
 533 
 534 # Color Syntax of all files in a Folder, showing line numbers
 535 csf() {
 536     local cmd
 537     cmd="bat"
 538     # debian linux uses a different name for the `bat` app
 539     if [ -e "/usr/bin/batcat" ]; then
 540         cmd="batcat"
 541     fi
 542 
 543     find "${1:-.}" -type f -print0 | xargs --null "$cmd" \
 544         --style=plain,header,numbers --theme='Monokai Extended Light' \
 545         --wrap=never --color=always |
 546     sed 's-\x1b\[38;5;70m-\x1b\[38;5;28m-g' | less -KiCRS
 547 }
 548 
 549 # DEcode BASE64 bytes
 550 debase64() {
 551     base64 -d "$@"
 552 }
 553 
 554 # ignore leading utf-8 BOM markers for each input, when present; CRLF
 555 # byte-pairs are also turned into single LF bytes
 556 debom() {
 557     awk 'FNR == 1 { gsub(/^\xef\xbb\xbf/, "") } 1' "$@"
 558 }
 559 
 560 # DECAPitate emits the first n lines to stderr, and the rest to stdout;
 561 # the name is a reference to the standard cmd-line app `head`, which
 562 # emits only the first n lines to stdout
 563 decap() {
 564     local n
 565     n="${1:-1}"
 566     shift
 567 
 568     awk -v n="${n}" '
 569         BEGIN {
 570             if (n !~ /^-?[0-9]+$/) {
 571                 fmt = "leading arg %s isn'\''t a valid line-count\n"
 572                 printf fmt, n > "/dev/stderr"
 573                 exit 1
 574             }
 575         }
 576 
 577         NR <= n { print > "/dev/stderr"; next }
 578         1' "$@"
 579 }
 580 
 581 # turn all CRLF byte-pairs into single line-feed bytes
 582 decrlf() {
 583     cat "$@" | sed 's-\r$--'
 584 }
 585 
 586 # ignore up to n leading spaces for each line, or up to 4 by default
 587 dedent() {
 588     local upto
 589     upto="${1:-4}"
 590     shift
 591     awk "{ gsub(/^ {0,${upto}}/, \"\"); print }" "$@"
 592 }
 593 
 594 # DEDUPlicate prevents lines from appearing more than once
 595 dedup() {
 596     awk '!c[$0]++' "$@"
 597 }
 598 
 599 # DEDUPlicatE prevents lines from appearing more than once
 600 dedupe() {
 601     awk '!c[$0]++' "$@"
 602 }
 603 
 604 # delay each input line, waiting the number of seconds given, or wait
 605 # 1 second before each line by default
 606 delay() {
 607     local delay
 608     local line
 609     IFS='' # keep all spaces from lines
 610     delay="${1:-1}"
 611     shift
 612 
 613     awk 1 "$@" | while read -r line; do
 614         sleep "${delay}"
 615         printf "%s\n" "${line}"
 616     done
 617 }
 618 
 619 # expand tabs into spaces using the tabstop given
 620 detab() {
 621     local tabstop
 622     tabstop="${1:-4}"
 623     shift
 624     expand -t "${tabstop}" "$@"
 625 }
 626 
 627 # DICtionary definitions, using an online service
 628 dic() {
 629     curl -s "dict://dict.org/d:$*" | awk '
 630         /^151 / { printf "\x1b[38;5;4m%s\x1b[0m\n", $0; next }
 631         /^[1-9][0-9]{2} / { printf "\x1b[38;5;244m%s\x1b[0m\n", $0; next }
 632         1'
 633 }
 634 
 635 # DICTionary definitions, using an online service
 636 dict() {
 637     curl -s "dict://dict.org/d:$*" | awk '
 638         /^151 / { printf "\x1b[38;5;4m%s\x1b[0m\n", $0; next }
 639         /^[1-9][0-9]{2} / { printf "\x1b[38;5;244m%s\x1b[0m\n", $0; next }
 640         1'
 641 }
 642 
 643 # divide 2 numbers both ways, also showing their proper complement
 644 div() {
 645     awk -v x="${1:-1}" -v y="${2:-1}" '
 646         BEGIN {
 647             gsub(/_/, "", x)
 648             gsub(/_/, "", y)
 649             x = x + 0
 650             y = y + 0
 651             min = x < y ? x : y
 652             max = x > y ? x : y
 653             print x/y
 654             print y/x
 655             print 1 - min / max
 656             exit
 657         }' < /dev/null
 658 }
 659 
 660 # show the current Date and Time, and the 3 `current` months
 661 dt() {
 662     # debian linux has a different `cal` app which highlights the day
 663     if [ -e "/usr/bin/ncal" ]; then
 664         ncal -C -3
 665     else
 666         cal -3
 667     fi
 668 
 669     # show the current time center-aligned
 670     # printf "%28s\x1b[34m%s\x1b[0m\n" " " "$(date +'%T')"
 671     printf "%22s\x1b[32m%s\x1b[0m \x1b[34m%s\x1b[0m\n" " " \
 672         "$(date +'%a %b %d')" "$(date +'%T')"
 673 }
 674 
 675 # run command using each stdin line as a stdin-redirected file
 676 each() {
 677     local arg
 678     while read -r arg; do
 679         "$@" < "${arg}"
 680     done
 681 }
 682 
 683 # Extended Grep
 684 eg() {
 685     grep -E "$@"
 686 }
 687 
 688 # dig into a folder recursively to show all empty files in it
 689 emptyfiles() {
 690     find "${1:-.}" -type f -empty
 691 }
 692 
 693 # dig into a folder recursively to show all empty folders in it
 694 emptyfolders() {
 695     find "${1:-.}" -type d -empty
 696 }
 697 
 698 # enumerate lines, starting from 1, and using a tab as the separator:
 699 # even empty lines are counted, unlike with `nl`
 700 enum() {
 701     awk '{ printf "%d\t", NR; print }' "$@"
 702 }
 703 
 704 # enumerate lines, starting from 1, and using a tab as the separator:
 705 # even empty lines are counted, unlike with `nl`
 706 enum1() {
 707     awk '{ printf "%d\t", NR; print }' "$@"
 708 }
 709 
 710 # enumerate lines, starting from 0, and using a tab as the separator:
 711 # even empty lines are counted, unlike with `nl`
 712 enum0() {
 713     awk '{ printf "%d\t", NR - 1; print }' "$@"
 714 }
 715 
 716 # avoid lines matching the regex given, or avoid empty(ish) lines by default
 717 except() {
 718     local regex
 719     regex="${1:-[^ *]\r?$}"
 720     shift
 721     grep -E -v "${regex}" "$@"
 722 }
 723 
 724 # show all Files in a folder, digging recursively
 725 f() {
 726     local arg
 727     for arg in "${@:-.}"; do
 728         find "${arg}" -type f
 729     done
 730 }
 731 
 732 # show an error message and fail
 733 fail() {
 734     printf "\x1b[41m\x1b[38;5;15m %s \x1b[0m\n" "$*" >&2 && return 255
 735 }
 736 
 737 # get/fetch data from the URI given
 738 fetch() {
 739     curl -s "$@" || (
 740         printf "\x1b[31mcan't get %s\x1b[0m\n" "$@" >&2
 741         return 1
 742     )
 743 }
 744 
 745 # asks webserver at URI given to respond with a JSON payload
 746 fetchjson() {
 747     curl -s "$@" -H 'Accept: application/json' || (
 748         printf "\x1b[31mcan't get JSON from %s\x1b[0m\n" "$@" >&2
 749         return 1
 750     )
 751 }
 752 
 753 # show/find all targets from makefiles
 754 findtargets() {
 755     grep -E -i '^[a-z0-9_\.$-]+.*:' "${@:-Makefile}"
 756 }
 757 
 758 # get the first n lines, or 1 by default
 759 first() {
 760     head -n "${1:-1}" "${2:--}"
 761 }
 762 
 763 # First Line AWK, and lines satisfying the optional condition
 764 flawk() {
 765     local cond
 766     cond="${1:-0}"
 767     shift
 768     awk "NR == 1 || ${cond}" "$@"
 769 }
 770 
 771 # keep (re)running the command given, until forced to quit, whether
 772 # directly or indirectly
 773 forever() {
 774     while true; do
 775         "$@" || return "$?"
 776     done
 777 }
 778 
 779 # run Grep (extended)
 780 g() {
 781     grep -E "$@"
 782 }
 783 
 784 # get/fetch data from the URI given
 785 get() {
 786     curl -s "$@" || (
 787         printf "\x1b[31mcan't get %s\x1b[0m\n" "$@" >&2
 788         return 1
 789     )
 790 }
 791 
 792 # view most apps' help messages using `less`; this command used to be
 793 # called `helpless`
 794 gethelp() {
 795     "${1}" "${2:---help}" 2>&1 | less -KiCRS
 796 }
 797 
 798 # asks webserver at URI given to respond with a JSON payload
 799 getjson() {
 800     curl -s "$@" -H 'Accept: application/json' || (
 801         printf "\x1b[31mcan't get JSON from %s\x1b[0m\n" "$@" >&2
 802         return 1
 803     )
 804 }
 805 
 806 # Green LEAK emits/tees input both to stdout and stderr, coloring green what
 807 # it emits to stderr using an ANSI-style; this cmd is useful to `debug` pipes
 808 # involving several steps
 809 gleak() {
 810     awk '
 811     {
 812         print
 813         fflush()
 814         gsub(/\x1b\[0m/, "\x1b[0m\x1b[38;5;29m")
 815         printf "\x1b[38;5;29m%s\x1b[0m\n", $0 > "/dev/stderr"
 816         fflush("/dev/stderr")
 817     }'
 818 }
 819 
 820 # join all input lines using the separator/joiner string given; the name
 821 # `join` is already taken by a standard(ish) cmd-line app
 822 glue() {
 823     local sep
 824     sep="${1:-}"
 825     shift
 826 
 827     awk -v sep="${sep}" '
 828         NR > 1 { printf "%s", sep }
 829         1 { printf "%s", $0 }
 830         END { printf "\n" }' "$@"
 831 }
 832 
 833 # GO Build Stripped: a common use-case for the go compiler
 834 gobs() {
 835     go build -ldflags "-s -w" -trimpath "$@"
 836 }
 837 
 838 # GO Build WINdows GUI: a common use-case for the go compiler
 839 gobwingui() {
 840     go build -ldflags "-s -w -H=windowsgui" -trimpath "$@"
 841 }
 842 
 843 # GO DEPendencies: shows all dependencies in a go project
 844 godep() {
 845     go list -f '{{ join .Deps "\n" }}' "$@"
 846 }
 847 
 848 # GO IMPorts: show all imports in a go project
 849 goimp() {
 850     go list -f '{{ join .Imports "\n" }}' "$@"
 851 }
 852 
 853 # transform lines using AWK's gsub func (global substitute)
 854 gsub() {
 855     local what
 856     local with
 857     what="${1}"
 858     shift
 859     with="${1}"
 860     shift
 861     # awk "{ gsub(/${what}/, \"${with}\"); print }" "$@"
 862     awk "{ gsub(${what}, \"${with}\"); print }" "$@"
 863 }
 864 
 865 # run my own `hat` (HAndy Tools) cmd-line app
 866 # h() {
 867 #     hat "$@"
 868 # }
 869 
 870 # Highlight (lines) with AWK
 871 hawk() {
 872     local cond
 873     cond="${1:-1}"
 874     shift
 875 
 876     awk "
 877         ${cond} {
 878             gsub(/\\x1b\\[0m/, \"\x1b[0m\\x1b[7m\")
 879             printf \"\\x1b[7m%s\\x1b[0m\\n\", \$0
 880             fflush()
 881             next
 882         }
 883 
 884         { print; fflush() }" "$@"
 885 }
 886 
 887 # view most apps' help messages using `less`
 888 helpless() {
 889     "${1}" "${2:---help}" 2>&1 | less -KiCRS
 890 }
 891 
 892 # Header Less runs `less` with line numbers, ANSI styles, no line-wrapping,
 893 # and using the first line as a sticky-header, so it always shows on top
 894 hl() {
 895     less --header=1 -KNiCRS "$@"
 896 }
 897 
 898 # Help Me Remember my custom shell commands
 899 hmr() {
 900     local cmd
 901     cmd="bat"
 902     # debian linux uses a different name for the `bat` app
 903     if [ -e "/usr/bin/batcat" ]; then
 904         cmd="batcat"
 905     fi
 906 
 907     "$cmd" --style=plain,header,numbers --theme='Monokai Extended Light' \
 908         --wrap=never --color=always "$(which clam)" |
 909     sed 's-\x1b\[38;5;70m-\x1b\[38;5;28m-g' | less -KiCRS
 910 }
 911 
 912 # Header View runs `less` without line numbers, with ANSI styles, with no
 913 # line-wrapping, and using the first line as a sticky-header, so it always
 914 # shows on top
 915 hv() {
 916     less --header=1 -KiCRS "$@"
 917 }
 918 
 919 # indent each line the number of spaces given, or 4 spaces by default
 920 indent() {
 921     local n
 922     n="${1:-4}"
 923     shift
 924 
 925     awk -v n="${n}" '
 926     BEGIN {
 927         # for (i = 1; i <= n; i++) pre = pre " "
 928         pre = " "
 929         while (length(pre) < n) pre = pre pre
 930         pre = substr(pre, 1, n)
 931     }
 932 
 933     { printf pre; print }' "$@"
 934 }
 935 
 936 # reformat JSON into multiple lines with 2-space indent levels
 937 # j2() {
 938 #     cat "${1:--}" | python -c "#!/usr/bin/python3
 939 # from json import load, dump
 940 # from sys import exit, stderr, stdin, stdout
 941 # try:
 942 #     seps = (', ', ': ')
 943 #     stdout.reconfigure(newline='\n', encoding='utf-8')
 944 #     dump(load(stdin), stdout, indent=2, allow_nan=False, separators=seps)
 945 #     stdout.write('\n')
 946 # except Exception as e:
 947 #     print('\x1b[31m' + str(e) + '\x1b[0m', file=stderr)
 948 #     exit(1)
 949 # "
 950 # }
 951 
 952 # Json Converter; uses python package `jc`, which isn't built-in
 953 # jc() {
 954 #     python -m jc "$@"
 955 # }
 956 
 957 # show a `dad` JOKE from the web, some of which are even funny
 958 joke() {
 959     curl -s https://icanhazdadjoke.com | fold -s | sed -E 's- *\r?$--'
 960     # plain-text output from previous cmd doesn't end with a line-feed
 961     printf "\n"
 962 }
 963 
 964 # run `less` with line numbers, ANSI styles, and no line-wrapping
 965 l() {
 966     less -KNiCRS "$@"
 967 }
 968 
 969 # Like A Book groups lines as 2 side-by-side pages, the same way books
 970 # do it; uses my own cmd-line app `book`
 971 # lab() {
 972 #     book "$(($(tput lines) - 1))" "$@" | less -KiCRS
 973 # }
 974 
 975 # Line xARGS: `xargs` using line separators, which handles filepaths
 976 # with spaces, as long as the standard input has 1 path per line
 977 largs() {
 978     xargs -d "\n" "$@"
 979 }
 980 
 981 # get the last n lines, or 1 by default
 982 last() {
 983     tail -n "${1:-1}" "${2:--}"
 984 }
 985 
 986 # leak emits/tees input both to stdout and stderr; useful in pipes
 987 leak() {
 988     tee /dev/stderr
 989 }
 990 
 991 # LEAK Orange emits/tees input both to stdout and stderr, coloring orange
 992 # what it emits to stderr using an ANSI-style; this cmd is useful to `debug`
 993 # pipes involving several steps
 994 leako() {
 995     awk '
 996     {
 997         print
 998         fflush()
 999         gsub(/\x1b\[0m/, "\x1b[0m\x1b[38;5;166m")
1000         printf "\x1b[38;5;166m%s\x1b[0m\n", $0 > "/dev/stderr"
1001         fflush("/dev/stderr")
1002     }'
1003 }
1004 
1005 # run `less` with line numbers, ANSI styles, and no line-wrapping
1006 least() {
1007     less -KNiCRS "$@"
1008 }
1009 
1010 # List Files, coloring folders and links
1011 lf() {
1012     ls -al --color=never --time-style iso "$@" | awk '
1013         /^d/ { printf "\x1b[38;5;33m%s\x1b[0m\n", $0; next }
1014         /^l/ { printf "\x1b[38;5;29m%s\x1b[0m\n", $0; next }
1015         1
1016     '
1017 }
1018 
1019 # Less with Header runs `less` with line numbers, ANSI styles, no line-wraps,
1020 # and using the first line as a sticky-header, so it always shows on top
1021 lh() {
1022     less --header=1 -KNiCRS "$@"
1023 }
1024 
1025 # regroup adjacent lines into n-item tab-separated lines
1026 lineup() {
1027     local n
1028     n="${1:-0}"
1029     shift
1030 
1031     if [ "${n}" -le 0 ]; then
1032         awk '
1033             NR > 1 { printf "\t" }
1034             { printf "%s", $0 }
1035             END { if (NR > 0) print "" }' "$@"
1036         return "$?"
1037     fi
1038 
1039     awk -v n="${n}" '
1040         NR % n != 1 { printf "\t" }
1041         { printf "%s", $0 }
1042         NR % n == 0 { print "" }
1043         END { if (NR % n != 0) print "" }' "$@"
1044 }
1045 
1046 # list files, coloring folders and links
1047 listfiles() {
1048     ls -al --color=never --time-style iso "$@" | awk '
1049         /^d/ { printf "\x1b[38;5;33m%s\x1b[0m\n", $0; next }
1050         /^l/ { printf "\x1b[38;5;29m%s\x1b[0m\n", $0; next }
1051         1
1052     '
1053 }
1054 
1055 # LOcal SERver webserves files in a folder as localhost, using the port
1056 # number given, or port 8080 by default
1057 loser() {
1058     printf "\x1b[38;5;26mserving files in ${2:-$(pwd)}\x1b[0m\n" >&2
1059     python -m http.server "${1:-8080}" -d "${2:-.}"
1060 }
1061 
1062 # make all text lowercase
1063 lower() {
1064     awk '{ print tolower($0) }' "$@"
1065 }
1066 
1067 # match the regex given, or match non-empty(ish) lines by default
1068 m() {
1069     local regex
1070     regex="${1:-[^ *]\r?$}"
1071     shift
1072     grep -E "${regex}" "$@"
1073 }
1074 
1075 # match the regex given, or match non-empty(ish) lines by default
1076 match() {
1077     local regex
1078     regex="${1:-[^ *]\r?$}"
1079     shift
1080     grep -E "${regex}" "$@"
1081 }
1082 
1083 # merge stderr into stdout without any keyboard-dancing
1084 merrge() {
1085     "$@" 2>&1
1086 }
1087 
1088 # Number all lines starting from 1, turning all CRLF into single LF bytes,
1089 # and ensuring lines aren't accidentally joined when changing input sources
1090 n() {
1091     # awk '{ printf "%6d  %s\n", NR, $0; fflush() }' "$@"
1092     awk '{ printf "%d\t%s\n", NR, $0; fflush() }' "$@"
1093 }
1094 
1095 # Number all lines starting from 0, turning all CRLF into single LF bytes,
1096 # and ensuring lines aren't accidentally joined when changing input sources
1097 n0() {
1098     # awk '{ printf "%6d  %s\n", NR - 1, $0; fflush() }' "$@"
1099     awk '{ printf "%d\t%s\n", NR - 1, $0; fflush() }' "$@"
1100 }
1101 
1102 # Number all lines starting from 1, turning all CRLF into single LF bytes,
1103 # and ensuring lines aren't accidentally joined when changing input sources
1104 n1() {
1105     # awk '{ printf "%6d  %s\n", NR, $0; fflush() }' "$@"
1106     awk '{ printf "%d\t%s\n", NR, $0; fflush() }' "$@"
1107 }
1108 
1109 # try to limit lines up to n symbols per line, or 80 by default
1110 narrow() {
1111     fold -s -w "${1:-80}" | sed -E 's- +$--'
1112 }
1113 
1114 # Nice File Sizes
1115 nfs() {
1116     # turn arg-list into single-item lines
1117     awk 'BEGIN { for (i = 1; i < ARGC; i++) print ARGV[i]; exit }' "$@" |
1118     # calculate file-sizes, and reverse-sort results
1119     xargs -d '\n' wc -c | sort -rn |
1120     # start output with a header-like line, and add a MiB field
1121     awk 'BEGIN { printf "%5s  %9s  %8s  name\n", "n", "bytes", "MiB" }
1122     { printf "%5d  %9d  %8.2f  %s\n", NR - 1, $1, $1 / 1048576, $2 }' |
1123     # make zeros in the MiB field stand out with a special color
1124     awk '{ gsub(/ 0.00 /, "\x1b[38;5;103m 0.00 \x1b[0m"); print }' |
1125     # make table breathe with empty lines, so tall outputs are readable
1126     awk '(NR - 2) % 5 == 1 && NR > 1 { print "" } 1'
1127 }
1128 
1129 # emit nothing to output and/or discard everything from input
1130 nil() {
1131     if [ -p /dev/stdin ]; then
1132         cat > /dev/null
1133     else
1134         head -c 0
1135     fi
1136 }
1137 
1138 # ignore stderr without any keyboard-dancing
1139 noerr() {
1140     "$@" 2> /dev/null
1141 }
1142 
1143 # show the current date and time
1144 now() {
1145     date +'%Y-%m-%d %H:%M:%S'
1146 }
1147 
1148 # keep only the nth line from the input, if it has at least that many lines
1149 nth() {
1150     local n
1151     n="${1}"
1152     shift
1153     awk -v n="${n}" 'BEGIN { if (n < 1) exit } NR == n { print; exit }' "$@"
1154 }
1155 
1156 # Orange LEAK emits/tees input both to stdout and stderr, coloring orange
1157 # what it emits to stderr using an ANSI-style; this cmd is useful to `debug`
1158 # pipes involving several steps
1159 oleak() {
1160     awk '
1161     {
1162         print
1163         fflush()
1164         gsub(/\x1b\[0m/, "\x1b[0m\x1b[38;5;166m")
1165         printf "\x1b[38;5;166m%s\x1b[0m\n", $0 > "/dev/stderr"
1166         fflush("/dev/stderr")
1167     }'
1168 }
1169 
1170 # Plain ignores ANSI terminal styling
1171 p() {
1172     awk '
1173     {
1174         # ignore notifications (code 9) and hyperlinks (code 8)
1175         gsub(/\x1b\](8|9);[^\x07]*\x07/, "")
1176         # ignore cursor-movers and style-changers
1177         gsub(/\x1b\[([0-9]*[A-HJKST]|[0-9;]*m)/, "")
1178 
1179         print
1180         fflush()
1181     }' "$@"
1182 }
1183 
1184 # Print AWK expressions
1185 pawk() {
1186     local arg
1187     local shown
1188     for arg in "$@"; do
1189         shown="END { print $(echo "${arg}" | sed 's-"-\\"-g') }"
1190         printf "\x1b[48;5;253m\x1b[38;5;26m%-80s\x1b[0m\n" \
1191             "awk \"${shown}\" < /dev/null" >&2
1192         awk "END { print ${arg} }" < /dev/null
1193     done
1194 }
1195 
1196 # ignore ANSI terminal styling
1197 plain() {
1198     awk '
1199     {
1200         # ignore notifications (code 9) and hyperlinks (code 8)
1201         gsub(/\x1b\](8|9);[^\x07]*\x07/, "")
1202         # ignore cursor-movers and style-changers
1203         gsub(/\x1b\[([0-9]*[A-HJKST]|[0-9;]*m)/, "")
1204 
1205         print
1206         fflush()
1207     }' "$@"
1208 }
1209 
1210 # reset ANSI styles at the end of each line
1211 plainend() {
1212     awk '{ printf "%s\x1b[0m\n", $0; fflush() }' "$@"
1213 }
1214 
1215 # PLay INput handles playable/multimedia streams from stdin
1216 plin() {
1217     mpv -
1218 }
1219 
1220 # Quiet cURL
1221 qurl() {
1222     curl -s "$@"
1223 }
1224 
1225 # reflow/trim lines of prose (text) to improve its legibility: it
1226 # seems especially useful when the text is pasted from web-pages
1227 # being viewed in reader mode
1228 reprose() {
1229     awk 'FNR == 1 && NR > 1 { print "" } 1' "$@" | fold -s |
1230         sed -E 's- *\r?$--'
1231 }
1232 
1233 # REPeat a STRing n times, or 80 times by default
1234 repstr() {
1235     awk -v what="${1}" -v times="${2:-80}" '
1236         BEGIN {
1237             if (length(what) == 0) exit 0
1238             for (i = 1; i <= times; i++) printf "%s", what
1239             printf "\n"
1240         }' < /dev/null
1241 }
1242 
1243 # RUN a command IN the folder given as the first argument
1244 runin() {
1245     local prev
1246     local res
1247 
1248     prev="${OLDPWD}"
1249     cd "${1}" || return 1
1250 
1251     shift
1252     "$@"
1253     res="$?"
1254 
1255     cd - || return 1
1256     OLDPWD="${prev}"
1257     return "${res}"
1258 }
1259 
1260 # run Sed (extended)
1261 s() {
1262     sed -E "$@"
1263 }
1264 
1265 # show a unique-looking separator line; useful to run between commands
1266 # which output walls of text
1267 sep() {
1268     printf "\x1b[48;5;253m"
1269     printf "·························································"
1270     printf "·······················"
1271     printf "\x1b[0m\n"
1272 }
1273 
1274 # start a local webserver from the current folder, using the port number
1275 # given, or port 8080 by default
1276 serve() {
1277     printf "\x1b[38;5;26mserving files in ${2:-$(pwd)}\x1b[0m\n" >&2
1278     python -m http.server "${1:-8080}" -d "${2:-.}"
1279 }
1280 
1281 # SET DIFFerence sorts its 2 inputs, then finds lines not in the 2nd input
1282 setdiff() {
1283     # comm -23 <(sort "$1") <(sort "$2")
1284     # dash doesn't support the process-sub syntax
1285     (sort "$1" | (sort "$2" | (comm -23 /dev/fd/3 /dev/fd/4) 4<&0) 3<&0)
1286 }
1287 
1288 # SET INtersection, sorts its 2 inputs, then finds common lines
1289 setin() {
1290     # comm -12 <(sort "$1") <(sort "$2")
1291     # dash doesn't support the process-sub syntax
1292     (sort "$1" | (sort "$2" | (comm -12 /dev/fd/3 /dev/fd/4) 4<&0) 3<&0)
1293 }
1294 
1295 # SET SUBtraction sorts its 2 inputs, then finds lines not in the 2nd input
1296 setsub() {
1297     # comm -23 <(sort "$1") <(sort "$2")
1298     # dash doesn't support the process-sub syntax
1299     (sort "$1" | (sort "$2" | (comm -23 /dev/fd/3 /dev/fd/4) 4<&0) 3<&0)
1300 }
1301 
1302 # show a command, then run it
1303 showrun() {
1304     printf "\x1b[7m%s\x1b[0m\n" "$*" && "$@"
1305 }
1306 
1307 # SKIP the first n lines, or the 1st line by default
1308 skip() {
1309     tail -n +$(("${1:-1}" + 1)) "${2:--}"
1310 }
1311 
1312 # SKIP the FIRST n lines, or the 1st line by default
1313 skipfirst() {
1314     tail -n +$(("${1:-1}" + 1)) "${2:--}"
1315 }
1316 
1317 # skip/ignore the last n lines, or only the very last line by default
1318 skiplast() {
1319     head -n -"${1:-1}" "${2:--}"
1320 }
1321 
1322 # dig into a folder recursively for files under n bytes
1323 smallfiles() {
1324     find "${2:-.}" -type f -size -"${1:-1000000}"c
1325 }
1326 
1327 # show the reverse-SOrted SIzes of various files
1328 sosi() {
1329     wc -c "$@" | sort -rn
1330 }
1331 
1332 # ignore leading spaces, trailing spaces, even runs of multiple spaces
1333 # in the middle of lines, as well as trailing carriage returns
1334 squeeze() {
1335     awk '
1336     {
1337         gsub(/  +/, " ")
1338         gsub(/ *\t */, "\t")
1339         gsub(/(^ +)|( *\r?$)/, "")
1340         print
1341     }' "$@"
1342 }
1343 
1344 # ssv2tsv turns each run of 1+ spaces into a single tab, while ignoring
1345 # leading spaces in each line
1346 ssv2tsv() {
1347     awk 1 "$@" | sed -E 's-^ +--; s- +-\t-g'
1348 }
1349 
1350 # underline every 5th line
1351 stripe() {
1352     awk '
1353         NR % 5 == 0 && NR != 1 { printf "\x1b[4m%s\x1b[0m\n", $0; next }
1354         1' "$@"
1355 }
1356 
1357 # Trim leading/trailing spaces and trailing carriage-returns
1358 t() {
1359     awk 'FNR == 1 { gsub(/^\xef\xbb\xbf/, "") } 1' "$@" |
1360         sed -E 's-^ +--; s- *\r?$--'
1361 }
1362 
1363 # Tab AWK: TSV-specific I/O settings for `awk`
1364 tawk() {
1365     awk -F "\t" -v OFS="\t" "$@"
1366 }
1367 
1368 # Trim End ignores trailing spaces and possibly a carriage return in all
1369 # lines; also, this command ensures separate lines from different inputs
1370 # will never join by accident
1371 te() {
1372     awk 'FNR == 1 { gsub(/^\xef\xbb\xbf/, "") } 1' "$@" | sed -E 's- *\r?$--'
1373 }
1374 
1375 # show current date in a specifc format, which is both people-friendly
1376 # and machine/tool/search/automation-friendly
1377 today() {
1378     date +'%Y-%m-%d %a %b %d'
1379 }
1380 
1381 # show all files directly in the folder given, without looking any deeper
1382 topfiles() {
1383     local arg
1384     for arg in "${@:-.}"; do
1385         find "${arg}" -maxdepth 1 -type f
1386     done
1387 }
1388 
1389 # show all folders directly in the folder given, without looking any deeper
1390 topfolders() {
1391     local arg
1392     for arg in "${@:-.}"; do
1393         find "${arg}" -maxdepth 1 -type d | awk 'NR > 1'
1394     done
1395 }
1396 
1397 # ignore leading spaces, trailing spaces, and carriage returns on lines
1398 trim() {
1399     awk 'FNR == 1 { gsub(/^\xef\xbb\xbf/, "") } 1' "$@" |
1400         sed -E 's-^ +--; s- *\r?$--'
1401 }
1402 
1403 # ignore trailing spaces and possibly a carriage return in all lines;
1404 # also, this command ensures separate lines from different inputs will
1405 # never join by accident
1406 trimend() {
1407     awk 'FNR == 1 { gsub(/^\xef\xbb\xbf/, "") } 1' "$@" | sed -E 's- *\r?$--'
1408 }
1409 
1410 # ignore the prefix given from input lines which start with it; input
1411 # lines which don't start with the prefix given will stay unchanged
1412 trimprefix() {
1413     local prefix
1414     prefix="${1:-}"
1415     shift
1416 
1417     awk -v pre="${prefix}" '
1418         index($0, pre) == 1 { $0 = substr($0, length(pre) + 1) }
1419         1' "$@"
1420 }
1421 
1422 # ignore the suffix given from input lines which end with it; input
1423 # lines which don't end with the suffix given will stay unchanged
1424 trimsuffix() {
1425     local suffix
1426     suffix="${1:-}"
1427     shift
1428 
1429     awk -v suf="${suffix}" '
1430     {
1431         i = index($0, suf)
1432         if (i != 0 && i == length - length(suf) + 1) {
1433             $0 = substr($0, 1, length - length(suf))
1434         }
1435     }
1436 
1437     1' "$@"
1438 }
1439 
1440 # ignore trailing spaces and possibly a carriage return in all lines;
1441 # also, this command ensures separate lines from different inputs will
1442 # never join by accident
1443 trimtrail() {
1444     awk 'FNR == 1 { gsub(/^\xef\xbb\xbf/, "") } 1' "$@" | sed -E 's- *\r?$--'
1445 }
1446 
1447 # ignore trailing spaces and possibly a carriage return in all lines;
1448 # also, this command ensures separate lines from different inputs will
1449 # never join by accident
1450 trimtrails() {
1451     awk 'FNR == 1 { gsub(/^\xef\xbb\xbf/, "") } 1' "$@" | sed -E 's- *\r?$--'
1452 }
1453 
1454 # try running a command, emitting an explicit message to standard-error
1455 # if the command given fails
1456 try() {
1457     "$@" || (
1458         printf "%s: failure running %s\n" "$0" "$*" >&2
1459         return 255
1460     )
1461 }
1462 
1463 # TimeStamp lines satisfying an AWK condition, ignoring all other lines
1464 tsawk() {
1465     # -v line="\x1b[38;5;27m%s\x1b[0m %s\n"
1466     awk \
1467         -v line="\x1b[48;5;255m\x1b[38;5;24m%s\x1b[0m %s\n" \
1468         -v time="%Y-%m-%d %H:%M:%S" \
1469         "${1:-1} { printf line, strftime(time), \$0; fflush() }"
1470 }
1471 
1472 # Unixify concatenates all named input sources, ignoring trailing CRLFs
1473 # into LFs, and guaranteeing lines from different sources are accidentally
1474 # joined, by adding a line-feed when an input's last line doesn't end with
1475 # one; also, ignore leading UTF-8 BOMs on the first line of each input, as
1476 # those are useless at best
1477 u() {
1478     awk 'FNR == 1 { gsub(/^\xef\xbb\xbf/, "") } 1' "$@" | sed -E 's-\r$--'
1479 }
1480 
1481 # decode base64 bytes
1482 unbase64() {
1483     base64 -d "$@"
1484 }
1485 
1486 # deduplicate lines, keeping them in their original order
1487 unique() {
1488     awk '!c[$0]++' "$@"
1489 }
1490 
1491 # concatenate all named input sources, ignoring trailing CRLFs into LFs,
1492 # and guaranteeing lines from different sources are accidentally joined,
1493 # by adding a line-feed when an input's last line doesn't end with one;
1494 # also, ignore leading UTF-8 BOMs on the first line of each input, as
1495 # those are useless at best
1496 unixify() {
1497     awk 'FNR == 1 { gsub(/^\xef\xbb\xbf/, "") } 1' "$@" | sed -E 's-\r$--'
1498 }
1499 
1500 # turn all CRLF byte-pairs into single line-feed bytes
1501 uncrlf() {
1502     cat "$@" | sed -E 's-\r$--'
1503 }
1504 
1505 # expand tabs into spaces using the tabstop given
1506 untab() {
1507     local tabstop
1508     tabstop="${1:-4}"
1509     shift
1510     expand -t "${tabstop}" "$@"
1511 }
1512 
1513 # go UP n folders, or go up 1 folder by default
1514 up() {
1515     if [ "${1:-1}" -le 0 ]; then
1516         cd .
1517         return "$?"
1518     fi
1519 
1520     cd "$(printf "%${1:-1}s" "" | sed 's- -../-g')" || return $?
1521 }
1522 
1523 # emit input lines in reverse order, or last to first
1524 upsidedown() {
1525     awk '
1526         { lines[NR] = $0 }
1527         END { for (i = NR; i >= 1; i--) print lines[i] }' "$@"
1528 }
1529 
1530 # Underline Table: underline the first line (the header), then
1531 # underline every 5th line after that
1532 ut() {
1533     awk '
1534         (NR - 1) % 5 == 0 {
1535             gsub(/\x1b\[0m/, "\x1b[0m\x1b[4m")
1536             printf "\x1b[4m%s\x1b[0m\n", $0
1537             next
1538         }
1539 
1540         1' "$@"
1541 }
1542 
1543 # View text: run `less` with ANSI styles and no line-wrapping
1544 v() {
1545     less -KiCRS "$@"
1546 }
1547 
1548 # run a command, showing its success/failure right after
1549 verdict() {
1550     local code
1551     local fs
1552     local msg
1553 
1554     "$@"
1555     code="$?"
1556 
1557     if [ "${code}" -eq 0 ]; then
1558         fs="\n\x1b[38;5;29m%s \x1b[48;5;29m\x1b[97m succeeded \x1b[0m\n"
1559         printf "${fs}" "$*" >&2
1560         return 0
1561     fi
1562 
1563     msg="failed with error code ${code}"
1564     printf "\n\x1b[31m%s \x1b[41m\x1b[97m ${msg} \x1b[0m\n" "$*" >&2
1565     return "${code}"
1566 }
1567 
1568 # View with Header runs `less` without line numbers, with ANSI styles, with
1569 # no line-wrapping, and using the first line as a sticky-header, so it always
1570 # shows on top
1571 vh() {
1572     less --header=1 -KiCRS "$@"
1573 }
1574 
1575 # What Are These (?) shows what the names given to it are/do
1576 wat() {
1577     local code
1578     local a
1579     local res
1580 
1581     code=0
1582     for a in "$@"; do
1583         printf "\x1b[48;5;253m\x1b[38;5;26m%-80s\x1b[0m\n" "${a}"
1584         (
1585             alias "${a}" || declare -f "${a}" || which "${a}" || type "${a}"
1586         ) 2> /dev/null
1587         res="$?"
1588 
1589         if [ "${res}" -ne 0 ]; then
1590             code="${res}"
1591             printf "\x1b[31m%s not found\x1b[0m\n" "${a}"
1592         fi
1593     done
1594 
1595     return "${code}"
1596 }
1597 
1598 # find all files which have at least 1 line with trailing spaces/CRs, with
1599 # the option to limit the (fully-recursive) search to the files/folders given
1600 wheretrails() {
1601     rg -c '[ \r]+$' "${@:-.}"
1602 }
1603 
1604 # find all files which have at least 1 line with trailing spaces/CRs, with
1605 # the option to limit the (fully-recursive) search to the files/folders given
1606 whichtrails() {
1607     rg -c '[ \r]+$' "${@:-.}"
1608 }
1609 
1610 # What Is This (?) shows what the name given to it is/does
1611 wit() {
1612     (
1613         alias "${1}" || declare -f "${1}" || which "${1}" || type "${1}"
1614     ) 2> /dev/null || (
1615         printf "\x1b[31m%s not found\x1b[0m\n" "${1}" >&2 && return 1
1616     )
1617 }
1618 
1619 # Youtube Audio Player
1620 yap() {
1621     mpv "$(yt-dlp -f 140 --get-url $(echo ${1} | sed 's-&.*--') 2> /dev/null)"
1622 }
1623 
1624 # Youtube Download
1625 yd() {
1626     yt-dlp "$@"
1627 }
1628 
1629 # Youtube Download AAC audio
1630 ydaac() {
1631     yt-dlp -f 140 "$@"
1632 }
1633 
1634 # Youtube Download MP4 video
1635 ydmp4() {
1636     yt-dlp -f 22 "$@"
1637 }
1638 
1639 # year shows a full calendar for the current year, or for the year given
1640 year() {
1641     cal -y "$@"
1642 }
1643 
1644 # show the current date in the YYYY-MM-DD format
1645 ymd() {
1646     date +'%Y-%m-%d'
1647 }