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