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 # This is a smaller version of the full `clam`.
  49 
  50 case "$1" in
  51     -h|--h|-help|--help)
  52         awk '
  53             /^case / { exit }
  54             /^# +clam$/, /^$/ { gsub(/^# ?/, ""); print }
  55         ' "$0"
  56         exit 0
  57     ;;
  58 esac
  59 
  60 
  61 # dash doesn't support regex-matching syntax, forcing to use case statements
  62 case "$0" in
  63     -bash|-dash|-sh|bash|dash|sh|/bin/sh)
  64         # script is being sourced with bash, dash, or ash, which is good
  65         :
  66     ;;
  67 
  68     *)
  69         case "$ZSH_EVAL_CONTEXT" in
  70             *:file)
  71                 # script is being sourced with zsh, which is good
  72                 :
  73             ;;
  74 
  75             *)
  76                 # script is being run normally, which is a waste of time
  77         printf "\e[7mDon't run this script directly: instead source it\e[0m\n"
  78         printf "\e[7mby running '. clam' (without the single quotes).\e[0m\n"
  79         printf "\n"
  80         printf "\e[7mBefore doing that, you may want to see the help,\e[0m\n"
  81         printf "\e[7mby running 'clam -h' (without the single quotes).\e[0m\n"
  82                 # exiting during shell-startup may deny shell access, even if
  83                 # the script is being run, instead of being sourced directly
  84             ;;
  85         esac
  86     ;;
  87 esac
  88 
  89 
  90 # use a compacter single-line output for keyword `time`
  91 # export TIMEFORMAT='real %2lR    user %2lU    sys %2lS'
  92 
  93 
  94 alias 0='sbs'
  95 
  96 alias 1='bsbs 1'
  97 alias 2='bsbs 2'
  98 alias 3='bsbs 3'
  99 alias 4='bsbs 4'
 100 alias 5='bsbs 5'
 101 alias 6='bsbs 6'
 102 alias 7='bsbs 7'
 103 alias 8='bsbs 8'
 104 alias 9='bsbs 9'
 105 
 106 # Less with Header n runs `less` with line numbers, ANSI styles, without
 107 # line-wraps, and using the first n lines as a sticky-header, so they always
 108 # show on top
 109 alias lh1='less --header=1 -MKNiCRS'
 110 alias lh2='less --header=2 -MKNiCRS'
 111 alias lh3='less --header=3 -MKNiCRS'
 112 alias lh4='less --header=4 -MKNiCRS'
 113 alias lh5='less --header=5 -MKNiCRS'
 114 alias lh6='less --header=6 -MKNiCRS'
 115 alias lh7='less --header=7 -MKNiCRS'
 116 alias lh8='less --header=8 -MKNiCRS'
 117 alias lh9='less --header=9 -MKNiCRS'
 118 
 119 # View with Header n runs `less` without line numbers, ANSI styles, without
 120 # line-wraps, and using the first n lines as a sticky-header, so they always
 121 # show on top
 122 alias vh1='less --header=1 -MKiCRS'
 123 alias vh2='less --header=2 -MKiCRS'
 124 alias vh3='less --header=3 -MKiCRS'
 125 alias vh4='less --header=4 -MKiCRS'
 126 alias vh5='less --header=5 -MKiCRS'
 127 alias vh6='less --header=6 -MKiCRS'
 128 alias vh7='less --header=7 -MKiCRS'
 129 alias vh8='less --header=8 -MKiCRS'
 130 alias vh9='less --header=9 -MKiCRS'
 131 
 132 alias c='cat'
 133 alias e='echo'
 134 alias r='reset'
 135 
 136 # Breathe periodically adds extra empty lines; uses my own `breathe` tool
 137 alias b='breathe'
 138 
 139 # Plain ignores ANSI-styles; uses my own `plain` tool
 140 alias p='plain'
 141 
 142 # Compile C Optimized
 143 alias cco='cc -Wall -O2 -s -march=native -mtune=native -flto'
 144 
 145 # Colored Json Query runs the `jq` app, allowing an optional filepath as the
 146 # data source, and even an optional transformation formula
 147 alias cjq='jq -C'
 148 
 149 # Compile C Plus Plus Optimized
 150 alias cppo='c++ -Wall -O2 -s -march=native -mtune=native -flto'
 151 
 152 # CURL Info only shows the response headers from the request given
 153 alias curli='curl -I'
 154 
 155 # CURL Silent spares you the progress bar, but still tells you about errors
 156 alias curls='curl --silent --show-error'
 157 
 158 # turn json lines into a proper json array using the `jq` app
 159 alias dejql='jq -s -M'
 160 
 161 # turn UTF-16 data into UTF-8
 162 alias deutf16='iconv -f utf16 -t utf8'
 163 
 164 # edit plain-text files
 165 # alias ed='TERM=xterm-truecolor micro'
 166 
 167 # edit plain-text files
 168 # alias edit='TERM=xterm-truecolor micro'
 169 
 170 # ENV with 0/null-terminated lines on stdout
 171 alias env0='env -0'
 172 
 173 # ENV Change folder, runs the command given in the folder given (first)
 174 alias envc='env -C'
 175 
 176 # Extended Plain Interactive Grep
 177 alias epig='ugrep --color=never -Q -E'
 178 
 179 # Editor Read-Only
 180 alias ero='micro -readonly true'
 181 
 182 # run the Fuzzy Finder (fzf) in multi-choice mode, with custom keybindings
 183 alias ff='fzf -m --bind ctrl-a:select-all,ctrl-space:toggle'
 184 
 185 # get FILE's MIME types
 186 alias filemime='file --mime-type'
 187 
 188 # run `gcc` with all optimizations on and with static analysis on
 189 alias gccmax='gcc -Wall -O2 -s -march=native -mtune=native -flto -fanalyzer'
 190 
 191 # hold stdout if used at the end of a pipe-chain
 192 alias hold='less -MKiCRS'
 193 
 194 # shrink/compact JSON using the `jq` app, allowing an optional filepath, and
 195 # even an optional transformation formula after that
 196 alias jq0='jq -c -M'
 197 
 198 # show JSON data on multiple lines, using 2 spaces for each indentation level,
 199 # allowing an optional filepath, and even an optional transformation formula
 200 # after that
 201 alias jq2='jq --indent 2 -M'
 202 
 203 # find the LAN (local-area network) IP address for this device
 204 alias lanip='hostname -I'
 205 
 206 # Listen To Youtube
 207 alias lty=yap
 208 
 209 # LXC-LS Fancy
 210 alias lxc-lsf='lxc-ls --fancy'
 211 
 212 # MAKE IN folder
 213 alias makein=mif
 214 
 215 # METAdata CURL only shows the response headers from the request given
 216 alias metacurl='curl -I'
 217 
 218 # run `less`, showing line numbers, among other settings
 219 alias most='less -MKNiCRS'
 220 
 221 # Plain Interactive Grep
 222 alias pig='ugrep --color=never -Q -E'
 223 
 224 # Quick Compile C Optimized
 225 alias qcco='cc -Wall -O2 -s -march=native -mtune=native -flto'
 226 
 227 # Quick Compile C Plus Plus Optimized
 228 alias qcppo='c++ -Wall -O2 -s -march=native -mtune=native -flto'
 229 
 230 # Read-Only Editor
 231 alias roe='micro -readonly true'
 232 
 233 # Read-Only Micro (text editor)
 234 alias rom='micro -readonly true'
 235 
 236 # Read-Only Top
 237 alias rot='htop --readonly'
 238 
 239 # RUN IN folder
 240 alias runin='env -C'
 241 
 242 # Silent CURL spares you the progress bar, but still tells you about errors
 243 alias scurl='curl --silent --show-error'
 244 
 245 # Stdbuf Output Line-buffered
 246 alias sol='stdbuf -oL'
 247 
 248 # TRY running a command, showing its outcome/error-code on failure; uses my
 249 # `verdict` tool
 250 alias try='verdict'
 251 
 252 # VERTical REVert emits lines in reverse order of appearance
 253 alias vertrev='tac'
 254 
 255 # UGREP in Query/interactive mode
 256 alias ugrepq='ugrep -Q'
 257 
 258 # emit lines in reverse order of appearance
 259 alias upsidedown='tac'
 260 
 261 # run `cppcheck` with even stricter options
 262 alias vetc='cppcheck --enable=portability,style --check-level=exhaustive'
 263 
 264 # run `cppcheck` with even stricter options, also checking for c89 compliance
 265 alias vetc89='cppcheck --enable=portability,style --check-level=exhaustive --std=c89'
 266 
 267 # run `cppcheck` with even stricter options
 268 alias vetcpp='cppcheck --enable=portability,style --check-level=exhaustive'
 269 
 270 # check shell scripts for common gotchas, avoiding complaints about using
 271 # the `local` keyword, which is widely supported in practice
 272 alias vetshell='shellcheck -e 3043'
 273 
 274 # run a command using an empty environment
 275 alias void='env -i'
 276 
 277 # turn plain-text from latin-1 into UTF-8; the name is from `vulgarization`,
 278 # which is the mutation of languages away from latin during the middle ages
 279 alias vulgarize='iconv -f latin-1 -t utf-8'
 280 
 281 # recursively find all files with trailing spaces/CRs
 282 alias wheretrails=whichtrails
 283 
 284 # run `xargs`, using zero/null bytes as the extra-arguments terminator
 285 alias x0='xargs -0'
 286 
 287 # Xargs Lines, runs `xargs` using whole lines as extra arguments
 288 alias xl=xargsl
 289 
 290 # Awk Begin Print
 291 abp() {
 292     local arg
 293     for arg in "$@"; do
 294         awk "BEGIN { print (${arg}); exit }"
 295     done
 296 }
 297 
 298 # APT UPdate/grade
 299 aptup() { sudo apt update && sudo apt upgrade "$@"; sudo -k; }
 300 
 301 # emit a colored bar which can help visually separate different outputs
 302 bar() {
 303     [ "${1:-80}" -gt 0 ] && printf "\e[48;2;218;218;218m%${1:-80}s\e[0m\n" ""
 304 }
 305 
 306 # emit a line with a repeating block-like symbol in it
 307 blocks() { [ "${1:-80}" -gt 0 ] && printf "%${1:-80}s\n" "" | sed 's- -█-g'; }
 308 
 309 # BOOK-like MANual, lays out `man` docs as pairs of side-by-side pages; uses
 310 # my tool `bsbs`
 311 bookman() {
 312     local w
 313     w="$(tput -T xterm cols)"
 314     w="$((w / 2 - 4))"
 315     if [ "$w" -lt 65 ]; then
 316         w=65
 317     fi
 318     MANWIDTH="$w" man "$@" | bsbs 2
 319 }
 320 
 321 # CAlculator with Nice numbers runs my tool `ca` and colors results with
 322 # my tool `nn`, alternating styles to make long numbers easier to read
 323 can() {
 324     local arg
 325     for arg in "$@"; do
 326         [ $# -ge 2 ] && printf "\e[7m%s\e[0m\n" "${arg}" >&2
 327         ca "${arg}" | nn
 328     done
 329 }
 330 
 331 # uppercase the first letter on each line, and lowercase all later letters
 332 capitalize() { sed -E 's-^(.*)-\L\1-; s-^(.)-\u\1-'; }
 333 
 334 # Colored Go Test on the folder given; uses my command `gbm`
 335 cgt() {
 336     local f='real %e    user %U    sys %S    mem %M    exit %x'
 337     /usr/bin/time -f "$f" go test "${@:-.}" 2>&1 \
 338     | gbm '^ok' '^[-]* ?FAIL' '^\?'
 339 }
 340 
 341 # Compile Rust Optimized
 342 cro() {
 343     rustc -C lto=true -C codegen-units=1 -C debuginfo=0 -C strip=symbols \
 344         -C opt-level=3 "$@"
 345 }
 346 
 347 # emit a line with a repeating cross-like symbol in it
 348 crosses() { [ "${1:-80}" -gt 0 ] && printf "%${1:-80}s\n" "" | sed 's- -×-g'; }
 349 
 350 # emit a line with a repeating dash-like symbol in it
 351 dashes() { [ "${1:-80}" -gt 0 ] && printf "%${1:-80}s\n" "" | sed 's- -—-g'; }
 352 
 353 # remove commas in numbers, as well as leading dollar signs in numbers
 354 decomma() {
 355     sed -E 's-([0-9]{3}),-\1-g; s-([0-9]{1,2}),-\1-g; s-\$([0-9\.]+)-\1-g'
 356 }
 357 
 358 dehtmlify() {
 359     local command='awk'
 360     if { [ -p /dev/stdout ] || [ -t 1 ]; } && [ -e /usr/bin/stdbuf ]; then
 361         command='stdbuf -oL awk'
 362     fi
 363 
 364     ${command} '
 365         {
 366             gsub(/<\/?[^>]+>/, "")
 367             gsub(/&amp;/, "&")
 368             gsub(/&lt;/, "<")
 369             gsub(/&gt;/, ">")
 370             gsub(/^ +| *\r?$/, "")
 371             gsub(/  +/, " ")
 372             print
 373         }
 374     ' "$@"
 375 }
 376 
 377 # expand tabs each into up to the number of space given, or 4 by default
 378 detab() {
 379     local tabstop="${1:-4}"
 380     [ $# -gt 0 ] && shift
 381     if { [ -p /dev/stdout ] || [ -t 1 ]; } && [ -e /usr/bin/stdbuf ]; then
 382         stdbuf -oL expand -t "${tabstop}" "$@"
 383     else
 384         expand -t "${tabstop}" "$@"
 385     fi
 386 }
 387 
 388 # DIVide 2 numbers 3 ways, including the complement
 389 div() {
 390     awk -v a="${1:-1}" -v b="${2:-1}" '
 391         BEGIN {
 392             gsub(/_/, "", a)
 393             gsub(/_/, "", b)
 394             if (a > b) { c = a; a = b; b = c }
 395             c = 1 - a / b
 396             if (0 <= c && c <= 1) printf "%f\n%f\n%f\n", a / b, b / a, c
 397             else printf "%f\n%f\n", a / b, b / a
 398             exit
 399         }'
 400 }
 401 
 402 # emit a line with a repeating dot-like symbol in it
 403 dots() { [ "${1:-80}" -gt 0 ] && printf "%${1:-80}s\n" "" | sed 's- -·-g'; }
 404 
 405 # show the current Date and Time
 406 dt() {
 407     printf "\e[38;2;78;154;6m%s\e[0m  \e[38;2;52;101;164m%s\e[0m\n" \
 408         "$(date +'%a %b %d')" "$(date +%T)"
 409 }
 410 
 411 # EDit RUN shell commands, using an interactive editor; uses my tool `leak`
 412 edrun() {
 413     # dash doesn't support the process-sub syntax
 414     # . <( micro -readonly true -filetype shell | leak --inv )
 415     micro -readonly true -filetype shell | leak --inv | . /dev/fd/0
 416 }
 417 
 418 # convert EURos into CAnadian Dollars, using the latest official exchange
 419 # rates from the bank of canada; during weekends, the latest rate may be
 420 # from a few days ago; the default amount of euros to convert is 1, when
 421 # not given
 422 eur2cad() {
 423     local url
 424     local site='https://www.bankofcanada.ca/valet/observations/group'
 425     local csv_rates="${site}/FX_RATES_DAILY/csv"
 426     url="${csv_rates}?start_date=$(date -d '3 days ago' +'%Y-%m-%d')"
 427     curl -s "${url}" | awk -F, -v amount="$(echo "${1:-1}" | sed 's-_--g')" '
 428         /EUR/ { for (i = 1; i <= NF; i++) if($i ~ /EUR/) j = i }
 429         END { gsub(/"/, "", $j); if (j != 0) printf "%.2f\n", amount * $j }
 430     '
 431 }
 432 
 433 # Fix Audio Duration on a separate copy of the file given
 434 fad() { ffmpeg -i "${1:-input.m4a}" -acodec copy "${2:-output.dat}"; }
 435 
 436 # start from the line number given, skipping all previous ones
 437 fromline() { tail -n +"${1:-1}" "${2:--}"; }
 438 
 439 # convert a mix of FeeT and INches into meters
 440 ftin() {
 441     local ft="${1:-0}"
 442     ft="$(echo "${ft}" | sed 's-_--g')"
 443     local in="${2:-0}"
 444     in="$(echo "${in}" | sed 's-_--g')"
 445     awk "BEGIN { print 0.3048 * ${ft} + 0.0254 * ${in}; exit }"
 446 }
 447 
 448 # glue/stick together various lines, only emitting a line-feed at the end; an
 449 # optional argument is the output-item-separator, which is empty by default
 450 glue() {
 451     local sep="${1:-}"
 452     [ $# -gt 0 ] && shift
 453     awk -v sep="${sep}" '
 454         NR > 1 { printf "%s", sep }
 455         { gsub(/\r/, ""); printf "%s", $0 }
 456         END { if (NR > 0) print "" }
 457     ' "$@"
 458 }
 459 
 460 # GO Build Stripped: a common use-case for the go compiler
 461 gobs() { go build -ldflags "-s -w" -trimpath "$@"; }
 462 
 463 # GO DEPendencieS: show all dependencies in a go project
 464 godeps() { go list -f '{{ join .Deps "\n" }}' "$@"; }
 465 
 466 # GO IMPortS: show all imports in a go project
 467 goimps() { go list -f '{{ join .Imports "\n" }}' "$@"; }
 468 
 469 # go to the folder picked using an interactive TUI; uses my tool `bf`
 470 goto() {
 471     local where
 472     where="$(bf "${1:-.}")"
 473     if [ $? -ne 0 ]; then
 474         return 0
 475     fi
 476 
 477     where="$(realpath "${where}")"
 478     if [ ! -d "${where}" ]; then
 479         where="$(dirname "${where}")"
 480     fi
 481     cd "${where}" || return
 482 }
 483 
 484 # show Help laid out on 2 side-by-side columns; uses my tool `bsbs`
 485 h2() { naman "$@" | bsbs 2; }
 486 
 487 # show each byte as a pair of HEXadecimal (base-16) symbols
 488 hexify() {
 489     cat "$@" | od -v -x -A n | awk '
 490         { gsub(/ +/, ""); printf "%s", $0 }
 491         END { print "" }
 492     '
 493 }
 494 
 495 # History Fuzzy-finder
 496 hf() {
 497     eval "$(
 498         history \
 499         | fzf --reverse --height 80% --tmux center,80% \
 500         | awk '
 501             {
 502                 $1 = ""
 503                 $2 = ""
 504                 gsub(/^ */, "")
 505                 printf("\x1b[7m%s\x1b[0m\n", $0) > "/dev/stderr"
 506                 print
 507             }
 508         '
 509     )"
 510 }
 511 
 512 # Help Me Remember my custom shell commands
 513 hmr() {
 514     local cmd="bat"
 515     # debian linux uses a different name for the `bat` app
 516     if [ -e /usr/bin/batcat ]; then
 517         cmd="batcat"
 518     fi
 519 
 520     "$cmd" \
 521         --style=plain,header,numbers --theme='Monokai Extended Light' \
 522         --wrap=never --color=always "$(which clamette)" |
 523             sed -e 's-\x1b\[38;5;70m-\x1b[38;5;28m-g' \
 524                 -e 's-\x1b\[38;5;214m-\x1b[38;5;208m-g' \
 525                 -e 's-\x1b\[38;5;243m-\x1b[38;5;103m-g' \
 526                 -e 's-\x1b\[38;5;238m-\x1b[38;5;245m-g' \
 527                 -e 's-\x1b\[38;5;228m-\x1b[48;5;228m-g' |
 528                 less -MKiCRS
 529 }
 530 
 531 # convert seconds into a colon-separated Hours-Minutes-Seconds triple
 532 hms() {
 533     echo "${@:-0}" | sed -E 's-_--g; s- +-\n-g' | awk '
 534         /./ {
 535             x = $0
 536             h = (x - x % 3600) / 3600
 537             m = (x % 3600) / 60
 538             s = x % 60
 539             printf "%02d:%02d:%05.2f\n", h, m, s
 540         }
 541     '
 542 }
 543 
 544 # find all hyperlinks inside HREF attributes in the input text
 545 href() {
 546     local arg
 547     local awk_cmd='awk'
 548     local grep_cmd='grep'
 549     if [ -p /dev/stdout ] || [ -t 1 ]; then
 550         grep_cmd='grep --line-buffered'
 551         if [ -e /usr/bin/stdbuf ]; then
 552             awk_cmd='stdbuf -oL awk'
 553         fi
 554     fi
 555 
 556     for arg in "${@:--}"; do
 557         ${grep_cmd} -i -E -o 'href="[^"]+"' "${arg}"
 558     done | ${awk_cmd} '{ gsub(/^href="|"\r?$/, ""); print }'
 559 }
 560 
 561 # ignore command in a pipe: this allows quick re-editing of pipes, while
 562 # still leaving signs of previously-used steps, as a memo
 563 idem() { cat; }
 564 
 565 # INSTall APT packages
 566 instapt() { sudo apt install "$@"; sudo -k; }
 567 
 568 # JSON Query Lines turns JSON top-level arrays into multiple individually-JSON
 569 # lines using the `jq` app, keeping all other top-level values as single line
 570 # JSON outputs
 571 jql() {
 572     local code="${1:-.}"
 573     [ $# -gt 0 ] && shift
 574     jq -c -M "${code} | .[]" "$@"
 575 }
 576 
 577 # JSON Query Keys runs `jq` to find all unique key-combos from tabular JSON
 578 jqk() {
 579     local code="${1:-.}"
 580     [ $# -gt 0 ] && shift
 581     jq -c -M "${code} | .[] | keys" "$@" | awk '!c[$0]++'
 582 }
 583 
 584 # JSON Keys finds all unique key-combos from tabular JSON data; uses my tools
 585 # `jsonl` and `tjp`
 586 jsonk() {
 587     tjp '[e.keys() for e in v] if isinstance(v, (list, tuple)) else v.keys()' \
 588         "${1:--}" | jsonl | awk '!c[$0]++'
 589 }
 590 
 591 # JSON Table, turns TSV tables into tabular JSON, where valid-JSON values are
 592 # auto-parsed into numbers, booleans, etc...; uses my tools `jsons` and `tjp`
 593 jsont() {
 594     jsons "$@" | tjp \
 595         '[{k: rescue(lambda: loads(v), v) for k, v in e.items()} for e in v]'
 596 }
 597 
 598 # emit the given number of random/junk bytes, or 1024 junk bytes by default
 599 junk() { head -c "$(echo "${1:-1024}" | sed 's-_--g')" /dev/urandom; }
 600 
 601 # convert pounds (LB) into kilograms
 602 lb() {
 603     echo "${@:-1}" | sed -E 's-_--g; s- +-\n-g' |
 604         awk '/./ { printf "%.2f\n", 0.45359237 * $0 }'
 605 }
 606 
 607 # convert a mix of pounds (LB) and weight-ounces (OZ) into kilograms
 608 lboz() {
 609     local lb="${1:-0}"
 610     lb="$(echo "${lb}" | sed 's-_--g')"
 611     local oz="${2:-0}"
 612     oz="$(echo "${oz}" | sed 's-_--g')"
 613     awk "BEGIN { print 0.45359237 * ${lb} + 0.028349523 * ${oz}; exit }"
 614 }
 615 
 616 # regroup adjacent lines into n-item tab-separated lines
 617 lineup() {
 618     local command='awk'
 619     if { [ -p /dev/stdout ] || [ -t 1 ]; } && [ -e /usr/bin/stdbuf ]; then
 620         command='stdbuf -oL awk'
 621     fi
 622 
 623     local n="${1:-0}"
 624     [ $# -gt 0 ] && shift
 625 
 626     if [ "$n" -le 0 ]; then
 627         ${command} '
 628             NR > 1 { printf "\t" }
 629             { printf "%s", $0 }
 630             END { if (NR > 0) print "" }
 631         ' "$@"
 632         return $?
 633     fi
 634 
 635     ${command} -v n="$n" '
 636         NR % n != 1 && n > 1 { printf "\t" }
 637         { printf "%s", $0 }
 638         NR % n == 0 { print "" }
 639         END { if (NR % n != 0) print "" }
 640     ' "$@"
 641 }
 642 
 643 # LiSt files, showing how many 4K-sized storage blocks they use
 644 ls4k() { ls -s --block-size=4096 "$@"; }
 645 
 646 # LiSt MAN pages
 647 lsman() { man -k "${1:-.}"; }
 648 
 649 # MARK the current tab with the message given, followed by the current folder;
 650 # works only on the `bash` shell
 651 mark() {
 652     if [ $# -eq 0 ]; then
 653         PS1="\[\e[0m\e]0;\w\a\$ "
 654     else
 655         PS1="\[\e[0m\e]0;${*} \w\a\$ "
 656     fi
 657 }
 658 
 659 marklinks() {
 660     local re='https?://[A-Za-z0-9+_.:%-]+(/[A-Za-z0-9+_.%/,#?&=-]*)*'
 661     sed -E 's-('"${re}"')-\x1b]8;;\1\x1b\\\1\x1b]8;;\x1b\\-g' "$@"
 662 }
 663 
 664 # Multi-Core MAKE runs `make` using all cores
 665 mcmake() { make -j "$(nproc)" "$@"; }
 666 
 667 # merge stderr into stdout, which is useful for piped commands
 668 merrge() { "${@:-cat /dev/null}" 2>&1; }
 669 
 670 metajq() {
 671     # https://github.com/stedolan/jq/issues/243#issuecomment-48470943
 672     jq -r -M '
 673         [
 674             path(..) |
 675             map(if type == "number" then "[]" else tostring end) |
 676             join(".") | split(".[]") | join("[]")
 677         ] | unique | map("." + .) | .[]
 678     ' "$@"
 679 }
 680 
 681 # Make In Folder, also showing time and max memory used
 682 mif() {
 683     local f='real %e    user %U    sys %S    mem %M    exit %x'
 684     local folder
 685     folder="${1:-.}"
 686     [ $# -gt 0 ] && shift
 687     env -C "${folder}" /usr/bin/time -f "$f" make "$@"
 688 }
 689 
 690 # MINimize DECimalS ignores all trailing decimal zeros in numbers, and even
 691 # the decimal dots themselves, when decimals in a number are all zeros
 692 # mindecs() {
 693 #     local cmd='sed -E'
 694 #     if { [ -p /dev/stdout ] || [ -t 1 ]; } && [ -e /usr/bin/stdbuf ]; then
 695 #         cmd='sed -E -u'
 696 #     fi
 697 #     ${cmd} 's-([0-9]+)\.0+\W-\1-g; s-([0-9]+\.[0-9]*[1-9])0+\W-\1-g' "$@"
 698 # }
 699 
 700 # NArrow MANual, keeps `man` narrow, even if the window/tab is wide when run
 701 naman() {
 702     local w
 703     w="$(tput -T xterm cols)"
 704     w="$((w / 2 - 4))"
 705     if [ "$w" -lt 80 ]; then
 706         w=80
 707     fi
 708     MANWIDTH="$w" man "$@"
 709 }
 710 
 711 # Not AND sorts its 2 inputs, then finds lines not in common
 712 nand() {
 713     # comm -3 <(sort "$1") <(sort "$2")
 714     # dash doesn't support the process-sub syntax
 715     (sort "$1" | (sort "$2" | (comm -3 /dev/fd/3 /dev/fd/4) 4<&0) 3<&0)
 716 }
 717 
 718 # listen to streaming NEW WAVE music
 719 newwave() {
 720     printf "streaming \e[7mNew Wave radio\e[0m\n"
 721     mpv --quiet https://puma.streemlion.com:2910/stream
 722 }
 723 
 724 # Nice Json Query Lines colors JSONL data using the `jq` app
 725 njql() {
 726     local code="${1:-.}"
 727     [ $# -gt 0 ] && shift
 728     jq -c -C "${code} | .[]" "$@"
 729 }
 730 
 731 # empty the clipboard
 732 noclip() { wl-copy --clear; }
 733 
 734 # Nice Print Awk result; uses my tool `nn`
 735 npa() {
 736     local arg
 737     for arg in "$@"; do
 738         awk "BEGIN { print(${arg}); exit }"
 739     done | nn
 740 }
 741 
 742 # Nice Print Guile (scheme) result; uses my tool `nn`
 743 npg() {
 744     local arg
 745     for arg in "$@"; do
 746         guile -c "(display ${arg}) (newline)"
 747     done | nn
 748 }
 749 
 750 # Nice Print Python result; uses my tool `nn`
 751 npp() {
 752     local arg
 753     for arg in "$@"; do
 754         python -c "print(${arg})"
 755     done | nn
 756 }
 757 
 758 # Nice Size, using my tool `nn`
 759 ns() { wc -c "$@" | nn; }
 760 
 761 # emit nothing to output and/or discard everything from input
 762 null() { [ $# -gt 0 ] && "$@" > /dev/null; }
 763 
 764 # Print Guile (scheme) result
 765 pg() {
 766     local arg
 767     for arg in "$@"; do
 768         guile -c "(display ${arg}) (newline)"
 769     done
 770 }
 771 
 772 # Print Python result
 773 pp() {
 774     local arg
 775     for arg in "$@"; do
 776         python -c "print(${arg})"
 777     done
 778 }
 779 
 780 # PRecede (input) ECHO, prepends a first line to stdin lines
 781 precho() { echo "$@" && cat /dev/stdin; }
 782 
 783 # LABEL/precede data with an ANSI-styled line
 784 prelabel() {
 785     printf "\e[7m%-*s\e[0m\n" "$(($(tput -T xterm cols) - 2))" "$*"
 786     cat -
 787 }
 788 
 789 # Quiet MPV
 790 # qmpv() { mpv --quiet "${@:--}"; }
 791 
 792 # Quiet MPV
 793 qmpv() { mpv --really-quiet "${@:--}"; }
 794 
 795 # keep only lines between the 2 line numbers given, inclusively
 796 rangelines() {
 797     { [ $# -eq 2 ] || [ $# -eq 3 ]; } && [ "${1}" -le "${2}" ] && {
 798         tail -n +"${1}" "${3:--}" | head -n $(("${2}" - "${1}" + 1))
 799     }
 800 }
 801 
 802 # RANdom MANual page
 803 ranman() {
 804     find "/usr/share/man/man${1:-1}" -type f | shuf -n 1 | xargs basename |
 805         sed 's-\.gz$--' | xargs man
 806 }
 807 
 808 # REPeat STRing emits a line with a repeating string in it, given both a
 809 # string and a number in either order
 810 repstr() {
 811     awk '
 812         BEGIN {
 813             if (ARGV[2] ~ /^[+-]?[0-9]+$/) {
 814                 symbol = ARGV[1]
 815                 times = ARGV[2] + 0
 816             } else {
 817                 symbol = ARGV[2]
 818                 times = ARGV[1] + 0
 819             }
 820 
 821             if (times < 0) exit
 822             if (symbol == "") symbol = "-"
 823             s = sprintf("%*s", times, "")
 824             gsub(/ /, symbol, s)
 825             print s
 826             exit
 827         }
 828     ' "$@"
 829 }
 830 
 831 # SystemCTL; `sysctl` is already taken for a separate/unrelated app
 832 sctl() { systemctl "$@" 2>&1 | less -MKiCRS; }
 833 
 834 # show a unique-looking SEParator line; useful to run between commands
 835 # which output walls of text
 836 sep() {
 837     [ "${1:-80}" -gt 0 ] &&
 838         printf "\e[48;2;218;218;218m%${1:-80}s\e[0m\n" "" | sed 's- -·-g'
 839 }
 840 
 841 # webSERVE files in a folder as localhost, using the port number given, or
 842 # port 8080 by default
 843 serve() {
 844     if [ -d "$1" ]; then
 845         printf "\e[7mserving files in %s\e[0m\n" "$1" >&2
 846         python3 -m http.server -d "$1" "${2:-8080}"
 847     else
 848         printf "\e[7mserving files in %s\e[0m\n" "${2:-$(pwd)}" >&2
 849         python3 -m http.server -d "${2:-$(pwd)}" "${1:-8080}"
 850     fi
 851 }
 852 
 853 # SET DIFFerence sorts its 2 inputs, then finds lines not in the 2nd input
 854 setdiff() {
 855     # comm -23 <(sort "$1") <(sort "$2")
 856     # dash doesn't support the process-sub syntax
 857     (sort "$1" | (sort "$2" | (comm -23 /dev/fd/3 /dev/fd/4) 4<&0) 3<&0)
 858 }
 859 
 860 # SET INtersection, sorts its 2 inputs, then finds common lines
 861 setin() {
 862     # comm -12 <(sort "$1") <(sort "$2")
 863     # dash doesn't support the process-sub syntax
 864     (sort "$1" | (sort "$2" | (comm -12 /dev/fd/3 /dev/fd/4) 4<&0) 3<&0)
 865 }
 866 
 867 # SET SUBtraction sorts its 2 inputs, then finds lines not in the 2nd input
 868 setsub() {
 869     # comm -23 <(sort "$1") <(sort "$2")
 870     # dash doesn't support the process-sub syntax
 871     (sort "$1" | (sort "$2" | (comm -23 /dev/fd/3 /dev/fd/4) 4<&0) 3<&0)
 872 }
 873 
 874 # skip the last n lines, or the last line by default
 875 skiplast() { head -n -"${1:-1}" "${2:--}"; }
 876 
 877 # SLOW/delay lines from the standard-input, waiting the number of seconds
 878 # given for each line, or waiting 1 second by default
 879 slow() {
 880     local seconds="${1:-1}"
 881     [ $# -gt 0 ] && shift
 882     (
 883         IFS="$(printf "\n")"
 884         awk 1 "$@" | while read -r line; do
 885             sleep "${seconds}"
 886             printf "%s\n" "${line}"
 887         done
 888     )
 889 }
 890 
 891 # Show Latest Podcasts, using my tools `podfeed` and `si`
 892 slp() {
 893     local title
 894     title="Latest Podcast Episodes as of $(date +'%F %T')"
 895     podfeed -title "${title}" "$@" | si
 896 }
 897 
 898 # emit the first line as is, sorting all lines after that, using the
 899 # `sort` command, passing all/any arguments/options to it
 900 sortrest() {
 901     awk -v sort="sort $*" '
 902         FNR == 1 { gsub(/^\xef\xbb\xbf/, "") }
 903         { gsub(/\r$/, "") }
 904         NR == 1 { print; fflush() }
 905         NR >= 2 { print | sort }
 906     '
 907 }
 908 
 909 # SORt Tab-Separated Values: emit the first line as is, sorting all lines after
 910 # that, using the `sort` command in TSV (tab-separated values) mode, passing
 911 # all/any arguments/options to it
 912 sortsv() {
 913     awk -v sort="sort -t \"$(printf '\t')\" $*" '
 914         FNR == 1 { gsub(/^\xef\xbb\xbf/, "") }
 915         { gsub(/\r$/, "") }
 916         NR == 1 { print; fflush() }
 917         NR >= 2 { print | sort }
 918     '
 919 }
 920 
 921 # emit a line with the number of spaces given in it
 922 spaces() { [ "${1:-80}" -gt 0 ] && printf "%${1:-80}s\n" ""; }
 923 
 924 # SQUeeze horizontal spaces and STOMP vertical gaps
 925 squomp() {
 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         FNR == 1 { gsub(/^\xef\xbb\xbf/, "") }
 933         /^\r?$/ { empty = 1; next }
 934         empty { if (n > 0) print ""; empty = 0 }
 935 
 936         {
 937             gsub(/^ +| *\r?$/, "")
 938             gsub(/ *\t */, "\t")
 939             gsub(/  +/, " ")
 940             print; n++
 941         }
 942     ' "$@"
 943 }
 944 
 945 # TAC Lines outputs input-lines in reverse order, last one first, and so on...
 946 tacl() {
 947     awk '
 948         { gsub(/\r$/, ""); lines[NR] = $0 }
 949         END { for (i = NR; i >= 1; i--) print lines[i] }
 950     ' "$@"
 951 }
 952 
 953 # TINY GO Build Optimized: a common use-case for the tinygo compiler
 954 tinygobo() { tinygo build -no-debug -opt=2 "$@"; }
 955 
 956 # show current date in a specifc format
 957 today() { date +'%Y-%m-%d %a %b %d'; }
 958 
 959 # get the first n lines, or 1 by default
 960 toline() { head -n "${1:-1}" "${2:--}"; }
 961 
 962 # get the processes currently using the most cpu
 963 topcpu() {
 964     local n="${1:-10}"
 965     [ "$n" -gt 0 ] && ps aux | awk '
 966         NR == 1 { print; fflush() }
 967         NR > 1 { print | "sort -rnk3,3" }
 968     ' | head -n "$(("$n" + 1))"
 969 }
 970 
 971 # get the processes currently using the most memory
 972 topmemory() {
 973     local n="${1:-10}"
 974     [ "$n" -gt 0 ] && ps aux | awk '
 975         NR == 1 { print; fflush() }
 976         NR > 1 { print | "sort -rnk6,6" }
 977     ' | head -n "$(("$n" + 1))"
 978 }
 979 
 980 # skip the first/leading n bytes
 981 unleaded() { tail -c +$(("$1" + 1)) "${2:--}"; }
 982 
 983 # go UP n folders, or go up 1 folder by default
 984 up() {
 985     if [ "${1:-1}" -le 0 ]; then
 986         cd .
 987     else
 988         cd "$(printf "%${1:-1}s" "" | sed 's- -../-g')" || return $?
 989     fi
 990 }
 991 
 992 # convert United States Dollars into CAnadian Dollars, using the latest
 993 # official exchange rates from the bank of canada; during weekends, the
 994 # latest rate may be from a few days ago; the default amount of usd to
 995 # convert is 1, when not given
 996 usd2cad() {
 997     local url
 998     local site='https://www.bankofcanada.ca/valet/observations/group'
 999     local csv_rates="${site}/FX_RATES_DAILY/csv"
1000     url="${csv_rates}?start_date=$(date -d '3 days ago' +'%Y-%m-%d')"
1001     curl -s "${url}" | awk -F, -v amount="$(echo "${1:-1}" | sed 's-_--g')" '
1002         /USD/ { for (i = 1; i <= NF; i++) if($i ~ /USD/) j = i }
1003         END { gsub(/"/, "", $j); if (j != 0) printf "%.2f\n", amount * $j }
1004     '
1005 }
1006 
1007 # View Nice Table / Very Nice Table; uses my own tools `catl`, `u`, and `ncol`
1008 vnt() {
1009     catl "$@" \
1010     | ncol \
1011     | u --top \
1012     | awk '{ printf "%6d  %s\n", NR - 1, $0 }' \
1013     | {
1014         less -MKiCRS --header=1 2> /dev/null \
1015         || less -RIMS 2> /dev/null \
1016         || cat
1017     }
1018 }
1019 
1020 # What Are These (?) shows what the names given to it are/do
1021 wat() {
1022     local arg
1023     local gap=0
1024     local less_options='-MKiCRS'
1025 
1026     if [ $# -eq 0 ]; then
1027         echo "$0"
1028         return 0
1029     fi
1030 
1031     if [ $# -lt 2 ]; then
1032         less_options='-MKiCRS --header=1'
1033     fi
1034 
1035     for arg in "$@"; do
1036         [ "${gap}" -gt 0 ] && printf "\n"
1037         gap=1
1038         printf "\e[7m%-80s\e[0m\n" "${arg}"
1039 
1040         while alias "${arg}" > /dev/null 2> /dev/null; do
1041             arg="$(alias "${arg}" | sed -E "s-^[^=]+=['\"](.+)['\"]\$-\\1-")"
1042         done
1043 
1044         if echo "${arg}" | grep -q ' '; then
1045             printf "%s\n" "${arg}"
1046             continue
1047         fi
1048 
1049         if declare -f "${arg}"; then
1050             continue
1051         fi
1052 
1053         if which "${arg}" > /dev/null 2> /dev/null; then
1054             which "${arg}"
1055             continue
1056         fi
1057 
1058         printf "\e[38;2;204;0;0m%s not found\e[0m\n" "${arg}"
1059     done | {
1060         less -MKiCRS ${less_options} 2> /dev/null \
1061         || less -MKiCRS 2> /dev/null \
1062         || less -RIMS 2> /dev/null \
1063         || cat
1064     }
1065 }
1066 
1067 # find all WEB/hyperLINKS (https:// and http://) in the input text
1068 weblinks() {
1069     local arg
1070     local re='https?://[A-Za-z0-9+_.:%-]+(/[A-Za-z0-9+_.%/,#?&=-]*)*'
1071     local grep_cmd='grep'
1072     if [ -p /dev/stdout ] || [ -t 1 ]; then
1073         grep_cmd='grep --line-buffered'
1074     fi
1075 
1076     for arg in "${@:--}"; do
1077         ${grep_cmd} -i -E -o "${re}" "${arg}"
1078     done
1079 }
1080 
1081 # recursively find all files with trailing spaces/CRs
1082 whichtrails() {
1083     if [ -p /dev/stdout ] || [ -t 1 ]; then
1084         rg --line-buffered  -c '[ \r]+$' "${@:-.}"
1085     else
1086         rg -c '[ \r]+$' "${@:-.}"
1087     fi
1088 }
1089 
1090 # turn all wsl/unix-style full-paths into WINdows-style full-PATHS
1091 winpaths() { sed -E 's-/mnt/(.)/-\u\1:/-' "$@"; }
1092 
1093 # XARGS Lines, runs `xargs` using whole lines as extra arguments
1094 xargsl() {
1095     if { [ -p /dev/stdout ] || [ -t 1 ]; } && [ -e /usr/bin/stdbuf ]; then
1096         stdbuf -oL awk -v ORS='\000' '
1097             FNR == 1 { gsub(/^\xef\xbb\xbf/, "") }
1098             { gsub(/\r$/, ""); print }
1099         ' | stdbuf -oL xargs -0 "$@"
1100     else
1101         awk -v ORS='\000' '
1102             FNR == 1 { gsub(/^\xef\xbb\xbf/, "") }
1103             { gsub(/\r$/, ""); print }
1104         ' | xargs -0 "$@"
1105     fi
1106 }
1107 
1108 # Youtube Audio Player
1109 yap() {
1110     local url
1111     # some youtube URIs end with extra playlist/tracker parameters
1112     url="$(echo "$1" | sed 's-&.*--')"
1113     mpv "$(yt-dlp -x --audio-format best --get-url "${url}" 2> /dev/null)"
1114 }
1115 
1116 # show the current date in the YYYY-MM-DD format
1117 ymd() { date +'%Y-%m-%d'; }