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