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