#!/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. # # 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[7mDon't run this script directly: instead source it\e[0m\n" printf "\e[7mby running '. clam' (without the single quotes).\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 alias 0=sbs 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 c=cat alias e=echo alias r='tput reset' # AWK in PARagraph-input mode alias awkpar=awkblock # Better Less runs `less`, showing line numbers, among other settings alias bl='less -MKNiCRS' # Better LESS runs `less`, showing line numbers, among other settings alias bless='less -MKNiCRS' # Breathe Lines 5: separate groups of 5 lines with empty lines alias bl5=b5 # Book-like MANual, lays out `man` docs as pairs of side-by-side pages; uses # my tool `bsbs` alias bman=bookman # load/concatenate BYTES from named data sources; uses my tool `get` alias bytes=get # Compile C Optimized alias cco='cc -Wall -O3 -s -march=native -mtune=native -flto' # Colored Json Query runs the `jq` app, allowing an optional filepath as the # data source, and even an optional transformation formula alias cjq='jq -C' # CLear Screen alias cls='tput reset' # Compile C Plus Plus Optimized alias cppo='c++ -Wall -O3 -s -march=native -mtune=native -flto' # Colored RipGrep ensures app `rg` emits colors when piped alias crg='rg --line-buffered --color=always' # CURL Silent spares you the progress bar, but still tells you about errors alias curls='curl --silent --show-error' # dictionary-DEFine the word given, using an online service alias def=define # turn JSON Lines into a proper json array alias dejsonl='jq -s -M' # turn UTF-16 data into UTF-8 alias deutf16='iconv -f utf16 -t utf8' # DIM (lines) with AWK alias dimawk=dawk # edit plain-text files alias edit=micro # ENV with 0/null-terminated lines on stdout alias env0='env -0' # ENV Change folder, runs the command given in the folder given (first) alias envc='env -C' # Extended Plain Interactive Grep alias epig='ugrep --color=never -Q -E' # Editor Read-Only alias ero='micro -readonly true' # Expand 4 turns each tab into up to 4 spaces alias expand4='expand -t 4' # run the Fuzzy Finder (fzf) in multi-choice mode, with custom keybindings alias ff='fzf -m --bind ctrl-a:select-all,ctrl-space:toggle' # get FILE's MIME types alias filemime='file --mime-type' # run `gcc` with all optimizations on and with static analysis on alias gccmax='gcc -Wall -O3 -s -march=native -mtune=native -flto -fanalyzer' # GRAY AWK styles lines satisfying an AWK condition/expression red alias grayawk=dawk # hold stdout if used at the end of a pipe-chain alias hold='less -MKiCRS' # find all hyperlinks inside HREF attributes in the input text alias hrefs=href # make JSON Lines out of JSON data alias jl=jsonl # shrink/compact JSON using the `jq` app, allowing an optional filepath, and # even an optional transformation formula after that alias jq0='jq -c -M' # 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 alias jq2='jq --indent 2 -M' # find the LAN (local-area network) IP address for this device alias lanip='hostname -I' # run `less`, showing line numbers, among other settings alias least='less -MKNiCRS' # 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 alias lh1='less --header=1 -MKNiCRS' # 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 alias lh2='less --header=2 -MKNiCRS' # try to run the command given using line-buffering for its (standard) output alias 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 alias loser=serve # LOWercase all ASCII symbols alias low=tolower # LOWERcase all ASCII symbols alias lower=tolower # run `ls` showing how many 4k pages each file takes alias lspages='ls -s --block-size=4096' # Listen To Youtube alias lty=yap # MAKE IN folder alias makein=mif # Multi-Core MaKe runs `make` using all cores alias mcmk=mcmake # run `less`, showing line numbers, among other settings alias most='less -MKNiCRS' # emit nothing to output and/or discard everything from input alias nil=null # Nice Json Query colors JSON data using the `jq` app alias njq=cjq # Narrow MANual, keeps `man` narrow, even if the window/tab is wide when run alias nman=naman # Plain Interactive Grep alias pig='ugrep --color=never -Q -E' # Plain RipGrep alias prg='rg --line-buffered --color=never' # Quick Compile C Optimized alias qcco='cc -Wall -O3 -s -march=native -mtune=native -flto' # Quick Compile C Plus Plus Optimized alias qcppo='c++ -Wall -O3 -s -march=native -mtune=native -flto' # RED AWK styles lines satisfying an AWK condition/expression red alias redawk=rawk # Run In Folder alias rif='env -C' # Read-Only Editor alias roe='micro -readonly true' # Read-Only Micro (text editor) alias rom='micro -readonly true' # Read-Only Top alias rot='htop --readonly' # RUN IN folder alias runin='env -C' # place lines Side-By-Side # alias sbs=column # Silent CURL spares you the progress bar, but still tells you about errors alias scurl='curl --silent --show-error' # Stdbuf Output Line-buffered alias sol='stdbuf -oL' # TRY running a command, showing its outcome/error-code on failure alias try=verdict # Time Verbosely the command given alias tv='/usr/bin/time -v' # run `cppcheck` with even stricter options alias vetc='cppcheck --enable=portability,style --check-level=exhaustive' # run `cppcheck` with even stricter options, also checking for c89 compliance alias vetc89='cppcheck --enable=portability,style --check-level=exhaustive --std=c89' # run `cppcheck` with even stricter options alias vetcpp='cppcheck --enable=portability,style --check-level=exhaustive' # VET SHell scripts alias vetsh=vetshell # check shell scripts for common gotchas, avoiding complaints about using # the `local` keyword, which is widely supported in practice alias 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 alias vh1='less --header=1 -MKiCRS' # 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 alias vh2='less --header=2 -MKiCRS' # run a command using an empty environment alias void='env -i' # turn plain-text from latin-1 into UTF-8; the name is from `vulgarization`, # which is the mutation of languages away from latin during the middle ages alias vulgarize='iconv -f latin-1 -t utf-8' # recursively find all files with trailing spaces/CRs alias wheretrails=whichtrails # run `xargs`, using zero/null bytes as the extra-arguments terminator alias x0='xargs -0' # Xargs Lines, runs `xargs` using whole lines as extra arguments alias xl=xargsl # find name from the local `apt` database of installable packages aptfind() { local arg local gap=0 local options='-MKiCRS' if [ $# -eq 1 ]; then options='--header=1 -MKiCRS' 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 }' "$@" # } # emit each argument given as its own line of output args() { printf "%s\n" "$@"; } # avoid/ignore lines which match any of the regexes given avoid() { local command='awk' if [ -p 1 ] || [ -t 1 ]; then command='stdbuf -oL awk' fi ${command} ' BEGIN { for (i = 1; i < ARGC; i++) { re[i] = ARGV[i]; delete ARGV[i] } } { for (i in re) if ($0 ~ re[i]) { next } } { print; got++ } END { exit(got == 0) } ' "${@:-^$}" } # 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 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 } # Breathe lines 5: separate groups of 5 lines with empty lines b5() { 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 } # 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" "" } # Bullets with AWK shows 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 bawk() { local code="${1:-\$0}" [ $# -gt 0 ] && shift printf "value\ttally\tbullets\n" awk ' FNR == 1 { FS = ($0 ~ /\t/) ? "\t" : " "; $0 = $0 } { low = lower = tolower($0) } { tally['"${code}"']++ } 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 -t "$(printf "\t")" -rnk2 -k1d } # 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 `sboard` beeps() { sboard beeps "${1:-1}" "${2:-1}"; } # play a repeating synthetic-bell-like sound lasting the number of seconds # given, or for 1 second by default; uses my tool `sboard` bell() { sboard bell "${1:-1}" "${2:-1}"; } # 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 { print "" } 1' "$@" else awk '(NR - 1) % 5 == 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` bookman() { local w w="$(tput cols)" w="$((w / 2 - 4))" if [ "$w" -lt 65 ]; then w=65 fi MANWIDTH="$w" man "$@" | bsbs 2 } # split lines using the separator given, turning them into single-item lines breakdown() { local sep="${1:- }" [ $# -gt 0 ] && shift local command='awk' if [ -p 1 ] || [ -t 1 ]; then command='stdbuf -oL awk' fi ${command} -F "${sep}" '{ for (i = 1; i <= NF; i++) print $i }' "$@" } # play a busy-phone-line sound lasting the number of seconds given, or for 1 # second by default; uses my tool `sboard` busy() { sboard busy "${1:-1}" "${2:-1}"; } # 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 ca "${arg}" done | nn --gray } # uppercase the first letter on each line, and lowercase all later letters capitalize() { sed -E -u 's-^(.*)-\L\1-; s-^(.)-\u\1-'; } # Count with AWK: count the times the AWK expression/condition given is true cawk() { local cond="${1:-1}" [ $# -gt 0 ] && shift awk ' BEGIN { count = c = 0 } FNR == 1 { FS = ($0 ~ /\t/) ? "\t" : " "; $0 = $0 } { low = lower = tolower($0) } '"${cond}"' { count++; c = count } END { print count } ' "$@" } # center-align lines of text, using the current screen width center() { local command='awk' if [ -e /usr/bin/gawk ]; then command='gawk' fi ${command} -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] } ' "$@" } # Color file-EXTensions, or any substring which looks like one cext() { local command='awk' if [ -p 1 ] || [ -t 1 ]; then command='stdbuf -oL awk' fi ${command} ' BEGIN { palette[n++] = "\x1b[38;2;0;95;215m" # blue palette[n++] = "\x1b[38;2;215;95;0m" # orange palette[n++] = "\x1b[38;2;135;95;255m" # purple palette[n++] = "\x1b[38;2;0;175;215m" # cyan palette[n++] = "\x1b[38;2;255;135;255m" # pink palette[n++] = "\x1b[38;2;0;135;95m" # green palette[n++] = "\x1b[38;2;204;0;0m" # red palette[n++] = "\x1b[38;2;168;168;168m" # gray palcount = length(palette) n = 0 } { # ignore cursor-movers and style-changers # gsub(/\x1b\[[0-9;]*[A-Za-z]/, "") rest = $0 while (match(rest, /\.[A-Za-z][A-Za-z0-9_-]*/)) { printf "%s", substr(rest, 1, RSTART - 1) ext = substr(rest, RSTART, RLENGTH) rest = substr(rest, RSTART + RLENGTH) style = ext2style[ext] if (style == "") { style = palette[n % palcount] ext2style[ext] = style n++ } printf "%s%s\x1b[0m", style, ext } print rest } ' "$@" } # Colored Go Test on the folder given; uses my command `jawk` cgt() { go test "${1:-.}" 2>&1 | jawk '/^ok/' '/^[-]* ?FAIL/' '/^\?/'; } # Compile Rust Optimized cro() { rustc -C lto=true -C codegen-units=1 -C debuginfo=0 -C strip=symbols \ -C opt-level=3 "$@" } # emit a line with a repeating cross-like symbol in it crosses() { [ "${1:-80}" -gt 0 ] && printf "%${1:-80}s\n" "" | sed 's- -×-g'; } # listen to streaming DANCE music dance() { printf "streaming \e[7mDance Wave Retro\e[0m\n" 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'; } # Dim (lines) with AWK dawk() { local command='awk' if [ -p 1 ] || [ -t 1 ]; then command='stdbuf -oL awk' fi local cond="${1:-1}" [ $# -gt 0 ] && shift ${command} ' FNR == 1 { FS = ($0 ~ /\t/) ? "\t" : " "; $0 = $0 } { low = lower = tolower($0) } '"${cond}"' { gsub(/\x1b\[0m/, "\x1b[0m\x1b[38;2;168;168;168m") printf "\x1b[38;2;168;168;168m%s\x1b[0m\n", $0 next } 1 ' "$@" } # remove indentations from lines dedent() { awk ' { lines[NR] = $0 } { if (match($0, /^ +/) && (n == 0 || n > RLENGTH)) n = RLENGTH } END { for (i = 1; i <= NR; i++) print substr(lines[i], n + 1) } ' "$@" } dehtmlify() { local command='awk' if [ -p 1 ] || [ -t 1 ]; then command='stdbuf -oL awk' fi ${command} ' { gsub(/<\/?[^>]+>/, "") gsub(/&/, "&") gsub(/</, "<") gsub(/>/, ">") gsub(/^ +| *\r?$/, "") gsub(/ +/, " ") print } ' "$@" } # expand tabs each into up to the number of space given, or 4 by default detab() { local tabstop="${1:-4}" [ $# -gt 0 ] && shift if [ -p 1 ] || [ -t 1 ]; then stdbuf -oL expand -t "${tabstop}" "$@" else expand -t "${tabstop}" "$@" fi } # DICtionary-define the word given locally dic() { local arg local gap=0 local options='-MKiCRS' if [ $# -eq 0 ]; then printf "\e[38;2;204;0;0mdic: no words given\e[0m\n" >&2 return 1 fi if [ $# -eq 1 ]; then options='--header=1 -MKiCRS' fi for arg in "$@"; do [ "${gap}" -gt 0 ] && printf "\n" gap=1 printf "\e[7m%-80s\e[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 }' } # 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 if [ -p 1 ] || [ -t 1 ]; then stdbuf -oL cat "$1"; else cat "$1"; fi return $? fi case "${1:--}" in -) if [ -p 1 ] || [ -t 1 ]; then stdbuf -oL cat -; else cat -; fi;; file://*|https://*|http://*) curl --show-error -s "$1";; ftp://*|ftps://*|sftp://*) curl --show-error -s "$1";; dict://*) curl --show-error -s "$1";; *) 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'; } # 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 -MKiCRS } # 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 } # 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="${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 arg for arg in "$@"; do case "${arg}" in file://*|https://*|http://*|ftp://*|ftps://*|sftp://*|dict://*) curl --silent --show-error "${arg}";; *) curl --silent --show-error "https://${arg}";; esac done } # get the first n lines, or 1 by default first() { head -n "${1:-1}" "${2:--}"; } # Field-Names AWK remembers field-positions by name, from the first input line fnawk() { local code="${1:-1}" [ $# -gt 0 ] && shift local buffering='' if [ -p 1 ] || [ -t 1 ]; then buffering='stdbuf -oL' fi ${buffering} awk -v OFS="\t" ' FNR == 1 { FS = ($0 ~ /\t/) ? "\t" : " "; $0 = $0 } NR == 1 { FS = ($0 ~ /\t/) ? "\t" : " " $0 = $0 for (i = 1; i <= NF; i++) names[$i] = i i = "" } { low = lower = tolower($0) } '"${code}"' ' "$@" } # 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*$--' } # Gawk Bignum Print gbp() { gawk --bignum "BEGIN { print $1; exit }"; } # 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 } # GRoup via AWK groups lines using common results of the AWK expression given grawk() { local code="${1:-\$0}" [ $# -gt 0 ] && shift local command='awk' if [ -p 1 ] || [ -t 1 ]; then command='stdbuf -oL awk' fi ${command} ' FNR == 1 { FS = ($0 ~ /\t/) ? "\t" : " "; $0 = $0 } { low = lower = tolower($0) } { k = '"${code}"' if (!(k in groups)) ordkeys[++oklen] = k groups[k][length(groups[k]) + 1] = $0 } END { for (i = 1; i <= oklen; i++) { k = ordkeys[i] n = length(groups[k]) for (j = 1; j <= n; j++) print groups[k][j] } } ' "$@" } # 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} ' FNR == 1 { FS = ($0 ~ /\t/) ? "\t" : " "; $0 = $0 } { 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 `sboard` heartbeat() { sboard heartbeat "${1:-1}" "${2:-1}"; } # 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 } 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 -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;243m-\x1b[38;5;103m-g' \ -e 's-\x1b\[38;5;238m-\x1b[38;5;245m-g' \ -e 's-\x1b\[38;5;228m-\x1b[48;5;228m-g' | less -MKiCRS } # 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 arg for arg in "${@:--}"; do grep --line-buffered -E -o 'href="[^"]+"' "${arg}" done | sed -u 's-^href="--; s-"$--' } # avoid/ignore lines which case-insensitively match any of the regexes given iavoid() { local command='awk' if [ -p 1 ] || [ -t 1 ]; then command='stdbuf -oL awk' fi ${command} ' BEGIN { if (IGNORECASE == "") { m = "this variant of AWK lacks case-insensitive regex-matching" printf("\x1b[38;2;204;0;0m%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 got++ } END { exit(got == 0) } ' "${@:-^\r?$}" } # ignore command in a pipe: this allows quick re-editing of pipes, while # still leaving signs of previously-used steps, as a memo idem() { cat; } # 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() { local command='awk' if [ -p 1 ] || [ -t 1 ]; then command='stdbuf -oL awk' fi ${command} ' BEGIN { if (IGNORECASE == "") { m = "this variant of AWK lacks case-insensitive regex-matching" printf("\x1b[38;2;204;0;0m%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 got++ next } } } END { exit(got == 0) } ' "${@:-[^\r]}" } # 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) } ' "$@" } # 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 } ' "$@" } # Judge with AWK colors lines using up to 3 (optional) AWK conditions, namely # `good` (green), `bad` (red), and `meh` (gray) jawk() { local code="${1:-\$0}" [ $# -gt 0 ] && shift local command='awk' if [ -p 1 ] || [ -t 1 ]; then command='stdbuf -oL awk' fi local good="${1:-0}" local bad="${2:-0}" local meh="${3:-0}" [ $# -gt 0 ] && shift [ $# -gt 0 ] && shift [ $# -gt 0 ] && shift ${command} ' BEGIN { # normal good-style is green, colorblind-friendly good-style is blue good_style = ENVIRON["COLORBLIND"] != 0 ? "\x1b[38;2;0;95;215m" : "\x1b[38;2;0;135;95m" good_fmt = good_style "%s\x1b[0m\n" good_reset = "\x1b[0m" good_style } FNR == 1 { FS = ($0 ~ /\t/) ? "\t" : " "; $0 = $0 } { low = lower = tolower($0) } '"${good}"' { gsub(/\x1b\[0m/, good_reset) printf good_fmt, $0 next } '"${bad}"' { gsub(/\x1b\[0m/, "\x1b[0m\x1b[38;2;204;0;0m") printf "\x1b[38;2;204;0;0m%s\x1b[0m\n", $0 next } '"${meh}"' { gsub(/\x1b\[0m/, "\x1b[0m\x1b[38;2;168;168;168m") printf "\x1b[38;2;168;168;168m%s\x1b[0m\n", $0 next } 1 ' "$@" } # listen to streaming JAZZ music jazz() { printf "streaming \e[7mSmooth Jazz Instrumental\e[0m\n" mpv --quiet https://stream.zeno.fm/00rt0rdm7k8uv } # show a `dad` JOKE from the web, sometimes even a very funny one joke() { curl --silent --show-error https://icanhazdadjoke.com | fold -s | awk '{ gsub(/ *\r?$/, ""); print }' } # JSON Query 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 jql() { local code="${1:-.}" [ $# -gt 0 ] && shift jq -c -M "${code} | .[]" "$@" } # JSON Query Keys runs `jq` to find all unique key-combos from tabular JSON jqk() { local code="${1:-.}" [ $# -gt 0 ] && shift jq -c -M "${code} | .[] | keys" "$@" | awk '!c[$0]++' } # JSON Keys finds all unique key-combos from tabular JSON data; uses my tools # `jsonl` and `zj` # jsonk() { cat "${1:--}" | zj . .keys | jsonl | awk '!c[$0]++'; } # JSON Keys finds all unique key-combos from tabular JSON data; uses my tools # `jsonl` and `tjp` jsonk() { tjp '[e.keys() for e in v] if isinstance(v, (list, tuple)) else v.keys()' \ "${1:--}" | jsonl | awk '!c[$0]++' } # JSON Table, turns TSV tables into tabular JSON, where valid-JSON values are # auto-parsed into numbers, booleans, etc...; uses my tools `jsons` and `tjp` jsont() { jsons "$@" | tjp \ '[{k: rescue(lambda: loads(v), v) for k, v in e.items()} for e in v]' } # 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*$--' } # play a stereotypical once-a-second laser sound for the number of seconds # given, or for 1 second (once) by default; uses my tool `sboard` laser() { sboard laser "${1:-1}" "${2:-1}"; } # 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:--}"; } # ensure LINES are never accidentally joined across files, by always emitting # a line-feed at the end of each line lines() { if [ -p 1 ] || [ -t 1 ]; then stdbuf -oL awk 1 "$@" else awk 1 "$@" fi } # 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 "" } ' "$@" } # LiSt MAN pages lsman() { man -k "${1:-.}"; } # only keep lines which match any of the regexes given match() { local command='awk' if [ -p 1 ] || [ -t 1 ]; then command='stdbuf -oL awk' fi ${command} ' BEGIN { for (i = 1; i < ARGC; i++) { re[i] = ARGV[i]; delete ARGV[i] } } { for (i in re) if ($0 ~ re[i]) { print; got++; next } } END { exit(got == 0) } ' "${@:-.}" } # 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)" "$@"; } # merge stderr into stdout, which is useful for piped commands merrge() { "${@:-cat /dev/null}" 2>&1; } metajq() { # https://github.com/stedolan/jq/issues/243#issuecomment-48470943 jq -r -M ' [ path(..) | map(if type == "number" then "[]" else tostring end) | join(".") | split(".[]") | join("[]") ] | unique | map("." + .) | .[] ' "$@" } # 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 folder folder="${1:-.}" [ $# -gt 0 ] && shift env -C "${folder}" make "$@" } # 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' # } # 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) } # Nice DEFine dictionary-defines the words given, using an online service ndef() { local arg local gap=0 local options='-MKiCRS' if [ $# -eq 0 ]; then printf "\e[38;2;204;0;0mndef: no words given\e[0m\n" >&2 return 1 fi if [ $# -eq 1 ]; then options='--header=1 -MKiCRS' fi for arg in "$@"; do [ "${gap}" -gt 0 ] && printf "\n" gap=1 printf "\e[7m%-80s\e[0m\n" "${arg}" curl --silent "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}" } # 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 Json Query Lines colors JSONL data using the `jq` app njql() { local code="${1:-.}" [ $# -gt 0 ] && shift jq -c -C "${code} | .[]" "$@" } # 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 `sboard` noise() { sboard noise "${1:-1}" "${2:-1}"; } # show the current date and time now() { date +'%Y-%m-%d %H:%M:%S'; } # Nice Print Python result; uses my tool `nn` npp() { local arg for arg in "$@"; do python -c "print(${arg})" done | nn --gray } # Nice Size, using my tool `nn` ns() { wc -c "$@" | nn --gray; } # Nice Systemctl Status nss() { systemctl status "$@" 2>&1 | sed -E \ -e 's-\x1b\[[^A-Za-z][A-Za-z]--g' \ -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-' | if [ "${COLORBLIND}" = 1 ]; then # color-blind-friendly version using blue instead of green sed 's-\x1b\[38;2;0;135;95m-\x1b[38;2;0;95;215m-g' else # leave green colors untouched cat fi | less -MKiCRS } # 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 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[7m%-${width}s\e[0m\n" "${place}" printf "%s~%s\r\n\r\n" "${place}" "${width}" | curl --silent --show-error 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 -MKiCRS } # Print AWK expression for each input line pawk() { local code="${1:-\$0}" [ $# -gt 0 ] && shift if [ -p 1 ] || [ -t 1 ]; then stdbuf -oL awk "{ print ${code} }" "$@" else awk "{ print ${code} }" "$@" fi } # play audio/video media play() { mpv "${@:--}"; } # Print Python result pp() { local arg for arg in "$@"; do python -c "print(${arg})" done } # 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 - } # Quiet MPV qmpv() { mpv --quiet "${@:--}"; } # ignore stderr, without any ugly keyboard-dancing quiet() { "$@" 2> /dev/null; } # Red AWK styles lines satisfying an AWK condition/expression red rawk() { local command='awk' if [ -p 1 ] || [ -t 1 ]; then command='stdbuf -oL awk' fi local cond="${1:-1}" [ $# -gt 0 ] && shift ${command} ' FNR == 1 { FS = ($0 ~ /\t/) ? "\t" : " "; $0 = $0 } { low = lower = tolower($0) } '"${cond}"' { gsub(/\x1b\[0m/, "\x1b[0m\x1b[38;2;204;0;0m") printf "\x1b[38;2;204;0;0m%s\x1b[0m\n", $0 next } 1 ' "$@" } # keep only lines between the 2 line numbers given, inclusively rangelines() { { [ $# -eq 2 ] || [ $# -eq 3 ]; } && [ "${1}" -le "${2}" ] && { tail -n +"${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 `sboard` ready() { sboard ready "${1:-1}" "${2:-1}"; } # 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 } ' "$@" } # 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-·····-····╵-' } # Summarize via AWK calculates some numeric statistics from an AWK expression sawk() { local code="${1:-\$0}" [ $# -gt 0 ] && shift awk ' BEGIN { numeric = ints = pos = zero = neg = 0 inf = "+inf" + 0 min = inf max = -inf sum = 0 mean = 0 prod = 1 } FNR == 1 { FS = ($0 ~ /\t/) ? "\t" : " "; $0 = $0 } { v = '"${code}"' if (v !~ /^ *(0|[0-9]+|[0-9]*\.[0-9]+) *$/) next v = v + 0 numeric++ ints += v % 1 == 0 if (v > 0) pos++ else if (v < 0) neg++ else if (v == 0) zero++ min = min < v ? min : v max = max > v ? max : v sum += v prod *= v lnSum += v <= 0 ? -inf : log(v) # advance welford`s algorithm d1 = v - mean mean += d1 / numeric d2 = v - mean meanSq += d1 * d2 } END { sum = mean * numeric if (numeric == 0) lnSum = -inf # separate name-value pairs using tabs, and prepare a # pipeable command which ignores all-zero decimals OFS = "\t" print "numeric", numeric if (numeric > 0) { print "min", sprintf("%f", min) print "max", sprintf("%f", max) print "sum", sprintf("%f", sum) print "mean", sprintf("%f", mean) print "geomean", (zero == 0 && neg == 0) ? sprintf("%f", exp(lnSum / numeric)) : "" print "sd", sprintf("%f", sqrt(meanSq / numeric)) print "product", sprintf("%g", prod) } else { print "min", "" print "max", "" print "sum", "" print "mean", "" print "geomean", "" print "sd", "" print "product", "" } print "integer", ints print "positive", pos print "zero", zero print "negative", neg } ' "$@" | sed -E 's-([0-9]+)\.0+$-\1-g; s-([0-9]+\.[0-9]*[1-9])0+$-\1-g' } # SystemCTL; `sysctl` is already taken for a separate/unrelated app sctl() { systemctl "$@" 2>&1 | less -MKiCRS --header=1; } # 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 sf() { local arg local gap=0 local options='-MKiCRS' if [ $# -le 1 ]; then options='--header=1 -MKiCRS' fi for arg in "${@:-.}"; do [ "${gap}" -gt 0 ] && printf "\n" printf "\e[7m%s\e[0m\n\n" "$(realpath "${arg}")" gap=1 ls -al --file-type --color=never --time-style iso "${arg}" | awk ' BEGIN { drep = "\x1b[38;2;0;135;255m\x1b[48;2;228;228;228m&\x1b[0m" lrep = "\x1b[38;2;0;135;95m\x1b[48;2;228;228;228m&\x1b[0m" } NR < 4 { next } (NR - 3) % 5 == 1 && (NR - 3) > 1 { print "" } { gsub(/^(d[rwx-]+)/, drep) gsub(/^(l[rwx-]+)/, lrep) printf "%6d %s\n", NR - 3, $0; fflush() } ' done | less "${options}" } # 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 } # 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 >= 2 { 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 >= 2 { print | sort } ' } # emit a line with the number of spaces given in it spaces() { [ "${1:-80}" -gt 0 ] && printf "%${1:-80}s\n" ""; } # SQUeeze horizontal spaces and STOMP vertical gaps squomp() { local command='awk' if [ -p 1 ] || [ -t 1 ]; then command='stdbuf -oL awk' fi ${command} ' FNR == 1 { gsub(/^\xef\xbb\xbf/, "") } /^\r?$/ { empty = 1; next } empty { if (n > 0) print ""; empty = 0 } { gsub(/^ +| *\r?$/, "") gsub(/ *\t */, "\t") gsub(/ +/, " ") print; n++ } ' "$@" } # STOMP vertical gaps, turning runs of empty lines into single empty lines stomp() { local command='awk' if [ -p 1 ] || [ -t 1 ]; then command='stdbuf -oL awk' fi ${command} ' /^\r?$/ { empty = 1; next } empty { if (n > 0) print ""; empty = 0 } { print; n++ } ' "$@" } 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"') }' } # add a final sums row after all input lines sums() { local command='awk' if [ -p 1 ] || [ -t 1 ]; then command='stdbuf -oL awk' fi ${command} ' { gsub(/\r$/, "") } NR == 1 { FS = ($0 ~ /\t/) ? "\t" : " "; $0 = $0 } { if (n < NF) n = NF for (i = 1; i <= NF; i++) sums[i] += $i print } END { for (i = 1; i <= n; i++) { if (i > 1) printf(FS) printf("%s", sums[i]) } if (n > 0) printf "\n" } ' "$@" } # 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 } } # ' "$@" # } # Tally (lines) with AWK tawk() { local code="${1:-\$0}" [ $# -gt 0 ] && shift awk -v sortcmd="sort -t '\t' -rnk1" ' BEGIN { print "tally\tvalue"; fflush() } FNR == 1 { FS = ($0 ~ /\t/) ? "\t" : " "; $0 = $0 } { v = '"${code}"' if (!tally[v]++) ordkeys[++oklen] = v } END { for (i = 1; i <= oklen; i++) { k = ordkeys[i] printf "%d\t%s\n", tally[k], k | sortcmd } } ' "$@" } # Simulate the cadence of old-fashioned TELETYPE machines teletype() { awk ' { gsub(/\r$/, "") n = length($0) for (i = 1; i <= n; i++) { if (code = system("sleep 0.015")) exit code printf "%s", substr($0, i, 1); fflush() } if (code = system("sleep 0.75")) exit code printf "\n"; fflush() } # END { if (NR > 0 && code != 0) printf "\n" } ' "$@" } # 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 tools `sboard` and `waveout` tone() { if [ "${3:-440}" -eq 440 ]; then sboard tone "${1:-1}" "${2:-1}" else waveout "${1:-1}" "${2:-1} * sin(${3:-440} * tau * t)" | mpv --really-quiet - fi } # 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))" } # 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" } } ' "$@" } # 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 (f[ARGV[i]]++) delete ARGV[i] } FNR == 1 { FS = ($0 ~ /\t/) ? "\t" : " "; $0 = $0 } !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 ' "$@" } # only keep UNIQUE lines, keeping them in their original order unique() { local command='awk' if [ -p 1 ] || [ -t 1 ]; then command='stdbuf -oL awk' fi ${command} ' BEGIN { for (i = 1; i < ARGC; i++) if (f[ARGV[i]]++) delete ARGV[i] } !c[$0]++ ' "$@" } # 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 unixify() { local command='awk' if [ -p 1 ] || [ -t 1 ]; then command='stdbuf -oL awk' fi ${command} ' FNR == 1 { gsub(/^\xef\xbb\xbf/, "") } { gsub(/ *\r?$/, ""); print } ' "$@" } # 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="${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 Nice Table / Very Nice Table; uses my tools `ntsv` and `nn` vnt() { nl -b a -w 1 -v 0 "$@" | ntsv | nn | awk '(NR - 1) % 5 == 1 { print "" } 1' | less -MKiCRS --header=1 } # View Text, turning documents into plain-text if needed; uses `pandoc` vt() { local arg local gap=0 local options='-MKiCRS' if [ $# -eq 1 ]; then options='--header=1 -MKiCRS' fi if [ $# -eq 0 ]; then pandoc -s -t plain - 2>&1 | less -MKiCRS 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 echo "$0" return 0 fi for arg in "$@"; do [ "${gap}" -gt 0 ] && printf "\n" gap=1 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 echo "${arg}" | grep -q ' '; then printf "%s\n" "${arg}" continue fi if declare -f "${arg}"; then continue fi if which "${arg}" > /dev/null 2> /dev/null; then which "${arg}" continue fi printf "\e[38;2;204;0;0m%s not found\e[0m\n" "${arg}" done | less -MKiCRS } # find all WEB/hyperLINKS (https:// and http://) in the input text weblinks() { local arg local re='https?://[A-Za-z0-9+_.:%-]+(/[A-Za-z0-9+_.%/,#?&=-]*)*' for arg in "${@:--}"; do grep --line-buffered -E -o "${re}" "${arg}" done } # recursively find all files with trailing spaces/CRs whichtrails() { rg -c --line-buffered '[ \r]+$' "${@:-.}"; } # XARGS Lines, runs `xargs` using whole lines as extra arguments xargsl() { if [ -p 1 ] || [ -t 1 ]; then stdbuf -oL awk -v ORS='\000' ' FNR == 1 { gsub(/^\xef\xbb\xbf/, "") } { gsub(/\r$/, ""); print } ' | stdbuf -oL xargs -0 "$@" else awk -v ORS='\000' ' FNR == 1 { gsub(/^\xef\xbb\xbf/, "") } { gsub(/\r$/, ""); print } ' | xargs -0 "$@" fi } # Youtube Audio Player yap() { # some youtube URIs end with extra playlist/tracker parameters local url="$(echo "$1" | sed 's-&.*--')" 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 \ "%21s\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 +'%H:%M')" # 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 -MKiCRS } # show the current date in the YYYY-MM-DD format ymd() { date +'%Y-%m-%d'; }