#!/bin/sh # The MIT License (MIT) # # Copyright © 2020-2025 pacman64 # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the “Software”), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # clam # # Command-Line Augmentation Module (clam): get the best out of your shell # # # This is a collection of arguably useful shell functions and shortcuts: # some of these extra commands can be real time/effort savers, ideally # letting you concentrate on getting things done. # # Some of these commands depend on my other scripts from the `pac-tools`, # others either rely on widely-preinstalled command-line apps, or ones # which are available on most of the major command-line `package` managers. # # Among these commands, you'll notice a preference for lines whose items # are tab-separated instead of space-separated, and unix-style lines, which # always end with a line-feed, instead of a CRLF byte-pair. This convention # makes plain-text data-streams less ambiguous and generally easier to work # with, especially when passing them along pipes. # # To use this script, you're supposed to `source` it, so its definitions # stay for your whole shell session: for that, you can run `source clam` or # `. clam` (no quotes either way), either directly or at shell startup. # # This script is compatible with `bash`, `zsh`, and even `dash`, which is # debian linux's default non-interactive shell. Some of its commands even # seem to work on busybox's shell. # handle help options case "$1" in -h|--h|-help|--help) # show help message, using the info-comment from this very script awk '/^# +clam/, /^$/ { gsub(/^# ?/, ""); print }' "$0" exit 0 ;; esac # dash doesn't support regex-matching syntax, forcing to use case statements case "$0" in -bash|-dash|-sh|bash|dash|sh) # script is being sourced with bash or dash, which is good : ;; *) case "$ZSH_EVAL_CONTEXT" in *:file) # script is being sourced with zsh, which is good : ;; *) # script is being run normally, which is a waste of time printf "\e[48;2;255;255;135m\e[30mDon't run this script, source it instead: to do that,\e[0m\n" printf "\e[48;2;255;255;135m\e[30mrun 'source clam' or '. clam' (no quotes either way).\e[0m\n" # failing during shell-startup may deny shell access, so exit # with a 0 error-code to declare success exit 0 ;; esac ;; esac # n-Column-layout shortcuts, using my script `bsbs` (Book-like Side By Side) c1() { bsbs 1 "$@"; } c2() { bsbs 2 "$@"; } c3() { bsbs 3 "$@"; } c4() { bsbs 4 "$@"; } c5() { bsbs 5 "$@"; } c6() { bsbs 6 "$@"; } c7() { bsbs 7 "$@"; } c8() { bsbs 8 "$@"; } c9() { bsbs 9 "$@"; } # n-Column-layout shortcuts, using my script `bsbs` (Book-like Side By Side) alias 1=c1 alias 2=c2 alias 3=c3 alias 4=c4 alias 5=c5 alias 6=c6 alias 7=c7 alias 8=c8 alias 9=c9 # n-Column-layout shortcuts, using my script `bsbs` (Book-like Side By Side) alias 1c=c1 alias 2c=c2 alias 3c=c3 alias 4c=c4 alias 5c=c5 alias 6c=c6 alias 7c=c7 alias 8c=c8 alias 9c=c9 # Avoid/ignore lines which match any of the regexes given a() { awk ' BEGIN { for (i = 1; i < ARGC; i++) { e[i] = ARGV[i] delete ARGV[i] } } { for (i = 1; i < ARGC; i++) if ($0 ~ e[i]) next print; fflush() got++ } END { exit(got == 0) } ' "${@:-^\r?$}" } # find name from the local `apt` database of installable packages # aptfind() { # # despite warnings, the `apt search` command has been around for years # # apt search "$1" 2>/dev/null | rg -A 1 "^$1" | sed -u 's/^--$//' # apt search "$1" 2>/dev/null | rg -A 1 "^[a-z0-9-]*$1" | sed -u 's/^--$//' # } # emit each argument given as its own line of output args() { awk 'BEGIN { for (i = 1; i < ARGC; i++) print ARGV[i]; exit }' "$@"; } # turn UTF-8 into visible pseudo-ASCII, where variants of latin letters become # their basic ASCII counterparts, and where non-ASCII symbols become question # marks, one question mark for each code-point byte asciify() { iconv -f utf-8 -t ascii//translit "$@"; } # avoid/ignore lines which match any of the regexes given avoid() { awk ' BEGIN { for (i = 1; i < ARGC; i++) { e[i] = ARGV[i] delete ARGV[i] } } { for (i = 1; i < ARGC; i++) if ($0 ~ e[i]) next print; fflush() got++ } END { exit(got == 0) } ' "${@:-^\r?$}" } # emit a line with a repeating ball-like symbol in it balls() { [ "${1:-80}" -gt 0 ] && printf "%${1:-80}s\n" "" | sed 's- -●-g'; } # show an ansi-styled BANNER-like line banner() { printf "\e[7m%s\e[0m\n" "$*"; } # emit a colored bar which can help visually separate different outputs bar() { [ "${1:-80}" -gt 0 ] && printf "\e[48;2;218;218;218m%${1:-80}s\e[0m\n" "" } # process Blocks/paragraphs of non-empty lines with AWK # bawk() { awk -F='' -v RS='' "$@"; } # process Blocks/paragraphs of non-empty lines with AWK bawk() { stdbuf -oL awk -F='' -v RS='' "$@"; } # play a repeating and annoying high-pitched beep sound a few times a second, # lasting the number of seconds given, or for 1 second by default; uses my # script `waveout` beeps() { local f='sin(2_000 * tau * t) * (t % 0.5 < 0.0625)' waveout "${1:-1}" "${2:-1} * $f" | mpv --really-quiet - } # start by joining all arguments given as a tab-separated-items line of output, # followed by all lines from stdin verbatim begintsv() { awk ' BEGIN { for (i = 1; i < ARGC; i++) { if (i > 1) printf "\t" printf "%s", ARGV[i] delete ARGV[i] } if (ARGC > 1) printf "\n" fflush() } { print; fflush() } ' "$@" } # play a repeating synthetic-bell-like sound lasting the number of seconds # given, or for 1 second by default; uses my script `waveout` bell() { local f='sin(880*tau*u) * exp(-10*u)' waveout "${1:-1}" "${2:-1} * $f" | mpv --really-quiet - } # play a repeating sound with synthetic-bells, lasting the number of seconds # given, or for 1 second by default; uses my script `waveout` bells() { local f="sum(sin(880*tau*v)*exp(-10*v) for v in (u, (u-0.25)%1)) / 2" waveout "${1:-1}" "${2:-1} * $f" | mpv --really-quiet - } # Breathe Header: add an empty line after the first one (the header), then # separate groups of 5 lines (by default) with empty lines between them bh() { local n="${1:-5}" [ $# -gt 0 ] && shift awk -v n="$n" ' BEGIN { if (n == 0) n = -1 } (NR - 1) % n == 1 && NR > 1 { print "" } { print; fflush() } ' "$@" } # recursively find all files with at least the number of bytes given; when # not given a minimum byte-count, the default is 100 binary megabytes bigfiles() { local n n="$(echo "${1:-104857600}" | sed -E 's-_--g; s-\.[0-9]+$--')" [ $# -gt 0 ] && shift local arg for arg in "${@:-.}"; do if [ ! -d "${arg}" ]; then printf "\e[31mno folder named %s\e[0m\n" "${arg}" > /dev/stderr return 1 fi stdbuf -oL find "${arg}" -type f -size "$n"c -o -size +"$n"c done } # Breathe Lines: separate groups of 5 lines (by default) with empty lines bl() { local n="${1:-5}" [ $# -gt 0 ] && shift awk -v n="$n" ' BEGIN { if (n == 0) n = -1 } NR % n == 1 && NR != 1 { print "" } { print; fflush() } ' "$@" } # process BLocks/paragraphs of non-empty lines with AWK # blawk() { awk -F='' -v RS='' "$@"; } # process BLocks/paragraphs of non-empty lines with AWK blawk() { stdbuf -oL awk -F='' -v RS='' "$@"; } # emit a line with a repeating block-like symbol in it blocks() { [ "${1:-80}" -gt 0 ] && printf "%${1:-80}s\n" "" | sed 's- -█-g'; } # Book-like MANual, lays out `man` docs as pairs of side-by-side pages; uses # my script `bsbs` bman() { local w w="$(tput cols)" if [ "$w" -gt 120 ]; then w="$((w / 2 - 1))" fi MANWIDTH="$w" man "$@" | bsbs 2 } # split lines using the regex given, turning them into single-item lines breakdown() { local sep="${1:- }" [ $# -gt 0 ] && shift awk -F "${sep}" '{ for (i = 1; i <= NF; i++) print $i; fflush() }' "$@" } # BOOK-like MANual, lays out `man` docs as pairs of side-by-side pages; uses # my script `bsbs` bookman() { local w w="$(tput cols)" if [ "$w" -gt 120 ]; then w="$((w / 2 - 1))" fi MANWIDTH="$w" man "$@" | bsbs 2 } # separate groups of 5 lines (by default) with empty lines breathe() { local n="${1:-5}" [ $# -gt 0 ] && shift awk -v n="$n" ' BEGIN { if (n == 0) n = -1 } NR % n == 1 && NR != 1 { print "" } { print; fflush() } ' "$@" } # Browse Text bt() { less -JMKNiCRS "$@"; } # show a reverse-sorted tally of all lines read, where ties are sorted # alphabetically, and where trailing bullets are added to quickly make # the tally counts comparable at a glance bully() { awk -v sort="sort -t \"$(printf '\t')\" -rnk2 -k1d" ' # reassure users by instantly showing the header BEGIN { print "value\ttally\tbullets"; fflush() } { gsub(/\r$/, ""); tally[$0]++ } END { # find the max tally, which is needed to build the bullets-string max = 0 for (k in tally) { if (max < tally[k]) max = tally[k] } # make enough bullets for all tallies: this loop makes growing the # string a task with complexity O(n * log n), instead of a naive # O(n**2), which can slow-down things when tallies are high enough bullets = "•" for (n = max; n > 1; n /= 2) { bullets = bullets bullets } # emit unsorted output lines to the sort cmd, which will emit the # final reverse-sorted tally lines for (k in tally) { s = substr(bullets, 1, tally[k]) printf("%s\t%d\t%s\n", k, tally[k], s) | sort } } ' "$@" } # play a busy-phone-line sound lasting the number of seconds given, or for 1 # second by default; uses my script `waveout` busy() { # local f='(u < 0.5) * (sin(480*tau * t) + sin(620*tau * t)) / 2' local f='min(1, exp(-90*(u-0.5))) * (sin(480*tau*t) + sin(620*tau*t)) / 2' # local f='(sin(350*tau*t) + sin(450*tau*t)) / 2 * min(1, exp(-90*(u-0.5)))' waveout "${1:-1}" "${2:-1} * $f" | mpv --really-quiet - } # keep all BUT the FIRST (skip) n lines, or skip just the 1st line by default butfirst() { tail -n +$(("${1:-1}" + 1)) "${2:--}"; } # keep all BUT the LAST n lines, or skip just the last line by default butlast() { head -n -"${1:-1}" "${2:--}"; } # load bytes from the filenames given bytes() { cat "$@"; } # quick alias for `cat` c() { cat "$@"; } # CAlculator with Nice numbers runs my script `ca` and colors results with # my script `nn`, alternating styles to make long numbers easier to read can() { ca "$@" | nn --gray; } # conCATenate Lines ignores leading byte-order marks on first lines, trailing # carriage-returns, and guarantees no lines are ever accidentally joined # across inputs, always emitting a line-feed at the end of every line catl() { awk ' FNR == 1 { gsub(/^\xef\xbb\xbf/, "") } { gsub(/\r$/, ""); print; fflush() } ' "$@" } # Csv AWK: CSV-specific input settings for `awk` # cawk() { awk --csv "$@"; } # Csv AWK: CSV-specific input settings for `awk` cawk() { stdbuf -oL awk --csv "$@"; } # Compile C Stripped ccs() { cc -Wall -O2 -s -fanalyzer "$@"; } # Colored Go Test on the folder given; uses my command `gbm` cgt() { go test "${1:-.}" 2>&1 | gbm '^ok' '^[-]* ?FAIL' '^\?'; } # ignore final life-feed from text, if it's the very last byte; also ignore # all trailing carriage-returns choplf() { awk ' FNR == 1 { gsub(/^\xef\xbb\xbf/, "") } NR > 1 { print ""; fflush() } { gsub(/\r$/, ""); printf "%s", $0; fflush() } ' "$@" } # Color Json using the `jq` app, allowing an optional filepath as the data # source, and even an optional transformation formula cj() { jq -C "${2:-.}" "${1:--}"; } # clean the screen, after running the command given clean() { tput smcup; "$@"; tput rmcup; } # show a live digital clock clock() { watch -n 1 echo 'Press Ctrl + C to quit this clock'; } # Colored Live/Line-buffered RipGrep ensures results show up immediately, # also emitting colors when piped clrg() { rg --color=always --line-buffered "$@"; } # CLear Screen, like the old dos command of the same name cls() { clear; } # COunt COndition: count how many times the AWK expression given is true coco() { local cond="${1:-1}" [ $# -gt 0 ] && shift awk " { low = lower = tolower(\$0) } ${cond} { count++ } END { print count } " "$@" } # Colored RipGrep ensures app `rg` emits colors when piped crg() { rg --color=always --line-buffered "$@"; } # emit a line with a repeating cross-like symbol in it crosses() { [ "${1:-80}" -gt 0 ] && printf "%${1:-80}s\n" "" | sed 's- -×-g' } # split lines using the regex given, turning them into single-item lines crumble() { local sep="${1:- }" [ $# -gt 0 ] && shift awk -F "${sep}" '{ for (i = 1; i <= NF; i++) print $i; fflush() }' "$@" } # turn Comma-Separated-Values tables into Tab-Separated-Values tables csv2tsv() { xsv fmt -t '\t' "$@"; } # Change Units turns common US units into international ones; uses my # scripts `bu` (Better Units) and `nn` (Nice Numbers) cu() { bu "$@" | awk ' NF == 5 || (NF == 4 && $NF == "s") { print $(NF-1), $NF } NF == 4 && $NF != "s" { print $NF } ' | nn --gray } # CURL Silent spares you the progress bar, but still tells you about errors curls() { curl --show-error -s "$@"; } # Count With AWK: count the times the AWK expression/condition given is true cwawk() { local cond="${1:-1}" [ $# -gt 0 ] && shift awk " { low = lower = tolower(\$0) } ${cond} { count++ } END { print count } " "$@" } # emit a line with a repeating dash-like symbol in it dashes() { [ "${1:-80}" -gt 0 ] && printf "%${1:-80}s\n" "" | sed 's- -—-g'; } # DEcode BASE64-encoded data, or even base64-encoded data-URIs, by ignoring # the leading data-URI declaration, if present debase64() { sed -E 's-^data:.{0,50};base64,--' "${1:--}" | base64 -d; } # DECAPitate (lines) emits the first line as is, piping all lines after that # to the command given, passing all/any arguments/options to it # decap() { # awk -v cmd="$*" 'NR == 1 { print; fflush() } NR > 1 { print | cmd }' # } # turn Comma-Separated-Values tables into tab-separated-values tables # decsv() { xsv fmt -t '\t' "$@"; } # DEDUPlicate prevents lines from appearing more than once dedup() { awk '!c[$0]++ { print; fflush() }' "$@"; } # dictionary-define the word given, using an online service define() { local arg local gap=0 for arg in "$@"; do [ "${gap}" -gt 0 ] && printf "\n" gap=1 printf "\x1b[7m%-80s\x1b[0m\n" "${arg}" curl -s "dict://dict.org/d:${arg}" | awk ' { gsub(/\r$/, "") } /^151 / { printf "\x1b[38;2;52;101;164m%s\x1b[0m\n", $0; fflush() next } /^[1-9][0-9]{2} / { printf "\x1b[38;2;128;128;128m%s\x1b[0m\n", $0; fflush() next } { print; fflush() } ' done | less -JMKiCRS } # DEcompress GZip-encoded data # degz() { zcat "$@"; } # turn JSON Lines into a proper json array dejsonl() { jq -s -M "${@:-.}"; } # delay lines from the standard-input, waiting the number of seconds given # for each line, or waiting 1 second by default # delay() { # local seconds="${1:-1}" # ( # IFS="$(printf "\n")" # while read -r line; do # sleep "${seconds}" # printf "%s\n" "${line}" # done # ) # } # expand tabs each into up to the number of space given, or 4 by default detab() { expand -t "${1:-4}"; } # ignore trailing spaces, as well as trailing carriage returns detrail() { awk '{ gsub(/ *\r?$/, ""); print; fflush() }' "$@"; } # turn UTF-16 data into UTF-8 deutf16() { iconv -f utf16 -t utf8 "$@"; } # DIVide 2 numbers 3 ways, including the complement div() { awk -v a="${1:-1}" -v b="${2:-1}" ' BEGIN { gsub(/_/, "", a) gsub(/_/, "", b) if (a > b) { c = a; a = b; b = c } c = 1 - a / b if (0 <= c && c <= 1) printf "%f\n%f\n%f\n", a / b, b / a, c else printf "%f\n%f\n", a / b, b / a exit }' } # get/fetch data from the filename or URI given; named `dog` because dogs can # `fetch` things for you # dog() { # if [ $# -gt 1 ]; then # printf "\e[31mdogs only have 1 mouth to fetch with\e[0m\n" >&2 # return 1 # fi # # if [ -e "$1" ]; then # cat "$1" # return $? # fi # # case "${1:--}" in # -) cat -;; # file://*|https://*|http://*) curl --show-error -s "$1";; # ftp://*|ftps://*|sftp://*) curl --show-error -s "$1";; # dict://*|telnet://*) curl --show-error -s "$1";; # data:*) echo "$1" | sed -E 's-^data:.{0,50};base64,--' | base64 -d;; # *) curl --show-error -s "https://$1";; # esac 2> /dev/null || { # printf "\e[31mcan't fetch %s\e[0m\n" "${1:--}" >&2 # return 1 # } # } # emit a line with a repeating dot-like symbol in it dots() { [ "${1:-80}" -gt 0 ] && printf "%${1:-80}s\n" "" | sed 's- -·-g'; } # ignore/remove all matched regexes given on all stdin lines drop() { awk ' BEGIN { for (i = 1; i < ARGC; i++) { e[i] = ARGV[i]; delete ARGV[i] } } { for (i = 1; i < ARGC; i++) gsub(e[i], "") print; fflush() } ' "${@:-\r$}" } # show the current Date and Time dt() { printf "\e[32m%s\e[0m \e[34m%s\e[0m\n" "$(date +'%a %b %d')" "$(date +%T)" } # show the current Date, Time, and a Calendar with the 3 `current` months dtc() { # show the current date/time center-aligned printf "%22s\e[32m%s\e[0m \e[34m%s\e[0m\n\n" \ "" "$(date +'%a %b %d')" "$(date +%T)" # debian linux has a different `cal` app which highlights the day if [ -e "/usr/bin/ncal" ]; then ncal -C -3 else cal -3 fi } # quick alias for `echo` e() { echo "$@"; } # Evaluate Awk expression ea() { local expr="${1:-0}" [ $# -gt 0 ] && shift awk "BEGIN { print ${expr}; exit }" "$@" } # Extended-mode Grep, enabling its full regex syntax eg() { grep -E --line-buffered "$@"; } # Extended Grep, Recursive Interactive and Plain # egrip() { ugrep -r -Q --color=never -E "$@"; } # show all empty files in a folder, digging recursively emptyfiles() { local arg for arg in "${@:-.}"; do if [ ! -d "${arg}" ]; then printf "\e[31mno folder named %s\e[0m\n" "${arg}" > /dev/stderr return 1 fi stdbuf -oL find "${arg}" -type f -size 0c done } # Evaluate Nodejs expression # en() { # local expr="${1:-null}" # expr="$(echo "${expr}" | sed 's-\\-\\\\-g; s-`-\`-g')" # node -e "console.log(${expr})" | sed -u 's-\x1b\[[^A-Za-z]+[A-Za-z]--g' # } # Evaluate Python expression ep() { python -c "print(${1:-None})"; } # Extended Plain Interactive Grep epig() { ugrep --color=never -Q -E "$@"; } # Extended Plain Recursive Interactive Grep eprig() { ugrep --color=never -Q -E "$@"; } # Evaluate Ruby expression er() { ruby -e "puts ${1:-nil}"; } # ignore/remove all matched regexes given on all stdin lines erase() { awk ' BEGIN { for (i = 1; i < ARGC; i++) { e[i] = ARGV[i]; delete ARGV[i] } } { for (i = 1; i < ARGC; i++) gsub(e[i], "") print; fflush() } ' "${@:-\r$}" } # Editor Read-Only ero() { micro -readonly true "$@"; } # Extended-mode Sed, enabling its full regex syntax es() { sed -E -u "$@"; } # convert EURos into CAnadian Dollars, using the latest official exchange # rates from the bank of canada; during weekends, the latest rate may be # from a few days ago; the default amount of euros to convert is 1, when # not given eur2cad() { local site='https://www.bankofcanada.ca/valet/observations/group' local csv_rates="${site}/FX_RATES_DAILY/csv" local url url="${csv_rates}?start_date=$(date -d '3 days ago' +'%Y-%m-%d')" curl -s "${url}" | awk -F, -v amount="$(echo "${1:-1}" | sed 's-_--g')" ' /EUR/ { for (i = 1; i <= NF; i++) if($i ~ /EUR/) j = i } END { gsub(/"/, "", $j); if (j != 0) printf "%.2f\n", amount * $j }' } # EValuate AWK expression evawk() { local expr="${1:-0}" [ $# -gt 0 ] && shift awk "BEGIN { print ${expr}; exit }" "$@" } # convert fahrenheit into celsius fahrenheit() { echo "${@:-0}" | sed -E 's-_--g; s- +-\n-g' | awk '/./ { printf "%.2f\n", ($0 - 32) * 5.0/9.0 }' } # Flushed AWK fawk() { stdbuf -oL awk "$@"; } # fetch/web-request all URIs given, using protcol HTTPS when none is given fetch() { local a for a in "$@"; do case "$a" in file://*|https://*|http://*) curl --show-error -s "$a";; ftp://*|ftps://*|sftp://*) curl --show-error -s "$a";; dict://*|telnet://*) curl --show-error -s "$a";; data:*) echo "$a" | sed -E 's-^data:.{0,50};base64,--' | base64 -d;; *) curl --show-error -s "https://$a";; esac done } # run the Fuzzy Finder (fzf) in multi-choice mode, with custom keybindings ff() { fzf -m --bind ctrl-a:select-all,ctrl-space:toggle "$@"; } # show all files in a folder, digging recursively files() { local arg for arg in "${@:-.}"; do if [ ! -d "${arg}" ]; then printf "\e[31mno folder named %s\e[0m\n" "${arg}" > /dev/stderr return 1 fi stdbuf -oL find "${arg}" -type f done } # recursively find all files with fewer bytes than the number given filesunder() { local n n="$(echo "${1:-4097}" | sed -E 's-_--g; s-\.[0-9]+$--')" [ $# -gt 0 ] && shift local arg for arg in "${@:-.}"; do if [ ! -d "${arg}" ]; then printf "\e[31mno folder named %s\e[0m\n" "${arg}" > /dev/stderr return 1 fi stdbuf -oL find "${arg}" -type f -size -"$n"c done } # get the first n lines, or 1 by default first() { head -n "${1:-1}" "${2:--}"; } # limit data up to the first n bytes firstbytes() { head -c "$1" "${2:--}"; } # get the first n lines, or 1 by default firstlines() { head -n "${1:-1}" "${2:--}"; } # fix lines, ignoring leading UTF-8_BOMs (byte-order-marks) on each input's # first line, turning all end-of-line CRLF byte-pairs into single line-feeds, # and ensuring each input's last line ends with a line-feed fixlines() { awk ' FNR == 1 { gsub(/^\xef\xbb\xbf/, "") } { gsub(/\r$/, ""); print; fflush() } ' "$@" } # FLushed AWK # flawk() { stdbuf -oL awk "$@"; } # First Line AWK, emits the first line as is, and uses the rest of the args # given by injecting the first into the script, and passing all later args as # later args to `awk` as given flawk() { local code="${1:-1}" [ $# -gt 0 ] && shift stdbuf -oL awk "NR == 1 { print; fflush(); next } ${code}" "$@" } # Faint LEAK emits/tees input both to stdout and stderr, coloring gray what # it emits to stderr using an ANSI-style; this cmd is useful to `debug` pipes # involving several steps fleak() { awk ' { gsub(/\x1b\[[0-9;]*[A-Za-z]/, "") printf "\x1b[38;2;168;168;168m%s\x1b[0m\n", $0 > "/dev/stderr" print; fflush() } ' "$@" } # try to run the command given using line-buffering for its (standard) output flushlines() { stdbuf -oL "$@"; } # show all folders in a folder, digging recursively folders() { local arg for arg in "${@:-.}"; do if [ ! -d "${arg}" ]; then printf "\e[31mno folder named %s\e[0m\n" "${arg}" > /dev/stderr return 1 fi stdbuf -oL find "${arg}" -type d | awk '!/^\.$/ { print; fflush() }' done } # start from the line number given, skipping all previous ones fromline() { tail -n +"${1:-1}" "${2:--}"; } # convert FeeT into meters ft() { echo "${@:-1}" | sed -E 's-_--g; s- +-\n-g' | awk '/./ { printf "%.2f\n", 0.3048 * $0; fflush() }' } # convert FeeT² (squared) into meters² ft2() { echo "${@:-1}" | sed -E 's-_--g; s- +-\n-g' | awk '/./ { printf "%.2f\n", 0.09290304 * $0 }' } # Get/fetch data from the filenames/URIs given; uses my script `get` # g() { get "$@"; } # run `grep` in extended-regex mode, enabling its full regex syntax # g() { grep -E --line-buffered "$@"; } # convert GALlons into liters gal() { echo "${@:-1}" | sed -E 's-_--g; s- +-\n-g' | awk '/./ { printf "%.2f\n", 3.785411784 * $0; fflush() }' } # convert binary GigaBytes into bytes gb() { echo "${@:-1}" | sed -E 's-_--g; s- +-\n-g' | awk '/./ { printf "%.4f\n", 1073741824 * $0; fflush() }' | sed 's-\.00*$--' } # Good, Bad, Meh colors lines using up to 3 regular expressions gbm() { local good="$1" local bad="$2" local meh="$3" [ $# -gt 0 ] && shift [ $# -gt 0 ] && shift [ $# -gt 0 ] && shift awk ' BEGIN { gotgood = ARGC > 1 && ARGV[1] != "" gotbad = ARGC > 2 && ARGV[2] != "" gotmeh = ARGC > 3 && ARGV[3] != "" good = ARGV[1] bad = ARGV[2] meh = ARGV[3] delete ARGV[1] delete ARGV[2] delete ARGV[3] } gotgood && $0 ~ good { # code to use a color-blind-friendlier blue, instead of green # gsub(/\x1b\[0m/, "\x1b[0m\x1b[38;2;0;95;215m") # printf "\x1b[38;2;0;95;215m%s\x1b[0m\n", $0 gsub(/\x1b\[0m/, "\x1b[0m\x1b[38;2;0;135;95m") printf "\x1b[38;2;0;135;95m%s\x1b[0m\n", $0; fflush() next } gotbad && $0 ~ bad { gsub(/\x1b\[0m/, "\x1b[0m\x1b[38;2;204;0;0m") printf "\x1b[38;2;204;0;0m%s\x1b[0m\n", $0; fflush() next } gotmeh && $0 ~ meh { gsub(/\x1b\[0m/, "\x1b[0m\x1b[38;2;168;168;168m") printf "\x1b[38;2;168;168;168m%s\x1b[0m\n", $0; fflush() next } { print; fflush() } ' "${good}" "${bad}" "${meh}" "$@" } # glue/stick together various lines, only emitting a line-feed at the end; an # optional argument is the output-item-separator, which is empty by default glue() { local sep="${1:-}" [ $# -gt 0 ] && shift awk -v sep="${sep}" ' NR > 1 { printf "%s", sep } { gsub(/\r/, ""); printf "%s", $0; fflush() } END { if (NR > 0) print ""; fflush() } ' "$@" } # GO Build Stripped: a common use-case for the go compiler gobs() { go build -ldflags "-s -w" -trimpath "$@"; } # GO DEPendencieS: show all dependencies in a go project godeps() { go list -f '{{ join .Deps "\n" }}' "$@"; } # GO IMPortS: show all imports in a go project goimps() { go list -f '{{ join .Imports "\n" }}' "$@"; } # go to the folder picked using an interactive TUI; uses my script `bf` goto() { local where where="$(bf "${1:-.}")" if [ $? -ne 0 ]; then return 0 fi where="$(realpath "${where}")" if [ ! -d "${where}" ]; then where="$(dirname "${where}")" fi cd "${where}" || return } # GRayed-out lines with AWK grawk() { local cond="${1:-1}" [ $# -gt 0 ] && shift awk "${cond}"' { gsub(/\x1b\[0m/, "\x1b[0m\x1b[38;2;168;168;168m") printf "\x1b[38;2;168;168;168m%s\x1b[0m\n", $0; fflush() next } { print; fflush() } ' "$@" } # Style lines using a GRAY-colored BACKground grayback() { awk ' { gsub(/\x1b\[0m/, "\x1b[0m\x1b[48;2;218;218;218m") printf "\x1b[48;2;218;218;218m%s\x1b[0m\n", $0; fflush() } ' "$@" } # Grep, Recursive Interactive and Plain # grip() { ugrep -r -Q --color=never -E "$@"; } # Global extended regex SUBstitute, using the AWK function of the same name: # arguments are used as regex/replacement pairs, in that order gsub() { awk ' BEGIN { for (i = 1; i < ARGC; i++) { args[++n] = ARGV[i] delete ARGV[i] } } { for (i = 1; i <= n; i += 2) gsub(args[i], args[i + 1]) print; fflush() } ' "$@" } # Highlight (lines) with AWK hawk() { local cond="${1:-1}" [ $# -gt 0 ] && shift awk ' { low = lower = tolower($0) } '"${cond}"' { gsub(/\x1b\[0m/, "\x1b[0m\x1b[7m") printf "\x1b[7m%s\x1b[0m\n", $0; fflush() next } { print; fflush() } ' "$@" } # play a heartbeat-like sound lasting the number of seconds given, or for 1 # second by default; uses my script `waveout` heartbeat() { local a='sin(v[0]*tau*exp(-20*v[1]))*exp(-2*v[1])' local b='((12, u), (8, (u-0.25)%1))' local f="sum($a for v in $b) / 2" # local f='sum(sin(10*tau*exp(-20*v))*exp(-2*v) for v in (u, (u-0.25)%1))/2' # local f='sum(sin(v[0]*tau*exp(-20*v[1]))*exp(-2*v[1]) for v in ((12, u), (8, (u-0.25)%1)))/2' waveout "${1:-1}" "${2:-1} * $f" | mpv --really-quiet - } # Highlighted-style ECHO hecho() { printf "\e[7m%s\e[0m\n" "$*"; } # show each byte as a pair of HEXadecimal (base-16) symbols hexify() { cat "$@" | od -x -A n | awk '{ gsub(/ +/, ""); printf "%s", $0; fflush() } END { printf "\n" }' } # HIghlighted-style ECHO hiecho() { printf "\e[7m%s\e[0m\n" "$*"; } # highlight lines highlight() { awk ' { gsub(/\x1b\[0m/, "\x1b[0m\x1b[7m") printf "\x1b[7m%s\x1b[0m\n", $0; fflush() } ' "$@" } # HIghlight LEAK emits/tees input both to stdout and stderr, highlighting what # it emits to stderr using an ANSI-style; this cmd is useful to `debug` pipes # involving several steps hileak() { awk ' { gsub(/\x1b\[[0-9;]*[A-Za-z]/, "") printf "\x1b[7m%s\x1b[0m\n", $0 > "/dev/stderr" print; fflush() } ' "$@" } # highlight lines hilite() { awk ' { gsub(/\x1b\[0m/, "\x1b[0m\x1b[7m") printf "\x1b[7m%s\x1b[0m\n", $0; fflush() } ' "$@" } # Help Me Remember my custom shell commands hmr() { local cmd="bat" # debian linux uses a different name for the `bat` app if [ -e "/usr/bin/batcat" ]; then cmd="batcat" fi "$cmd" \ --style=plain,header,numbers --theme='Monokai Extended Light' \ --wrap=never --color=always "$(which clam)" | sed -u 's-\x1b\[38;5;70m-\x1b\[38;5;28m-g' | less -JMKiCRS } # convert seconds into a colon-separated Hours-Minutes-Seconds triple hms() { echo "${@:-0}" | sed -E 's-_--g; s- +-\n-g' | awk '/./ { x = $0 h = (x - x % 3600) / 3600 m = (x % 3600) / 60 s = x % 60 printf "%02d:%02d:%05.2f\n", h, m, s; fflush() }' } # find all hyperlinks inside HREF attributes in the input text href() { awk ' BEGIN { e = "href=\"[^\"]+\"" } { for (s = $0; match(s, e); s = substr(s, RSTART + RLENGTH)) { print substr(s, RSTART + 6, RLENGTH - 7); fflush() } } ' "$@" } # Index all lines starting from 0, using a tab right after each line number # i() { # local start="${1:-0}" # [ $# -gt 0 ] && shift # nl -b a -w 1 -v "${start}" "$@" # } # Index all lines starting from 0, using a tab right after each line number i() { stdbuf -oL nl -b a -w 1 -v 0 "$@"; } # avoid/ignore lines which case-insensitively match any of the regexes given iavoid() { awk ' BEGIN { if (IGNORECASE == "") { m = "this variant of AWK lacks case-insensitive regex-matching" printf("\x1b[31m%s\x1b[0m\n", m) > "/dev/stderr" exit 125 } IGNORECASE = 1 for (i = 1; i < ARGC; i++) { e[i] = ARGV[i] delete ARGV[i] } } { for (i = 1; i < ARGC; i++) if ($0 ~ e[i]) next print; fflush(); got++ } END { exit(got == 0) } ' "${@:-^\r?$}" } # case-Insensitively DEDUPlicate prevents lines from appearing more than once idedup() { awk '!c[tolower($0)]++ { print; fflush() }' "$@"; } # ignore/remove all case-insensitively matched regexes given on all stdin lines idrop() { awk ' BEGIN { if (IGNORECASE == "") { m = "this variant of AWK lacks case-insensitive regex-matching" printf("\x1b[31m%s\x1b[0m\n", m) > "/dev/stderr" exit 125 } IGNORECASE = 1 for (i = 1; i < ARGC; i++) { e[i] = ARGV[i]; delete ARGV[i] } } { for (i = 1; i < ARGC; i++) gsub(e[i], "") print; fflush() } ' "${@:-\r$}" } # ignore/remove all case-insensitively matched regexes given on all stdin lines ierase() { awk ' BEGIN { if (IGNORECASE == "") { m = "this variant of AWK lacks case-insensitive regex-matching" printf("\x1b[31m%s\x1b[0m\n", m) > "/dev/stderr" exit 125 } IGNORECASE = 1 for (i = 1; i < ARGC; i++) { e[i] = ARGV[i]; delete ARGV[i] } } { for (i = 1; i < ARGC; i++) gsub(e[i], "") print; fflush() } ' "${@:-\r$}" } # ignore command in a pipe: this allows quick re-editing of pipes, while # still leaving signs of previously-used steps, as a memo ignore() { cat; } # only keep lines which case-insensitively match any of the regexes given imatch() { awk ' BEGIN { if (IGNORECASE == "") { m = "this variant of AWK lacks case-insensitive regex-matching" printf("\x1b[31m%s\x1b[0m\n", m) > "/dev/stderr" exit 125 } IGNORECASE = 1 for (i = 1; i < ARGC; i++) { e[i] = ARGV[i] delete ARGV[i] } } { for (i = 1; i < ARGC; i++) { if ($0 ~ e[i]) { print; fflush() got++ next } } } END { exit(got == 0) } ' "${@:-[^\r]}" } # emit each word-like item from each input line on its own line items() { awk '{ for (i = 1; i <= NF; i++) print $i; fflush() }' "$@"; } # case-insensitively deduplicate lines, keeping them in their original order: # the checking/matching is case-insensitive, but each first match is output # exactly as is iunique() { awk '!c[tolower($0)]++ { print; fflush() }' "$@"; } # shrink/compact Json data, allowing an optional filepath # j0() { python -m json.tool --compact "${1:--}"; } # shrink/compact Json using the `jq` app, allowing an optional filepath, and # even an optional transformation formula after that # j0() { jq -c -M "${2:-.}" "${1:--}"; } # show Json data on multiple lines, using 2 spaces for each indentation level, # allowing an optional filepath # j2() { python -m json.tool --indent 2 "${1:--}"; } # show Json data on multiple lines, using 2 spaces for each indentation level, # allowing an optional filepath, and even an optional transformation formula # after that # j2() { jq --indent 2 -M "${2:-.}" "${1:--}"; } # listen to streaming JAZZ music jazz() { printf "streaming \e[7mSmooth Jazz Instrumental\e[0m\n" # mpv https://stream.zeno.fm/00rt0rdm7k8uv mpv --quiet https://stream.zeno.fm/00rt0rdm7k8uv } # show a `dad` JOKE from the web, sometimes even a very funny one joke() { curl -s https://icanhazdadjoke.com | fold -s | sed -u -E 's- *\r?$--' # plain-text output from previous cmd doesn't end with a line-feed printf "\n" } # shrink/compact JSON data, allowing an optional filepath # json0() { python -m json.tool --compact "${1:--}"; } # shrink/compact JSON using the `jq` app, allowing an optional filepath, and # even an optional transformation formula after that json0() { jq -c -M "${2:-.}" "${1:--}"; } # show JSON data on multiple lines, using 2 spaces for each indentation level, # allowing an optional filepath # json2() { python -m json.tool --indent 2 "${1:--}"; } # show JSON data on multiple lines, using 2 spaces for each indentation level, # allowing an optional filepath, and even an optional transformation formula # after that json2() { jq --indent 2 -M "${2:-.}" "${1:--}"; } # turn JSON Lines into a proper JSON array jsonl2json() { jq -s -M "${@:-.}"; } # emit the given number of random/junk bytes, or 1024 junk bytes by default junk() { head -c "$(echo "${1:-1024}" | sed 's-_--g')" /dev/urandom; } # only keep the file-extension part from lines ending with file-extensions # justext() { # awk ' # !/^\./ && /\./ { gsub(/^.+\.+/, ""); printf ".%s\n", $0; fflush() } # ' "$@" # } # only keep the file-extension part from lines ending with file-extensions justext() { awk ' !/^\./ && /\./ { if (match($0, /((\.[A-Za-z0-9]+)+) *\r?$/)) { print substr($0, RSTART, RLENGTH); fflush() } } ' "$@" } # only keep lines ending with a file-extension of any popular picture format justpictures() { awk ' /.\.(bmp|gif|heic|ico|jfif|jpe?g|png|svg|tiff?|webp) *\r?$/ { gsub(/ *\r?$/, ""); print; fflush() } ' "$@" } # only keep lines ending with a file-extension of any popular sound format justsounds() { awk ' /.\.(aac|aif[cf]?|au|flac|m4a|m4b|mp[23]|ogg|snd|wav|wma) *\r?$/ { gsub(/ *\r?$/, ""); print; fflush() } ' "$@" } # only keep lines ending with a file-extension of any popular video format justvideos() { awk ' /.\.(avi|mkv|mov|mp4|mpe?g|ogv|webm|wmv) *\r?$/ { gsub(/ *\r?$/, ""); print; fflush() } ' "$@" } # convert binary KiloBytes into bytes kb() { echo "${@:-1}" | sed -E 's-_--g; s- +-\n-g' | awk '/./ { printf "%.2f\n", 1024 * $0; fflush() }' | sed 's-\.00*$--' } # run `less`, showing line numbers, among other settings l() { less -JMKNiCRS "$@"; } # Like A Book groups lines as 2 side-by-side pages, the same way books # do it; uses my script `book` lab() { book "$(($(tput lines) - 1))" "$@" | less -JMKiCRS; } # find the LAN (local-area network) IP address for this device lanip() { hostname -I; } # Line xARGS: `xargs` using line separators, which handles filepaths # with spaces, as long as the standard input has 1 path per line largs() { xargs -d '\n' "$@"; } # get the last n lines, or 1 by default # last() { tail -n "${1:-1}" "${2:--}"; } # get up to the last given number of bytes lastbytes() { tail -c "${1:-1}" "${2:--}"; } # get the last n lines, or 1 by default lastlines() { tail -n "${1:-1}" "${2:--}"; } # turn UTF-8 into its latin-like subset, where variants of latin letters stay # as given, and where all other symbols become question marks, one question # mark for each code-point byte latinize() { iconv -f utf-8 -t latin-1//translit "$@" | iconv -f latin-1 -t utf-8 } # Lowercased (lines) AWK lawk() { local code="${1:-1}" [ $# -gt 0 ] && shift awk " { line = orig = original = \$0 low = lower = tolower(\$0) \$0 = lower } ${code} { fflush() } " "$@"; } # convert pounds (LB) into kilograms lb() { echo "${@:-1}" | sed -E 's-_--g; s- +-\n-g' | awk '/./ { printf "%.2f\n", 0.45359237 * $0; fflush() }' } # turn the first n space-separated fields on each line into tab-separated # ones; this behavior is useful to make the output of many cmd-line tools # into TSV, since filenames are usually the last fields, and these may # contain spaces which aren't meant to be split into different fields leadtabs() { local n="$1" local cmd="$([ "${1:-80}" -gt 0 ] && printf "%${1:-80}s\n" "")" cmd="s-^ *--; s- *\\r?\$--; $(echo "${cmd}" | sed 's/ /s- +-\\t-1;/g')" sed -u -E "${cmd}" } # run `less`, showing line numbers, among other settings least() { less -JMKNiCRS "$@"; } # limit stops at the first n bytes, or 1024 bytes by default limit() { head -c "$(echo "${1:-1024}" | sed 's-_--g')" "${2:--}"; } # Less with Header runs `less` with line numbers, ANSI styles, no line-wraps, # and using the first n lines as a sticky-header (1 by default), so they # always show on top lh() { local n="${1:-1}" [ $# -gt 0 ] && shift less --header="$n" -JMKNiCRS "$@" } # fix lines, ignoring leading UTF-8_BOMs (byte-order-marks) on each input's # first line, turning all end-of-line CRLF byte-pairs into single line-feeds, # and ensuring each input's last line ends with a line-feed lines() { awk ' FNR == 1 { gsub(/^\xef\xbb\xbf/, "") } { gsub(/\r$/, ""); print; fflush() } ' "$@" } # regroup adjacent lines into n-item tab-separated lines lineup() { local n="${1:-0}" [ $# -gt 0 ] && shift if [ "$n" -le 0 ]; then awk ' NR > 1 { printf "\t" } { printf "%s", $0; fflush() } END { if (NR > 0) print "" } ' "$@" return $? fi awk -v n="$n" ' NR % n != 1 && n > 1 { printf "\t" } { printf "%s", $0; fflush() } NR % n == 0 { print ""; fflush() } END { if (NR % n != 0) print "" } ' "$@" } # find all hyperLINKS (https:// and http://) in the input text links() { awk ' BEGIN { e = "https?://[A-Za-z0-9+_.:%-]+(/[A-Za-z0-9+_.%/,#?&=-]*)*" } { # match all links in the current line for (s = $0; match(s, e); s = substr(s, RSTART + RLENGTH)) { print substr(s, RSTART, RLENGTH); fflush() } } ' "$@" } # List files, using the `Long` option # ll() { ls -l "$@"; } # LOAD data from the filename or URI given; uses my script `get` load() { get "$@"; } # LOwercase line, check (awk) COndition: on each success, the original line # is output with its original letter-casing, as its lower-cased version is # only a convenience meant for the condition loco() { local cond="${1:-1}" [ $# -gt 0 ] && shift awk " { line = orig = original = \$0 low = lower = tolower(\$0) \$0 = lower } ${cond} { print line; fflush() } " "$@" } # LOcal SERver webserves files in a folder as localhost, using the port # number given, or port 8080 by default loser() { printf "\e[7mserving files in %s\e[0m\n" "${2:-$(pwd)}" >&2 python3 -m http.server "${1:-8080}" -d "${2:-.}" } # LOWercase all ASCII symbols low() { awk '{ print tolower($0); fflush() }' "$@"; } # LOWERcase all ASCII symbols lower() { awk '{ print tolower($0); fflush() }' "$@"; } # Live/Line-buffered RipGrep ensures results show/pipe up immediately lrg() { rg --line-buffered "$@"; } # Listen To Youtube lty() { local url # some youtube URIs end with extra playlist/tracker parameters url="$(echo "$1" | sed 's-&.*--')" mpv "$(yt-dlp -x --audio-format aac --get-url "${url}" 2> /dev/null)" } # Match lines with any of the regexes given m() { awk ' BEGIN { for (i = 1; i < ARGC; i++) { e[i] = ARGV[i] delete ARGV[i] } } { for (i = 1; i < ARGC; i++) { if ($0 ~ e[i]) { print; fflush() got++ next } } } END { exit(got == 0) } ' "${@:-[^\r]}" } # only keep lines which match any of the regexes given match() { awk ' BEGIN { for (i = 1; i < ARGC; i++) { e[i] = ARGV[i] delete ARGV[i] } } { for (i = 1; i < ARGC; i++) { if ($0 ~ e[i]) { print; fflush() got++ next } } } END { exit(got == 0) } ' "${@:-[^\r]}" } # MAX Width truncates lines up to the given number of items/bytes given, or up # to 80 by default; output lines end with an ANSI reset-code, in case input # lines use ANSI styles maxw() { local maxwidth="${1:-80}" [ $# -gt 0 ] && shift awk -v maxw="${maxwidth}" ' { gsub(/\r$/, "") printf("%s\x1b[0m\n", substr($0, 1, maxw)); fflush() } ' "$@" } # convert binary MegaBytes into bytes mb() { echo "${@:-1}" | sed -E 's-_--g; s- +-\n-g' | awk '/./ { printf "%.2f\n", 1048576 * $0; fflush() }' | sed 's-\.00*$--' } # Multi-Core MAKE runs `make` using all cores mcmake() { make -j "$(nproc)" "$@"; } # Multi-Core MaKe runs `make` using all cores mcmk() { make -j "$(nproc)" "$@"; } # merge stderr into stdout, without any ugly keyboard-dancing # merrge() { "$@" 2>&1; } # convert MIles into kilometers mi() { echo "${@:-1}" | sed -E 's-_--g; s- +-\n-g' | awk '/./ { printf "%.2f\n", 1.609344 * $0; fflush() }' } # convert MIles² (squared) into kilometers² mi2() { echo "${@:-1}" | sed -E 's-_--g; s- +-\n-g' | awk '/./ { printf "%.2f\n", 2.5899881103360 * $0 }' } # Make In Folder mif() { local code pushd "${1:-.}" > /dev/null || return [ $# -gt 0 ] && shift make "$@" code=$? popd > /dev/null || return "${code}" return "${code}" } # Media INFO # minfo() { mediainfo "$@" | less -JMKiCRS; } # Media INFO # minfo() { ffprobe "$@" |& less -JMKiCRS; } # quick alias for `make` # mk() { make "$@"; } # run `make` mk() { make "$@"; } # convert Miles Per Hour into kilometers per hour mph() { echo "${@:-1}" | sed -E 's-_--g; s- +-\n-g' | awk '/./ { printf "%.2f\n", 1.609344 * $0 }' } # Number all lines, using a tab right after each line number # n() { # local start="${1:-1}" # [ $# -gt 0 ] && shift # nl -b a -w 1 -v "${start}" "$@" # } # Number all lines, using a tab right after each line number n() { stdbuf -oL nl -b a -w 1 -v 1 "$@"; } # Not AND sorts its 2 inputs, then finds lines not in common nand() { # comm -3 <(sort "$1") <(sort "$2") # dash doesn't support the process-sub syntax (sort "$1" | (sort "$2" | (comm -3 /dev/fd/3 /dev/fd/4) 4<&0) 3<&0) } # Nice Byte Count, using my scripts `nn` and `cext` nbc() { wc -c "$@" | nn --gray | cext; } # NIce(r) COlumns makes the output of many commands whose output starts with # a header line easier to read; uses my script `nn` nico() { awk ' (NR - 1) % 5 == 1 && NR > 1 { print "" } { printf "%5d %s\n", NR - 1, $0; fflush() } ' "$@" | nn --gray | less -JMKiCRS } # emit nothing to output and/or discard everything from input nil() { if [ $# -gt 0 ]; then "$@" > /dev/null else cat < /dev/null fi } # pipe-run my scripts `nj` (Nice Json) and `nn` (Nice Numbers) njnn() { nj "$@" | nn --gray; } # NArrow MANual, keeps `man` narrow, even if the window/tab is wide when run naman() { local w w="$(tput cols)" if [ "$w" -gt 120 ]; then w="$((w / 2 - 1))" fi MANWIDTH="$w" man "$@" } # Narrow MANual, keeps `man` narrow, even if the window/tab is wide when run nman() { local w w="$(tput cols)" if [ "$w" -gt 120 ]; then w="$((w / 2 - 1))" fi MANWIDTH="$w" man "$@" } # convert Nautical MIles into kilometers nmi() { echo "${@:-1}" | sed -E 's-_--g; s- +-\n-g' | awk '/./ { printf "%.2f\n", 1.852 * $0; fflush() }' } # NO (standard) ERRor ignores stderr, without any ugly keyboard-dancing # noerr() { "$@" 2> /dev/null; } # play a white-noise sound lasting the number of seconds given, or for 1 # second by default; uses my script `waveout` noise() { waveout "${1:-1}" "${2:-0.05} * random()" | mpv --really-quiet -; } # show the current date and time now() { date +'%Y-%m-%d %H:%M:%S'; } # Nice Processes shows/lists all current processes; uses my script `nn` np() { local res local code # res="$(ps "${@:-auxf}")" res="$(ps "${@:-aux}")" code=$? if [ "${code}" -ne 0 ]; then return "${code}" fi echo "${res}" | awk ' BEGIN { d = strftime("%a %b %d") t = strftime("%H:%M:%S") # printf "%s %s\n\n", d, t # printf "\x1b[32m%s\x1b[0m \x1b[34m%s\x1b[0m\n\n", d, t # printf "%30s\x1b[32m%s\x1b[0m \x1b[34m%s\x1b[0m\n\n", "", d, t # printf "%30s%s %s\n\n", "", d, t printf "\x1b[7m%30s%s %s%30s\x1b[0m\n\n", "", d, t, "" } (NR - 1) % 5 == 1 && NR > 1 { print "" } $1 == "root" { # gsub(/^/, "\x1b[36m") # gsub(/\x1b\[0m/, "\x1b[0m\x1b[36m") gsub(/^/, "\x1b[34m") gsub(/ +/, "&\x1b[0m\x1b[34m") gsub(/$/, "\x1b[0m") } { gsub(/ \? /, "\x1b[38;2;135;135;175m&\x1b[0m") gsub(/0[:\.]00*/, "\x1b[38;2;135;135;175m&\x1b[0m") printf "%3d %s\n", NR - 1, $0 } ' | nn --gray | less -JMKiCRS } # Nice Size, using my scripts `nn` and `cext` ns() { wc -c "$@" | nn --gray | cext; } # Nice Transform Json, using my scripts `tj`, and `nj` ntj() { tj "$@" | nj; } # Nice TimeStamp nts() { ts '%Y-%m-%d %H:%M:%S' | sed -u 's-^-\x1b[48;2;218;218;218m\x1b[38;2;0;95;153m-; s- -\x1b[0m\t-2' } # emit nothing to output and/or discard everything from input null() { if [ $# -gt 0 ]; then "$@" > /dev/null else cat < /dev/null fi } # NULl-terminate LINES ends each stdin line with a null byte, instead of a # line-feed byte nullines() { awk ' FNR == 1 { gsub(/^\xef\xbb\xbf/, "") } { gsub(/\r$/, ""); printf "%s\x00", $0; fflush() } ' "$@" } # (Nice) What Are These (?) shows what the names given to it are/do, coloring # the syntax of shell functions nwat() { local a if [ $# -eq 0 ]; then printf "\e[38;2;204;0;0mnwat: no names given\e[0m\n" > /dev/stderr return 1 fi local cmd="bat" # debian linux uses a different name for the `bat` app if [ -e "/usr/bin/batcat" ]; then cmd="batcat" fi for a in "$@"; do # printf "\e[7m%-80s\e[0m\n" "$a" printf "\e[48;2;218;218;218m%-80s\e[0m\n" "$a" # resolve 1 alias level if alias "$a" 2> /dev/null > /dev/null; then a="$(alias "$a" | sed "s-.*=--; s-['\"]--g")" fi if echo "$a" | grep -E '[^ ]+ +[^ ]+' > /dev/null; then # resolved aliases with args/spaces in them would otherwise fail echo "$a" elif whence -f "$a" > /dev/null 2> /dev/null; then # zsh seems to show a shell function's code only via `whence -f` whence -f "$a" elif type "$a" > /dev/null 2> /dev/null; then # dash doesn't support `declare`, and `type` in bash emits # a redundant first output line, when it's a shell function type "$a" | awk ' NR == 1 && /^[a-z0-9_-]+ is a function$/ { skipped = $0; next } { print; fflush() } END { if (NR < 2 && skipped) print skipped } ' | "$cmd" -l sh --style=plain --theme='Monokai Extended Light' \ --wrap=never --color=always | sed -u 's-\x1b\[38;5;70m-\x1b\[38;5;28m-g' else printf "\e[38;2;204;0;0m%s not found\e[0m\n" "$a" fi done | less -JMKiCRS } # Nice numbers Word-Count runs `wc` and colors results with my script `nn`, # alternating styles to make long numbers easier to read # nwc() { wc "$@" | nn --gray; } # Nice numbers Word-Count runs `wc` and colors results with my script `nn`, # alternating styles to make long numbers easier to read # nwc() { wc "$@" | nn --gray | awk '{ printf "%5d %s\n", NR, $0; fflush() }'; } # Nice Word-Count runs `wc` and colors results, using my scripts `nn` and # `cext`, alternating styles to make long numbers easier to read nwc() { wc "$@" | sort -rn | nn --gray | cext | awk '{ printf "%5d %s\n", NR - 1, $0; fflush() }' } # Nice Zoom Json, using my scripts `zj`, and `nj` nzj() { zj "$@" | nj; } # Paragraph AWK runs `awk` in block/paragraph/multiline input-mode # pawk() { awk -F='' -v RS='' "$@"; } # Paragraph AWK runs `awk` in block/paragraph/multiline input-mode pawk() { stdbuf -oL awk -F='' -v RS='' "$@"; } # Plain `fd` pfd() { fd --color=never "$@"; } # pick lines, using all the 1-based line-numbers given picklines() { awk ' BEGIN { m = ARGC - 1; if (ARGC == 1) exit 0 } BEGIN { for (i = 1; i <= m; i++) { p[i] = ARGV[i]; delete ARGV[i] } } { l[++n] = $0 } END { for (i = 1; i <= m; i++) { j = p[i] if (j < 0) j += NR + 1 if (0 < j && j <= NR) print l[j] } } ' "$@" } # Plain Interactive Grep pig() { ugrep --color=never -Q -E "$@"; } # make text plain, by ignoring ANSI terminal styling # plain() { # awk ' # { # gsub(/\x1b\[[0-9;]*[A-Za-z]/, "") # ANSI style-changers # print; fflush() # } # ' "$@" # } # end all lines with an ANSI-code to reset styles plainend() { awk '{ printf "%s\x1b[0m\n", $0; fflush() }' "$@"; } # end all lines with an ANSI-code to reset styles plainends() { awk '{ printf "%s\x1b[0m\n", $0; fflush() }' "$@"; } # play audio/video media # play() { mplayer -msglevel all=-1 "${@:--}"; } # play audio/video media play() { mpv "${@:--}"; } # Pick LINE, using the 1-based line-number given pline() { local line="$1" [ $# -gt 0 ] && shift awk -v n="${line}" ' BEGIN { if (n < 1) exit 0 } NR == n { print; exit 0 } ' "$@" } # Paused MPV; especially useful when trying to view pictures via `mpv` pmpv() { mpv --pause "${@:--}"; } # Print Python result pp() { python -c "print($1)"; } # PRecede (input) ECHO, prepends a first line to stdin lines precho() { echo "$@" && cat /dev/stdin; } # PREcede (input) MEMO, prepends a first highlighted line to stdin lines prememo() { awk ' BEGIN { if (ARGC > 1) printf "\x1b[7m" for (i = 1; i < ARGC; i++) { if (i > 1) printf " " printf "%s", ARGV[i] delete ARGV[i] } if (ARGC > 1) printf "\x1b[0m\n" fflush() } { print; fflush() } ' "$@" } # start by joining all arguments given as a tab-separated-items line of output, # followed by all lines from stdin verbatim pretsv() { awk ' BEGIN { for (i = 1; i < ARGC; i++) { if (i > 1) printf "\t" printf "%s", ARGV[i] delete ARGV[i] } if (ARGC > 1) printf "\n" fflush() } { print; fflush() } ' "$@" } # Plain Recursive Interactive Grep prig() { ugrep --color=never -r -Q -E "$@"; } # show/list all current processes processes() { local res res="$(ps aux)" echo "${res}" | awk 1 | sed -E -u \ -e 's- +-\t-1; s- +-\t-1; s- +-\t-1; s- +-\t-1; s- +-\t-1' \ -e 's- +-\t-1; s- +-\t-1; s- +-\t-1; s- +-\t-1; s- +-\t-1' } # Play Youtube Audio pya() { local url # some youtube URIs end with extra playlist/tracker parameters url="$(echo "$1" | sed 's-&.*--')" mpv "$(yt-dlp -x --audio-format aac --get-url "${url}" 2> /dev/null)" } # Quiet ignores stderr, without any ugly keyboard-dancing q() { "$@" 2> /dev/null; } # Quiet MPV qmpv() { mpv --quiet "${@:--}"; } # ignore stderr, without any ugly keyboard-dancing quiet() { "$@" 2> /dev/null; } # Reset the screen, which empties it and resets the current style r() { reset; } # keep only lines between the 2 line numbers given, inclusively rangelines() { { [ "$#" -eq 2 ] || [ "$#" -eq 3 ]; } && [ "${1}" -le "${2}" ] && { tail -n +"${1:-1}" "${3:--}" | head -n "$(("${2}" - "${1}" + 1))"; } } # RANdom MANual page ranman() { find "/usr/share/man/man${1:-1}" -type f | shuf -n 1 | xargs basename | sed 's-\.gz$--' | xargs man } # Run AWK expression rawk() { local expr="${1:-0}" [ $# -gt 0 ] && shift awk "BEGIN { print ${expr}; exit }" "$@" } # play a ready-phone-line sound lasting the number of seconds given, or for 1 # second by default; uses my script `waveout` ready() { local f='0.5 * sin(350*tau*t) + 0.5 * sin(450*tau*t)' waveout "${1:-1}" "${2:-1} * $f" | mpv --really-quiet - } # reflow/trim lines of prose (text) to improve its legibility: it's especially # useful when the text is pasted from web-pages being viewed in reader mode reprose() { local w="${1:-80}" [ $# -gt 0 ] && shift awk 'FNR == 1 && NR > 1 { print "" } { print; fflush() }' "$@" | fold -s -w "$w" | sed -u -E 's- *\r?$--' } # ignore ansi styles from stdin and restyle things using the style-name given; # uses my script `style` restyle() { style "$@"; } # change the tab-title on your terminal app retitle() { printf "\e]0;%s\a\n" "$*"; } # REVerse-order SIZE (byte-count) revsize() { wc -c "$@" | sort -rn; } # Run In Folder rif() { local code pushd "${1:-.}" > /dev/null || return [ $# -gt 0 ] && shift "$@" code=$? popd > /dev/null || return "${code}" return "${code}" } # play a ringtone-style sound lasting the number of seconds given, or for 1 # second by default; uses my script `waveout` ringtone() { local f='sin(2048 * tau * t) * exp(-50 * (t%0.1))' waveout "${1:-1}" "${2:-1} * $f" | mpv --really-quiet - } # Read-Only Micro (text editor) rom() { micro -readonly true "$@"; } # run the command given, trying to turn its output into TSV (tab-separated # values); uses my script `dejson` rtab() { jc "$@" | dejson; } # Right TRIM ignores trailing spaces, as well as trailing carriage returns rtrim() { awk '{ gsub(/ *\r?$/, ""); print; fflush() }' "$@"; } # show a RULER-like width-measuring line ruler() { [ "${1:-80}" -gt 0 ] && printf "%${1:-80}s\n" "" | sed -E \ 's- {10}-····╵····│-g; s- -·-g; s-·····-····╵-' } # run the command given, trying to turn its output into TSV (tab-separated # values); uses my script `dejson` runtab() { jc "$@" | dejson; } # run the command given, trying to turn its output into TSV (tab-separated # values); uses my script `dejson` runtsv() { jc "$@" | dejson; } # Reverse-order WC rwc() { wc "$@" | sort -rn; } # extended-mode Sed, enabling its full regex syntax # s() { sed -E -u "$@"; } # Silent CURL spares you the progress bar, but still tells you about errors scurl() { curl --show-error -s "$@"; } # show a unique-looking SEParator line; useful to run between commands # which output walls of text sep() { [ "${1:-80}" -gt 0 ] && printf "\e[48;2;218;218;218m%${1:-80}s\e[0m\n" "" | sed 's- -·-g' } # webSERVE files in a folder as localhost, using the port number given, or # port 8080 by default serve() { printf "\e[7mserving files in %s\e[0m\n" "${2:-$(pwd)}" >&2 python3 -m http.server "${1:-8080}" -d "${2:-.}" } # SET DIFFerence sorts its 2 inputs, then finds lines not in the 2nd input setdiff() { # comm -23 <(sort "$1") <(sort "$2") # dash doesn't support the process-sub syntax (sort "$1" | (sort "$2" | (comm -23 /dev/fd/3 /dev/fd/4) 4<&0) 3<&0) } # SET INtersection, sorts its 2 inputs, then finds common lines setin() { # comm -12 <(sort "$1") <(sort "$2") # dash doesn't support the process-sub syntax (sort "$1" | (sort "$2" | (comm -12 /dev/fd/3 /dev/fd/4) 4<&0) 3<&0) } # SET SUBtraction sorts its 2 inputs, then finds lines not in the 2nd input setsub() { # comm -23 <(sort "$1") <(sort "$2") # dash doesn't support the process-sub syntax (sort "$1" | (sort "$2" | (comm -23 /dev/fd/3 /dev/fd/4) 4<&0) 3<&0) } # Show Files (and folders), coloring folders and links; uses my script `nn` sf() { ls -al --file-type --color=never --time-style iso "$@" | awk ' (NR - 1) % 5 == 1 && NR > 1 { print "" } { gsub(/^(d[rwx-]+)/, "\x1b[38;2;0;135;255m\x1b[48;2;228;228;228m&\x1b[0m") gsub(/^(l[rwx-]+)/, "\x1b[38;2;0;135;95m\x1b[48;2;228;228;228m&\x1b[0m") printf "%6d %s\n", NR - 1, $0; fflush() } ' | nn --gray | less -JMKiCRS } # Show Files (and folders) Plus, by coloring folders, links, and extensions; # uses my scripts `nn` and `cext` sfp() { ls -al --file-type --color=never --time-style iso "$@" | awk ' (NR - 1) % 5 == 1 && NR > 1 { print "" } { gsub(/^(d[rwx-]+)/, "\x1b[38;2;0;135;255m\x1b[48;2;228;228;228m&\x1b[0m") gsub(/^(l[rwx-]+)/, "\x1b[38;2;0;135;95m\x1b[48;2;228;228;228m&\x1b[0m") printf "%6d %s\n", NR - 1, $0; fflush() } ' | nn --gray | cext | less -JMKiCRS } # Show File Sizes, using my scripts `nn` and `cext` sfs() { # turn arg-list into single-item lines printf "%s\n" "$@" | # calculate file-sizes, and reverse-sort results xargs -d '\n' wc -c | sort -rn | # add/realign fields to improve legibility awk ' # start output with a header-like line, and add a MiB field BEGIN { printf "%6s %10s %8s name\n", "n", "bytes", "MiB"; fflush() } # make table breathe with empty lines, so tall outputs are readable (NR - 1) % 5 == 1 && NR > 1 { print "" } # emit regular output lines { printf "%6d %10d %8.2f ", NR - 1, $1, $1 / 1048576 # first field is likely space-padded gsub(/^ */, "") # slice line after the first field, as filepaths can have spaces $0 = substr($0, length($1) + 1) # first field is likely space-padded gsub(/^ /, "") printf "%s\n", $0; fflush() } ' | # make zeros in the MiB field stand out with a special color awk ' { gsub(/ 00*\.00* /, "\x1b[38;2;135;135;175m&\x1b[0m") print; fflush() } ' | # make numbers nice, alternating styles along 3-digit groups nn --gray | # color-code file extensions cext | # make result interactively browsable less -JMKiCRS } # SHell-run AWK output # shawk() { stdbuf -oL awk "$@" | sh; } # time-run various tools given one-per-line from stdin, giving them extra # common arguments passed as explicit arguments showdown() { awk ' BEGIN { for (i = 1; i < ARGC; i++) { a[i] = ARGV[i]; delete ARGV[i] } } { printf "%s", $0 for (i = 1; i < ARGC; i++) printf " %s", a[i] printf "\n"; fflush() } ' "$@" | xargs -d '\n' hyperfine --style full } # SHOW a command, then RUN it showrun() { printf "\e[7m%s\e[0m\n" "$*" && "$@"; } # clean the screen, after running the command given sideshow() { tput smcup; "$@"; tput rmcup; } # skip the first n lines, or the 1st line by default skip() { tail -n +$(("${1:-1}" + 1)) "${2:--}"; } # skip the first n bytes skipbytes() { tail -c +$(("$1" + 1)) "${2:--}"; } # skip the last n lines, or the last line by default skiplast() { head -n -"${1:-1}" "${2:--}"; } # skip the last n bytes skiplastbytes() { head -c -"$1" "${2:--}"; } # skip the last n lines, or the last line by default skiplastlines() { head -n -"${1:-1}" "${2:--}"; } # skip the first n lines, or the 1st line by default skiplines() { tail -n +$(("${1:-1}" + 1)) "${2:--}"; } # SLOW/delay lines from the standard-input, waiting the number of seconds # given for each line, or waiting 1 second by default slow() { local seconds="${1:-1}" ( IFS="$(printf "\n")" while read -r line; do sleep "${seconds}" printf "%s\n" "${line}" done ) } # Show Latest Podcasts, using my scripts `podfeed` and `si` slp() { local title title="Latest Podcast Episodes as of $(date +'%F %T')" podfeed -title "${title}" "$@" | si } # recursively find all files with fewer bytes than the number given smallfiles() { local n n="$(echo "${1:-4097}" | sed -E 's-_--g; s-\.[0-9]+$--')" [ $# -gt 0 ] && shift local arg for arg in "${@:-.}"; do if [ ! -d "${arg}" ]; then printf "\e[31mno folder named %s\e[0m\n" "${arg}" > /dev/stderr return 1 fi stdbuf -oL find "${arg}" -type f -size -"$n"c done } # emit the first line as is, sorting all lines after that, using the # `sort` command, passing all/any arguments/options to it sortrest() { awk -v sort="sort $*" ' { gsub(/\r$/, "") } NR == 1 { print; fflush() } NR > 1 { print | sort } ' } # SORt Tab-Separated Values: emit the first line as is, sorting all lines after # that, using the `sort` command in TSV (tab-separated values) mode, passing # all/any arguments/options to it sortsv() { awk -v sort="sort -t \"$(printf '\t')\" $*" ' { gsub(/\r$/, "") } NR == 1 { print; fflush() } NR > 1 { print | sort } ' } # emit a line with the number of spaces given in it spaces() { [ "${1:-80}" -gt 0 ] && printf "%${1:-80}s\n" ""; } # ignore leading spaces, trailing spaces, even runs of multiple spaces # in the middle of lines, as well as trailing carriage returns squeeze() { awk ' FNR == 1 { gsub(/^\xef\xbb\xbf/, "") } { gsub(/^ +| *\r?$/, "") gsub(/ *\t */, "\t") gsub(/ +/, " ") print; fflush() } ' "$@" } # SQUeeze and stOMP, by ignoring leading spaces, trailing spaces, even runs # of multiple spaces in the middle of lines, as well as trailing carriage # returns, while also turning runs of empty lines into single empty lines, # and ignoring leading/trailing empty lines, effectively also `squeezing` # lines vertically squomp() { awk ' FNR == 1 { gsub(/^\xef\xbb\xbf/, "") } /^\r?$/ { empty = 1; next } empty { if (n > 0) print ""; empty = 0 } { gsub(/^ +| *\r?$/, "") gsub(/ *\t */, "\t") gsub(/ +/, " ") print; fflush() n++ } ' "$@" } # Show a command, then Run it sr() { printf "\e[7m%s\e[0m\n" "$*" && "$@"; } # turn runs of empty lines into single empty lines, effectively squeezing # paragraphs vertically, so to speak; runs of empty lines both at the start # and at the end are ignored stomp() { awk ' /^\r?$/ { empty = 1; next } empty { if (n > 0) print ""; empty = 0 } { print; fflush(); n++ } ' "$@" } # STRike-thru (lines) with AWK strawk() { local cond="${1:-1}" [ $# -gt 0 ] && shift awk ' { low = lower = tolower($0) } '"${cond}"' { gsub(/\x1b\[0m/, "\x1b[0m\x1b[9m") printf "\x1b[9m%s\x1b[0m\n", $0; fflush() next } { print; fflush() } ' "$@" } # Sort Tab-Separated Values: emit the first line as is, sorting all lines after # that, using the `sort` command in TSV (tab-separated values) mode, passing # all/any arguments/options to it stsv() { awk -v sort="sort -t \"$(printf '\t')\" $*" ' { gsub(/\r$/, "") } NR == 1 { print; fflush() } NR > 1 { print | sort } ' } # use the result of the `awk` function `substr` for each line substr() { local start="${1:-1}" local length="${2:-80}" [ $# -gt 0 ] && shift [ $# -gt 0 ] && shift awk -v start="${start}" -v len="${length}" \ '{ printf "%s\n", substr($0, start, len); fflush() }' "$@" } # turn SUDo privileges OFF right away: arguments also cause `sudo` to run with # what's given, before relinquishing existing privileges # sudoff() { # local code=0 # if [ $# -gt 0 ]; then # sudo "$@" # code=$? # fi # sudo -k # return "${code}" # } # append a final Tab-Separated-Values line with the sums of all columns from # the input table(s) given; items from first lines aren't counted/added sumtsv() { awk -F "\t" ' { print; fflush() if (width < NF) width = NF } FNR > 1 { for (i = 1; i <= NF; i++) sums[i] += $i + 0 } END { for (i = 1; i <= width; i++) { if (i > 1) printf "\t" printf "%s", sums[i] "" } if (width > 0) printf "\n" } ' "$@" } # show a random command defined in `clam`, using `wat` from `clam` itself surprise() { wat "$(grep -E '^[a-z]+\(' "$(which clam)" | shuf -n 1 | sed -E 's-\(.*--')" } # Time the command given t() { time "$@"; } # show a reverse-sorted tally of all lines read, where ties are sorted # alphabetically tally() { awk -v sort="sort -t \"$(printf '\t')\" -rnk2 -k1d" ' # reassure users by instantly showing the header BEGIN { print "value\ttally"; fflush() } { gsub(/\r$/, ""); t[$0]++ } END { for (k in t) { printf("%s\t%d\n", k, t[k]) | sort } } ' "$@" } # Tab AWK: TSV-specific I/O settings for `awk` # tawk() { awk -F "\t" -v OFS="\t" "$@"; } # Tab AWK: TSV-specific I/O settings for `awk` tawk() { stdbuf -oL awk -F "\t" -v OFS="\t" "$@"; } # quick alias for my script `tbp` tb() { tbp "$@"; } # Title ECHO changes the tab-title on your terminal app techo() { printf "\e]0;%s\a\n" "$*"; } # simulate the cadence of old-fashioned teletype machines, by slowing down # the output of ASCII/UTF-8 symbols from the standard-input teletype() { awk '{ gsub(/\r$/, ""); print; fflush() }' "$@" | ( IFS="$(printf "\n")" while read -r line; do echo "${line}" | sed -E 's-(.)-\1\n-g' | while read -r item; do sleep 0.01 printf "%s" "${item}" done sleep 0.75 printf "\n" done ) } # run `top` without showing any of its output after quitting it tip() { tput smcup; top "$@"; tput rmcup; } # change the tab-title on your terminal app title() { printf "\e]0;%s\a\n" "$*"; } # quick alias for my script `tjp` tj() { tjp "$@"; } # quick alias for my script `tlp` tl() { tlp "$@"; } # show current date in a specifc format today() { date +'%Y-%m-%d %a %b %d'; } # get the first n lines, or 1 by default toline() { head -n "${1:-1}" "${2:--}"; } # lowercase all ASCII symbols tolower() { awk '{ print tolower($0); fflush() }' "$@"; } # play a tone/sine-wave sound lasting the number of seconds given, or for 1 # second by default: after the optional duration, the next optional arguments # are the volume and the tone-frequency; uses my script `waveout` tone() { waveout "${1:-1}" "${2:-1} * sin(${3:-440} * 2 * pi * t)" | mpv --really-quiet - } # get the processes currently using the most cpu topcpu() { local n="${1:-10}" [ "$n" -gt 0 ] && ps aux | awk ' NR == 1 { print; fflush() } NR > 1 { print | "sort -rnk3" } ' | head -n "$(("$n" + 1))" } # show all files directly in the folder given, without looking any deeper topfiles() { local arg for arg in "${@:-.}"; do if [ ! -d "${arg}" ]; then printf "\e[31mno folder named %s\e[0m\n" "${arg}" > /dev/stderr return 1 fi stdbuf -oL find "${arg}" -maxdepth 1 -type f done } # show all folders directly in the folder given, without looking any deeper topfolders() { local arg for arg in "${@:-.}"; do if [ ! -d "${arg}" ]; then printf "\e[31mno folder named %s\e[0m\n" "${arg}" > /dev/stderr return 1 fi stdbuf -oL find "${arg}" -maxdepth 1 -type d | awk '!/^\.$/ { print; fflush() }' done } # get the processes currently using the most memory topmemory() { local n="${1:-10}" [ "$n" -gt 0 ] && ps aux | awk ' NR == 1 { print; fflush() } NR > 1 { print | "sort -rnk6" } ' | head -n "$(("$n" + 1))" } # transpose (switch) rows and columns from tables transpose() { awk ' { gsub(/\r$/, "") } NR == 1 && /\t/ { FS = "\t"; $0 = $0 } { for (i = 1; i <= NF; i++) lines[i][NR] = $i if (maxitems < NF) maxitems = NF } END { for (j = 1; j <= maxitems; j++) { for (i = 1; i <= NR; i++) { if (i > 1) printf "\t" printf "%s", lines[j][i] } printf "\n" } } ' "$@" } # ignore leading/trailing spaces, as well as trailing carriage returns trim() { awk '{ gsub(/^ +| *\r?$/, ""); print; fflush() }' "$@"; } # TRIM DECimalS ignores all trailing decimal zeros in numbers, even the # decimal dots themselves, when decimals in a number are all zeros; works # on gawk and busybox awk, but not on mawk, as the latter lacks `gensub` # trimdecs() { # awk ' # { # $0 = gensub(/([0-9]+)\.0+/, "\\1", "g") # $0 = gensub(/([0-9]+\.[0-9]*[1-9]+)0+/, "\\1", "g") # print; fflush() # } # ' "$@" # } # TRIM DECimalS ignores all trailing decimal zeros in numbers, even the # decimal dots themselves, when decimals in a number are all zeros trimdecs() { awk '{ print; fflush() }' "$@" | sed -u -E 's-([0-9]+)\.0+-\1-g; s-([0-9]+\.[0-9]*[1-9]+)0+-\1-g' } # ignore trailing spaces, as well as trailing carriage returns trimend() { awk '{ gsub(/ *\r?$/, ""); print; fflush() }' "$@"; } # ignore trailing spaces, as well as trailing carriage returns trimends() { awk '{ gsub(/ *\r?$/, ""); print; fflush() }' "$@"; } # ignore leading/trailing spaces, as well as trailing carriage returns trimlines() { awk '{ gsub(/^ +| *\r?$/, ""); print; fflush() }' "$@"; } # ignore leading/trailing spaces, as well as trailing carriage returns trimsides() { awk '{ gsub(/^ +| *\r?$/, ""); print; fflush() }' "$@"; } # ignore trailing spaces, as well as trailing carriage returns trimtrail() { awk '{ gsub(/ *\r?$/, ""); print; fflush() }' "$@"; } # ignore trailing spaces, as well as trailing carriage returns trimtrails() { awk '{ gsub(/ *\r?$/, ""); print; fflush() }' "$@"; } # try running a command, emitting an explicit message to standard-error # if the command given fails try() { "$@" || { printf "\n\e[31m%s \e[41m\e[97m failed \e[0m\n" "$*" >&2 return 255 } } # Transform Strings with Python; uses my script `tbp` tsp() { tbp -s "$@"; } # run the command given, trying to turn its output into TSV (tab-separated # values); uses my script `dejson` tsvrun() { jc "$@" | dejson; } # deduplicate lines, keeping them in their original order unique() { awk '!c[$0]++ { print; fflush() }' "$@"; } # concatenate all named input sources unix-style: all trailing CRLFs become # single LFs, each non-empty input will always end in a LF, so lines from # different sources are accidentally joined; also leading UTF-8 BOMs on the # first line of each input are ignored, as those are useless at best unixify() { awk ' FNR == 1 { gsub(/^\xef\xbb\xbf/, "") } { gsub(/\r$/, ""); print; fflush() } ' "$@" } # go UP n folders, or go up 1 folder by default up() { if [ "${1:-1}" -le 0 ]; then cd . return $? fi cd "$(printf "%${1:-1}s" "" | sed 's- -../-g')" || return $? } # convert United States Dollars into CAnadian Dollars, using the latest # official exchange rates from the bank of canada; during weekends, the # latest rate may be from a few days ago; the default amount of usd to # convert is 1, when not given usd2cad() { local site='https://www.bankofcanada.ca/valet/observations/group' local csv_rates="${site}/FX_RATES_DAILY/csv" local url url="${csv_rates}?start_date=$(date -d '3 days ago' +'%Y-%m-%d')" curl -s "${url}" | awk -F, -v amount="$(echo "${1:-1}" | sed 's-_--g')" ' /USD/ { for (i = 1; i <= NF; i++) if($i ~ /USD/) j = i } END { gsub(/"/, "", $j); if (j != 0) printf "%.2f\n", amount * $j }' } # View with `less` v() { less -JMKiCRS "$@"; } # run a command, showing its success/failure right after verdict() { local code "$@" code=$? if [ "${code}" -eq 0 ]; then 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" "$*" >&2 else 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}" >&2 fi return "${code}" } # run `cppcheck` with even stricter options vetc() { cppcheck --enable=portability --enable=style "$@"; } # run `cppcheck` with even stricter options vetcpp() { cppcheck --enable=portability --enable=style "$@"; } # check shell scripts for common gotchas, avoiding complaints about using # the `local` keyword, which is widely supported in practice vetshell() { shellcheck -e 3043 "$@"; } # View with Header runs `less` without line numbers, with ANSI styles, no # line-wraps, and using the first n lines as a sticky-header (1 by default), # so they always show on top vh() { local n="${1:-1}" [ $# -gt 0 ] && shift less --header="$n" -JMKiCRS "$@" } # View Nice Columns; uses my scripts `realign` and `nn` vnc() { realign "$@" | nn --gray | less -JMKiCRS; } # View Nice Hexadecimals; uses my script `nh` vnh() { nh "$@" | less -JMKiCRS; } # View Nice Json / Very Nice Json; uses my scripts `nj` and `nn` vnj() { nj "$@" | less -JMKiCRS; } # View Very Nice Json with Nice Numbers; uses my scripts `nj` and `nn` vnjnn() { nj "$@" | nn --gray | less -JMKiCRS; } # View Nice Numbers; uses my script `nn` vnn() { nn "${@:---gray}" | less -JMKiCRS; } # View Nice Table / Very Nice Table; uses my scripts `nt` and `nn` vnt() { awk '{ gsub(/\r$/, ""); printf "%d\t%s\n", NR - 1, $0; fflush() }' "$@" | nt | nn --gray | awk '(NR - 1) % 5 == 1 && NR > 1 { print "" } { print; fflush() }' | less -JMKiCRS #--header=1 } # View Text with `less` # vt() { less -JMKiCRS "$@"; } # What are these (?); uses my command `nwat` # w() { nwat "$@"; } # What Are These (?) shows what the names given to it are/do wat() { local a if [ $# -eq 0 ]; then printf "\e[31mwat: no names given\e[0m\n" > /dev/stderr return 1 fi for a in "$@"; do # printf "\e[48;2;218;218;218m%-80s\e[0m\n" "$a" printf "\e[7m%-80s\e[0m\n" "$a" # resolve 1 alias level if alias "$a" 2> /dev/null > /dev/null; then a="$(alias "$a" | sed "s-.*=--; s-['\"]--g")" fi if echo "$a" | grep -E '[^ ]+ +[^ ]+' > /dev/null; then # resolved aliases with args/spaces in them would otherwise fail echo "$a" elif whence -f "$a" > /dev/null 2> /dev/null; then # zsh seems to show a shell function's code only via `whence -f` whence -f "$a" elif type "$a" > /dev/null 2> /dev/null; then # dash doesn't support `declare`, and `type` in bash emits # a redundant first output line, when it's a shell function type "$a" | awk ' NR == 1 && /^[a-z0-9_-]+ is a function$/ { skipped = $0; next } { print; fflush() } END { if (NR < 2 && skipped) print skipped } ' else printf "\e[31m%s not found\e[0m\n" "$a" fi done | less -JMKiCRS } # Word-Count TSV, runs the `wc` app using all stats, emitting tab-separated # lines instead wctsv() { printf "file\tbytes\tlines\tcharacters\twords\tlongest\n" stdbuf -oL wc -cmlLw "${@:--}" | sed -E -u \ 's-^ *([^ ]*) *([^ ]*) *([^ ]*) *([^ ]*) *([^ ]*) *([^\r]*)$-\6\t\4\t\1\t\3\t\2\t\5-' | awk ' NR > 1 { print prev; fflush() } { prev = $0 } END { if (NR == 1 || !/^total\t/) print } ' } # get weather forecasts, almost filling the terminal's current width weather() { # ● ⬤ printf "%s~%s\r\n\r\n" "$*" "$(($(tput cols) - 2))" | curl --show-error -s telnet://graph.no:79 | sed -u -E \ -e 's/ *\r?$//' \ -e '/^\[/d' \ -e 's/^ *-= *([^=]+) +=- *$/\1\n/' \ -e 's/-/\x1b[38;2;196;160;0m●\x1b[0m/g' \ -e 's/^( +)\x1b\[38;2;196;160;0m●\x1b\[0m/\1-/g' \ -e 's/\|/\x1b[38;2;52;101;164m█\x1b[0m/g' \ -e 's/#/\x1b[38;2;218;218;218m█\x1b[0m/g' \ -e 's/\^/\x1b[38;2;164;164;164m^\x1b[0m/g' \ -e 's/\*/○/g' } # recursively find all files with trailing spaces/CRs wheretrails() { rg -c --line-buffered '[ \r]+$' "${@:-.}"; } # recursively find all files with trailing spaces/CRs whichtrails() { rg -c --line-buffered '[ \r]+$' "${@:-.}"; } # run `xargs`, using whole lines as extra arguments x() { xargs -d '\n' "$@"; } # run `xargs`, using zero/null bytes as the extra-arguments terminator x0() { xargs -0 "$@"; } # run `xargs`, using whole lines as extra arguments xl() { xargs -d '\n' "$@"; } # Youtube Audio Player yap() { local url # some youtube URIs end with extra playlist/tracker parameters url="$(echo "$1" | sed 's-&.*--')" mpv "$(yt-dlp -x --audio-format aac --get-url "${url}" 2> /dev/null)" } # show a calendar for the current year, or for the year given year() { { # show the current date/time center-aligned printf "%22s\e[32m%s\e[0m \e[34m%s\e[0m\n\n" \ "" "$(date +'%a %b %d')" "$(date +%T)" # show a whole-year calendar cal -y "$@" } | less -JMKiCRS } # show the current date in the YYYY-MM-DD format ymd() { date +'%Y-%m-%d'; } # YouTube Url ytu() { local url # some youtube URIs end with extra playlist/tracker parameters url="$(echo "$1" | sed 's-&.*--')" [ $# -gt 0 ] && shift yt-dlp "$@" --get-url "${url}" }