File: shshsh.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 # shshsh
  27 # SHort SHell SHortcuts: 1..3-letter commands for your shell
  28 #
  29 # This is a collection of arguably useful shell functions and shortcuts:
  30 # some of these can be real time/effort savers, letting you concentrate
  31 # on getting things done.
  32 #
  33 # You're supposed to `source` this script, so its definitions stay for
  34 # your whole shell session: for that, you can run `source shshsh` or
  35 # `. shshsh` (no quotes either way), either directly or at shell startup.
  36 #
  37 # These shell functions rely on tools which are almost always available.
  38 #
  39 #
  40 # Full list of funcs/commands added, with the memo-word each 1-letter name
  41 # stands for
  42 #
  43 # a    awk        run `awk`
  44 # b    blow       blow/expand tabs into spaces, using the tabstop given, or 4
  45 # bar  bar        emit a styled bar to visually separate different outputs
  46 # bh              Breathe Header makes lines breathe starting from the 2nd one
  47 # bl              Breathe Lines regularly adds extra empty lines every few
  48 # c    cat        run `cat`, which is useful, despite claims to the contrary
  49 # cjq             Colored JQ
  50 # cls             CLear Screen
  51 # crg             Colored RipGrep
  52 # cs              Color Syntax, using the `bat` CLI app
  53 # csf             Color Syntax for a whole Folder, even its subfolders
  54 # d    date       run `date`
  55 # def  define     DEFine the word given, using an online service
  56 # dic  dictionary DICtionary definitions, using an online service
  57 # div  divide     DIVide 2 numbers 3 ways, including the complement
  58 # dt   date/time  show the current Date and Time, and the 3 `current` months
  59 # e    each       run command using Each stdin line as a stdin-redirected file
  60 # eg              Extended-mode Grep
  61 # es              Extended-mode Sed
  62 # f    files      show all Files in a folder, digging recursively
  63 # ff              run the Fuzzy Finder (fzf) in multi-choice mode, and more...
  64 # fl   fix lines  Fix Lines, by ensuring they're unix-like
  65 # fz   FuZzy      run the FuZzy finder (fzf) in multi-choice mode, and more...
  66 # g    grep       run `grep` in extended mode
  67 # gbm             Good, Bad, Meh colors lines using 1..3 awk conditions
  68 # get  get        GET/fetch data from the filename or URI given
  69 # h    help       show help messages for the command given
  70 # hl              Header Less runs `less` and always shows the 1st line on top
  71 # hv              Header View runs `less` and always shows the 1st line on top
  72 # i    index      index/number all lines, starting from 0
  73 # j    join       join lines with tabs, optionally as n-item lines
  74 # k    kill       Kill processes, using all ID numbers given
  75 # l    less       run `less`, with line numbers, scrolling, and ANSI styles
  76 # lf              List Files, coloring folders and links
  77 # lh              Less with Header runs `less` and always shows the 1st line
  78 # m    match      case-sensitively match the extended-mode regex given
  79 # n    number     Number all lines, starting from the number given, or from 1
  80 # nil             emit nothing to stdout and/or discard everything from stdin
  81 # now  now        show the current date and time
  82 # o    output     unify output, by merging stderr into stdout
  83 # p    plain      ignore ANSI terminal styling, resulting in proper plain-text
  84 # q    quiet      ignore stderr
  85 # r    reflow     reflow text/prose
  86 # ro   read-only  view Read-Only, using a text editor
  87 # s    sed        run `sed` in extended mode
  88 # t    trim       trim leading/trailing spaces and carriage-returns
  89 # try  try        try running the command given
  90 # u    unique     avoid repeating lines, unlike the defective `uniq`
  91 # ul              Unique Lines
  92 # up   up         go UP n folders, or go up 1 folder by default
  93 # v    view       run `less`, enabling scrolling and ANSI styles
  94 # vh              View with Header runs `less` and always shows the 1st line
  95 # w    web        fetch a URI with `wget`, without saving the result to a file
  96 # wat             What Are These (?) shows what the names given to it are/do
  97 # wt              Which Trails finds all files with lines ending in spaces/CRs
  98 # x    xargs      run `xargs`, using whole lines as extra arguments
  99 # y    year       show a calendar for the current year, or for the year given
 100 # yap             Youtube Audio Player
 101 # yd              Youtube Download
 102 # ymd             show the current date in the YYYY-MM-DD format
 103 # z    zip        run `gzip`
 104 
 105 
 106 case "$1" in
 107     -h|--h|-help|--help)
 108         awk '/^# +shshsh/, /^$/ { gsub(/^# ?/, ""); print }' "$0"
 109         exit 0
 110     ;;
 111 esac
 112 
 113 
 114 # use a simple shell prompt
 115 # PS1="\$ "
 116 # PS2="> "
 117 
 118 # use a simple shell prompt, showing the current folder in the title
 119 # PS1="\[\e]0;\w\a\]\$ "
 120 # PS2="> "
 121 
 122 # use a simple shell prompt, showing the current folder both in the prompt
 123 # itself, and in the title
 124 # PS1="\[\e]0;\w\a\]\w\$ "
 125 # PS2="> "
 126 
 127 # prevent `less` from saving commands
 128 # export LESSHISTFILE="-"
 129 # export LESSSECURE=1
 130 
 131 # prevent the shell from saving commands
 132 # unset HISTFILE
 133 
 134 
 135 # Awk
 136 a() { awk "$@"; }
 137 
 138 # Blow tabs into spaces, using the tabstop given, or 4 by default
 139 b() {
 140     local tabstop="${1:-4}"
 141     shift
 142     expand -t "${tabstop}" "$@"
 143 }
 144 
 145 # emit a colored bar which can help visually separate different outputs
 146 bar() { printf "\e[48;5;253m%${1:-80}s\e[0m\n" " "; }
 147 
 148 # Breathe Header: add an empty line after the first one (the header), then
 149 # separate groups of 5 lines (by default) with empty lines between them
 150 bh() {
 151     local n="${1:-5}"
 152     shift
 153     awk -v n="$n" '(NR - 1) % n == 1 && NR > 1 { print "" } 1' "$@"
 154 }
 155 
 156 # Breathe Lines: separate groups of 5 lines (by default) with empty lines
 157 bl() {
 158     local n="${1:-5}"
 159     shift
 160     awk -v n="$n" 'NR % n == 1 && NR != 1 { print "" } 1' "$@"
 161 }
 162 
 163 # Cat
 164 c() { cat "$@"; }
 165 
 166 # CLear Screen
 167 cls() { clear; }
 168 
 169 # Colored JQ
 170 cjq() { jq -C "$@"; }
 171 
 172 # Colored RipGrep: ensures app `rg` emits colors when piped
 173 crg() { rg --color=always "$@"; }
 174 
 175 # Color Syntax: run syntax-coloring app `bat` without line-wrapping
 176 cs() {
 177     local cmd="bat"
 178     # debian linux uses a different name for the `bat` app
 179     if [ -e "/usr/bin/batcat" ]; then
 180         cmd="batcat"
 181     fi
 182 
 183     "$cmd" --style=plain,header,numbers --theme='Monokai Extended Light' \
 184         --wrap=never --color=always "$@" |
 185     sed 's-\x1b\[38;5;70m-\x1b\[38;5;28m-g' | less -JMKiCRS
 186 }
 187 
 188 # Color Syntax of all files in a Folder, showing line numbers
 189 csf() {
 190     local cmd="bat"
 191     # debian linux uses a different name for the `bat` app
 192     if [ -e "/usr/bin/batcat" ]; then
 193         cmd="batcat"
 194     fi
 195 
 196     find "${1:-.}" -type f -print0 | xargs --null "$cmd" \
 197         --style=plain,header,numbers --theme='Monokai Extended Light' \
 198         --wrap=never --color=always |
 199     sed 's-\x1b\[38;5;70m-\x1b\[38;5;28m-g' | less -JMKiCRS
 200 }
 201 
 202 # Date
 203 d() { date "$@"; }
 204 
 205 # DEFine the word given, using an online service
 206 def() {
 207     curl -s "dict://dict.org/d:$*" | awk '
 208         /^151 / { printf "\x1b[38;5;4m%s\x1b[0m\n", $0; next }
 209         /^[1-9][0-9]{2} / { printf "\x1b[38;5;244m%s\x1b[0m\n", $0; next }
 210         1'
 211 }
 212 
 213 # DICtionary definitions, using an online service
 214 dic() {
 215     curl -s "dict://dict.org/d:$*" | awk '
 216         /^151 / { printf "\x1b[38;5;4m%s\x1b[0m\n", $0; next }
 217         /^[1-9][0-9]{2} / { printf "\x1b[38;5;244m%s\x1b[0m\n", $0; next }
 218         1'
 219 }
 220 
 221 # DIVide 2 numbers 3 ways, including the complement
 222 div() {
 223     awk -v a="${1:-1}" -v b="${2:-1}" '
 224     BEGIN {
 225         gsub(/_/, "", a)
 226         gsub(/_/, "", b)
 227         if (a > b) { c = a; a = b; b = c; }
 228         print a / b
 229         print b / a
 230         print 1 - a / b
 231         exit
 232     }'
 233 }
 234 
 235 # show the current Date and Time, and the 3 `current` months
 236 dt() {
 237     # debian linux has a different `cal` app which highlights the day
 238     if [ -e "/usr/bin/ncal" ]; then
 239         ncal -C -3
 240     else
 241         cal -3
 242     fi
 243 
 244     # show the current time center-aligned
 245     # printf "%28s\e[34m%s\e[0m\n" " " "$(date +'%T')"
 246     printf "%22s\e[32m%s\e[0m  \e[34m%s\e[0m\n" " " \
 247         "$(date +'%a %b %d')" "$(date +'%T')"
 248 }
 249 
 250 # run command using each stdin line as a stdin-redirected file
 251 # e() {
 252 #     local arg
 253 #     IFS=""
 254 #     while read -r arg; do
 255 #         "$@" < "${arg}"
 256 #     done
 257 # }
 258 
 259 # Extended-mode Grep
 260 eg() { grep -E "$@"; }
 261 
 262 # Extended-mode Sed
 263 es() { sed -E "$@"; }
 264 
 265 # Find all files recursively in all folders given, or the current one
 266 f() {
 267     local arg
 268     for arg in "${@:-.}"; do
 269         find "${arg}" -type f
 270     done
 271 }
 272 
 273 # run the Fuzzy Finder (fzf) in multi-choice mode, with custom keybindings
 274 ff() { fzf -m --bind ctrl-a:select-all,ctrl-space:toggle "$@"; }
 275 
 276 # Fix lines, ignoring leading UTF-8_BOMs (byte-order-marks) on each input's
 277 # first line, turning all end-of-line CRLF byte-pairs into single line-feeds,
 278 # and ensuring each input's last line ends with a line-feed
 279 fl() {
 280     awk 'FNR == 1 { gsub(/^\xef\xbb\xbf/, "") } 1' "$@" | sed -E 's-\r$--'
 281 }
 282 
 283 # run the FuZzy finder (fzf) in multi-choice mode, with custom keybindings
 284 fz() { fzf -m --bind ctrl-a:select-all,ctrl-space:toggle "$@"; }
 285 
 286 # run Grep in extended mode
 287 g() { grep -E "$@"; }
 288 
 289 # GET/fetch data from the filename or URI given
 290 get() {
 291     case "$1" in
 292         http://*|https://*|file://*|ftp://*|ftps://*|sftp://*|dict://*)
 293             # curl -s "$1"
 294             curl -s "$1" || (
 295                 printf "\e[31mcan't get %s\e[0m\n" "$1" >&2
 296                 return 1
 297             )
 298         ;;
 299         *)
 300             cat "$1"
 301         ;;
 302     esac
 303 }
 304 
 305 # Good, Bad, Meh colors lines using 1..3 awk conditions
 306 gbm() {
 307     local good="${1:-0}"
 308     shift
 309     local bad="${1:-0}"
 310     shift
 311     local meh="${1:-0}"
 312     shift
 313 
 314     awk "
 315         ${good} {
 316             # code to use a color-blind-friendlier blue, instead of green
 317             # gsub(/\\x1b\\[0m/, \"\\x1b[0m\\x1b[38;5;26m\")
 318             # printf \"\\x1b[38;5;26m%s\\x1b[0m\\n\", \$0
 319             gsub(/\\x1b\\[0m/, \"\\x1b[0m\\x1b[38;5;29m\")
 320             printf \"\\x1b[38;5;29m%s\\x1b[0m\\n\", \$0
 321             next
 322         }
 323 
 324         ${bad} {
 325             gsub(/\\x1b\\[0m/, \"\\x1b[0m\\x1b[38;5;1m\")
 326             printf \"\\x1b[38;5;1m%s\\x1b[0m\\n\", \$0
 327             next
 328         }
 329 
 330         ${meh} {
 331             gsub(/\\x1b\\[0m/, \"\\x1b[0m\\x1b[38;5;248m\")
 332             printf \"\\x1b[38;5;248m%s\\x1b[0m\\n\", \$0
 333             next
 334         }
 335 
 336         { print }
 337     " "$@"
 338 }
 339 
 340 # Help for the command given
 341 h() { man "${1}" || ("${1}" "${2:---help}" 2>&1 | less -JMKiCRS); }
 342 
 343 # Header Less runs `less` with line numbers, ANSI styles, no line-wrapping,
 344 # and using the first line as a sticky-header, so it always shows on top
 345 hl() { less --header=1 -JMKNiCRS "$@"; }
 346 
 347 # Header View runs `less` without line numbers, with ANSI styles, with no
 348 # line-wrapping, and using the first line as a sticky-header, so it always
 349 # shows on top
 350 hv() { less --header=1 -JMKiCRS "$@"; }
 351 
 352 # Index/number all lines, starting from 0
 353 i() { awk '{ printf "%d\t%s\n", NR - 1, $0 }' "$@"; }
 354 
 355 # Join lines with tabs, optionally as n-item lines
 356 j() {
 357     local n="${1:-0}"
 358     shift
 359 
 360     if [ "${n}" -le 0 ]; then
 361         awk '
 362             NR > 1 { printf "\t" }
 363             { printf "%s", $0 }
 364             END { if (NR > 0) print "" }' "$@"
 365         return "$?"
 366     fi
 367 
 368     awk -v n="${n}" '
 369         NR % n != 1 { printf "\t" }
 370         { printf "%s", $0 }
 371         NR % n == 0 { print "" }
 372         END { if (NR % n != 0) print "" }' "$@"
 373 }
 374 
 375 # Kill processes, using all ID numbers given
 376 # k() { kill -9 "$@"; }
 377 
 378 # run `less`, showing line numbers, among other settings
 379 l() { less -JMKNiCRS "$@"; }
 380 
 381 # list files, coloring folders and links
 382 lf() {
 383     ls -al --file-type --color=never --time-style iso "$@" | awk '
 384         /^d/ { printf "\x1b[38;5;33m%s\x1b[0m\n", $0; next }
 385         /^l/ { printf "\x1b[38;5;29m%s\x1b[0m\n", $0; next }
 386         1'
 387 }
 388 
 389 # Less with Header runs `less` with line numbers, ANSI styles, no line-wraps,
 390 # and using the first line as a sticky-header, so it always shows on top
 391 lh() { less --header=1 -JMKNiCRS "$@"; }
 392 
 393 # Match regex given, or non-empty lines by default
 394 m() {
 395     local regex="${1:-[^ *]\r?$}"
 396     shift
 397     grep -E "${regex}" "$@"
 398 }
 399 
 400 # Number all lines, starting from the number given, or 1 by default
 401 n() {
 402     local n="${1:-1}"
 403     shift
 404     awk -v n="$n" '{ printf "%d\t%s\n", NR - 1 + n, $0 }' "$@"
 405 }
 406 
 407 # emit nothing to output and/or discard everything from input
 408 nil() {
 409     if [ -p /dev/stdin ]; then
 410         cat > /dev/null
 411     else
 412         head -c 0
 413     fi
 414 }
 415 
 416 # show the current date and time
 417 now() { date +'%Y-%m-%d %H:%M:%S'; }
 418 
 419 # unify Output, by merging stderr into stdout
 420 o() { "$@" 2>&1; }
 421 
 422 # Plain text
 423 p() {
 424     awk '
 425     {
 426         # ignore cursor-movers and style-changers
 427         gsub(/\x1b\[([0-9]*[A-HJKSThl]|[0-9;]*m)/, "")
 428 
 429         print
 430     }' "$@"
 431 }
 432 
 433 # make stderr `Quiet`, by ignoring it
 434 q() { "$@" 2> /dev/null; }
 435 
 436 # Reflow text/prose
 437 r() {
 438     local width="${1:-80}"
 439     shift
 440     awk '
 441         FNR == 1 { gsub(/^\xef\xbb\xbf/, "") }
 442         FNR == 1 && NR > 1 { print "" }
 443         1' "$@" | sed -E 's-\r$--; s- +$- -' |
 444             fold -s -w="$width" | sed -E 's- +$--'
 445 }
 446 
 447 # view in Read-Only mode, using a text editor
 448 ro() { micro --readonly true "$@"; }
 449 
 450 # run Sed in extended mode
 451 s() { sed -E "$@"; }
 452 
 453 # Trim leading/trailing spaces in lines, even trim trailing carriage-returns;
 454 # also ignore leading UTF-8_BOMs (byte-order-marks) on each input's first line
 455 t() {
 456     awk 'FNR == 1 { gsub(/^\xef\xbb\xbf/, "") } 1' "$@" |
 457         sed -E 's-^ +--; s- *\r?$--'
 458 }
 459 
 460 # try running a command, emitting an explicit message to standard-error
 461 # if the command given fails
 462 try() {
 463     "$@" || (
 464         printf "%s: failure running %s\n" "$0" "$*" >&2
 465         return 255
 466     )
 467 }
 468 
 469 # Unique lines, which actually works unlike the purposely-defective `uniq`
 470 u() { awk '!c[$0]++' "$@"; }
 471 
 472 # Unique Lines
 473 ul() { awk '!c[$0]++' "$@"; }
 474 
 475 # go UP n folders, or go up 1 folder by default
 476 up() {
 477     if [ "${1:-1}" -le 0 ]; then
 478         cd .
 479         return "$?"
 480     fi
 481 
 482     cd "$(printf "%${1:-1}s" "" | sed 's- -../-g')" || return $?
 483 }
 484 
 485 # View with `less`
 486 v() { less -JMKiCRS "$@"; }
 487 
 488 # View with Header runs `less` without line numbers, with ANSI styles, with
 489 # no line-wrapping, and using the first line as a sticky-header, so it always
 490 # shows on top
 491 vh() { less --header=1 -JMKiCRS "$@"; }
 492 
 493 # fetch a URI with `wget`, without saving the result to a file
 494 w() {
 495     # wget -O - "$@"
 496     # curl --no-progress-meter "$@"
 497     wget --no-verbose -O - "$@"
 498 }
 499 
 500 # What Are These (?) shows what the names given to it are/do
 501 wat() {
 502     local a
 503     local res
 504     local code=0
 505 
 506     for a in "$@"; do
 507         printf "\e[48;5;253m\e[38;5;26m%-80s\e[0m\n" "${a}"
 508         (
 509             alias "${a}" || declare -f "${a}" || which "${a}" || type "${a}"
 510         ) 2> /dev/null
 511         res="$?"
 512 
 513         if [ "${res}" -ne 0 ]; then
 514             code="${res}"
 515             printf "\e[31m%s not found\e[0m\n" "${a}"
 516         fi
 517     done
 518 
 519     return "${code}"
 520 }
 521 
 522 # Which/where Trails finds all files with lines ending in spaces/CRs
 523 wt() { rg -c '[ \r]+$' "${@:-.}"; }
 524 
 525 # run `xargs`, using whole lines as extra arguments
 526 x() { xargs -d '\n' "$@"; }
 527 
 528 # run `xargs`, using whole lines as extra arguments
 529 xl() { xargs -d '\n' "$@"; }
 530 
 531 # show a calendar for the current year, or for the year given
 532 y() {
 533     # debian linux has a different `cal` app which highlights the day
 534     if [ -e "/usr/bin/ncal" ]; then
 535         ncal -C -y "$@"
 536     else
 537         cal -y "$@"
 538     fi
 539 }
 540 
 541 # Youtube Audio Player
 542 yap() {
 543     local url="$(echo "${1}" | sed 's-&.*--')"
 544     mpv "$(yt-dlp -f 140 --get-url "${url}" 2> /dev/null)"
 545 }
 546 
 547 # Youtube Download
 548 yd() { yt-dlp "$@"; }
 549 
 550 # show the current date in the YYYY-MM-DD format
 551 ymd() { date +'%Y-%m-%d'; }
 552 
 553 # zip/unzip, using the `gzip` format
 554 z() { gzip "$@"; }