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