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