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