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