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