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