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