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