#!/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. case "$1" in -h|--h|-help|--help) # show help message, using the info-comment from this very script awk ' /^case / { exit } /^# +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[38;2;0;0;0mDon't run this script, source it instead: to do that,\e[0m\n" printf "\e[48;2;255;255;135m\e[38;2;0;0;0mrun '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 tool `bsbs` (Book-like Side By Side) alias 1='bsbs 1' alias 2='bsbs 2' alias 3='bsbs 3' alias 4='bsbs 4' alias 5='bsbs 5' alias 6='bsbs 6' alias 7='bsbs 7' alias 8='bsbs 8' alias 9='bsbs 9' alias 0='bsbs 10' # find name from the local `apt` database of installable packages aptfind() { local arg local gap=0 local options='-JMKiCRS' if [ $# -eq 1 ]; then options='--header=1 -JMKiCRS' fi for arg in "$@"; do [ "${gap}" -gt 0 ] && printf "\n" gap=1 printf "\e[7m%-80s\e[0m\n\n" "${arg}" # despite warnings, the `search` command has been around for years apt search "${arg}" 2>/dev/null | grep -E -A 1 "^[a-z0-9-]*${arg}" | sed -u 's/^--$//' done | less "${options}" } # APT UPdate/grade aptup() { sudo apt update && sudo apt upgrade "$@"; sudo -k; } # emit each argument given as its own line of output args() { awk 'BEGIN { for (i = 1; i < ARGC; i++) print ARGV[i]; exit }' "$@"; } # AWK in BLOCK/paragraph-input mode awkblock() { if [ -p 1 ] || [ -t 1 ]; then stdbuf -oL awk -F='' -v RS='' "$@" else awk -F='' -v RS='' "$@" fi } # AWK in Paragraph-input mode alias awkpar=awkblock # AWK in TSV input/output mode awktsv() { if [ -p 1 ] || [ -t 1 ]; then stdbuf -oL awk -F "\t" -v OFS="\t" "$@" else awk -F "\t" -v OFS="\t" "$@" fi } # show an ansi-styled BANNER-like line banner() { printf "\e[7m%-$(tput cols)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" "" } # 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 - } # play a repeating synthetic-bell-like sound lasting the number of seconds # given, or for 1 second by default; uses my tool `waveout` bell() { local f='sin(880*tau*u) * exp(-10*u)' waveout "${1:-1}" "${2:-1} * $f" | mpv --really-quiet - } # Breathe Header 5: add an empty line after the first one (the header), # then separate groups of 5 lines with empty lines between them bh5() { if [ -p 1 ] || [ -t 1 ]; then stdbuf -oL awk '(NR - 1) % 5 == 1 && NR > 1 { print "" } 1' "$@" else awk '(NR - 1) % 5 == 1 && NR > 1 { print "" } 1' "$@" fi } # 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[38;2;204;0;0mno folder named %s\e[0m\n" "${arg}" >&2 return 1 fi stdbuf -oL find "${arg}" -type f -size "$n"c -o -size +"$n"c done } # Breathe Lines 5: separate groups of 5 lines with empty lines bl5() { if [ -p 1 ] || [ -t 1 ]; then stdbuf -oL awk 'NR % 5 == 1 && NR != 1 { print "" } 1' "$@" else awk 'NR % 5 == 1 && NR != 1 { print "" } 1' "$@" fi } # 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 tool `bsbs` alias bman=bookman # BOOK-like MANual, lays out `man` docs as pairs of side-by-side pages; uses # my tool `bsbs` bookman() { local w w="$(tput cols)" w="$((w / 2 - 4))" if [ "$w" -lt 65 ]; then w=65 fi MANWIDTH="$w" man "$@" | bsbs 2 } # play a busy-phone-line sound lasting the number of seconds given, or for 1 # second by default; uses my tool `waveout` busy() { local f='min(1, exp(-90*(u-0.5))) * (sin(480*tau*t) + sin(620*tau*t)) / 2' waveout "${1:-1}" "${2:-1} * $f" | mpv --really-quiet - } alias c=cat # CAlculator with Nice numbers runs my tool `ca` and colors results with # my tool `nn`, alternating styles to make long numbers easier to read can() { local arg for arg in "$@"; do printf "\e[7m%s\e[0m\n" "${arg}" >&2 ca "${arg}" | nn --gray done } # uppercase the first letter on each line, and lowercase all later letters capitalize() { sed -E -u 's-^(.*)-\L\1-; s-^(.)-\u\1-'; } # conCATenate Lines guarantees no lines are ever accidentally joined # across inputs, always emitting a line-feed at the end of every line catl() { awk 1 "$@"; } # Count with AWK: count the times the AWK expression/condition given is true cawk() { local cond="${1:-1}" [ $# -gt 0 ] && shift awk ' { low = lower = tolower($0) } '"${cond}"' { count++; c = count } END { print count } ' "$@" } # Compile C Optimized cco () { cc -Wall -O3 -s -fanalyzer -march=native -mtune=native -flto "$@"; } # Compile C Plus Plus Optimized cppo () { c++ -Wall -O3 -s -fanalyzer -march=native -mtune=native -flto "$@"; } # center-align lines of text, using the current screen width center() { awk -v width="$(tput cols)" ' { gsub(/\r$/, "") lines[NR] = $0 s = $0 gsub(/\x1b\[[0-9;]*[A-Za-z]/, "", s) # ANSI style-changers l = length(s) if (maxlen < l) maxlen = l } END { n = (width - maxlen) / 2 if (n % 1) n = n - (n % 1) fmt = sprintf("%%%ds%%s\n", (n > 0) ? n : 0) for (i = 1; i <= NR; i++) printf fmt, "", lines[i] } ' "$@" } # Colored Go Test on the folder given; uses my command `jawk` cgt() { go test "${1:-.}" 2>&1 | jawk '/^ok/' '/^[-]* ?FAIL/' '/^\?/'; } # 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() { local res if [ -p /dev/stdout ]; then "$@" return $? fi tput smcup "$@" res=$? tput rmcup return "${res}" } # 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 alias cls=clear # 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'; } # Color Syntax: run syntax-coloring app `bat` without line-wrapping cs() { 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 --paging=never "$@" | # make colors readable even on light backgrounds sed -e 's-\x1b\[38;5;70m-\x1b[38;5;28m-g' \ -e 's-\x1b\[38;5;214m-\x1b[38;5;208m-g' \ -e 's-\x1b\[38;5;228m-\x1b[48;5;228m-g' | less -JMKiCRS } # Color Syntax of all files in a Folder, showing line numbers csf() { local cmd="bat" # debian linux uses a different name for the `bat` app if [ -e "/usr/bin/batcat" ]; then cmd="batcat" fi find "${1:-.}" -type f -print0 | xargs --null "${cmd}" \ --style=plain,header,numbers --theme='Monokai Extended Light' \ --wrap=never --color=always | # make colors readable even on light backgrounds sed -e 's-\x1b\[38;5;70m-\x1b[38;5;28m-g' \ -e 's-\x1b\[38;5;214m-\x1b[38;5;208m-g' \ -e 's-\x1b\[38;5;228m-\x1b[48;5;228m-g' | less -JMKiCRS } # CURL Silent spares you the progress bar, but still tells you about errors curls() { curl --show-error -s "$@"; } # listen to streaming DANCE music dance() { printf "streaming \e[7mDance Wave Retro\e[0m\n" # mpv --quiet https://retro.dancewave.online/retrodance.mp3 mpv --really-quiet https://retro.dancewave.online/retrodance.mp3 } # 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; } # DEDUPlicate prevents lines from appearing more than once dedup() { if [ -p 1 ] || [ -t 1 ]; then stdbuf -oL awk '!c[$0]++' "$@" else awk '!c[$0]++' "$@" fi } # dictionary-DEFine the word given, using an online service alias def=define # dictionary-define the word given, using an online service define() { local arg local gap=0 local options='-JMKiCRS' if [ $# -eq 0 ]; then printf "\e[38;2;204;0;0mdefine: no names given\e[0m\n" >&2 return 1 fi if [ $# -eq 1 ]; then options='--header=1 -JMKiCRS' fi for arg in "$@"; do [ "${gap}" -gt 0 ] && printf "\n" gap=1 printf "\e[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 next } /^[1-9][0-9]{2} / { printf "\x1b[38;2;128;128;128m%s\x1b[0m\n", $0 next } 1 ' done | less "${options}" } # turn JSON Lines into a proper json array dejsonl() { jq -s -M "${@:-.}"; } # 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() { if [ -p 1 ] || [ -t 1 ]; then stdbuf -oL awk '{ gsub(/ *\r?$/, ""); print }' "$@" else awk '{ gsub(/ *\r?$/, ""); print }' "$@" fi } # turn UTF-16 data into UTF-8 deutf16() { iconv -f utf16 -t utf8 "$@"; } # DICtionary-define the word given locally dic() { local arg local gap=0 local options='-JMKiCRS' if [ $# -eq 0 ]; then printf "\e[38;2;204;0;0mdic: no names given\e[0m\n" >&2 return 1 fi if [ $# -eq 1 ]; then options='--header=1 -JMKiCRS' fi for arg in "$@"; do [ "${gap}" -gt 0 ] && printf "\n" gap=1 printf "\e[7m%-80s\x1b[0m\n" "${arg}" dict "${arg}" 2>&1 | awk ' NR == 1 && /^No definitions found for / { err = 1 } err { printf "\x1b[38;2;204;0;0m%s\x1b[0m\n", $0; next } 1 ' done | less "${options}" } # 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 }' } # emit a line with a repeating dot-like symbol in it dots() { [ "${1:-80}" -gt 0 ] && printf "%${1:-80}s\n" "" | sed 's- -·-g'; } # show the current Date and Time dt() { printf "\e[38;2;78;154;6m%s\e[0m \e[38;2;52;101;164m%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 "%20s\e[38;2;78;154;6m%s\e[0m \e[38;2;52;101;164m%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 # fix debian/ncal's weird way to highlight the current day ncal -C -3 | sed -E 's/_\x08(.)/\x1b[7m\1\x1b[0m/g' else cal -3 fi } | less -JMKiCRS } alias e=echo # edit plain-text files edit() { micro "$@"; } # EDit RUN shell commands, using an interactive editor; uses my tool `leak` edrun() { # dash doesn't support the process-sub syntax # . <( micro -readonly true -filetype shell | leak --inv ) micro -readonly true -filetype shell | leak --inv | . /dev/fd/0 } # show all empty files in a folder, digging recursively emptyfiles() { local arg for arg in "${@:-.}"; do if [ ! -d "${arg}" ]; then printf "\e[38;2;204;0;0mno folder named %s\e[0m\n" "${arg}" >&2 return 1 fi stdbuf -oL find "${arg}" -type f -empty done } # show all empty folders in a folder, digging recursively emptyfolders() { local arg for arg in "${@:-.}"; do if [ ! -d "${arg}" ]; then printf "\e[38;2;204;0;0mno folder named %s\e[0m\n" "${arg}" >&2 return 1 fi stdbuf -oL find "${arg}" -type d -empty done } # Extended Plain Interactive Grep epig() { ugrep --color=never -Q -E "$@"; } # Editor Read-Only ero() { micro -readonly true "$@"; } # Expand 4 turns each tab into up to 4 spaces expand4() { expand -t 4 "$@"; } # 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 } ' } # 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 "$@"; } filemime() { file --mime-type "$@"; } # show all files in folders, digging recursively files() { local arg for arg in "${@:-.}"; do if [ -f "${arg}" ]; then printf "%s\n" "$(realpath "${arg}")" continue fi if [ -d "${arg}" ]; then stdbuf -oL find "${arg}" -type f fi done | stdbuf -oL awk '!c[$0]++' } # 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[38;2;204;0;0mno folder named %s\e[0m\n" "${arg}" >&2 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:--}"; } # 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; trailing spaces # are also ignored fixlines() { local command="awk" if [ -p 1 ] || [ -t 1 ]; then command="stdbuf -oL awk" fi ${command} ' FNR == 1 { gsub(/^\xef\xbb\xbf/, "") } { gsub(/ *\r?$/, ""); print } ' "$@" } # show all folders in other folders, digging recursively folders() { local arg for arg in "${@:-.}"; do if [ -f "${arg}" ]; then continue fi if [ ! -d "${arg}" ]; then printf "\e[31mno folder named %s\e[0m\n" "${arg}" > /dev/stderr fi stdbuf -oL find "${arg}" -type d | stdbuf -oL awk '!/^\.$/' done | stdbuf -oL awk '!c[$0]++' } # show total sizes for the all folders given, digging recursively foldersizes() { local arg for arg in "${@:-.}"; do if [ ! -d "${arg}" ]; then printf "\e[38;2;204;0;0mno folder named %s\e[0m\n" "${arg}" >&2 return 1 fi du "${arg}" | sort -rnk1 done | awk '{ $1 *= 1024; print }' | sed -u 's-^ *--; s- *-\t-1' } # 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 }' } # convert FeeT² (squared) into meters² ft2() { echo "${@:-1}" | sed -E 's-_--g; s- +-\n-g' | awk '/./ { printf "%.2f\n", 0.09290304 * $0 }' } # convert a mix of FeeT and INches into meters ftin() { local ft="${1:-0}" ft="$(echo "${ft}" | sed 's-_--g')" local in="${2:-0}" in="$(echo "${in}" | sed 's-_--g')" awk "BEGIN { print 0.3048 * ${ft} + 0.0254 * ${in}; exit }" } # convert GALlons into liters gal() { echo "${@:-1}" | sed -E 's-_--g; s- +-\n-g' | awk '/./ { printf "%.2f\n", 3.785411784 * $0 }' } # convert binary GigaBytes into bytes gb() { echo "${@:-1}" | sed -E 's-_--g; s- +-\n-g' | awk '/./ { printf "%.4f\n", 1073741824 * $0 }' | sed 's-\.00*$--' } # 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 } END { if (NR > 0) print "" } ' "$@" } # 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 tool `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 } # Style lines using a GRAY-colored BACKground grayback() { local command="awk" if [ -p 1 ] || [ -t 1 ]; then command="stdbuf -oL awk" fi ${command} ' { gsub(/\x1b\[0m/, "\x1b[0m\x1b[48;2;218;218;218m") printf "\x1b[48;2;218;218;218m%s\x1b[0m\n", $0 } ' "$@" } # Global extended regex SUBstitute, using the AWK function of the same name: # arguments are used as regex/replacement pairs, in that order gsub() { local command="awk" if [ -p 1 ] || [ -t 1 ]; then command="stdbuf -oL awk" fi ${command} ' 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 } ' "$@" } # show Help laid out on 2 side-by-side columns; uses my tool `bsbs` h2() { naman "$@" | bsbs 2; } # Highlight (lines) with AWK hawk() { local command="awk" if [ -p 1 ] || [ -t 1 ]; then command="stdbuf -oL awk" fi local cond="${1:-1}" [ $# -gt 0 ] && shift ${command} ' { low = lower = tolower($0) } '"${cond}"' { gsub(/\x1b\[0m/, "\x1b[0m\x1b[7m") printf "\x1b[7m%s\x1b[0m\n", $0 next } 1 ' "$@" } # play a heartbeat-like sound lasting the number of seconds given, or for 1 # second by default; uses my tool `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" 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" }' } # 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 '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 }' } # find all hyperlinks inside HREF attributes in the input text href() { local command="awk" if [ -p 1 ] || [ -t 1 ]; then command="stdbuf -oL awk" fi ${command} ' BEGIN { e = "href=\"[^\"]+\"" } { for (s = $0; match(s, e); s = substr(s, RSTART + RLENGTH)) { print substr(s, RSTART + 6, RLENGTH - 7) } } ' "$@" } # find all hyperlinks inside HREF attributes in the input text alias hrefs=href # 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; } # start each non-empty line with extra n spaces indent() { local command="awk" if [ -p 1 ] || [ -t 1 ]; then command="stdbuf -oL awk" fi ${command} ' BEGIN { n = ARGV[1] + 0 delete ARGV[1] fmt = sprintf("%%%ds%%s\n", (n > 0) ? n : 0) } /^\r?$/ { print ""; next } { gsub(/\r$/, ""); printf(fmt, "", $0) } ' "$@" } # Interactive Read-Only Top irot() { htop --readonly "$@"; } # emit each word-like item from each input line on its own line; when a file # has tabs on its first line, items are split using tabs alone, which allows # items to have spaces in them items() { local command="awk" if [ -p 1 ] || [ -t 1 ]; then command="stdbuf -oL awk" fi ${command} ' FNR == 1 { FS = ($0 ~ /\t/) ? "\t" : " "; $0 = $0 } { gsub(/\r$/, ""); for (i = 1; i <= NF; i++) print $i } ' "$@" } alias j0=json0 alias j2=json2 # 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 } # Json Lines turns JSON top-level arrays into multiple individually-JSON lines # using the `jq` app, keeping all other top-level values as single line JSON # outputs, allowing an optional filepath as the data source jl() { jq -c -C ".[]" "${1:--}"; } # show a `dad` JOKE from the web, sometimes even a very funny one joke() { curl --show-error -s https://icanhazdadjoke.com | fold -s | awk '{ gsub(/ *\r?$/, ""); print }' } # 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, and even an optional transformation formula # after that json2() { jq --indent 2 -M "${2:-.}" "${1:--}"; } # JSON Lines turns JSON top-level arrays into multiple individually-JSON lines # using the `jq` app, keeping all other top-level values as single line JSON # outputs, allowing an optional filepath as the data source jsonl() { jq -c -C ".[]" "${1:--}"; } # 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; } # convert binary KiloBytes into bytes kb() { echo "${@:-1}" | sed -E 's-_--g; s- +-\n-g' | awk '/./ { printf "%.2f\n", 1024 * $0 }' | sed 's-\.00*$--' } # run `less`, showing line numbers, among other settings l() { less -JMKNiCRS "$@"; } # find the LAN (local-area network) IP address for this device lanip() { hostname -I; } # play a stereotypical once-a-second laser sound for the number of seconds # given, or for 1 second (once) by default; uses my tool `waveout` laser() { local f='sin(100 * tau * exp(-40 * u))' waveout "${1:-1}" "${2:-1} * $f" | mpv --really-quiet - } # get the last n lines, or 1 by default last() { tail -n "${1:-1}" "${2:--}"; } # convert pounds (LB) into kilograms lb() { echo "${@:-1}" | sed -E 's-_--g; s- +-\n-g' | awk '/./ { printf "%.2f\n", 0.45359237 * $0 }' } # convert a mix of pounds (LB) and weight-ounces (OZ) into kilograms lboz() { local lb="${1:-0}" lb="$(echo "${lb}" | sed 's-_--g')" local oz="${2:-0}" oz="$(echo "${oz}" | sed 's-_--g')" awk "BEGIN { print 0.45359237 * ${lb} + 0.028349523 * ${oz}; exit }" } # 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 1 runs `less` with line numbers, ANSI styles, without # line-wraps, and using the first line as a sticky-header, so it always # shows on top lh1() { less --header=1 -JMKNiCRS "$@"; } # Less with Header 2 runs `less` with line numbers, ANSI styles, without # line-wraps, and using the first 2 lines as a sticky-header, so they # always show on top lh2() { less --header=2 -JMKNiCRS "$@"; } # ensure lines are never accidentally joined across files, by always emitting # a line-feed at the end of each line alias lines=catl # regroup adjacent lines into n-item tab-separated lines lineup() { local command="awk" if [ -p 1 ] || [ -t 1 ]; then command="stdbuf -oL awk" fi local n="${1:-0}" [ $# -gt 0 ] && shift if [ "$n" -le 0 ]; then ${command} ' NR > 1 { printf "\t" } { printf "%s", $0 } END { if (NR > 0) print "" } ' "$@" return $? fi ${command} -v n="$n" ' NR % n != 1 && n > 1 { printf "\t" } { printf "%s", $0 } NR % n == 0 { print "" } END { if (NR % n != 0) print "" } ' "$@" } # try to run the command given using line-buffering for its (standard) output livelines() { stdbuf -oL "$@"; } # LOAD data from the filename or URI given; uses my tool `get` alias load=get # 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 alias low=tolower # LOWERcase all ASCII symbols alias lower=tolower # Listen To Youtube alias lty=yap # convert binary MegaBytes into bytes mb() { echo "${@:-1}" | sed -E 's-_--g; s- +-\n-g' | awk '/./ { printf "%.2f\n", 1048576 * $0 }' | sed 's-\.00*$--' } # Multi-Core MAKE runs `make` using all cores mcmake() { make -j "$(nproc)" "$@"; } # convert MIles into kilometers mi() { echo "${@:-1}" | sed -E 's-_--g; s- +-\n-g' | awk '/./ { printf "%.2f\n", 1.609344 * $0 }' } # 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}" } # MINimize DECimalS ignores all trailing decimal zeros in numbers, and even # the decimal dots themselves, when decimals in a number are all zeros # mindecs() { # sed -u -E 's-([0-9]+)\.0+\W-\1-g; s-([0-9]+\.[0-9]*[1-9])0+\W-\1-g' # } alias 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 counting from 0, using a tab right after each line number n0() { if [ -p 1 ] || [ -t 1 ]; then stdbuf -oL nl -b a -w 1 -v 0 "$@" else nl -b a -w 1 -v 0 "$@" fi } # Number all lines counting from 1, using a tab right after each line number n1() { if [ -p 1 ] || [ -t 1 ]; then stdbuf -oL nl -b a -w 1 -v 1 "$@" else nl -b a -w 1 -v 1 "$@" fi } # NArrow MANual, keeps `man` narrow, even if the window/tab is wide when run naman() { local w w="$(tput cols)" w="$((w / 2 - 4))" if [ "$w" -lt 80 ]; then w=80 fi MANWIDTH="$w" man "$@" } # 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) } # listen to streaming NEW WAVE music newwave() { printf "streaming \e[7mNew Wave radio\e[0m\n" mpv --quiet https://puma.streemlion.com:2910/stream } # Nice CAlculator runs my tool `ca` and colors results with my tool `nn`, # alternating styles to make long numbers easier to read nica() { local arg for arg in "$@"; do printf "\e[7m%s\e[0m\n" "${arg}" >&2 ca "${arg}" | nn --gray done } # NIce(r) COlumns makes the output of many commands whose output starts with # a header line easier to read; uses my tool `nn` nico() { local command="awk" if [ -p 1 ] || [ -t 1 ]; then command="stdbuf -oL awk" fi ${command} ' (NR - 1) % 5 == 1 && NR > 1 { print "" } { printf "%5d %s\n", NR - 1, $0 } ' "$@" | 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 } # Nice Json colors data using the `jq` app, allowing an optional filepath as # the data source, and even an optional transformation formula nj() { jq -C "${2:-.}" "${1:--}"; } # Nice Json Lines colors data using the `jq` app, allowing an optional filepath # as the data source njl() { jq -c -C ".[]" "${1:--}"; } # pipe-run my tools `nj` (Nice Json) and `nn` (Nice Numbers) njnn() { nj "$@" | nn --gray; } # Nice JSON Lines colors data using the `jq` app, allowing an optional filepath # as the data source njsonl() { jq -c -C ".[]" "${1:--}"; } # Narrow MANual, keeps `man` narrow, even if the window/tab is wide when run alias nman=naman # convert Nautical MIles into kilometers nmi() { echo "${@:-1}" | sed -E 's-_--g; s- +-\n-g' | awk '/./ { printf "%.2f\n", 1.852 * $0 }' } # play a white-noise sound lasting the number of seconds given, or for 1 # second by default; uses my tool `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 Print Python result; uses my tool `nn` npp() { python -c "print($1)" | nn "${2:---gray}"; } # Nice Size, using my tools `nn` and `cext` ns() { wc -c "$@" | nn --gray | cext; } # Nice Systemctl Status nss() { systemctl status "$@" 2>&1 | sed 's-\x1b\[[^A-Za-z][A-Za-z]--g' | sed -E \ -e 's-(^[^ ] )([^ ]+\.service)-\1\x1b[7m\2\x1b[0m-' \ -e 's- (enabled)- \x1b[38;2;0;135;95m\x1b[7m\1\x1b[0m-g' \ -e 's- (disabled)- \x1b[38;2;215;95;0m\x1b[7m\1\x1b[0m-g' \ -e 's- (active \(running\))- \x1b[38;2;0;135;95m\x1b[7m\1\x1b[0m-g' \ -e 's- (inactive \(dead\))- \x1b[38;2;204;0;0m\x1b[7m\1\x1b[0m-g' \ -e 's-^(Unit .* could not .*)$-\x1b[38;2;204;0;0m\x1b[7m\1\x1b[0m\n-' \ -e 's-(\[WARN\].*)$-\x1b[38;2;215;95;0m\x1b[7m\1\x1b[0m\n-' \ -e 's-(\[ERR\].*)$-\x1b[38;2;204;0;0m\x1b[7m\1\x1b[0m\n-' | less -JMKiCRS } # 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 } # (Nice) What Are These (?) shows what the names given to it are/do, coloring # the syntax of shell functions nwat() { local arg local gap=0 if [ $# -eq 0 ]; then printf "\e[38;2;204;0;0mnwat: no names given\e[0m\n" >&2 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 arg in "$@"; do [ "${gap}" -gt 0 ] && printf "\n" gap=1 printf "\e[48;2;218;218;218m%-80s\e[0m\n" "${arg}" # printf "\e[7m%-80s\e[0m\n" "${arg}" while alias "${arg}" > /dev/null 2> /dev/null; do arg="$(alias "${arg}" | sed -E "s-^[^=]+=['\"](.+)['\"]\$-\\1-")" done if type "${arg}" > /dev/null 2> /dev/null; then type "${arg}" | awk 'NR == 1 && / is a function$/ { next } 1' | "$cmd" -l sh --style=plain --theme='Monokai Extended Light' \ --wrap=never --color=always | sed 's-\x1b\[38;5;70m-\x1b\[38;5;28m-g' else printf "\e[38;2;204;0;0m%s not found\e[0m\n" "${arg}" fi done | less -JMKiCRS } # Nice Weather Forecast gets weather forecasts, using ANSI styles and almost # filling the terminal's current width nwf() { local gap=0 local width="$(($(tput cols) - 2))" local place for place in "$@"; do [ "${gap}" -gt 0 ] && printf "\n" gap=1 # printf "\e[48;2;218;218;218m%-${width}s\e[0m\n" "${place}" printf "\e[7m%-${width}s\e[0m\n" "${place}" printf "%s~%s\r\n\r\n" "${place}" "${width}" | 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\1\x1b[0m/g' \ -e 's/\*/○/g' \ -e 's/_/\x1b[48;2;216;200;0m_\x1b[0m/g' \ -e 's/([0-9][0-9]\/[0-9][0-9])/\x1b[7m\1\x1b[0m/g' | awk 1 done | less -JMKiCRS } # Nice Zoom Json, using my tools `zj`, and `nj` nzj() { zj "$@" | nj; } # Plain text, by ignoring ANSI terminal styling alias p=plain # Print Awk expression pa() { awk "BEGIN { print ${1:-0}; exit }"; } # Plain Interactive Grep pig() { ugrep --color=never -Q -E "$@"; } # make text plain, by ignoring ANSI terminal styling plain() { if [ -p 1 ] || [ -t 1 ]; then stdbuf -oL awk '{ gsub(/\x1b\[[0-9;]*[A-Za-z]/, ""); print }' "$@" else awk '{ gsub(/\x1b\[[0-9;]*[A-Za-z]/, ""); print }' "$@" fi } # play audio/video media play() { mpv "${@:--}"; } # Print Python result pp() { python -c "print($1)"; } # PRecede (input) ECHO, prepends a first line to stdin lines precho() { echo "$@" && cat /dev/stdin; } # LABEL/precede data with an ANSI-styled line prelabel() { printf "\e[7m%-*s\e[0m\n" "$(($(tput cols) - 2))" "$*"; cat -; } # PREcede (input) MEMO, prepends a first highlighted line to stdin lines prememo() { printf "\e[7m%s\e[0m\n" "$*"; cat -; } # 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] } if (ARGC > 1) printf "\n" exit } ' "$@" cat - } # Plain RipGrep prg() { rg --color=never "$@"; } # show/list all current processes processes() { local res res="$(ps aux)" echo "${res}" | awk '!/ps aux$/' | sed -E \ -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 alias pya=yap # Quiet ignores stderr, without any ugly keyboard-dancing alias q=quiet # 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 alias 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 } # play a ready-phone-line sound lasting the number of seconds given, or for 1 # second by default; uses my tool `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 command="awk" if [ -p 1 ] || [ -t 1 ]; then command="stdbuf -oL awk" fi local w="${1:-80}" [ $# -gt 0 ] && shift ${command} ' FNR == 1 && NR > 1 { print "" } { gsub(/\r$/, ""); print } ' "$@" | fold -s -w "$w" | sed -u -E 's- *\r?$--' } # REPeat STRing emits a line with a repeating string in it, given both a # string and a number in either order repstr() { awk ' BEGIN { if (ARGV[2] ~ /^[+-]?[0-9]+$/) { symbol = ARGV[1] times = ARGV[2] + 0 } else { symbol = ARGV[2] times = ARGV[1] + 0 } if (times < 0) exit if (symbol == "") symbol = "-" s = sprintf("%*s", times, "") gsub(/ /, symbol, s) print s exit } ' "$@" } # 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 tool `waveout` ringtone() { local f='sin(2048 * tau * t) * exp(-50 * (t%0.1))' waveout "${1:-1}" "${2:-1} * $f" | mpv --really-quiet - } # Read-Only Editor roe() { micro -readonly true "$@"; } # Read-Only Micro (text editor) rom() { micro -readonly true "$@"; } # 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-·····-····╵-' } # SystemCTL; `sysctl` is already taken for a separate/unrelated app alias sctl=systemctl # 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 a command, then RUN it showrun() { printf "\e[7m%s\e[0m\n" "$*"; "$@"; } # skip the first n lines, or the 1st line by default skip() { tail -n +$(("${1:-1}" + 1)) "${2:--}"; } # skip the last n lines, or the last line by default skiplast() { head -n -"${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 tools `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[38;2;204;0;0mno folder named %s\e[0m\n" "${arg}" >&2 return 1 fi stdbuf -oL find "${arg}" -type f -size -"$n"c done } # Stdbuf Output Line-buffered sol() { stdbuf -oL "$@"; } # 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 $*" ' FNR == 1 { gsub(/^\xef\xbb\xbf/, "") } { 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')\" $*" ' FNR == 1 { gsub(/^\xef\xbb\xbf/, "") } { 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() { local command="awk" if [ -p 1 ] || [ -t 1 ]; then command="stdbuf -oL awk" fi ${command} ' FNR == 1 { gsub(/^\xef\xbb\xbf/, "") } { gsub(/^ +| *\r?$/, "") gsub(/ *\t */, "\t") gsub(/ +/, " ") print } ' "$@" } substr() { local command="awk" if [ -p 1 ] || [ -t 1 ]; then command="stdbuf -oL awk" fi if [ $# -lt 2 ]; then printf "missing 1-based start index, and substring length\n" >&2 exit 1 fi ${command} '{ print substr($0, '"$1"', '"$2"') }' } # 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}" } # Time the command given alias t=time # show a reverse-sorted tally of all lines read, where ties are sorted # alphabetically tally() { awk -v sortcmd="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]) | sortcmd } } ' "$@" } # Titled conCATenate Lines highlights each filename, before emitting its lines tcatl() { local command="awk" if [ -p 1 ] || [ -t 1 ]; then command="stdbuf -oL awk" fi ${command} ' FNR == 1 { printf "\x1b[7m%s\x1b[0m\n", FILENAME; fflush() } 1 ' "$@" } # turn documents into plain-TEXT text() { pandoc -s -t plain "$@"; } # run `top` without showing any of its output after quitting it tip() { tput smcup; top "$@"; tput rmcup; } # 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() { if [ -p 1 ] || [ -t 1 ]; then stdbuf -oL awk '{ print tolower($0) }' "$@" else awk '{ print tolower($0) }' "$@" fi } # 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 tool `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[38;2;204;0;0mno folder named %s\e[0m\n" "${arg}" >&2 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[38;2;204;0;0mno folder named %s\e[0m\n" "${arg}" >&2 return 1 fi stdbuf -oL find "${arg}" -maxdepth 1 -type d | stdbuf -oL awk '!/^\.$/' 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$/, "") } FNR == 1 { FS = ($0 ~ /\t/) ? "\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" } } ' "$@" } # try running a command, emitting an explicit message to standard-error # if the command given fails try() { local code "$@" code=$? if [ "${code}" -ne 0 ]; then 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}" fi >&2 return "${code}" } # Time Verbosely the command given tv() { /usr/bin/time -v "$@"; } # Unique via AWK, avoids lines duplicating the expression given # uawk() { # local code="${1:-\$0}" # [ $# -gt 0 ] && shift # if [ -p 1 ] || [ -t 1 ]; then # stdbuf -oL awk '!c['"${code}"']++' "$@" # else # awk '!c['"${code}"']++' "$@" # fi # } # Unique via AWK, avoids lines duplicating the expression given uawk() { local code="${1:-\$0}" [ $# -gt 0 ] && shift local command="awk" if [ -p 1 ] || [ -t 1 ]; then command="stdbuf -oL awk" fi ${command} ' BEGIN { for (i = 1; i < ARGC; i++) if (files[ARGV[i]]++) delete ARGV[i] } !c['"${code}"']++ ' "$@" } # Underline Every 5 lines: make groups of 5 lines stand out by underlining # the last line of each such group ue5() { local command="awk" if [ -p 1 ] || [ -t 1 ]; then command="stdbuf -oL awk" fi ${command} ' NR % 5 == 0 && NR != 1 { gsub(/\x1b\[0m/, "\x1b[0m\x1b[4m") printf("\x1b[4m%s\x1b[0m\n", $0) next } 1 ' "$@" } # deduplicate lines, keeping them in their original order # alias unique=dedup # skip the first/leading n bytes unleaded() { tail -c +$(("$1" + 1)) "${2:--}"; } # go UP n folders, or go up 1 folder by default up() { if [ "${1:-1}" -le 0 ]; then cd . else cd "$(printf "%${1:-1}s" "" | sed 's- -../-g')" || return $? fi } # 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 if [ "${COLORBLIND}" = 1 ]; then # color-blind-friendly version using a blue `success` message printf "\n\e[38;2;0;95;215m%s \e[48;2;0;95;215m\e[38;2;255;255;255m succeeded \e[0m\n" "$*" else 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" "$*" fi 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}" fi >&2 return "${code}" } # run `cppcheck` with even stricter options vetc() { cppcheck --enable=portability --enable=style --check-level=exhaustive "$@" } # run `cppcheck` with even stricter options, also checking for c89 compliance vetc89() { cppcheck --enable=portability --enable=style --check-level=exhaustive \ --std=c89 "$@" } # run `cppcheck` with even stricter options vetcpp() { cppcheck --enable=portability --enable=style --check-level=exhaustive "$@" } # 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 1 runs `less` without line numbers, ANSI styles, without # line-wraps, and using the first line as a sticky-header, so it always shows # on top vh1() { less --header=1 -JMKiCRS "$@"; } # View with Header 2 runs `less` without line numbers, ANSI styles, without # line-wraps, and using the first 2 lines as sticky-headers, so they always # show on top vh2() { less --header=2 -JMKiCRS "$@"; } # View Nice Table / Very Nice Table; uses my tools `nt` and `nn` # vnt() { n0 "$@" | nt | nn --gray | bh5 | less -JMKiCRS; } # View Nice Table / Very Nice Table; uses my tools `nt` and `nn` vnt() { n0 "$@" | nt | nn --gray | bh5 | less -JMKiCRS --header=1; } # View Text, turning documents into plain-text if needed vt() { local arg local gap=0 local options='-JMKiCRS' if [ $# -eq 1 ]; then options='--header=1 -JMKiCRS' fi if [ $# -eq 0 ]; then pandoc -s -t plain - 2>&1 | less -JMKiCRS else for arg in "$@"; do [ "${gap}" -eq 1 ] && printf "\n" gap=1 printf "\e[7m%-80s\e[0m\n" "${arg}" pandoc -s -t plain "${arg}" 2>&1 | awk 1 done | less "${options}" fi } # What Are These (?) shows what the names given to it are/do wat() { local arg local gap=0 if [ $# -eq 0 ]; then printf "\e[38;2;204;0;0mwat: no names given\e[0m\n" >&2 return 1 fi for arg in "$@"; do [ "${gap}" -gt 0 ] && printf "\n" gap=1 # printf "\e[48;2;218;218;218m%-80s\e[0m\n" "${arg}" printf "\e[7m%-80s\e[0m\n" "${arg}" while alias "${arg}" > /dev/null 2> /dev/null; do arg="$(alias "${arg}" | sed -E "s-^[^=]+=['\"](.+)['\"]\$-\\1-")" done if type "${arg}" > /dev/null 2> /dev/null; then type "${arg}" | awk 'NR == 1 && / is a function$/ { next } 1' else printf "\e[38;2;204;0;0m%s not found\e[0m\n" "${arg}" fi done | less -JMKiCRS } # Word-Count TSV, runs the `wc` app using all stats, emitting tab-separated # lines instead wctsv() { local command="awk" if [ -p 1 ] || [ -t 1 ]; then command="stdbuf -oL awk" fi 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-' | ${command} ' NR > 1 { print prev } { prev = $0 } END { if (NR == 1 || !/^total\t/) print } ' } # find all WEB/hyperLINKS (https:// and http://) in the input text weblinks() { local command="awk" if [ -p 1 ] || [ -t 1 ]; then command="stdbuf -oL awk" fi ${command} ' 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) } } ' "$@" } # 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 zero/null bytes as the extra-arguments terminator x0() { xargs -0 "$@"; } # run `xargs`, using whole lines as extra arguments xl() { local command="awk" if [ -p 1 ] || [ -t 1 ]; then command="stdbuf -oL awk" fi ${command} -v ORS='\000' ' FNR == 1 { gsub(/^\xef\xbb\xbf/, "") } { gsub(/\r$/, ""); print } ' | xargs -0 "$@" } # Youtube audio ya() { yt-dlp -x --audio-format best "$@"; } # Youtube AAC audio # yaac() { yt-dlp -f 140 "$@"; } # Youtube AAC audio yaac() { yt-dlp -x --audio-format aac "$@"; } # Youtube MP4 video ymp4() { yt-dlp -f 22 "$@"; } # 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)" mpv "$(yt-dlp -x --audio-format best --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 "%20s\e[38;2;78;154;6m%s\e[0m \e[38;2;52;101;164m%s\e[0m\n\n" \ "" "$(date +'%a %b %d %Y')" "$(date +%T)" # debian linux has a different `cal` app which highlights the day if [ -e "/usr/bin/ncal" ]; then # fix debian/ncal's weird way to highlight the current day ncal -C -y "$@" | sed -E 's/_\x08(.)/\x1b[7m\1\x1b[0m/g' else cal -y "$@" fi } | less -JMKiCRS } # show the current date in the YYYY-MM-DD format ymd() { date +'%Y-%m-%d'; } # YouTube DownLoad Plus alias ytdlp=yt-dlp # 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}" } # underline every few lines: make groups of 5 lines (by default) stand out by # underlining the last line of each alias zebra=ue5