File: clam.sh 1 #!/bin/sh 2 3 # The MIT License (MIT) 4 # 5 # Copyright © 2020-2025 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 # This script is compatible with `bash`, `zsh`, and even `dash`, which is 44 # debian linux's default non-interactive shell. Some of its commands even 45 # 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) 63 # script is being sourced with bash or dash, which is good 64 : 65 ;; 66 *) 67 case "$ZSH_EVAL_CONTEXT" in 68 *:file) 69 # script is being sourced with zsh, which is good 70 : 71 ;; 72 *) 73 # script is being run normally, which is a waste of time 74 printf "\e[7mDon't run this script directly: instead source it\e[0m\n" 75 printf "\e[7mby running '. clam' (without the single quotes).\e[0m\n" 76 # failing during shell-startup may deny shell access, so exit 77 # with a 0 error-code to declare success 78 exit 0 79 ;; 80 esac 81 ;; 82 esac 83 84 85 alias 0=sbs 86 alias 1='bsbs 1' 87 alias 2='bsbs 2' 88 alias 3='bsbs 3' 89 alias 4='bsbs 4' 90 alias 5='bsbs 5' 91 alias 6='bsbs 6' 92 alias 7='bsbs 7' 93 alias 8='bsbs 8' 94 alias 9='bsbs 9' 95 96 alias c=cat 97 alias e=echo 98 alias r='tput reset' 99 100 # AWK in PARagraph-input mode 101 alias awkpar=awkblock 102 103 # Better Less runs `less`, showing line numbers, among other settings 104 alias bl='less -MKNiCRS' 105 106 # Better LESS runs `less`, showing line numbers, among other settings 107 alias bless='less -MKNiCRS' 108 109 # Breathe Lines 5: separate groups of 5 lines with empty lines 110 alias bl5=b5 111 112 # Book-like MANual, lays out `man` docs as pairs of side-by-side pages; uses 113 # my tool `bsbs` 114 alias bman=bookman 115 116 # load/concatenate BYTES from named data sources; uses my tool `get` 117 alias bytes=get 118 119 # Compile C Optimized 120 alias cco='cc -Wall -O3 -s -march=native -mtune=native -flto' 121 122 # Colored Json Query runs the `jq` app, allowing an optional filepath as the 123 # data source, and even an optional transformation formula 124 alias cjq='jq -C' 125 126 # CLear Screen 127 alias cls='tput reset' 128 129 # Compile C Plus Plus Optimized 130 alias cppo='c++ -Wall -O3 -s -march=native -mtune=native -flto' 131 132 # Colored RipGrep ensures app `rg` emits colors when piped 133 alias crg='rg --line-buffered --color=always' 134 135 # CURL Silent spares you the progress bar, but still tells you about errors 136 alias curls='curl --silent --show-error' 137 138 # dictionary-DEFine the word given, using an online service 139 alias def=define 140 141 # turn JSON Lines into a proper json array 142 alias dejsonl='jq -s -M' 143 144 # turn UTF-16 data into UTF-8 145 alias deutf16='iconv -f utf16 -t utf8' 146 147 # DIM (lines) with AWK 148 alias dimawk=dawk 149 150 # edit plain-text files 151 alias edit=micro 152 153 # ENV with 0/null-terminated lines on stdout 154 alias env0='env -0' 155 156 # ENV Change folder, runs the command given in the folder given (first) 157 alias envc='env -C' 158 159 # Extended Plain Interactive Grep 160 alias epig='ugrep --color=never -Q -E' 161 162 # Editor Read-Only 163 alias ero='micro -readonly true' 164 165 # Expand 4 turns each tab into up to 4 spaces 166 alias expand4='expand -t 4' 167 168 # run the Fuzzy Finder (fzf) in multi-choice mode, with custom keybindings 169 alias ff='fzf -m --bind ctrl-a:select-all,ctrl-space:toggle' 170 171 # get FILE's MIME types 172 alias filemime='file --mime-type' 173 174 # run `gcc` with all optimizations on and with static analysis on 175 alias gccmax='gcc -Wall -O3 -s -march=native -mtune=native -flto -fanalyzer' 176 177 # GRAY AWK styles lines satisfying an AWK condition/expression red 178 alias grayawk=dawk 179 180 # hold stdout if used at the end of a pipe-chain 181 alias hold='less -MKiCRS' 182 183 # find all hyperlinks inside HREF attributes in the input text 184 alias hrefs=href 185 186 # make JSON Lines out of JSON data 187 alias jl=jsonl 188 189 # shrink/compact JSON using the `jq` app, allowing an optional filepath, and 190 # even an optional transformation formula after that 191 alias jq0='jq -c -M' 192 193 # show JSON data on multiple lines, using 2 spaces for each indentation level, 194 # allowing an optional filepath, and even an optional transformation formula 195 # after that 196 alias jq2='jq --indent 2 -M' 197 198 # JSON Query Lines turns JSON top-level arrays into multiple individually-JSON 199 # lines using the `jq` app, keeping all other top-level values as single line 200 # JSON outputs, allowing an optional filepath as the data source 201 alias jql=jqlines 202 203 # JSON Query LINES turns JSON top-level arrays into multiple individually-JSON 204 # lines using the `jq` app, keeping all other top-level values as single line 205 # JSON outputs, allowing an optional filepath as the data source 206 alias jqlines='jq -c -M ".[]"' 207 208 # JSON Query Keys runs `jq` to find all unique key-combos from tabular JSON 209 alias jqk=jqkeys 210 211 # find the LAN (local-area network) IP address for this device 212 alias lanip='hostname -I' 213 214 # run `less`, showing line numbers, among other settings 215 alias least='less -MKNiCRS' 216 217 # Less with Header 1 runs `less` with line numbers, ANSI styles, without 218 # line-wraps, and using the first line as a sticky-header, so it always 219 # shows on top 220 alias lh1='less --header=1 -MKNiCRS' 221 222 # Less with Header 2 runs `less` with line numbers, ANSI styles, without 223 # line-wraps, and using the first 2 lines as a sticky-header, so they 224 # always show on top 225 alias lh2='less --header=2 -MKNiCRS' 226 227 # try to run the command given using line-buffering for its (standard) output 228 alias livelines='stdbuf -oL' 229 230 # LOAD data from the filename or URI given; uses my tool `get` 231 alias load=get 232 233 # LOcal SERver webserves files in a folder as localhost, using the port 234 # number given, or port 8080 by default 235 alias loser=serve 236 237 # LOWercase all ASCII symbols 238 alias low=tolower 239 240 # LOWERcase all ASCII symbols 241 alias lower=tolower 242 243 # run `ls` showing how many 4k pages each file takes 244 alias lspages='ls -s --block-size=4096' 245 246 # Listen To Youtube 247 alias lty=yap 248 249 # MAKE IN folder 250 alias makein=mif 251 252 # Multi-Core MaKe runs `make` using all cores 253 alias mcmk=mcmake 254 255 # run `less`, showing line numbers, among other settings 256 alias most='less -MKNiCRS' 257 258 # emit nothing to output and/or discard everything from input 259 alias nil=null 260 261 # Nice Json Query colors JSON data using the `jq` app 262 alias njq=cjq 263 264 # Nice Json Query Lines colors JSONL data using the `jq` app, allowing an 265 # optional filepath as the JSON-data source 266 alias njql='jq -c -C ".[]"' 267 268 # Narrow MANual, keeps `man` narrow, even if the window/tab is wide when run 269 alias nman=naman 270 271 # Plain Interactive Grep 272 alias pig='ugrep --color=never -Q -E' 273 274 # Plain RipGrep 275 alias prg='rg --line-buffered --color=never' 276 277 # Quick Compile C Optimized 278 alias qcco='cc -Wall -O3 -s -march=native -mtune=native -flto' 279 280 # Quick Compile C Plus Plus Optimized 281 alias qcppo='c++ -Wall -O3 -s -march=native -mtune=native -flto' 282 283 # RED AWK styles lines satisfying an AWK condition/expression red 284 alias redawk=rawk 285 286 # Run In Folder 287 alias rif='env -C' 288 289 # Read-Only Editor 290 alias roe='micro -readonly true' 291 292 # Read-Only Micro (text editor) 293 alias rom='micro -readonly true' 294 295 # Read-Only Top 296 alias rot='htop --readonly' 297 298 # RUN IN folder 299 alias runin='env -C' 300 301 # place lines Side-By-Side 302 # alias sbs=column 303 304 # Silent CURL spares you the progress bar, but still tells you about errors 305 alias scurl='curl --silent --show-error' 306 307 # Stdbuf Output Line-buffered 308 alias sol='stdbuf -oL' 309 310 # TRY running a command, showing its outcome/error-code on failure 311 alias try=verdict 312 313 # Time Verbosely the command given 314 alias tv='/usr/bin/time -v' 315 316 # run `cppcheck` with even stricter options 317 alias vetc='cppcheck --enable=portability,style --check-level=exhaustive' 318 319 # run `cppcheck` with even stricter options, also checking for c89 compliance 320 alias vetc89='cppcheck --enable=portability,style --check-level=exhaustive --std=c89' 321 322 # run `cppcheck` with even stricter options 323 alias vetcpp='cppcheck --enable=portability,style --check-level=exhaustive' 324 325 # VET SHell scripts 326 alias vetsh=vetshell 327 328 # check shell scripts for common gotchas, avoiding complaints about using 329 # the `local` keyword, which is widely supported in practice 330 alias vetshell='shellcheck -e 3043' 331 332 # View with Header 1 runs `less` without line numbers, ANSI styles, without 333 # line-wraps, and using the first line as a sticky-header, so it always shows 334 # on top 335 alias vh1='less --header=1 -MKiCRS' 336 337 # View with Header 2 runs `less` without line numbers, ANSI styles, without 338 # line-wraps, and using the first 2 lines as sticky-headers, so they always 339 # show on top 340 alias vh2='less --header=2 -MKiCRS' 341 342 # run a command using an empty environment 343 alias void='env -i' 344 345 # turn plain-text from latin-1 into UTF-8; the name is from `vulgarization`, 346 # which is the mutation of languages away from latin during the middle ages 347 alias vulgarize='iconv -f latin-1 -t utf-8' 348 349 # recursively find all files with trailing spaces/CRs 350 alias wheretrails=whichtrails 351 352 # run `xargs`, using zero/null bytes as the extra-arguments terminator 353 alias x0='xargs -0' 354 355 # Xargs Lines, runs `xargs` using whole lines as extra arguments 356 alias xl=xargsl 357 358 # find name from the local `apt` database of installable packages 359 aptfind() { 360 local arg 361 local gap=0 362 local options='-MKiCRS' 363 364 if [ $# -eq 1 ]; then 365 options='--header=1 -MKiCRS' 366 fi 367 368 for arg in "$@"; do 369 [ "${gap}" -gt 0 ] && printf "\n" 370 gap=1 371 printf "\e[7m%-80s\e[0m\n\n" "${arg}" 372 373 # despite warnings, the `search` command has been around for years 374 apt search "${arg}" 2> /dev/null | 375 grep -E -A 1 "^[a-z0-9-]*${arg}" | sed -u 's/^--$//' 376 done | less "${options}" 377 } 378 379 # APT UPdate/grade 380 aptup() { sudo apt update && sudo apt upgrade "$@"; sudo -k; } 381 382 # emit each argument given as its own line of output 383 args() { awk 'BEGIN { for (i = 1; i < ARGC; i++) print ARGV[i]; exit }' "$@"; } 384 385 # avoid/ignore lines which match any of the regexes given 386 avoid() { 387 local command='awk' 388 if [ -p 1 ] || [ -t 1 ]; then 389 command='stdbuf -oL awk' 390 fi 391 392 ${command} ' 393 BEGIN { for (i = 1; i < ARGC; i++) { re[i] = ARGV[i]; delete ARGV[i] } } 394 { for (i in re) if ($0 ~ re[i]) { next } } 395 { print; got++ } 396 END { exit(got == 0) } 397 ' "${@:-^$}" 398 } 399 400 # AWK in BLOCK/paragraph-input mode 401 awkblock() { 402 if [ -p 1 ] || [ -t 1 ]; then 403 stdbuf -oL awk -F='' -v RS='' "$@" 404 else 405 awk -F='' -v RS='' "$@" 406 fi 407 } 408 409 # AWK in TSV input/output mode 410 awktsv() { 411 if [ -p 1 ] || [ -t 1 ]; then 412 stdbuf -oL awk -F "\t" -v OFS="\t" "$@" 413 else 414 awk -F "\t" -v OFS="\t" "$@" 415 fi 416 } 417 418 # Breathe lines 5: separate groups of 5 lines with empty lines 419 b5() { 420 if [ -p 1 ] || [ -t 1 ]; then 421 stdbuf -oL awk 'NR % 5 == 1 && NR != 1 { print "" } 1' "$@" 422 else 423 awk 'NR % 5 == 1 && NR != 1 { print "" } 1' "$@" 424 fi 425 } 426 427 # show an ansi-styled BANNER-like line 428 banner() { printf "\e[7m%-$(tput cols)s\e[0m\n" "$*"; } 429 430 # emit a colored bar which can help visually separate different outputs 431 bar() { 432 [ "${1:-80}" -gt 0 ] && printf "\e[48;2;218;218;218m%${1:-80}s\e[0m\n" "" 433 } 434 435 # Bullets with AWK shows a reverse-sorted tally of all lines read, where ties 436 # are sorted alphabetically, and where trailing bullets are added to quickly 437 # make the tally counts comparable at a glance 438 bawk() { 439 local code="${1:-\$0}" 440 [ $# -gt 0 ] && shift 441 442 printf "value\ttally\tbullets\n" 443 awk ' 444 { low = lower = tolower($0) } 445 { tally['"${code}"']++ } 446 447 END { 448 # find the max tally, which is needed to build the bullets-string 449 max = 0 450 for (k in tally) { 451 if (max < tally[k]) max = tally[k] 452 } 453 454 # make enough bullets for all tallies: this loop makes growing the 455 # string a task with complexity O(n * log n), instead of a naive 456 # O(n**2), which can slow-down things when tallies are high enough 457 bullets = "•" 458 for (n = max; n > 1; n /= 2) { 459 bullets = bullets bullets 460 } 461 462 # emit unsorted output lines to the sort cmd, which will emit the 463 # final reverse-sorted tally lines 464 for (k in tally) { 465 s = substr(bullets, 1, tally[k]) 466 printf "%s\t%d\t%s\n", k, tally[k], s 467 } 468 } 469 ' "$@" | sort -t "$(printf "\t")" -rnk2 -k1d 470 } 471 472 # play a repeating and annoying high-pitched beep sound a few times a second, 473 # lasting the number of seconds given, or for 1 second by default; uses my 474 # script `sboard` 475 beeps() { sboard beeps "${1:-1}" "${2:-1}"; } 476 477 # play a repeating synthetic-bell-like sound lasting the number of seconds 478 # given, or for 1 second by default; uses my tool `sboard` 479 bell() { sboard bell "${1:-1}" "${2:-1}"; } 480 481 # Breathe Header 5: add an empty line after the first one (the header), 482 # then separate groups of 5 lines with empty lines between them 483 bh5() { 484 if [ -p 1 ] || [ -t 1 ]; then 485 stdbuf -oL awk '(NR - 1) % 5 == 1 { print "" } 1' "$@" 486 else 487 awk '(NR - 1) % 5 == 1 { print "" } 1' "$@" 488 fi 489 } 490 491 # emit a line with a repeating block-like symbol in it 492 blocks() { [ "${1:-80}" -gt 0 ] && printf "%${1:-80}s\n" "" | sed 's- -█-g'; } 493 494 # BOOK-like MANual, lays out `man` docs as pairs of side-by-side pages; uses 495 # my tool `bsbs` 496 bookman() { 497 local w 498 w="$(tput cols)" 499 w="$((w / 2 - 4))" 500 if [ "$w" -lt 65 ]; then 501 w=65 502 fi 503 MANWIDTH="$w" man "$@" | bsbs 2 504 } 505 506 # split lines using the separator given, turning them into single-item lines 507 breakdown() { 508 local sep="${1:- }" 509 [ $# -gt 0 ] && shift 510 local command='awk' 511 if [ -p 1 ] || [ -t 1 ]; then 512 command='stdbuf -oL awk' 513 fi 514 515 ${command} -F "${sep}" '{ for (i = 1; i <= NF; i++) print $i }' "$@" 516 } 517 518 # play a busy-phone-line sound lasting the number of seconds given, or for 1 519 # second by default; uses my tool `sboard` 520 busy() { sboard busy "${1:-1}" "${2:-1}"; } 521 522 # CAlculator with Nice numbers runs my tool `ca` and colors results with 523 # my tool `nn`, alternating styles to make long numbers easier to read 524 can() { 525 local arg 526 for arg in "$@"; do 527 ca "${arg}" 528 done | nn --gray 529 } 530 531 # uppercase the first letter on each line, and lowercase all later letters 532 capitalize() { sed -E -u 's-^(.*)-\L\1-; s-^(.)-\u\1-'; } 533 534 # Count with AWK: count the times the AWK expression/condition given is true 535 cawk() { 536 local cond="${1:-1}" 537 [ $# -gt 0 ] && shift 538 awk ' 539 BEGIN { count = c = 0 } 540 { low = lower = tolower($0) } 541 '"${cond}"' { count++; c = count } 542 END { print count } 543 ' "$@" 544 } 545 546 # center-align lines of text, using the current screen width 547 center() { 548 local command='awk' 549 if [ -e /usr/bin/gawk ]; then 550 command='gawk' 551 fi 552 553 ${command} -v width="$(tput cols)" ' 554 { 555 gsub(/\r$/, "") 556 lines[NR] = $0 557 s = $0 558 gsub(/\x1b\[[0-9;]*[A-Za-z]/, "", s) # ANSI style-changers 559 l = length(s) 560 if (maxlen < l) maxlen = l 561 } 562 563 END { 564 n = (width - maxlen) / 2 565 if (n % 1) n = n - (n % 1) 566 fmt = sprintf("%%%ds%%s\n", (n > 0) ? n : 0) 567 for (i = 1; i <= NR; i++) printf fmt, "", lines[i] 568 } 569 ' "$@" 570 } 571 572 # Color file-EXTensions, or any substring which looks like one 573 cext() { 574 local command='awk' 575 if [ -p 1 ] || [ -t 1 ]; then 576 command='stdbuf -oL awk' 577 fi 578 579 ${command} ' 580 BEGIN { 581 palette[n++] = "\x1b[38;2;0;95;215m" # blue 582 palette[n++] = "\x1b[38;2;215;95;0m" # orange 583 palette[n++] = "\x1b[38;2;135;95;255m" # purple 584 palette[n++] = "\x1b[38;2;0;175;215m" # cyan 585 palette[n++] = "\x1b[38;2;255;135;255m" # pink 586 palette[n++] = "\x1b[38;2;0;135;95m" # green 587 palette[n++] = "\x1b[38;2;204;0;0m" # red 588 palette[n++] = "\x1b[38;2;168;168;168m" # gray 589 palcount = length(palette) 590 n = 0 591 } 592 593 { 594 # ignore cursor-movers and style-changers 595 # gsub(/\x1b\[[0-9;]*[A-Za-z]/, "") 596 597 rest = $0 598 599 while (match(rest, /\.[A-Za-z][A-Za-z0-9_-]*/)) { 600 printf "%s", substr(rest, 1, RSTART - 1) 601 602 ext = substr(rest, RSTART, RLENGTH) 603 rest = substr(rest, RSTART + RLENGTH) 604 605 style = ext2style[ext] 606 if (style == "") { 607 style = palette[n % palcount] 608 ext2style[ext] = style 609 n++ 610 } 611 612 printf "%s%s\x1b[0m", style, ext 613 } 614 615 print rest 616 } 617 ' "$@" 618 } 619 620 # Colored Go Test on the folder given; uses my command `jawk` 621 cgt() { go test "${1:-.}" 2>&1 | jawk '/^ok/' '/^[-]* ?FAIL/' '/^\?/'; } 622 623 # Compile Rust Optimized 624 cro() { 625 rustc -C lto=true -C codegen-units=1 -C debuginfo=0 -C strip=symbols \ 626 -C opt-level=3 "$@" 627 } 628 629 # emit a line with a repeating cross-like symbol in it 630 crosses() { [ "${1:-80}" -gt 0 ] && printf "%${1:-80}s\n" "" | sed 's- -×-g'; } 631 632 # listen to streaming DANCE music 633 dance() { 634 printf "streaming \e[7mDance Wave Retro\e[0m\n" 635 mpv --really-quiet https://retro.dancewave.online/retrodance.mp3 636 } 637 638 # emit a line with a repeating dash-like symbol in it 639 dashes() { [ "${1:-80}" -gt 0 ] && printf "%${1:-80}s\n" "" | sed 's- -—-g'; } 640 641 # Dim (lines) with AWK 642 dawk() { 643 local command='awk' 644 if [ -p 1 ] || [ -t 1 ]; then 645 command='stdbuf -oL awk' 646 fi 647 648 local cond="${1:-1}" 649 [ $# -gt 0 ] && shift 650 651 ${command} ' 652 { low = lower = tolower($0) } 653 '"${cond}"' { 654 gsub(/\x1b\[0m/, "\x1b[0m\x1b[38;2;168;168;168m") 655 printf "\x1b[38;2;168;168;168m%s\x1b[0m\n", $0 656 next 657 } 658 1 659 ' "$@" 660 } 661 662 # remove indentations from lines 663 dedent() { 664 awk ' 665 { lines[NR] = $0 } 666 { if (match($0, /^ +/) && (n == 0 || n > RLENGTH)) n = RLENGTH } 667 END { for (i = 1; i <= NR; i++) print substr(lines[i], n + 1) } 668 ' "$@" 669 } 670 671 dehtmlify() { 672 local command='awk' 673 if [ -p 1 ] || [ -t 1 ]; then 674 command='stdbuf -oL awk' 675 fi 676 677 ${command} ' 678 { 679 gsub(/<\/?[^>]+>/, "") 680 gsub(/&/, "&") 681 gsub(/</, "<") 682 gsub(/>/, ">") 683 gsub(/^ +| *\r?$/, "") 684 gsub(/ +/, " ") 685 print 686 } 687 ' "$@" 688 } 689 690 # expand tabs each into up to the number of space given, or 4 by default 691 detab() { 692 local tabstop="${1:-4}" 693 [ $# -gt 0 ] && shift 694 if [ -p 1 ] || [ -t 1 ]; then 695 stdbuf -oL expand -t "${tabstop}" "$@" 696 else 697 expand -t "${tabstop}" "$@" 698 fi 699 } 700 701 # DICtionary-define the word given locally 702 dic() { 703 local arg 704 local gap=0 705 local options='-MKiCRS' 706 707 if [ $# -eq 0 ]; then 708 printf "\e[38;2;204;0;0mdic: no words given\e[0m\n" >&2 709 return 1 710 fi 711 712 if [ $# -eq 1 ]; then 713 options='--header=1 -MKiCRS' 714 fi 715 716 for arg in "$@"; do 717 [ "${gap}" -gt 0 ] && printf "\n" 718 gap=1 719 printf "\e[7m%-80s\e[0m\n" "${arg}" 720 dict "${arg}" 2>&1 | awk ' 721 NR == 1 && /^No definitions found for / { err = 1 } 722 err { printf "\x1b[38;2;204;0;0m%s\x1b[0m\n", $0; next } 723 1 724 ' 725 done | less "${options}" 726 } 727 728 # DIVide 2 numbers 3 ways, including the complement 729 div() { 730 awk -v a="${1:-1}" -v b="${2:-1}" ' 731 BEGIN { 732 gsub(/_/, "", a) 733 gsub(/_/, "", b) 734 if (a > b) { c = a; a = b; b = c } 735 c = 1 - a / b 736 if (0 <= c && c <= 1) printf "%f\n%f\n%f\n", a / b, b / a, c 737 else printf "%f\n%f\n", a / b, b / a 738 exit 739 }' 740 } 741 742 # get/fetch data from the filename or URI given; named `dog` because dogs can 743 # `fetch` things for you 744 dog() { 745 if [ $# -gt 1 ]; then 746 printf "\e[31mdogs only have 1 mouth to fetch with\e[0m\n" >&2 747 return 1 748 fi 749 750 if [ -e "$1" ]; then 751 if [ -p 1 ] || [ -t 1 ]; then stdbuf -oL cat "$1"; else cat "$1"; fi 752 return $? 753 fi 754 755 case "${1:--}" in 756 -) if [ -p 1 ] || [ -t 1 ]; then stdbuf -oL cat -; else cat -; fi;; 757 file://*|https://*|http://*) curl --show-error -s "$1";; 758 ftp://*|ftps://*|sftp://*) curl --show-error -s "$1";; 759 dict://*) curl --show-error -s "$1";; 760 *) curl --show-error -s "https://$1";; 761 esac 2> /dev/null || { 762 printf "\e[31mcan't fetch %s\e[0m\n" "${1:--}" >&2 763 return 1 764 } 765 } 766 767 # emit a line with a repeating dot-like symbol in it 768 dots() { [ "${1:-80}" -gt 0 ] && printf "%${1:-80}s\n" "" | sed 's- -·-g'; } 769 770 # show the current Date and Time 771 dt() { 772 printf "\e[38;2;78;154;6m%s\e[0m \e[38;2;52;101;164m%s\e[0m\n" \ 773 "$(date +'%a %b %d')" "$(date +%T)" 774 } 775 776 # show the current Date, Time, and a Calendar with the 3 `current` months 777 dtc() { 778 { 779 # show the current date/time center-aligned 780 printf "%20s\e[38;2;78;154;6m%s\e[0m \e[38;2;52;101;164m%s\e[0m\n\n" \ 781 "" "$(date +'%a %b %d')" "$(date +%T)" 782 # debian linux has a different `cal` app which highlights the day 783 if [ -e "/usr/bin/ncal" ]; then 784 # fix debian/ncal's weird way to highlight the current day 785 ncal -C -3 | sed -E 's/_\x08(.)/\x1b[7m\1\x1b[0m/g' 786 else 787 cal -3 788 fi 789 } | less -MKiCRS 790 } 791 792 # EDit RUN shell commands, using an interactive editor; uses my tool `leak` 793 edrun() { 794 # dash doesn't support the process-sub syntax 795 # . <( micro -readonly true -filetype shell | leak --inv ) 796 micro -readonly true -filetype shell | leak --inv | . /dev/fd/0 797 } 798 799 # convert EURos into CAnadian Dollars, using the latest official exchange 800 # rates from the bank of canada; during weekends, the latest rate may be 801 # from a few days ago; the default amount of euros to convert is 1, when 802 # not given 803 eur2cad() { 804 local site='https://www.bankofcanada.ca/valet/observations/group' 805 local csv_rates="${site}/FX_RATES_DAILY/csv" 806 local url="${csv_rates}?start_date=$(date -d '3 days ago' +'%Y-%m-%d')" 807 curl -s "${url}" | awk -F, -v amount="$(echo "${1:-1}" | sed 's-_--g')" ' 808 /EUR/ { for (i = 1; i <= NF; i++) if($i ~ /EUR/) j = i } 809 END { gsub(/"/, "", $j); if (j != 0) printf "%.2f\n", amount * $j } 810 ' 811 } 812 813 # fetch/web-request all URIs given, using protcol HTTPS when none is given 814 fetch() { 815 local arg 816 for arg in "$@"; do 817 case "${arg}" in 818 file://*|https://*|http://*|ftp://*|ftps://*|sftp://*|dict://*) 819 curl --silent --show-error "${arg}";; 820 *) 821 curl --silent --show-error "https://${arg}";; 822 esac 823 done 824 } 825 826 # get the first n lines, or 1 by default 827 first() { head -n "${1:-1}" "${2:--}"; } 828 829 # Field-Names AWK remembers field-positions by name, from the first input line 830 fnawk() { 831 local code="${1:-1}" 832 [ $# -gt 0 ] && shift 833 834 local buffering='' 835 if [ -p 1 ] || [ -t 1 ]; then 836 buffering='stdbuf -oL' 837 fi 838 839 ${buffering} awk -v OFS="\t" ' 840 NR == 1 { 841 FS = ($0 ~ /\t/) ? "\t" : " " 842 $0 = $0 843 for (i = 1; i <= NF; i++) names[$i] = i 844 i = "" 845 } 846 { low = lower = tolower($0) } 847 '"${code}"' 848 ' "$@" 849 } 850 851 # start from the line number given, skipping all previous ones 852 fromline() { tail -n +"${1:-1}" "${2:--}"; } 853 854 # convert FeeT into meters 855 ft() { 856 echo "${@:-1}" | sed -E 's-_--g; s- +-\n-g' | 857 awk '/./ { printf "%.2f\n", 0.3048 * $0 }' 858 } 859 860 # convert FeeT² (squared) into meters² 861 ft2() { 862 echo "${@:-1}" | sed -E 's-_--g; s- +-\n-g' | 863 awk '/./ { printf "%.2f\n", 0.09290304 * $0 }' 864 } 865 866 # convert a mix of FeeT and INches into meters 867 ftin() { 868 local ft="${1:-0}" 869 ft="$(echo "${ft}" | sed 's-_--g')" 870 local in="${2:-0}" 871 in="$(echo "${in}" | sed 's-_--g')" 872 awk "BEGIN { print 0.3048 * ${ft} + 0.0254 * ${in}; exit }" 873 } 874 875 # convert GALlons into liters 876 gal() { 877 echo "${@:-1}" | sed -E 's-_--g; s- +-\n-g' | 878 awk '/./ { printf "%.2f\n", 3.785411784 * $0 }' 879 } 880 881 # convert binary GigaBytes into bytes 882 gb() { 883 echo "${@:-1}" | sed -E 's-_--g; s- +-\n-g' | 884 awk '/./ { printf "%.4f\n", 1073741824 * $0 }' | sed 's-\.00*$--' 885 } 886 887 # Gawk Bignum Print 888 gbp() { gawk --bignum "BEGIN { print $1; exit }"; } 889 890 # glue/stick together various lines, only emitting a line-feed at the end; an 891 # optional argument is the output-item-separator, which is empty by default 892 glue() { 893 local sep="${1:-}" 894 [ $# -gt 0 ] && shift 895 awk -v sep="${sep}" ' 896 NR > 1 { printf "%s", sep } 897 { gsub(/\r/, ""); printf "%s", $0 } 898 END { if (NR > 0) print "" } 899 ' "$@" 900 } 901 902 # GO Build Stripped: a common use-case for the go compiler 903 gobs() { go build -ldflags "-s -w" -trimpath "$@"; } 904 905 # GO DEPendencieS: show all dependencies in a go project 906 godeps() { go list -f '{{ join .Deps "\n" }}' "$@"; } 907 908 # GO IMPortS: show all imports in a go project 909 goimps() { go list -f '{{ join .Imports "\n" }}' "$@"; } 910 911 # go to the folder picked using an interactive TUI; uses my tool `bf` 912 goto() { 913 local where 914 where="$(bf "${1:-.}")" 915 if [ $? -ne 0 ]; then 916 return 0 917 fi 918 919 where="$(realpath "${where}")" 920 if [ ! -d "${where}" ]; then 921 where="$(dirname "${where}")" 922 fi 923 cd "${where}" || return 924 } 925 926 # GRoup via AWK groups lines using common results of the AWK expression given 927 grawk() { 928 local code="${1:-\$0}" 929 [ $# -gt 0 ] && shift 930 931 local command='awk' 932 if [ -p 1 ] || [ -t 1 ]; then 933 command='stdbuf -oL awk' 934 fi 935 936 ${command} ' 937 { low = lower = tolower($0) } 938 939 { 940 k = '"${code}"' 941 if (!(k in groups)) ordkeys[++oklen] = k 942 groups[k][length(groups[k]) + 1] = $0 943 } 944 945 END { 946 for (i = 1; i <= oklen; i++) { 947 k = ordkeys[i] 948 n = length(groups[k]) 949 for (j = 1; j <= n; j++) print groups[k][j] 950 } 951 } 952 ' "$@" 953 } 954 955 # Global extended regex SUBstitute, using the AWK function of the same name: 956 # arguments are used as regex/replacement pairs, in that order 957 gsub() { 958 local command='awk' 959 if [ -p 1 ] || [ -t 1 ]; then 960 command='stdbuf -oL awk' 961 fi 962 963 ${command} ' 964 BEGIN { 965 for (i = 1; i < ARGC; i++) { 966 args[++n] = ARGV[i] 967 delete ARGV[i] 968 } 969 } 970 971 { 972 for (i = 1; i <= n; i += 2) gsub(args[i], args[i + 1]) 973 print 974 } 975 ' "$@" 976 } 977 978 # show Help laid out on 2 side-by-side columns; uses my tool `bsbs` 979 h2() { naman "$@" | bsbs 2; } 980 981 # Highlight (lines) with AWK 982 hawk() { 983 local command='awk' 984 if [ -p 1 ] || [ -t 1 ]; then 985 command='stdbuf -oL awk' 986 fi 987 988 local cond="${1:-1}" 989 [ $# -gt 0 ] && shift 990 991 ${command} ' 992 { low = lower = tolower($0) } 993 '"${cond}"' { 994 gsub(/\x1b\[0m/, "\x1b[0m\x1b[7m") 995 printf "\x1b[7m%s\x1b[0m\n", $0 996 next 997 } 998 1 999 ' "$@" 1000 } 1001 1002 # play a heartbeat-like sound lasting the number of seconds given, or for 1 1003 # second by default; uses my tool `sboard` 1004 heartbeat() { sboard heartbeat "${1:-1}" "${2:-1}"; } 1005 1006 # Highlighted-style ECHO 1007 hecho() { printf "\e[7m%s\e[0m\n" "$*"; } 1008 1009 # show each byte as a pair of HEXadecimal (base-16) symbols 1010 hexify() { 1011 cat "$@" | od -x -A n | 1012 awk '{ gsub(/ +/, ""); printf "%s", $0 } END { printf "\n" }' 1013 } 1014 1015 # Help Me Remember my custom shell commands 1016 hmr() { 1017 local cmd="bat" 1018 # debian linux uses a different name for the `bat` app 1019 if [ -e "/usr/bin/batcat" ]; then 1020 cmd="batcat" 1021 fi 1022 1023 "$cmd" \ 1024 --style=plain,header,numbers --theme='Monokai Extended Light' \ 1025 --wrap=never --color=always "$(which clam)" | 1026 sed -e 's-\x1b\[38;5;70m-\x1b[38;5;28m-g' \ 1027 -e 's-\x1b\[38;5;214m-\x1b[38;5;208m-g' \ 1028 -e 's-\x1b\[38;5;243m-\x1b[38;5;103m-g' \ 1029 -e 's-\x1b\[38;5;238m-\x1b[38;5;245m-g' \ 1030 -e 's-\x1b\[38;5;228m-\x1b[48;5;228m-g' | 1031 less -MKiCRS 1032 } 1033 1034 # convert seconds into a colon-separated Hours-Minutes-Seconds triple 1035 hms() { 1036 echo "${@:-0}" | sed -E 's-_--g; s- +-\n-g' | awk '/./ { 1037 x = $0 1038 h = (x - x % 3600) / 3600 1039 m = (x % 3600) / 60 1040 s = x % 60 1041 printf "%02d:%02d:%05.2f\n", h, m, s 1042 }' 1043 } 1044 1045 # find all hyperlinks inside HREF attributes in the input text 1046 href() { 1047 local arg 1048 for arg in "${@:--}"; do 1049 grep --line-buffered -E -o 'href="[^"]+"' "${arg}" 1050 done | sed -u 's-^href="--; s-"$--' 1051 } 1052 1053 # avoid/ignore lines which case-insensitively match any of the regexes given 1054 iavoid() { 1055 local command='awk' 1056 if [ -p 1 ] || [ -t 1 ]; then 1057 command='stdbuf -oL awk' 1058 fi 1059 1060 ${command} ' 1061 BEGIN { 1062 if (IGNORECASE == "") { 1063 m = "this variant of AWK lacks case-insensitive regex-matching" 1064 printf("\x1b[38;2;204;0;0m%s\x1b[0m\n", m) > "/dev/stderr" 1065 exit 125 1066 } 1067 IGNORECASE = 1 1068 1069 for (i = 1; i < ARGC; i++) { 1070 e[i] = ARGV[i] 1071 delete ARGV[i] 1072 } 1073 } 1074 1075 { 1076 for (i = 1; i < ARGC; i++) if ($0 ~ e[i]) next 1077 print 1078 got++ 1079 } 1080 1081 END { exit(got == 0) } 1082 ' "${@:-^\r?$}" 1083 } 1084 1085 # ignore command in a pipe: this allows quick re-editing of pipes, while 1086 # still leaving signs of previously-used steps, as a memo 1087 idem() { cat; } 1088 1089 # ignore command in a pipe: this allows quick re-editing of pipes, while 1090 # still leaving signs of previously-used steps, as a memo 1091 ignore() { cat; } 1092 1093 # only keep lines which case-insensitively match any of the regexes given 1094 imatch() { 1095 local command='awk' 1096 if [ -p 1 ] || [ -t 1 ]; then 1097 command='stdbuf -oL awk' 1098 fi 1099 1100 ${command} ' 1101 BEGIN { 1102 if (IGNORECASE == "") { 1103 m = "this variant of AWK lacks case-insensitive regex-matching" 1104 printf("\x1b[38;2;204;0;0m%s\x1b[0m\n", m) > "/dev/stderr" 1105 exit 125 1106 } 1107 IGNORECASE = 1 1108 1109 for (i = 1; i < ARGC; i++) { 1110 e[i] = ARGV[i] 1111 delete ARGV[i] 1112 } 1113 } 1114 1115 { 1116 for (i = 1; i < ARGC; i++) { 1117 if ($0 ~ e[i]) { 1118 print 1119 got++ 1120 next 1121 } 1122 } 1123 } 1124 1125 END { exit(got == 0) } 1126 ' "${@:-[^\r]}" 1127 } 1128 1129 # start each non-empty line with extra n spaces 1130 indent() { 1131 local command='awk' 1132 if [ -p 1 ] || [ -t 1 ]; then 1133 command='stdbuf -oL awk' 1134 fi 1135 1136 ${command} ' 1137 BEGIN { 1138 n = ARGV[1] + 0 1139 delete ARGV[1] 1140 fmt = sprintf("%%%ds%%s\n", (n > 0) ? n : 0) 1141 } 1142 1143 /^\r?$/ { print ""; next } 1144 { gsub(/\r$/, ""); printf(fmt, "", $0) } 1145 ' "$@" 1146 } 1147 1148 # emit each word-like item from each input line on its own line; when a file 1149 # has tabs on its first line, items are split using tabs alone, which allows 1150 # items to have spaces in them 1151 items() { 1152 local command='awk' 1153 if [ -p 1 ] || [ -t 1 ]; then 1154 command='stdbuf -oL awk' 1155 fi 1156 1157 ${command} ' 1158 FNR == 1 { FS = ($0 ~ /\t/) ? "\t" : " "; $0 = $0 } 1159 { gsub(/\r$/, ""); for (i = 1; i <= NF; i++) print $i } 1160 ' "$@" 1161 } 1162 1163 # Judge with AWK colors lines using up to 3 (optional) AWK conditions, namely 1164 # `good` (green), `bad` (red), and `meh` (gray) 1165 jawk() { 1166 local code="${1:-\$0}" 1167 [ $# -gt 0 ] && shift 1168 1169 local command='awk' 1170 if [ -p 1 ] || [ -t 1 ]; then 1171 command='stdbuf -oL awk' 1172 fi 1173 1174 local good="${1:-0}" 1175 local bad="${2:-0}" 1176 local meh="${3:-0}" 1177 1178 [ $# -gt 0 ] && shift 1179 [ $# -gt 0 ] && shift 1180 [ $# -gt 0 ] && shift 1181 1182 ${command} ' 1183 BEGIN { 1184 # normal good-style is green, colorblind-friendly good-style is blue 1185 good_style = ENVIRON["COLORBLIND"] != 0 ? 1186 "\x1b[38;2;0;95;215m" : 1187 "\x1b[38;2;0;135;95m" 1188 good_fmt = good_style "%s\x1b[0m\n" 1189 good_reset = "\x1b[0m" good_style 1190 } 1191 1192 { low = lower = tolower($0) } 1193 1194 '"${good}"' { 1195 gsub(/\x1b\[0m/, good_reset) 1196 printf good_fmt, $0 1197 next 1198 } 1199 1200 '"${bad}"' { 1201 gsub(/\x1b\[0m/, "\x1b[0m\x1b[38;2;204;0;0m") 1202 printf "\x1b[38;2;204;0;0m%s\x1b[0m\n", $0 1203 next 1204 } 1205 1206 '"${meh}"' { 1207 gsub(/\x1b\[0m/, "\x1b[0m\x1b[38;2;168;168;168m") 1208 printf "\x1b[38;2;168;168;168m%s\x1b[0m\n", $0 1209 next 1210 } 1211 1212 1 1213 ' "$@" 1214 } 1215 1216 # listen to streaming JAZZ music 1217 jazz() { 1218 printf "streaming \e[7mSmooth Jazz Instrumental\e[0m\n" 1219 mpv --quiet https://stream.zeno.fm/00rt0rdm7k8uv 1220 } 1221 1222 # show a `dad` JOKE from the web, sometimes even a very funny one 1223 joke() { 1224 curl --silent --show-error https://icanhazdadjoke.com | fold -s | 1225 awk '{ gsub(/ *\r?$/, ""); print }' 1226 } 1227 1228 # JSON Query Keys runs `jq` to find all unique key-combos from tabular JSON 1229 jqkeys() { jq -c -M '.[] | keys' "${1:--}" | awk '!c[$0]++'; } 1230 1231 # JSON Keys finds all unique key-combos from tabular JSON data; uses my tools 1232 # `jsonl` and `zj` 1233 # jsonk() { cat "${1:--}" | zj . .keys | jsonl | awk '!c[$0]++'; } 1234 1235 # JSON Keys finds all unique key-combos from tabular JSON data; uses my tools 1236 # `jsonl` and `tjp` 1237 jsonk() { 1238 tjp '[e.keys() for e in v] if isinstance(v, (list, tuple)) else v.keys()' \ 1239 "${1:--}" | jsonl | awk '!c[$0]++' 1240 } 1241 1242 # JSON Table, turns TSV tables into tabular JSON, where valid-JSON values are 1243 # auto-parsed into numbers, booleans, etc...; uses my tools `jsons` and `tjp` 1244 jsont() { 1245 jsons "$@" | tjp \ 1246 '[{k: rescue(lambda: loads(v), v) for k, v in e.items()} for e in v]' 1247 } 1248 1249 # emit the given number of random/junk bytes, or 1024 junk bytes by default 1250 junk() { head -c "$(echo "${1:-1024}" | sed 's-_--g')" /dev/urandom; } 1251 1252 # convert binary KiloBytes into bytes 1253 kb() { 1254 echo "${@:-1}" | sed -E 's-_--g; s- +-\n-g' | 1255 awk '/./ { printf "%.2f\n", 1024 * $0 }' | sed 's-\.00*$--' 1256 } 1257 1258 # play a stereotypical once-a-second laser sound for the number of seconds 1259 # given, or for 1 second (once) by default; uses my tool `sboard` 1260 laser() { sboard laser "${1:-1}" "${2:-1}"; } 1261 1262 # get the last n lines, or 1 by default 1263 last() { tail -n "${1:-1}" "${2:--}"; } 1264 1265 # convert pounds (LB) into kilograms 1266 lb() { 1267 echo "${@:-1}" | sed -E 's-_--g; s- +-\n-g' | 1268 awk '/./ { printf "%.2f\n", 0.45359237 * $0 }' 1269 } 1270 1271 # convert a mix of pounds (LB) and weight-ounces (OZ) into kilograms 1272 lboz() { 1273 local lb="${1:-0}" 1274 lb="$(echo "${lb}" | sed 's-_--g')" 1275 local oz="${2:-0}" 1276 oz="$(echo "${oz}" | sed 's-_--g')" 1277 awk "BEGIN { print 0.45359237 * ${lb} + 0.028349523 * ${oz}; exit }" 1278 } 1279 1280 # limit stops at the first n bytes, or 1024 bytes by default 1281 limit() { head -c "$(echo "${1:-1024}" | sed 's-_--g')" "${2:--}"; } 1282 1283 # ensure LINES are never accidentally joined across files, by always emitting 1284 # a line-feed at the end of each line 1285 lines() { 1286 if [ -p 1 ] || [ -t 1 ]; then 1287 stdbuf -oL awk 1 "$@" 1288 else 1289 awk 1 "$@" 1290 fi 1291 } 1292 1293 # regroup adjacent lines into n-item tab-separated lines 1294 lineup() { 1295 local command='awk' 1296 if [ -p 1 ] || [ -t 1 ]; then 1297 command='stdbuf -oL awk' 1298 fi 1299 1300 local n="${1:-0}" 1301 [ $# -gt 0 ] && shift 1302 1303 if [ "$n" -le 0 ]; then 1304 ${command} ' 1305 NR > 1 { printf "\t" } 1306 { printf "%s", $0 } 1307 END { if (NR > 0) print "" } 1308 ' "$@" 1309 return $? 1310 fi 1311 1312 ${command} -v n="$n" ' 1313 NR % n != 1 && n > 1 { printf "\t" } 1314 { printf "%s", $0 } 1315 NR % n == 0 { print "" } 1316 END { if (NR % n != 0) print "" } 1317 ' "$@" 1318 } 1319 1320 # LiSt MAN pages 1321 lsman() { man -k "${1:-.}"; } 1322 1323 # only keep lines which match any of the regexes given 1324 match() { 1325 local command='awk' 1326 if [ -p 1 ] || [ -t 1 ]; then 1327 command='stdbuf -oL awk' 1328 fi 1329 1330 ${command} ' 1331 BEGIN { for (i = 1; i < ARGC; i++) { re[i] = ARGV[i]; delete ARGV[i] } } 1332 { for (i in re) if ($0 ~ re[i]) { print; got++; next } } 1333 END { exit(got == 0) } 1334 ' "${@:-.}" 1335 } 1336 1337 # convert binary MegaBytes into bytes 1338 mb() { 1339 echo "${@:-1}" | sed -E 's-_--g; s- +-\n-g' | 1340 awk '/./ { printf "%.2f\n", 1048576 * $0 }' | sed 's-\.00*$--' 1341 } 1342 1343 # Multi-Core MAKE runs `make` using all cores 1344 mcmake() { make -j "$(nproc)" "$@"; } 1345 1346 # merge stderr into stdout, which is useful for piped commands 1347 merrge() { "${@:-cat /dev/null}" 2>&1; } 1348 1349 metajq() { 1350 # https://github.com/stedolan/jq/issues/243#issuecomment-48470943 1351 jq -r -M ' 1352 [ 1353 path(..) | 1354 map(if type == "number" then "[]" else tostring end) | 1355 join(".") | split(".[]") | join("[]") 1356 ] | unique | map("." + .) | .[] 1357 ' "$@" 1358 } 1359 1360 # convert MIles into kilometers 1361 mi() { 1362 echo "${@:-1}" | sed -E 's-_--g; s- +-\n-g' | 1363 awk '/./ { printf "%.2f\n", 1.609344 * $0 }' 1364 } 1365 1366 # convert MIles² (squared) into kilometers² 1367 mi2() { 1368 echo "${@:-1}" | sed -E 's-_--g; s- +-\n-g' | 1369 awk '/./ { printf "%.2f\n", 2.5899881103360 * $0 }' 1370 } 1371 1372 # Make In Folder 1373 mif() { 1374 local folder 1375 folder="${1:-.}" 1376 [ $# -gt 0 ] && shift 1377 env -C "${folder}" make "$@" 1378 } 1379 1380 # MINimize DECimalS ignores all trailing decimal zeros in numbers, and even 1381 # the decimal dots themselves, when decimals in a number are all zeros 1382 # mindecs() { 1383 # sed -u -E 's-([0-9]+)\.0+\W-\1-g; s-([0-9]+\.[0-9]*[1-9])0+\W-\1-g' 1384 # } 1385 1386 # convert Miles Per Hour into kilometers per hour 1387 mph() { 1388 echo "${@:-1}" | sed -E 's-_--g; s- +-\n-g' | 1389 awk '/./ { printf "%.2f\n", 1.609344 * $0 }' 1390 } 1391 1392 # Number all lines counting from 0, using a tab right after each line number 1393 n0() { 1394 if [ -p 1 ] || [ -t 1 ]; then 1395 stdbuf -oL nl -b a -w 1 -v 0 "$@" 1396 else 1397 nl -b a -w 1 -v 0 "$@" 1398 fi 1399 } 1400 1401 # Number all lines counting from 1, using a tab right after each line number 1402 n1() { 1403 if [ -p 1 ] || [ -t 1 ]; then 1404 stdbuf -oL nl -b a -w 1 -v 1 "$@" 1405 else 1406 nl -b a -w 1 -v 1 "$@" 1407 fi 1408 } 1409 1410 # NArrow MANual, keeps `man` narrow, even if the window/tab is wide when run 1411 naman() { 1412 local w 1413 w="$(tput cols)" 1414 w="$((w / 2 - 4))" 1415 if [ "$w" -lt 80 ]; then 1416 w=80 1417 fi 1418 MANWIDTH="$w" man "$@" 1419 } 1420 1421 # Not AND sorts its 2 inputs, then finds lines not in common 1422 nand() { 1423 # comm -3 <(sort "$1") <(sort "$2") 1424 # dash doesn't support the process-sub syntax 1425 (sort "$1" | (sort "$2" | (comm -3 /dev/fd/3 /dev/fd/4) 4<&0) 3<&0) 1426 } 1427 1428 # Nice DEFine dictionary-defines the words given, using an online service 1429 ndef() { 1430 local arg 1431 local gap=0 1432 local options='-MKiCRS' 1433 1434 if [ $# -eq 0 ]; then 1435 printf "\e[38;2;204;0;0mndef: no words given\e[0m\n" >&2 1436 return 1 1437 fi 1438 1439 if [ $# -eq 1 ]; then 1440 options='--header=1 -MKiCRS' 1441 fi 1442 1443 for arg in "$@"; do 1444 [ "${gap}" -gt 0 ] && printf "\n" 1445 gap=1 1446 printf "\e[7m%-80s\e[0m\n" "${arg}" 1447 curl --silent "dict://dict.org/d:${arg}" | awk ' 1448 { gsub(/\r$/, "") } 1449 /^151 / { 1450 printf "\x1b[38;2;52;101;164m%s\x1b[0m\n", $0 1451 next 1452 } 1453 /^[1-9][0-9]{2} / { 1454 printf "\x1b[38;2;128;128;128m%s\x1b[0m\n", $0 1455 next 1456 } 1457 1 1458 ' 1459 done | less "${options}" 1460 } 1461 1462 # listen to streaming NEW WAVE music 1463 newwave() { 1464 printf "streaming \e[7mNew Wave radio\e[0m\n" 1465 mpv --quiet https://puma.streemlion.com:2910/stream 1466 } 1467 1468 # convert Nautical MIles into kilometers 1469 nmi() { 1470 echo "${@:-1}" | sed -E 's-_--g; s- +-\n-g' | 1471 awk '/./ { printf "%.2f\n", 1.852 * $0 }' 1472 } 1473 1474 # play a white-noise sound lasting the number of seconds given, or for 1 1475 # second by default; uses my tool `sboard` 1476 noise() { sboard noise "${1:-1}" "${2:-1}"; } 1477 1478 # show the current date and time 1479 now() { date +'%Y-%m-%d %H:%M:%S'; } 1480 1481 # Nice Print Python result; uses my tool `nn` 1482 npp() { 1483 local arg 1484 for arg in "$@"; do 1485 python -c "print(${arg})" 1486 done | nn --gray 1487 } 1488 1489 # Nice Size, using my tool `nn` 1490 ns() { wc -c "$@" | nn --gray; } 1491 1492 # Nice Systemctl Status 1493 nss() { 1494 systemctl status "$@" 2>&1 | sed -E \ 1495 -e 's-\x1b\[[^A-Za-z][A-Za-z]--g' \ 1496 -e 's-(^[^ ] )([^ ]+\.service)-\1\x1b[7m\2\x1b[0m-' \ 1497 -e 's- (enabled)- \x1b[38;2;0;135;95m\x1b[7m\1\x1b[0m-g' \ 1498 -e 's- (disabled)- \x1b[38;2;215;95;0m\x1b[7m\1\x1b[0m-g' \ 1499 -e 's- (active \(running\))- \x1b[38;2;0;135;95m\x1b[7m\1\x1b[0m-g' \ 1500 -e 's- (inactive \(dead\))- \x1b[38;2;204;0;0m\x1b[7m\1\x1b[0m-g' \ 1501 -e 's-^(Unit .* could not .*)$-\x1b[38;2;204;0;0m\x1b[7m\1\x1b[0m\n-' \ 1502 -e 's-(\[WARN\].*)$-\x1b[38;2;215;95;0m\x1b[7m\1\x1b[0m\n-' \ 1503 -e 's-(\[ERR\].*)$-\x1b[38;2;204;0;0m\x1b[7m\1\x1b[0m\n-' | 1504 if [ "${COLORBLIND}" = 1 ]; then 1505 # color-blind-friendly version using blue instead of green 1506 sed 's-\x1b\[38;2;0;135;95m-\x1b[38;2;0;95;215m-g' 1507 else 1508 # leave green colors untouched 1509 cat 1510 fi | less -MKiCRS 1511 } 1512 1513 # Nice TimeStamp 1514 nts() { 1515 ts '%Y-%m-%d %H:%M:%S' | sed -u \ 1516 's-^-\x1b[48;2;218;218;218m\x1b[38;2;0;95;153m-; s- -\x1b[0m\t-2' 1517 } 1518 1519 # emit nothing to output and/or discard everything from input 1520 null() { 1521 if [ $# -gt 0 ]; then 1522 "$@" > /dev/null 1523 else 1524 cat < /dev/null 1525 fi 1526 } 1527 1528 # Nice Weather Forecast gets weather forecasts, using ANSI styles and almost 1529 # filling the terminal's current width 1530 nwf() { 1531 local gap=0 1532 local width="$(($(tput cols) - 2))" 1533 local place 1534 1535 for place in "$@"; do 1536 [ "${gap}" -gt 0 ] && printf "\n" 1537 gap=1 1538 1539 printf "\e[7m%-${width}s\e[0m\n" "${place}" 1540 1541 printf "%s~%s\r\n\r\n" "${place}" "${width}" | 1542 curl --silent --show-error telnet://graph.no:79 | 1543 sed -u -E \ 1544 -e 's/ *\r?$//' \ 1545 -e '/^\[/d' \ 1546 -e 's/^ *-= *([^=]+) +=- *$/\1\n/' \ 1547 -e 's/-/\x1b[38;2;196;160;0m●\x1b[0m/g' \ 1548 -e 's/^( +)\x1b\[38;2;196;160;0m●\x1b\[0m/\1-/g' \ 1549 -e 's/\|/\x1b[38;2;52;101;164m█\x1b[0m/g' \ 1550 -e 's/#/\x1b[38;2;218;218;218m█\x1b[0m/g' \ 1551 -e 's/([=\^][=\^]*)/\x1b[38;2;164;164;164m\1\x1b[0m/g' \ 1552 -e 's/\*/○/g' \ 1553 -e 's/_/\x1b[48;2;216;200;0m_\x1b[0m/g' \ 1554 -e 's/([0-9][0-9]\/[0-9][0-9])/\x1b[7m\1\x1b[0m/g' | awk 1 1555 done | less -MKiCRS 1556 } 1557 1558 # Print AWK expression for each input line 1559 pawk() { 1560 local code="${1:-\$0}" 1561 [ $# -gt 0 ] && shift 1562 if [ -p 1 ] || [ -t 1 ]; then 1563 stdbuf -oL awk "{ print ${code} }" "$@" 1564 else 1565 awk "{ print ${code} }" "$@" 1566 fi 1567 } 1568 1569 # play audio/video media 1570 play() { mpv "${@:--}"; } 1571 1572 # Print Python result 1573 pp() { 1574 local arg 1575 for arg in "$@"; do 1576 python -c "print(${arg})" 1577 done 1578 } 1579 1580 # PRecede (input) ECHO, prepends a first line to stdin lines 1581 precho() { echo "$@" && cat /dev/stdin; } 1582 1583 # LABEL/precede data with an ANSI-styled line 1584 prelabel() { printf "\e[7m%-*s\e[0m\n" "$(($(tput cols) - 2))" "$*"; cat -; } 1585 1586 # PREcede (input) MEMO, prepends a first highlighted line to stdin lines 1587 prememo() { printf "\e[7m%s\e[0m\n" "$*"; cat -; } 1588 1589 # start by joining all arguments given as a tab-separated-items line of output, 1590 # followed by all lines from stdin verbatim 1591 pretsv() { 1592 awk ' 1593 BEGIN { 1594 for (i = 1; i < ARGC; i++) { 1595 if (i > 1) printf "\t" 1596 printf "%s", ARGV[i] 1597 } 1598 if (ARGC > 1) printf "\n" 1599 exit 1600 } 1601 ' "$@" 1602 cat - 1603 } 1604 1605 # Quiet MPV 1606 qmpv() { mpv --quiet "${@:--}"; } 1607 1608 # ignore stderr, without any ugly keyboard-dancing 1609 quiet() { "$@" 2> /dev/null; } 1610 1611 # Red AWK styles lines satisfying an AWK condition/expression red 1612 rawk() { 1613 local command='awk' 1614 if [ -p 1 ] || [ -t 1 ]; then 1615 command='stdbuf -oL awk' 1616 fi 1617 1618 local cond="${1:-1}" 1619 [ $# -gt 0 ] && shift 1620 1621 ${command} ' 1622 { low = lower = tolower($0) } 1623 '"${cond}"' { 1624 gsub(/\x1b\[0m/, "\x1b[0m\x1b[38;2;204;0;0m") 1625 printf "\x1b[38;2;204;0;0m%s\x1b[0m\n", $0 1626 next 1627 } 1628 1 1629 ' "$@" 1630 } 1631 1632 # keep only lines between the 2 line numbers given, inclusively 1633 rangelines() { 1634 { [ $# -eq 2 ] || [ $# -eq 3 ]; } && [ "${1}" -le "${2}" ] && { 1635 tail -n +"${1}" "${3:--}" | head -n $(("${2}" - "${1}" + 1)) 1636 } 1637 } 1638 1639 # RANdom MANual page 1640 ranman() { 1641 find "/usr/share/man/man${1:-1}" -type f | shuf -n 1 | xargs basename | 1642 sed 's-\.gz$--' | xargs man 1643 } 1644 1645 # play a ready-phone-line sound lasting the number of seconds given, or for 1 1646 # second by default; uses my tool `sboard` 1647 ready() { sboard ready "${1:-1}" "${2:-1}"; } 1648 1649 # reflow/trim lines of prose (text) to improve its legibility: it's especially 1650 # useful when the text is pasted from web-pages being viewed in reader mode 1651 reprose() { 1652 local command='awk' 1653 if [ -p 1 ] || [ -t 1 ]; then 1654 command='stdbuf -oL awk' 1655 fi 1656 1657 local w="${1:-80}" 1658 [ $# -gt 0 ] && shift 1659 1660 ${command} ' 1661 FNR == 1 && NR > 1 { print "" } 1662 { gsub(/\r$/, ""); print } 1663 ' "$@" | fold -s -w "$w" | sed -u -E 's- *\r?$--' 1664 } 1665 1666 # REPeat STRing emits a line with a repeating string in it, given both a 1667 # string and a number in either order 1668 repstr() { 1669 awk ' 1670 BEGIN { 1671 if (ARGV[2] ~ /^[+-]?[0-9]+$/) { 1672 symbol = ARGV[1] 1673 times = ARGV[2] + 0 1674 } else { 1675 symbol = ARGV[2] 1676 times = ARGV[1] + 0 1677 } 1678 1679 if (times < 0) exit 1680 if (symbol == "") symbol = "-" 1681 s = sprintf("%*s", times, "") 1682 gsub(/ /, symbol, s) 1683 print s 1684 exit 1685 } 1686 ' "$@" 1687 } 1688 1689 # show a RULER-like width-measuring line 1690 ruler() { 1691 [ "${1:-80}" -gt 0 ] && printf "%${1:-80}s\n" "" | sed -E \ 1692 's- {10}-····╵····│-g; s- -·-g; s-·····-····╵-' 1693 } 1694 1695 # Summarize via AWK calculates some numeric statistics from an AWK expression 1696 sawk() { 1697 local code="${1:-\$0}" 1698 [ $# -gt 0 ] && shift 1699 1700 awk ' 1701 BEGIN { 1702 numeric = ints = pos = zero = neg = 0 1703 1704 inf = "+inf" + 0 1705 1706 min = inf 1707 max = -inf 1708 sum = 0 1709 mean = 0 1710 prod = 1 1711 } 1712 1713 { 1714 v = '"${code}"' 1715 if (v !~ /^ *(0|[0-9]+|[0-9]*\.[0-9]+) *$/) next 1716 v = v + 0 1717 1718 numeric++ 1719 ints += v % 1 == 0 1720 if (v > 0) pos++ 1721 else if (v < 0) neg++ 1722 else if (v == 0) zero++ 1723 1724 min = min < v ? min : v 1725 max = max > v ? max : v 1726 sum += v 1727 prod *= v 1728 lnSum += v <= 0 ? -inf : log(v) 1729 1730 # advance welford`s algorithm 1731 d1 = v - mean 1732 mean += d1 / numeric 1733 d2 = v - mean 1734 meanSq += d1 * d2 1735 } 1736 1737 END { 1738 sum = mean * numeric 1739 if (numeric == 0) lnSum = -inf 1740 1741 # separate name-value pairs using tabs, and prepare a 1742 # pipeable command which ignores all-zero decimals 1743 OFS = "\t" 1744 1745 print "numeric", numeric 1746 if (numeric > 0) { 1747 print "min", sprintf("%f", min) 1748 print "max", sprintf("%f", max) 1749 print "sum", sprintf("%f", sum) 1750 print "mean", sprintf("%f", mean) 1751 print "geomean", (zero == 0 && neg == 0) ? 1752 sprintf("%f", exp(lnSum / numeric)) : 1753 "" 1754 print "sd", sprintf("%f", sqrt(meanSq / numeric)) 1755 print "product", sprintf("%g", prod) 1756 } else { 1757 print "min", "" 1758 print "max", "" 1759 print "sum", "" 1760 print "mean", "" 1761 print "geomean", "" 1762 print "sd", "" 1763 print "product", "" 1764 } 1765 print "integer", ints 1766 print "positive", pos 1767 print "zero", zero 1768 print "negative", neg 1769 } 1770 ' "$@" | sed -E 's-([0-9]+)\.0+$-\1-g; s-([0-9]+\.[0-9]*[1-9])0+$-\1-g' 1771 } 1772 1773 # SystemCTL; `sysctl` is already taken for a separate/unrelated app 1774 sctl() { systemctl "$@" 2>&1 | less -MKiCRS --header=1; } 1775 1776 # show a unique-looking SEParator line; useful to run between commands 1777 # which output walls of text 1778 sep() { 1779 [ "${1:-80}" -gt 0 ] && 1780 printf "\e[48;2;218;218;218m%${1:-80}s\e[0m\n" "" | sed 's- -·-g' 1781 } 1782 1783 # webSERVE files in a folder as localhost, using the port number given, or 1784 # port 8080 by default 1785 serve() { 1786 printf "\e[7mserving files in %s\e[0m\n" "${2:-$(pwd)}" >&2 1787 python3 -m http.server "${1:-8080}" -d "${2:-.}" 1788 } 1789 1790 # SET DIFFerence sorts its 2 inputs, then finds lines not in the 2nd input 1791 setdiff() { 1792 # comm -23 <(sort "$1") <(sort "$2") 1793 # dash doesn't support the process-sub syntax 1794 (sort "$1" | (sort "$2" | (comm -23 /dev/fd/3 /dev/fd/4) 4<&0) 3<&0) 1795 } 1796 1797 # SET INtersection, sorts its 2 inputs, then finds common lines 1798 setin() { 1799 # comm -12 <(sort "$1") <(sort "$2") 1800 # dash doesn't support the process-sub syntax 1801 (sort "$1" | (sort "$2" | (comm -12 /dev/fd/3 /dev/fd/4) 4<&0) 3<&0) 1802 } 1803 1804 # SET SUBtraction sorts its 2 inputs, then finds lines not in the 2nd input 1805 setsub() { 1806 # comm -23 <(sort "$1") <(sort "$2") 1807 # dash doesn't support the process-sub syntax 1808 (sort "$1" | (sort "$2" | (comm -23 /dev/fd/3 /dev/fd/4) 4<&0) 3<&0) 1809 } 1810 1811 # Show Files (and folders), coloring folders and links 1812 sf() { 1813 local arg 1814 local gap=0 1815 local options='-MKiCRS' 1816 1817 if [ $# -le 1 ]; then 1818 options='--header=1 -MKiCRS' 1819 fi 1820 1821 for arg in "${@:-.}"; do 1822 [ "${gap}" -gt 0 ] && printf "\n" 1823 printf "\e[7m%s\e[0m\n\n" "$(realpath "${arg}")" 1824 gap=1 1825 1826 ls -al --file-type --color=never --time-style iso "${arg}" | awk ' 1827 BEGIN { 1828 drep = "\x1b[38;2;0;135;255m\x1b[48;2;228;228;228m&\x1b[0m" 1829 lrep = "\x1b[38;2;0;135;95m\x1b[48;2;228;228;228m&\x1b[0m" 1830 } 1831 1832 NR < 4 { next } 1833 (NR - 3) % 5 == 1 && (NR - 3) > 1 { print "" } 1834 1835 { 1836 gsub(/^(d[rwx-]+)/, drep) 1837 gsub(/^(l[rwx-]+)/, lrep) 1838 printf "%6d %s\n", NR - 3, $0; fflush() 1839 } 1840 ' 1841 done | less "${options}" 1842 } 1843 1844 # skip the first n lines, or the 1st line by default 1845 skip() { tail -n +$(("${1:-1}" + 1)) "${2:--}"; } 1846 1847 # skip the last n lines, or the last line by default 1848 skiplast() { head -n -"${1:-1}" "${2:--}"; } 1849 1850 # SLOW/delay lines from the standard-input, waiting the number of seconds 1851 # given for each line, or waiting 1 second by default 1852 slow() { 1853 local seconds="${1:-1}" 1854 ( 1855 IFS="$(printf "\n")" 1856 while read -r line; do 1857 sleep "${seconds}" 1858 printf "%s\n" "${line}" 1859 done 1860 ) 1861 } 1862 1863 # Show Latest Podcasts, using my tools `podfeed` and `si` 1864 slp() { 1865 local title 1866 title="Latest Podcast Episodes as of $(date +'%F %T')" 1867 podfeed -title "${title}" "$@" | si 1868 } 1869 1870 # emit the first line as is, sorting all lines after that, using the 1871 # `sort` command, passing all/any arguments/options to it 1872 sortrest() { 1873 awk -v sort="sort $*" ' 1874 FNR == 1 { gsub(/^\xef\xbb\xbf/, "") } 1875 { gsub(/\r$/, "") } 1876 NR == 1 { print; fflush() } 1877 NR >= 2 { print | sort } 1878 ' 1879 } 1880 1881 # SORt Tab-Separated Values: emit the first line as is, sorting all lines after 1882 # that, using the `sort` command in TSV (tab-separated values) mode, passing 1883 # all/any arguments/options to it 1884 sortsv() { 1885 awk -v sort="sort -t \"$(printf '\t')\" $*" ' 1886 FNR == 1 { gsub(/^\xef\xbb\xbf/, "") } 1887 { gsub(/\r$/, "") } 1888 NR == 1 { print; fflush() } 1889 NR >= 2 { print | sort } 1890 ' 1891 } 1892 1893 # emit a line with the number of spaces given in it 1894 spaces() { [ "${1:-80}" -gt 0 ] && printf "%${1:-80}s\n" ""; } 1895 1896 # SQUeeze horizontal spaces and STOMP vertical gaps 1897 squomp() { 1898 local command='awk' 1899 if [ -p 1 ] || [ -t 1 ]; then 1900 command='stdbuf -oL awk' 1901 fi 1902 1903 ${command} ' 1904 FNR == 1 { gsub(/^\xef\xbb\xbf/, "") } 1905 /^\r?$/ { empty = 1; next } 1906 empty { if (n > 0) print ""; empty = 0 } 1907 1908 { 1909 gsub(/^ +| *\r?$/, "") 1910 gsub(/ *\t */, "\t") 1911 gsub(/ +/, " ") 1912 print; n++ 1913 } 1914 ' "$@" 1915 } 1916 1917 # STOMP vertical gaps, turning runs of empty lines into single empty lines 1918 stomp() { 1919 local command='awk' 1920 if [ -p 1 ] || [ -t 1 ]; then 1921 command='stdbuf -oL awk' 1922 fi 1923 1924 ${command} ' 1925 /^\r?$/ { empty = 1; next } 1926 empty { if (n > 0) print ""; empty = 0 } 1927 { print; n++ } 1928 ' "$@" 1929 } 1930 1931 substr() { 1932 local command='awk' 1933 if [ -p 1 ] || [ -t 1 ]; then 1934 command='stdbuf -oL awk' 1935 fi 1936 if [ $# -lt 2 ]; then 1937 printf "missing 1-based start index, and substring length\n" >&2 1938 exit 1 1939 fi 1940 1941 ${command} '{ print substr($0, '"$1"', '"$2"') }' 1942 } 1943 1944 # add a final sums row after all input lines 1945 sums() { 1946 local command='awk' 1947 if [ -p 1 ] || [ -t 1 ]; then 1948 command='stdbuf -oL awk' 1949 fi 1950 1951 ${command} ' 1952 { gsub(/\r$/, "") } 1953 1954 NR == 1 { FS = ($0 ~ /\t/) ? "\t" : " "; $0 = $0 } 1955 1956 { 1957 if (n < NF) n = NF 1958 for (i = 1; i <= NF; i++) sums[i] += $i 1959 print 1960 } 1961 1962 END { 1963 for (i = 1; i <= n; i++) { 1964 if (i > 1) printf(FS) 1965 printf("%s", sums[i]) 1966 } 1967 if (n > 0) printf "\n" 1968 } 1969 ' "$@" 1970 } 1971 1972 # show a reverse-sorted tally of all lines read, where ties are sorted 1973 # alphabetically 1974 # tally() { 1975 # awk -v sortcmd="sort -t \"$(printf '\t')\" -rnk2 -k1d" ' 1976 # # reassure users by instantly showing the header 1977 # BEGIN { print "value\ttally"; fflush() } 1978 # { gsub(/\r$/, ""); t[$0]++ } 1979 # END { for (k in t) { printf("%s\t%d\n", k, t[k]) | sortcmd } } 1980 # ' "$@" 1981 # } 1982 1983 # Tally (lines) with AWK 1984 tawk() { 1985 local code="${1:-\$0}" 1986 [ $# -gt 0 ] && shift 1987 1988 awk -v sortcmd="sort -t '\t' -rnk1" ' 1989 BEGIN { print "tally\tvalue"; fflush() } 1990 1991 { 1992 v = '"${code}"' 1993 if (!tally[v]++) ordkeys[++oklen] = v 1994 } 1995 1996 END { 1997 for (i = 1; i <= oklen; i++) { 1998 k = ordkeys[i] 1999 printf "%d\t%s\n", tally[k], k | sortcmd 2000 } 2001 } 2002 ' "$@" 2003 } 2004 2005 # Simulate the cadence of old-fashioned TELETYPE machines 2006 teletype() { 2007 awk ' 2008 { 2009 gsub(/\r$/, "") 2010 2011 n = length($0) 2012 for (i = 1; i <= n; i++) { 2013 if (code = system("sleep 0.015")) exit code 2014 printf "%s", substr($0, i, 1); fflush() 2015 } 2016 2017 if (code = system("sleep 0.75")) exit code 2018 printf "\n"; fflush() 2019 } 2020 2021 # END { if (NR > 0 && code != 0) printf "\n" } 2022 ' "$@" 2023 } 2024 2025 # show current date in a specifc format 2026 today() { date +'%Y-%m-%d %a %b %d'; } 2027 2028 # get the first n lines, or 1 by default 2029 toline() { head -n "${1:-1}" "${2:--}"; } 2030 2031 # lowercase all ASCII symbols 2032 tolower() { 2033 if [ -p 1 ] || [ -t 1 ]; then 2034 stdbuf -oL awk '{ print tolower($0) }' "$@" 2035 else 2036 awk '{ print tolower($0) }' "$@" 2037 fi 2038 } 2039 2040 # play a tone/sine-wave sound lasting the number of seconds given, or for 1 2041 # second by default: after the optional duration, the next optional arguments 2042 # are the volume and the tone-frequency; uses my tools `sboard` and `waveout` 2043 tone() { 2044 if [ "${3:-440}" -eq 440 ]; then 2045 sboard tone "${1:-1}" "${2:-1}" 2046 else 2047 waveout "${1:-1}" "${2:-1} * sin(${3:-440} * tau * t)" | 2048 mpv --really-quiet - 2049 fi 2050 } 2051 2052 # get the processes currently using the most cpu 2053 topcpu() { 2054 local n="${1:-10}" 2055 [ "$n" -gt 0 ] && ps aux | awk ' 2056 NR == 1 { print; fflush() } 2057 NR > 1 { print | "sort -rnk3" } 2058 ' | head -n "$(("$n" + 1))" 2059 } 2060 2061 # get the processes currently using the most memory 2062 topmemory() { 2063 local n="${1:-10}" 2064 [ "$n" -gt 0 ] && ps aux | awk ' 2065 NR == 1 { print; fflush() } 2066 NR > 1 { print | "sort -rnk6" } 2067 ' | head -n "$(("$n" + 1))" 2068 } 2069 2070 # transpose (switch) rows and columns from tables 2071 transpose() { 2072 awk ' 2073 { gsub(/\r$/, "") } 2074 2075 FNR == 1 { FS = ($0 ~ /\t/) ? "\t" : " "; $0 = $0 } 2076 2077 { 2078 for (i = 1; i <= NF; i++) lines[i][NR] = $i 2079 if (maxitems < NF) maxitems = NF 2080 } 2081 2082 END { 2083 for (j = 1; j <= maxitems; j++) { 2084 for (i = 1; i <= NR; i++) { 2085 if (i > 1) printf "\t" 2086 printf "%s", lines[j][i] 2087 } 2088 printf "\n" 2089 } 2090 } 2091 ' "$@" 2092 } 2093 2094 # Unique via AWK, avoids lines duplicating the expression given 2095 uawk() { 2096 local code="${1:-\$0}" 2097 [ $# -gt 0 ] && shift 2098 2099 local command='awk' 2100 if [ -p 1 ] || [ -t 1 ]; then 2101 command='stdbuf -oL awk' 2102 fi 2103 2104 ${command} ' 2105 BEGIN { for (i = 1; i < ARGC; i++) if (f[ARGV[i]]++) delete ARGV[i] } 2106 !c['"${code}"']++ 2107 ' "$@" 2108 } 2109 2110 # Underline Every 5 lines: make groups of 5 lines stand out by underlining 2111 # the last line of each such group 2112 ue5() { 2113 local command='awk' 2114 if [ -p 1 ] || [ -t 1 ]; then 2115 command='stdbuf -oL awk' 2116 fi 2117 2118 ${command} ' 2119 NR % 5 == 0 && NR != 1 { 2120 gsub(/\x1b\[0m/, "\x1b[0m\x1b[4m") 2121 printf("\x1b[4m%s\x1b[0m\n", $0) 2122 next 2123 } 2124 1 2125 ' "$@" 2126 } 2127 2128 # only keep UNIQUE lines, keeping them in their original order 2129 unique() { 2130 local command='awk' 2131 if [ -p 1 ] || [ -t 1 ]; then 2132 command='stdbuf -oL awk' 2133 fi 2134 2135 ${command} ' 2136 BEGIN { for (i = 1; i < ARGC; i++) if (f[ARGV[i]]++) delete ARGV[i] } 2137 !c[$0]++ 2138 ' "$@" 2139 } 2140 2141 # fix lines, ignoring leading UTF-8_BOMs (byte-order-marks) on each input's 2142 # first line, turning all end-of-line CRLF byte-pairs into single line-feeds, 2143 # and ensuring each input's last line ends with a line-feed; trailing spaces 2144 # are also ignored 2145 unixify() { 2146 local command='awk' 2147 if [ -p 1 ] || [ -t 1 ]; then 2148 command='stdbuf -oL awk' 2149 fi 2150 2151 ${command} ' 2152 FNR == 1 { gsub(/^\xef\xbb\xbf/, "") } 2153 { gsub(/ *\r?$/, ""); print } 2154 ' "$@" 2155 } 2156 2157 # skip the first/leading n bytes 2158 unleaded() { tail -c +$(("$1" + 1)) "${2:--}"; } 2159 2160 # go UP n folders, or go up 1 folder by default 2161 up() { 2162 if [ "${1:-1}" -le 0 ]; then 2163 cd . 2164 else 2165 cd "$(printf "%${1:-1}s" "" | sed 's- -../-g')" || return $? 2166 fi 2167 } 2168 2169 # convert United States Dollars into CAnadian Dollars, using the latest 2170 # official exchange rates from the bank of canada; during weekends, the 2171 # latest rate may be from a few days ago; the default amount of usd to 2172 # convert is 1, when not given 2173 usd2cad() { 2174 local site='https://www.bankofcanada.ca/valet/observations/group' 2175 local csv_rates="${site}/FX_RATES_DAILY/csv" 2176 local url="${csv_rates}?start_date=$(date -d '3 days ago' +'%Y-%m-%d')" 2177 curl -s "${url}" | awk -F, -v amount="$(echo "${1:-1}" | sed 's-_--g')" ' 2178 /USD/ { for (i = 1; i <= NF; i++) if($i ~ /USD/) j = i } 2179 END { gsub(/"/, "", $j); if (j != 0) printf "%.2f\n", amount * $j } 2180 ' 2181 } 2182 2183 # View Nice Table / Very Nice Table; uses my tools `ntsv` and `nn` 2184 vnt() { 2185 nl -b a -w 1 -v 0 "$@" | ntsv | nn | 2186 awk '(NR - 1) % 5 == 1 { print "" } 1' | less -MKiCRS --header=1 2187 } 2188 2189 # View Text, turning documents into plain-text if needed; uses `pandoc` 2190 vt() { 2191 local arg 2192 local gap=0 2193 local options='-MKiCRS' 2194 2195 if [ $# -eq 1 ]; then 2196 options='--header=1 -MKiCRS' 2197 fi 2198 2199 if [ $# -eq 0 ]; then 2200 pandoc -s -t plain - 2>&1 | less -MKiCRS 2201 else 2202 for arg in "$@"; do 2203 [ "${gap}" -eq 1 ] && printf "\n" 2204 gap=1 2205 printf "\e[7m%-80s\e[0m\n" "${arg}" 2206 pandoc -s -t plain "${arg}" 2>&1 | awk 1 2207 done | less "${options}" 2208 fi 2209 } 2210 2211 # What Are These (?) shows what the names given to it are/do 2212 wat() { 2213 local arg 2214 local gap=0 2215 2216 if [ $# -eq 0 ]; then 2217 echo "$0" 2218 return 0 2219 fi 2220 2221 for arg in "$@"; do 2222 [ "${gap}" -gt 0 ] && printf "\n" 2223 gap=1 2224 printf "\e[7m%-80s\e[0m\n" "${arg}" 2225 2226 while alias "${arg}" > /dev/null 2> /dev/null; do 2227 arg="$(alias "${arg}" | sed -E "s-^[^=]+=['\"](.+)['\"]\$-\\1-")" 2228 done 2229 2230 if echo "${arg}" | grep -q ' '; then 2231 printf "%s\n" "${arg}" 2232 continue 2233 fi 2234 2235 if declare -f "${arg}"; then 2236 continue 2237 fi 2238 2239 if which "${arg}" > /dev/null 2> /dev/null; then 2240 which "${arg}" 2241 continue 2242 fi 2243 2244 printf "\e[38;2;204;0;0m%s not found\e[0m\n" "${arg}" 2245 done | less -MKiCRS 2246 } 2247 2248 # find all WEB/hyperLINKS (https:// and http://) in the input text 2249 weblinks() { 2250 local arg 2251 local re='https?://[A-Za-z0-9+_.:%-]+(/[A-Za-z0-9+_.%/,#?&=-]*)*' 2252 for arg in "${@:--}"; do 2253 grep --line-buffered -E -o "${re}" "${arg}" 2254 done 2255 } 2256 2257 # recursively find all files with trailing spaces/CRs 2258 whichtrails() { rg -c --line-buffered '[ \r]+$' "${@:-.}"; } 2259 2260 # XARGS Lines, runs `xargs` using whole lines as extra arguments 2261 xargsl() { 2262 if [ -p 1 ] || [ -t 1 ]; then 2263 stdbuf -oL awk -v ORS='\000' ' 2264 FNR == 1 { gsub(/^\xef\xbb\xbf/, "") } 2265 { gsub(/\r$/, ""); print } 2266 ' | stdbuf -oL xargs -0 "$@" 2267 else 2268 awk -v ORS='\000' ' 2269 FNR == 1 { gsub(/^\xef\xbb\xbf/, "") } 2270 { gsub(/\r$/, ""); print } 2271 ' | xargs -0 "$@" 2272 fi 2273 } 2274 2275 # Youtube Audio Player 2276 yap() { 2277 # some youtube URIs end with extra playlist/tracker parameters 2278 local url="$(echo "$1" | sed 's-&.*--')" 2279 mpv "$(yt-dlp -x --audio-format best --get-url "${url}" 2> /dev/null)" 2280 } 2281 2282 # show a calendar for the current YEAR, or for the year given 2283 year() { 2284 { 2285 # show the current date/time center-aligned 2286 printf \ 2287 "%21s\e[38;2;78;154;6m%s\e[0m \e[38;2;52;101;164m%s\e[0m\n\n" \ 2288 "" "$(date +'%a %b %d %Y')" "$(date +'%H:%M')" 2289 # debian linux has a different `cal` app which highlights the day 2290 if [ -e "/usr/bin/ncal" ]; then 2291 # fix debian/ncal's weird way to highlight the current day 2292 ncal -C -y "$@" | sed -E 's/_\x08(.)/\x1b[7m\1\x1b[0m/g' 2293 else 2294 cal -y "$@" 2295 fi 2296 } | less -MKiCRS 2297 } 2298 2299 # show the current date in the YYYY-MM-DD format 2300 ymd() { date +'%Y-%m-%d'; }