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