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