File: clam.sh 1 #!/bin/sh 2 3 # The MIT License (MIT) 4 # 5 # Copyright © 2024 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 # Command-Line Augmentation Module: get the best out of your shell 28 # 29 # This is a collection of arguably useful shell functions and shortcuts, 30 # beyond the ones defined in `shshsh` (SHort SHell SHortcuts): some of 31 # these extra commands can be real time/effort savers, ideally letting 32 # you concentrate on getting things done. 33 # 34 # Some of these commands depend on my other scripts from the `pac-tools`, 35 # others either rely on widely-preinstalled command-line apps, or ones 36 # which are available on most command-line `package` managers. 37 # 38 # You're supposed to `source` this script, so its definitions stay for 39 # your whole shell session: for that, you can run `source clam` or 40 # `. clam` (no quotes either way), either directly or at shell startup. 41 # 42 # 43 # Partial list of funcs/commands added 44 # 45 # args emit each argument given to it on its own line 46 # blawk process BLocks of non-empty lines with AWK 47 # can CAlculate with Nice numbers, using my scripts `ca` and `nn` 48 # coco COunt COndition, uses an AWK condition as its first arg 49 # cu Change Units, using my scripts `bu` and `nn` 50 # hawk Highlight lines matching the AWK condition given 51 # lab Like A Book, shows lines the way books do; uses my script `book` 52 # largs run `xargs` taking the extra arguments from whole stdin lines 53 # loco LOwercase line, check (awk) COndition 54 # loser LOcal SERver webserves files in a folder as localhost 55 # lineup regroup adjacent lines into n-item tab-separated lines 56 # merrge merge stderr into stdout, without any ugly keyboard-dancing 57 # n Number all lines, starting from the number given, or 1 by default 58 # nfs Nice File Sizes, using my scripts `nn` and `cext` 59 # noerr ignore stderr, without any ugly finger-dancing 60 # plain ignore all ANSI styles 61 # restyle change color/style of lines, using my script `ecoli` 62 # sf Show Files (and folders), using colors 63 # skip skip the first n lines, or the first 1 by default 64 # skiplast skip the last n lines, or the last 1 by default 65 # slp Show Latest Podcasts, using my scripts `podfeed` and `si` 66 # style change color/style of lines, using my script `ecoli` 67 # surprise show a random command defined in `shshsh` or `clam`, using `wat` 68 # tawk Tab AWK, runs AWK using tab as its IO item-separator 69 # today show current date in a way friendly both to people and tools 70 # tsawk TimeStamp lines satisfying AWK condition, ignoring the rest 71 # tsv2ssv run my script `realign`, using TSV-input settings 72 # unixify ensure plain-text lines are unix-like 73 # verdict run a command, showing its success/failure right after 74 # wcp Word-Count Plus runs `wc`, enriching its output using my tools 75 # year show a calendar for the current year, or for the year given 76 77 78 # handle help options 79 case "$1" in 80 -h|--h|-help|--help) 81 # show help message, extracting the info-comment at the start 82 # of this file, and quit 83 awk '/^# +clam/, /^$/ { gsub(/^# ?/, ""); print }' "$0" 84 exit 0 85 ;; 86 esac 87 88 89 # use a simple shell prompt 90 # PS1="\$ " 91 # PS2="> " 92 93 # use a simple shell prompt, showing the current folder in the title 94 # PS1="\[\e]0;\w\a\]\$ " 95 # PS2="> " 96 97 # use a simple shell prompt, showing the current folder both in the prompt 98 # itself, and in the title 99 # PS1="\[\e]0;\w\a\]\w\$ " 100 # PS2="> " 101 102 # prevent `less` from saving commands 103 # LESSHISTFILE="-" 104 # LESSSECURE=1 105 106 # prevent the shell from saving commands 107 # unset HISTFILE 108 109 110 # aliases for external scripts from my `pac-tools` 111 alias json0='j0' 112 alias uncsv='decsv' 113 alias untsv='detsv' 114 115 116 # column-layout shortcuts, using my script `sbs` (Side By Side); the `c` in 117 # the names stands for `columns` 118 # 2c() { sbs 2 "$@"; } 119 # 3c() { sbs 3 "$@"; } 120 # 4c() { sbs 4 "$@"; } 121 # 5c() { sbs 5 "$@"; } 122 # 6c() { sbs 6 "$@"; } 123 # 7c() { sbs 7 "$@"; } 124 # 8c() { sbs 8 "$@"; } 125 # 9c() { sbs 9 "$@"; } 126 127 # shortcuts for my script `ca`, an arbitrary-precision calculator, using 128 # various decimal precisions 129 ca10() { ca "scale=10; $*"; } 130 ca20() { ca "scale=20; $*"; } 131 ca30() { ca "scale=30; $*"; } 132 ca40() { ca "scale=40; $*"; } 133 ca50() { ca "scale=50; $*"; } 134 ca60() { ca "scale=60; $*"; } 135 ca70() { ca "scale=70; $*"; } 136 ca80() { ca "scale=80; $*"; } 137 ca90() { ca "scale=90; $*"; } 138 139 # run `awk` 140 a() { 141 awk "$@" 142 } 143 144 # emit each argument given as its own line of output 145 args() { 146 awk 'BEGIN { for (i = 1; i < ARGC; i++) print ARGV[i]; exit }' "$@" 147 } 148 149 # emit a colored bar which can help visually separate different outputs 150 bar() { 151 printf "\x1b[48;5;253m%${1:-80}s\x1b[0m\n" " " 152 } 153 154 # Breathe Header: add an empty line after the first one (the header), then 155 # separate groups of 5 lines (by default) with empty lines between them 156 bh() { 157 local n="${1:-5}" 158 shift 159 awk -v n="$n" '(NR - 1) % n == 1 && NR > 1 { print "" } 1' "$@" 160 } 161 162 # Breathe Lines: separate groups of 5 lines (by default) with empty lines 163 bl() { 164 local n="${1:-5}" 165 shift 166 awk -v n="$n" 'NR % n == 1 && NR != 1 { print "" } 1' "$@" 167 } 168 169 # process BLocks of non-empty lines with AWK 170 blawk() { 171 awk -F='' -v RS='' "$@" 172 } 173 174 # show a reverse-sorted tally of all lines read, where ties are sorted 175 # alphabetically, and where trailing bullets are added to quickly make 176 # the tally counts comparable at a glance 177 bully() { 178 printf "value\ttally\tbullets\n" 179 awk ' 180 { tally[$0]++ } 181 182 END { 183 # find the max tally, which is needed to build the bullets-string 184 max = 0 185 for (k in tally) { 186 if (max < tally[k]) max = tally[k] 187 } 188 189 # make enough bullets for all tallies: this loop makes growing the 190 # string a task with complexity O(n * log n), instead of a naive 191 # O(n**2), which can slow-down things when tallies are high enough 192 bullets = "•" 193 for (n = max; n > 1; n /= 2) { 194 bullets = bullets bullets 195 } 196 197 # emit unsorted output lines to the sort cmd, which will emit the 198 # final reverse-sorted tally lines 199 for (k in tally) { 200 s = substr(bullets, 1, tally[k]) 201 printf "%s\t%d\t%s\n", k, tally[k], s 202 } 203 } 204 ' "$@" | sort -t ' ' -rnk2 -k1d 205 } 206 207 # Cat 208 c() { 209 cat "$@" 210 } 211 212 # CAlculator with Nice numbers runs my script `ca` and colors results with 213 # my script `nn`, alternating styles to make long numbers easier to read 214 can() { 215 ca "$@" | nn 216 } 217 218 # CHOP DECimalS ignores all trailing decimal zeros in numbers, even the 219 # decimal dots themselves, when decimals in a number are all zeros 220 chopdecs() { 221 awk 1 "$@" | sed -E 's-(\.[0-9]+[1-9]+)0+$-\1-g; s-([0-9]+)\.0*$-\1-g' 222 } 223 224 # ignore final life-feed from text, if it's the very last byte; also, all 225 # trailing carriage-returns are ignored 226 choplf() { 227 awk ' 228 FNR == 1 { gsub(/^\xef\xbb\xbf/, "") } 229 NR > 1 { print "" } 230 { gsub(/\r$/, ""); printf "%s", $0 } 231 ' "$@" 232 } 233 234 # CLear Screen 235 cls() { 236 clear 237 } 238 239 # COunt COndition: count how many times the AWK expression given is true 240 coco() { 241 local cond="${1:-1}" 242 shift 243 awk "${cond} { c++ } END { print c }" "$@" 244 } 245 246 # Colored RipGrep: ensures app `rg` emits colors when piped 247 crg() { 248 rg --color=always "$@" 249 } 250 251 # Color Syntax: run syntax-coloring app `bat` without line-wrapping 252 cs() { 253 local cmd="bat" 254 # debian linux uses a different name for the `bat` app 255 if [ -e "/usr/bin/batcat" ]; then 256 cmd="batcat" 257 fi 258 259 "$cmd" --style=plain,header,numbers --theme='Monokai Extended Light' \ 260 --wrap=never --color=always "$@" | 261 sed 's-\x1b\[38;5;70m-\x1b\[38;5;28m-g' | less -JMKiCRS 262 } 263 264 # Color Syntax of all files in a Folder, showing line numbers 265 csf() { 266 local cmd="bat" 267 # debian linux uses a different name for the `bat` app 268 if [ -e "/usr/bin/batcat" ]; then 269 cmd="batcat" 270 fi 271 272 find "${1:-.}" -type f -print0 | xargs --null "$cmd" \ 273 --style=plain,header,numbers --theme='Monokai Extended Light' \ 274 --wrap=never --color=always | 275 sed 's-\x1b\[38;5;70m-\x1b\[38;5;28m-g' | less -JMKiCRS 276 } 277 278 # Change Units turns common US units into international ones; uses my 279 # scripts `bu` (Better Units) and `nn` (Nice Numbers) 280 cu() { 281 bu "$@" | awk ' 282 NF == 5 { print $(NF-1), $NF } 283 NF == 4 && $NF == "s" { print $(NF-1), $NF } 284 NF == 4 && $NF != "s" { print $NF } 285 ' | nn 286 } 287 288 # DEDUPlicate prevents lines from appearing more than once 289 dedup() { 290 awk '!c[$0]++' "$@" 291 } 292 293 # DEFine the word given, using an online service 294 def() { 295 curl -s "dict://dict.org/d:$*" | awk ' 296 /^151 / { printf "\x1b[38;5;4m%s\x1b[0m\n", $0; next } 297 /^[1-9][0-9]{2} / { printf "\x1b[38;5;244m%s\x1b[0m\n", $0; next } 298 1' 299 } 300 301 # DEcompress GZip data 302 degz() { 303 gzip -d 304 } 305 306 # DEcompress GZIP data 307 degzip() { 308 gzip -d 309 } 310 311 # turn lines of Space-Separated Values into lines of Tab-Separated Values 312 dessv() { 313 awk 1 "$@" | sed -E 's-^ +--; s- *\r?--; s- +-\t-g' 314 } 315 316 # DICtionary definitions, using an online service 317 dic() { 318 curl -s "dict://dict.org/d:$*" | awk ' 319 /^151 / { printf "\x1b[38;5;4m%s\x1b[0m\n", $0; next } 320 /^[1-9][0-9]{2} / { printf "\x1b[38;5;244m%s\x1b[0m\n", $0; next } 321 1' 322 } 323 324 # DIVide 2 numbers 3 ways, including the complement 325 div() { 326 awk -v a="${1:-1}" -v b="${2:-1}" ' 327 BEGIN { 328 gsub(/_/, "", a) 329 gsub(/_/, "", b) 330 if (a > b) { c = a; a = b; b = c; } 331 print a / b 332 print b / a 333 print 1 - a / b 334 exit 335 }' 336 } 337 338 # show the current Date and Time, and the 3 `current` months 339 dt() { 340 # debian linux has a different `cal` app which highlights the day 341 if [ -e "/usr/bin/ncal" ]; then 342 ncal -C -3 343 else 344 cal -3 345 fi 346 347 # show the current time center-aligned 348 # printf "%28s\x1b[34m%s\x1b[0m\n" " " "$(date +'%T')" 349 printf "%22s\x1b[32m%s\x1b[0m \x1b[34m%s\x1b[0m\n" " " \ 350 "$(date +'%a %b %d')" "$(date +'%T')" 351 } 352 353 # show all files in a folder, digging recursively 354 files() { 355 local arg 356 for arg in "${@:-.}"; do 357 find "${arg}" -type f 358 done 359 } 360 361 # get the first n lines, or 1 by default 362 first() { 363 head -n "${1:-1}" "${2:--}" 364 } 365 366 # fix lines, ignoring leading UTF-8_BOMs (byte-order-marks) on each input's 367 # first line, turning all end-of-line CRLF byte-pairs into single line-feeds, 368 # and ensuring each input's last line ends with a line-feed 369 fixlines() { 370 awk 'FNR == 1 { gsub(/^\xef\xbb\xbf/, "") } 1' "$@" | sed -E 's-\r$--' 371 } 372 373 # show all folders in a folder, digging recursively 374 folders() { 375 local arg 376 for arg in "${@:-.}"; do 377 find "${arg}" -type d | awk 'NR > 1' 378 done 379 } 380 381 # run the FuZzy finder (fzf) in multi-choice mode, with custom keybindings 382 fz() { 383 fzf -m --bind ctrl-a:select-all,ctrl-space:toggle "$@" 384 } 385 386 # run `grep` in extended mode 387 g() { 388 grep -E "$@" 389 } 390 391 # GET/fetch data from the filename or URI given 392 get() { 393 case "$1" in 394 http://*|https://*|file://*|ftp://*|ftps://*|sftp://*|dict://*) 395 # curl -s "$1" 396 curl -s "$1" || ( 397 printf "\x1b[31mcan't get %s\x1b[0m\n" "$1" >&2 398 return 1 399 ) 400 ;; 401 *) 402 cat "$1" 403 ;; 404 esac 405 } 406 407 # Good, Bad, Meh colors lines using 1..3 awk conditions 408 gbm() { 409 local good="${1:-0}" 410 shift 411 local bad="${1:-0}" 412 shift 413 local meh="${1:-0}" 414 shift 415 416 awk " 417 { lower = tolower(\$0) } 418 419 ${good} { 420 # code to use a color-blind-friendlier blue, instead of green 421 # gsub(/\\x1b\\[0m/, \"\\x1b[0m\\x1b[38;5;26m\") 422 # printf \"\\x1b[38;5;26m%s\\x1b[0m\\n\", \$0 423 gsub(/\\x1b\\[0m/, \"\\x1b[0m\\x1b[38;5;29m\") 424 printf \"\\x1b[38;5;29m%s\\x1b[0m\\n\", \$0 425 fflush() 426 next 427 } 428 429 ${bad} { 430 gsub(/\\x1b\\[0m/, \"\\x1b[0m\\x1b[38;5;1m\") 431 printf \"\\x1b[38;5;1m%s\\x1b[0m\\n\", \$0 432 fflush() 433 next 434 } 435 436 ${meh} { 437 gsub(/\\x1b\\[0m/, \"\\x1b[0m\\x1b[38;5;248m\") 438 printf \"\\x1b[38;5;248m%s\\x1b[0m\\n\", \$0 439 fflush() 440 next 441 } 442 443 { 444 print 445 fflush() 446 } 447 " "$@" 448 } 449 450 # Highlight (lines) with AWK 451 hawk() { 452 local cond="${1:-1}" 453 shift 454 455 awk " 456 ${cond} { 457 gsub(/\\x1b\\[0m/, \"\x1b[0m\\x1b[7m\") 458 printf \"\\x1b[7m%s\\x1b[0m\\n\", \$0 459 fflush() 460 next 461 } 462 463 { print; fflush() }" "$@" 464 } 465 466 highlight() { 467 awk '{ 468 gsub(/\x1b\[[0-9;]*[A-Za-z]/, "") 469 printf "\x1b[7m%s\x1b[0m\n", $0 470 }' "$@" 471 } 472 473 hilite() { 474 awk '{ 475 gsub(/\x1b\[[0-9;]*[A-Za-z]/, "") 476 printf "\x1b[7m%s\x1b[0m\n", $0 477 }' "$@" 478 } 479 480 # Header Less runs `less` with line numbers, ANSI styles, no line-wrapping, 481 # and using the first line as a sticky-header, so it always shows on top 482 hl() { 483 less --header=1 -JMKNiCRS "$@" 484 } 485 486 # Help Me Remember my custom shell commands 487 hmr() { 488 local cmd 489 cmd="bat" 490 # debian linux uses a different name for the `bat` app 491 if [ -e "/usr/bin/batcat" ]; then 492 cmd="batcat" 493 fi 494 495 "$cmd" \ 496 --style=plain,header,numbers --theme='Monokai Extended Light' \ 497 --wrap=never --color=always "$(which shshsh)" "$(which clam)" | 498 sed 's-\x1b\[38;5;70m-\x1b\[38;5;28m-g' | less -JMKiCRS 499 } 500 501 # Header View runs `less` without line numbers, with ANSI styles, with no 502 # line-wrapping, and using the first line as a sticky-header, so it always 503 # shows on top 504 hv() { 505 less --header=1 -JMKiCRS "$@" 506 } 507 508 # show a `dad` JOKE from the web, sometimes even a very funny one 509 joke() { 510 curl -s https://icanhazdadjoke.com | fold -s | sed -E 's- *\r?$--' 511 # plain-text output from previous cmd doesn't end with a line-feed 512 printf "\n" 513 } 514 515 # run `less`, showing line numbers, among other settings 516 l() { 517 less -JMKNiCRS "$@" 518 } 519 520 # Like A Book groups lines as 2 side-by-side pages, the same way books 521 # do it; uses my script `book` 522 lab() { 523 book "$(($(tput lines) - 1))" "$@" | less -JMKiCRS 524 } 525 526 # Line xARGS: `xargs` using line separators, which handles filepaths 527 # with spaces, as long as the standard input has 1 path per line 528 largs() { 529 xargs -d "\n" "$@" 530 } 531 532 # get the last n lines, or 1 by default 533 last() { 534 tail -n "${1:-1}" "${2:--}" 535 } 536 537 # limit stops at the first n bytes, or 1024 bytes by default 538 limit() { 539 head -c "${1:-1024}" "${2:--}" 540 } 541 542 # list files, coloring folders and links 543 lf() { 544 ls -al --file-type --color=never --time-style iso "$@" | awk ' 545 /^d/ { printf "\x1b[38;5;33m%s\x1b[0m\n", $0; next } 546 /^l/ { printf "\x1b[38;5;29m%s\x1b[0m\n", $0; next } 547 1' 548 } 549 550 # Less with Header runs `less` with line numbers, ANSI styles, no line-wraps, 551 # and using the first line as a sticky-header, so it always shows on top 552 lh() { 553 less --header=1 -JMKNiCRS "$@" 554 } 555 556 # regroup adjacent lines into n-item tab-separated lines 557 lineup() { 558 local n="${1:-0}" 559 shift 560 561 if [ "${n}" -le 0 ]; then 562 awk ' 563 NR > 1 { printf "\t" } 564 { printf "%s", $0 } 565 END { if (NR > 0) print "" }' "$@" 566 return "$?" 567 fi 568 569 awk -v n="${n}" ' 570 NR % n != 1 { printf "\t" } 571 { printf "%s", $0 } 572 NR % n == 0 { print "" } 573 END { if (NR % n != 0) print "" }' "$@" 574 } 575 576 # LOwercase line, check (awk) COndition 577 loco() { 578 local cond="${1:-1}" 579 shift 580 # awk "{ \$0 = tolower(\$0) } ${cond}" "$@" 581 awk "{ line = \$0; l = line; \$0 = tolower(\$0) } 582 ${cond} { print line }" "$@" 583 } 584 585 # LOcal SERver webserves files in a folder as localhost, using the port 586 # number given, or port 8080 by default 587 loser() { 588 printf "\x1b[38;5;26mserving files in %s\x1b[0m\n" "${2:-$(pwd)}" >&2 589 python3 -m http.server "${1:-8080}" -d "${2:-.}" 590 } 591 592 # merge stderr into stdout, without any ugly keyboard-dancing 593 merrge() { 594 "$@" 2>&1 595 } 596 597 # Number all lines, starting from the number given, or 1 by default 598 n() { 599 local n="${1:-1}" 600 shift 601 awk -v n="$n" '{ printf "%d\t%s\n", NR - 1 + n, $0; fflush() }' "$@" 602 } 603 604 # emit nothing to output and/or discard everything from input 605 nil() { 606 if [ -p /dev/stdin ]; then 607 cat > /dev/null 608 else 609 head -c 0 610 fi 611 } 612 613 # Nice File Sizes, using my scripts `nn` and `cext` 614 nfs() { 615 # turn arg-list into single-item lines 616 awk 'BEGIN { for (i = 1; i < ARGC; i++) print ARGV[i]; exit }' "$@" | 617 # calculate file-sizes, and reverse-sort results 618 xargs -d '\n' wc -c | sort -rn | 619 # start output with a header-like line, and add a MiB field 620 awk 'BEGIN { printf "%5s %9s %8s name\n", "n", "bytes", "MiB" } 621 { printf "%6d %9d %8.2f %s\n", NR - 1, $1, $1 / 1048576, $2 }' | 622 # make zeros in the MiB field stand out with a special color 623 awk '{ gsub(/ 0.00 /, "\x1b[38;5;103m 0.00 \x1b[0m"); print }' | 624 # make numbers nice, alternating styles along 3-digit groups 625 nn | 626 # color-code file extensions 627 cext | 628 # make table breathe with empty lines, so tall outputs are readable 629 awk '(NR - 2) % 5 == 1 && NR > 1 { print "" } 1' 630 } 631 632 # ignore stderr, without any ugly keyboard-dancing 633 noerr() { 634 "$@" 2> /dev/null 635 } 636 637 # show the current date and time 638 now() { 639 date +'%Y-%m-%d %H:%M:%S' 640 } 641 642 # ignore ANSI terminal styling 643 plain() { 644 awk ' 645 { 646 # ignore notifications (code 9) and hyperlinks (code 8) 647 gsub(/\x1b\](8|9);[^\x07]*\x07/, "") 648 # ignore cursor-movers and style-changers 649 gsub(/\x1b\[([0-9]*[A-HJKST]|[0-9;]*m)/, "") 650 651 print 652 fflush() 653 }' "$@" 654 } 655 656 # Paused MPV 657 pmpv() { 658 mpv --pause "$@" 659 } 660 661 # reflow/trim lines of prose (text) to improve its legibility: it 662 # seems especially useful when the text is pasted from web-pages 663 # being viewed in reader mode 664 reprose() { 665 local w="${1:-80}" 666 shift 667 awk 'FNR == 1 && NR > 1 { print "" } 1' "$@" | sed -E 's- *\r?$--' | 668 fold -s -w="$w" | sed -E 's- +$--' 669 } 670 671 # change color/style of lines, using my script `ecoli` 672 restyle() { 673 local style="${1:-plain}" 674 shift 675 awk 1 "$@" | ecoli . "${style}" 676 } 677 678 # Read-Only Micro (text editor) 679 rom() { 680 micro --readonly true "$@" 681 } 682 683 # run `sed` in extended mode 684 s() { 685 sed -E "$@" 686 } 687 688 # show a unique-looking SEParator line; useful to run between commands 689 # which output walls of text 690 sep() { 691 printf "\x1b[48;5;253m" 692 printf "·························································" 693 printf "·······················" 694 printf "\x1b[0m\n" 695 } 696 697 # webserves files in a folder as localhost, using the port number given, or 698 # port 8080 by default 699 serve() { 700 printf "\x1b[38;5;26mserving files in %s\x1b[0m\n" "${2:-$(pwd)}" >&2 701 python3 -m http.server "${1:-8080}" -d "${2:-.}" 702 } 703 704 # SET DIFFerence sorts its 2 inputs, then finds lines not in the 2nd input 705 setdiff() { 706 # comm -23 <(sort "$1") <(sort "$2") 707 # dash doesn't support the process-sub syntax 708 (sort "$1" | (sort "$2" | (comm -23 /dev/fd/3 /dev/fd/4) 4<&0) 3<&0) 709 } 710 711 # SET INtersection, sorts its 2 inputs, then finds common lines 712 setin() { 713 # comm -12 <(sort "$1") <(sort "$2") 714 # dash doesn't support the process-sub syntax 715 (sort "$1" | (sort "$2" | (comm -12 /dev/fd/3 /dev/fd/4) 4<&0) 3<&0) 716 } 717 718 # SET SUBtraction sorts its 2 inputs, then finds lines not in the 2nd input 719 setsub() { 720 # comm -23 <(sort "$1") <(sort "$2") 721 # dash doesn't support the process-sub syntax 722 (sort "$1" | (sort "$2" | (comm -23 /dev/fd/3 /dev/fd/4) 4<&0) 3<&0) 723 } 724 725 # Show Files (and folders), using colors 726 sf() { 727 ls -al --file-type --color=never --time-style iso "$@" | 728 nn | cext | awk ' 729 /^d/ { gsub(/\x1b\[0m/, "\x1b[38;5;33m") } 730 /^d/ { printf "\x1b[38;5;33m%s\x1b[0m\n", $0; next } 731 /^l/ { gsub(/\x1b\[0m/, "\x1b[38;5;29m") } 732 /^l/ { printf "\x1b[38;5;29m%s\x1b[0m\n", $0; next } 733 1' 734 } 735 736 # show a command, then run it 737 showrun() { 738 printf "\x1b[7m%s\x1b[0m\n" "$*" && "$@" 739 } 740 741 # SKIP the first n lines, or the 1st line by default 742 skip() { 743 tail -n +$(("${1:-1}" + 1)) "${2:--}" 744 } 745 746 # SKIP the LAST n lines, or the last line by default 747 skiplast() { 748 head -n -"${1:-1}" "${2:--}" 749 } 750 751 # Styled LEAK, runs my script `leak` 752 sleak() { 753 leak "$@" 754 } 755 756 # Show Latest Podcasts, using my scripts `podfeed` and `si` 757 slp() { 758 local title 759 title="Latest Podcast Episodes as of $(date +'%F %T')" 760 podfeed -title "${title}" "$@" | si 761 } 762 763 # turn lines of Space-Separated Values into lines of Tab-Separated Values 764 ssv2tsv() { 765 awk 1 "$@" | sed -E 's-^ +--; s- *\r?--; s- +-\t-g' 766 } 767 768 # change color/style of lines, using my script `ecoli` 769 style() { 770 local style="${1:-plain}" 771 shift 772 awk 1 "$@" | ecoli . "${style}" 773 } 774 775 # show a random command defined in `shshsh` or `clam`, using `wat` 776 # surprise() { 777 # wat "$( 778 # cat "$(which shshsh)" "$(which clam)" | grep -E '^[a-z]+\(' | 779 # shuf -n 1 | sed -E 's-\(.*--' 780 # )" 781 # } 782 783 # show a random command defined in `clam`, using `wat` 784 surprise() { 785 wat "$( 786 grep -E '^[a-z]+\(' "$(which clam)" | shuf -n 1 | sed -E 's-\(.*--' 787 )" 788 } 789 790 # show a reverse-sorted tally of all lines read, where ties are sorted 791 # alphabetically 792 tally() { 793 printf "value\ttally\n" 794 awk ' 795 { tally[$0]++ } 796 END { for (k in tally) { printf "%s\t%d\n", k, tally[k] } } 797 ' "$@" | sort -t ' ' -rnk2 -k1d 798 } 799 800 # Tab AWK: TSV-specific I/O settings for `awk` 801 tawk() { 802 awk -F "\t" -v OFS="\t" "$@" 803 } 804 805 title() { 806 local title="${1:-no title given}" 807 shift 808 awk -v t="${title}" 'BEGIN { printf "\x1b[7m%s\x1b[0m\n", t } 1' "$@" 809 } 810 811 # Transform Json, using an interactive Editor for the formula/expression 812 tje() { 813 if [ "$1" = "=" ]; then 814 shift 815 tj '=' "$(micro --readonly true -filetype python | leak --inv)" "$@" 816 else 817 tj "$(micro --readonly true -filetype python | leak --inv)" "$@" 818 fi 819 } 820 821 # Transform Json, using the Micro editor for the formula/expression 822 tjm() { 823 if [ "$1" = "=" ]; then 824 shift 825 tj '=' "$(micro --readonly true -filetype python | leak --inv)" "$@" 826 else 827 tj "$(micro --readonly true -filetype python | leak --inv)" "$@" 828 fi 829 } 830 831 # Transform Json (Node), using an interactive Editor for the expression 832 tjne() { 833 if [ "$1" = "=" ]; then 834 shift 835 tjn '=' "$(micro --readonly true -filetype js | leak --inv)" "$@" 836 else 837 tjn "$(micro --readonly true -filetype js | leak --inv)" "$@" 838 fi 839 } 840 841 # Transform Json (Node), using the Micro editor for the formula/expression 842 tjnm() { 843 if [ "$1" = "=" ]; then 844 shift 845 tjn '=' "$(micro --readonly true -filetype js | leak --inv)" "$@" 846 else 847 tjn "$(micro --readonly true -filetype js | leak --inv)" "$@" 848 fi 849 } 850 851 # Transform Lines, using an interactive Editor for the formula/expression 852 tle() { 853 if [ "$1" = "=" ]; then 854 shift 855 tl '=' "$(micro --readonly true -filetype python | leak --inv)" "$@" 856 else 857 tl "$(micro --readonly true -filetype python | leak --inv)" "$@" 858 fi 859 } 860 861 # Transform Lines, using the Micro editor for the formula/expression 862 tlm() { 863 if [ "$1" = "=" ]; then 864 shift 865 tl '=' "$(micro --readonly true -filetype python | leak --inv)" "$@" 866 else 867 tl "$(micro --readonly true -filetype python | leak --inv)" "$@" 868 fi 869 } 870 871 # Transform Lines (Node), using the Micro editor for the formula/expression 872 tlne() { 873 if [ "$1" = "=" ]; then 874 shift 875 tln '=' "$(micro --readonly true -filetype js | leak --inv)" "$@" 876 else 877 tln "$(micro --readonly true -filetype js | leak --inv)" "$@" 878 fi 879 } 880 881 # Transform Lines (Node), using the Micro editor for the formula/expression 882 tlnm() { 883 if [ "$1" = "=" ]; then 884 shift 885 tln '=' "$(micro --readonly true -filetype python | leak --inv)" "$@" 886 else 887 tln "$(micro --readonly true -filetype python | leak --inv)" "$@" 888 fi 889 } 890 891 # show current date in a specifc format, which is both people-friendly 892 # and machine/tool/search/automation-friendly 893 today() { 894 date +'%Y-%m-%d %a %b %d' 895 } 896 897 # show all files directly in the folder given, without looking any deeper 898 topfiles() { 899 local arg 900 for arg in "${@:-.}"; do 901 find "${arg}" -maxdepth 1 -type f 902 done 903 } 904 905 # show all folders directly in the folder given, without looking any deeper 906 topfolders() { 907 local arg 908 for arg in "${@:-.}"; do 909 find "${arg}" -maxdepth 1 -type d | awk 'NR > 1' 910 done 911 } 912 913 # try running a command, emitting an explicit message to standard-error 914 # if the command given fails 915 try() { 916 "$@" || ( 917 # printf "%s: failure running %s\n" "$0" "$*" >&2 918 printf "\n\x1b[31mrunning \x1b[41m\x1b[97m %s \x1b[0m\x1b[31m failed\x1b[0m\n" "$*" >&2 919 return 255 920 ) 921 } 922 923 # TimeStamp lines satisfying an AWK condition, ignoring all other lines 924 tsawk() { 925 # -v line="\x1b[38;5;27m%s\x1b[0m %s\n" 926 awk \ 927 -v line="\x1b[48;5;255m\x1b[38;5;24m%s\x1b[0m %s\n" \ 928 -v time="%Y-%m-%d %H:%M:%S" \ 929 "${1:-1} { printf line, strftime(time), \$0; fflush() }" 930 } 931 932 # run my script `realign` using tab as the only field-separator, 933 # which means the resulting TSV items can have spaces in them 934 tsv2ssv() { 935 realign --tsv "$@" 936 } 937 938 # UNcompress GZip data 939 ungz() { 940 gzip -d 941 } 942 943 # UNcompress GZIP data 944 ungzip() { 945 gzip -d 946 } 947 948 # deduplicate lines, keeping them in their original order 949 unique() { 950 awk '!c[$0]++' "$@" 951 } 952 953 # concatenate all named input sources, ignoring trailing CRLFs into LFs, 954 # and guaranteeing lines from different sources are accidentally joined, 955 # by adding a line-feed when an input's last line doesn't end with one; 956 # also, ignore leading UTF-8 BOMs on the first line of each input, as 957 # those are useless at best 958 unixify() { 959 awk 'FNR == 1 { gsub(/^\xef\xbb\xbf/, "") } 1' "$@" | sed -E 's-\r$--' 960 } 961 962 # turn lines of Space-Separated Values into lines of Tab-Separated Values 963 unssv() { 964 awk 1 "$@" | sed -E 's-^ +--; s- *\r?--; s- +-\t-g' 965 } 966 967 # go UP n folders, or go up 1 folder by default 968 up() { 969 if [ "${1:-1}" -le 0 ]; then 970 cd . 971 return "$?" 972 fi 973 974 cd "$(printf "%${1:-1}s" "" | sed 's- -../-g')" || return $? 975 } 976 977 # View with `less` 978 v() { 979 less -JMKiCRS "$@" 980 } 981 982 # run a command, showing its success/failure right after 983 verdict() { 984 local code 985 986 "$@" 987 code="$?" 988 989 if [ "${code}" -eq 0 ]; then 990 printf "\n\x1b[38;5;29m%s \x1b[48;5;29m\x1b[97m succeeded \x1b[0m\n" "$*" >&2 991 return 0 992 fi 993 994 printf "\n\x1b[31m%s \x1b[41m\x1b[97m failed with error code ${code} \x1b[0m\n" "$*" >&2 995 return "${code}" 996 } 997 998 # View with Header runs `less` without line numbers, with ANSI styles, with 999 # no line-wrapping, and using the first line as a sticky-header, so it always 1000 # shows on top 1001 vh() { 1002 less --header=1 -JMKiCRS "$@" 1003 } 1004 1005 # What Are These (?) shows what the names given to it are/do 1006 wat() { 1007 local a 1008 local res 1009 local code=0 1010 1011 for a in "$@"; do 1012 printf "\x1b[48;5;253m\x1b[38;5;26m%-80s\x1b[0m\n" "${a}" 1013 ( 1014 alias "${a}" || declare -f "${a}" || which "${a}" || type "${a}" 1015 ) 2> /dev/null 1016 res="$?" 1017 1018 if [ "${res}" -ne 0 ]; then 1019 code="${res}" 1020 printf "\x1b[31m%s not found\x1b[0m\n" "${a}" 1021 fi 1022 done 1023 1024 return "${code}" 1025 } 1026 1027 # Word-Count Plus runs `wc` and enriches its output; uses my scripts `nn` 1028 # and `cext` 1029 wcp() { 1030 wc "$@" | sort -rn | nn | cext | awk '{ printf "%6d %s\n", NR - 1, $0 }' 1031 } 1032 1033 # find all files which have at least 1 line with trailing spaces/CRs, with 1034 # the option to limit the (fully-recursive) search to the files/folders given 1035 wheretrails() { 1036 rg -c '[ \r]+$' "${@:-.}" 1037 } 1038 1039 # find all files which have at least 1 line with trailing spaces/CRs, with 1040 # the option to limit the (fully-recursive) search to the files/folders given 1041 whichtrails() { 1042 rg -c '[ \r]+$' "${@:-.}" 1043 } 1044 1045 # emit each word-like item from each input line on its own line 1046 words() { 1047 awk '{ for (i = 1; i <= NF; i++) print $i }' "$@" 1048 } 1049 1050 # run `xargs`, using whole lines as extra arguments 1051 x() { 1052 xargs -d '\n' "$@" 1053 } 1054 1055 # Youtube Audio Player 1056 yap() { 1057 local url="$(echo "${1}" | sed 's-&.*--')" 1058 mpv "$(yt-dlp -f 140 --get-url "${url}" 2> /dev/null)" 1059 } 1060 1061 # Youtube Download 1062 yd() { 1063 yt-dlp "$@" 1064 } 1065 1066 # Youtube Download AAC audio 1067 ydaac() { 1068 yt-dlp -f 140 "$@" 1069 } 1070 1071 # Youtube Download MP4 video 1072 ydmp4() { 1073 yt-dlp -f 22 "$@" 1074 } 1075 1076 # show a calendar for the current year, or for the year given 1077 year() { 1078 # debian linux has a different `cal` app which highlights the day 1079 if [ -e "/usr/bin/ncal" ]; then 1080 ncal -C -y "$@" 1081 else 1082 cal -y "$@" 1083 fi 1084 } 1085 1086 # show the current date in the YYYY-MM-DD format 1087 ymd() { 1088 date +'%Y-%m-%d' 1089 }