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