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(/&/, "&") 368 gsub(/</, "<") 369 gsub(/>/, ">") 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'; }