diff options
| author | 2026-01-14 22:22:22 +0100 | |
|---|---|---|
| committer | 2026-01-15 06:37:04 +0100 | |
| commit | 74e70ef6e33cc57a1077892deccf6424199ac7ab (patch) | |
| tree | 3ba24598a6bb4226b188f060154092d0db4313b0 | |
initial importshlib
| -rw-r--r-- | README.md | 6 | ||||
| -rw-r--r-- | args.bash | 166 | ||||
| -rw-r--r-- | fetch.sh | 128 | ||||
| -rw-r--r-- | log.sh | 204 |
4 files changed, 504 insertions, 0 deletions
diff --git a/README.md b/README.md new file mode 100644 index 0000000..f031254 --- /dev/null +++ b/README.md @@ -0,0 +1,6 @@ +# Shell Libraries +Libraries and scripts for shell. +They're not that complicated or anything, but they're nice :) + +The extension tells you what shell they're for (`.sh` means any POSIX sh). +Comments should tell you dependencies. diff --git a/args.bash b/args.bash new file mode 100644 index 0000000..464e73c --- /dev/null +++ b/args.bash @@ -0,0 +1,166 @@ +#!/bin/bash +# a schemaless argument parser + +# this file is licensed under any of the following SPDX licenses +# to be chosen by the user: +# * 0BSD (https://spdx.org/licenses/0BSD.html) +# * BlueOak-1.0.0 (https://blueoakcouncil.org/license/1.0.0) +# * CC0-1.0 (https://creativecommons.org/publicdomain/zero/1.0/) +# * Unlicense (https://unlicense.org/) + +# things you can do with it: +# -fval | -f=val | -f val: set "f" to "val" +# --foo=val | --foo val: set "foo" to "val" +# @oval | @o=val | @o val: add "val" to the "o" array +# @@opt=val | @@opt val: add "val" to the "opt" array +# --: stop parsing options, put the rest into $@ + +# you may have noticed: +# * there are no booleans, long or short. you can emulate them by =yes|no, or have a dedicated options|o array. +# * you cannot reset arrays: you actually can, by using -oval and --opt=val, but you can never make them empty + +# something you may not notice without reading the code: +# * values may contain spaces, but may *not* contain literal 's, arg names may not contain spaces, which will break horribly +# * this leaks __die(), __verify(), and $__options. Technically also parse(). +# * this is not compatible with numerical args, like -9 + +# reading notes: +# let NUM is true if num > 0 +# this is the same as (( NUM )) + +# usage: +# 1. save this to some file (let's say ./args.bash) +# 2. optionally, define default values for any of your arguments +# 3. optionally, define any namerefs to have long/short variants (e.g declare -n r=repo, which will make @r and @@repo the same) +# 4. . ./args.bash "$@" + +__die() { + echo "$@" >&2 + exit +} + +# $1 is arg, $2 is val +__verify() { + [[ ${1% *} != $1 ]] && __die "args may not contain spaces, but `$1` does" + [[ "${2%\'*}" != $2 ]] && __die "values may not contain 's, but `$2` does" + true +} + +parse() { + declare one two val arg + declare -g __options + + while let $#; do + arg="$1" + one="${arg::1}" + two="${arg::2}" + # needed because we check -v + unset val; declare val + shift + + case "$arg" in + # literal --, stop processing options + --) __options+=("$@"); return ;; + # long option + --*) + # the arg without the -- + arg=${arg:2} + + # if there's a = in there, we know the value + if [[ ${arg%%=*} != $arg ]]; then + val=${arg#*=} + arg=${arg%%=*} + fi + + # else, use the next arg + if [[ ! -v val ]]; then + let $# || __die "--$arg needs a value, found none" + val=$1 + shift + fi + + __verify "$arg" "$val" + eval $arg="'$val'" + ;; + # short option + -*) + # the arg without the - + arg=${arg:1} + + # we're dealing with -f=val + if [[ ${arg:1:1} = '=' ]]; then + val=${arg:2} + arg=${arg::1} + fi + + # we haven't found an =, and there's space left - that must be the value + if [[ ! -v val ]] && (( ${#arg} > 1 )); then + val=${arg:1} + arg=${arg::1} + fi + + # we still haven't found the value, use the next arg + if [[ ! -v val ]]; then + let $# || __die "-$arg needs a value, found none" + val=$1 + shift + fi + + __verify "$arg" "$val" + eval $arg="'$val'" + ;; + # long array + @@*) + # the arg without the @@ + arg=${arg:2} + + # if there's a = in there, we know the value + if [[ ${arg%%=*} != $arg ]]; then + val=${arg#*=} + arg=${arg%%=*} + fi + + # else, use the next arg + if [[ ! -v val ]]; then + let $# || __die "@@$arg needs a value, found none" + val=$1 + shift + fi + + __verify "$arg" "$val" + eval $arg+="('$val')" + ;; + # short array + @*) + # the arg without the @ + arg=${arg:1} + + # we're dealing with @f=val + if [[ ${arg:1:1} = '=' ]]; then + val=${arg:2} + arg=${arg::1} + fi + + # we haven't found an =, and there's space left - that must be the value + if [[ ! -v val ]] && (( ${#arg} > 1 )); then + val=${arg:1} + arg=${arg::1} + fi + + # we still haven't found the value, use the next arg + if [[ ! -v val ]]; then + let $# || __die "@$arg needs a value, found none" + val=$1 + shift + fi + + __verify "$arg" "$val" + eval $arg+="('$val')" + ;; + *) __options+=("$arg") ;; + esac + done +} + +parse "$@" +set -- "${__options[@]}" diff --git a/fetch.sh b/fetch.sh new file mode 100644 index 0000000..aaac4ff --- /dev/null +++ b/fetch.sh @@ -0,0 +1,128 @@ +#!/bin/sh +# this file is licensed under any of the following SPDX licenses +# to be chosen by the user: +# * 0BSD (https://spdx.org/licenses/0BSD.html) +# * BlueOak-1.0.0 (https://blueoakcouncil.org/license/1.0.0) +# * CC0-1.0 (https://creativecommons.org/publicdomain/zero/1.0/) +# * Unlicense (https://unlicense.org/) + +# this all works with toybox, busybox and coreutils +# everything except http_src and http_run is POSIXLY correct +# http_src and http_run are exceptions due to needing mktemp + +# the main issue with this is you ultimately need to fetch it too +# because of this, http_eval, http_run and http_src are provided +# the idea is that you copy paste this at the start of your script, +# then use those to run your *actual* script, with all the stuff set +# this is a bit awkward, but tends to provide the best user experience +# as such, this file is the only code that gets duplicated + +# priority list for utilities, they're tried in order +# you're not allowed to overwrite them because _fetch and _print +# behavior is hardcoded + +# download speed order +HTTP_FETCH_LIST='curl wget ht xh' + +# wget is a bit awkward on busybox-likes with tls +HTTP_PRINT_LIST='curl ht xh wget' + +# $1: the command to check +# returns true on null input +hascmd() { + test -x "$(command -v $1)" +} + +# detect http getting for downloads +if [ -z "$HTTP_FETCH" ]; then + for c in $HTTP_FETCH_LIST + do + hascmd $c || continue + HTTP_FETCH=$c + break + done + echo "Using ${HTTP_FETCH:?could not find an http fetcher} for http fetching." +fi + +# detect http getting for piping +if [ -z "$HTTP_PRINT" ]; then + for c in $HTTP_PRINT_LIST + do + hascmd $c || continue + HTTP_PRINT=$c + break + done + echo "Using ${HTTP_PRINT:?could not find an http printer} for http piping." +fi + +# for http_run +export HTTP_FETCH HTTP_PRINT + +# $1 is the url +# $2 is the destination file +# intermediate directories are automatically handled +http_fetch() { + mkdir -p "$(dirname $2)" + + echo "fetch $2" >&2 + case "$HTTP_FETCH" in + curl) curl -so "$2" "$1" ;; + ht) ht --ignore-stdin -bdo "$2" --overwrite "$1" ;; + wget) wget -qO "$2" "$1" ;; + xh) xh -Ibdo "$2" "$1" ;; + *) echo 'Invalid http fetching utility.' >&2; exit 1 ;; + esac >/dev/null +} + +# $1 is the url +http_print() { + case "$HTTP_PRINT" in + curl) curl -s "$1" ;; + ht) ht --ignore-stdin -b "$1" ;; + wget) wget -qO- "$1" ;; + xh) xh -Ib "$1" ;; + *) echo 'Invalid http printing utility.' >&2; exit 1 ;; + esac +} + +# $1 is the url +# prints the file to use +http_run_src_prep() { + f=$(mktemp) + http_fetch "$1" "$f" + echo "$f" +} + +# $1 is the url +# args are not allowed +# don't use this unless you know what you're doing +http_eval() { + eval "$(http_print $1)" +} + +# $1 is the url +# rest are args to pass +http_run() { + f="$(http_run_src_prep $1)" + shift + chmod +x "$f" + "$f" "$@" + rm "$f" +} + +# $1 is the url +# rest are args to pass +http_src() { + f="$(http_run_src_prep $1)" + shift + . "$f" "$@" + rm "$f" +} + +# print all keys in an s3 bucket +# $1 is the bucket url +s3_list() { + http_print "$1" | + sed 's/</\n</g' | + sed '/<Key>/!d;s/^<Key>//' +} @@ -0,0 +1,204 @@ +#!/bin/sh +# this file is licensed under any of the following SPDX licenses +# to be chosen by the user: +# * 0BSD (https://spdx.org/licenses/0BSD.html) +# * BlueOak-1.0.0 (https://blueoakcouncil.org/license/1.0.0) +# * CC0-1.0 (https://creativecommons.org/publicdomain/zero/1.0/) +# * Unlicense (https://unlicense.org/) + +## log.sh - Bunker Log +# This is the bunker logging system for POSIX sh. +# It provides you with stack-based logging semantics, +# and includes advances features like stdin-forwarding. +# It should not leak any environment variables, +# besides those documented, and potentially `add` on buggy shells. +# Everything here should be POSIXLY correct. +# If it isn't, let me know! <toast (at) bunkerlabs.net> + +## Usage +# Call `log some text here` or `something_with_output | log_stdin`. +# It will then be formatted under the current log tree. +# +# You can manipulate the log tree using log_push and log_pop. +# log_push can have multiple levels pushed on it at once. +# For example, `log_push one two` from the default tree state will result +# in the new tree being blog/one/two. +# log_pop can pop multiple levels at once. To undo the above example, you could +# call `log_pop 2` instead of calling log_pop twice. +# +# Whenever the log tree changes, the next call to `log` or `log_stdin` will +# cause the state of the log tree to be printed in full. + +## Customizing +# You can customize the "root" node of the log tree by setting log_tree to a +# single word (no whitespace as per IFS) before you source this. +# You can set a hard limit to the maximum length of any log level by setting +# log_maxlen. This will truncate log levels when they are pushed. +# You can also force padding on the output by setting log_minlen. +# If log_minlen is negative, it will right-pad, like with printf. +# You can change the way truncation is done by changing log_tstyles, +# which is a space-delineated list of strategies. + +## API Summary +# The following is a summary of all user-facing functions and environment +# variables. Functions are denoted with `(args...)`s. +# * log(msg...): log a message +# * log_stdin(): log lines from stdin; do not use on interactive programs +# * log[_stdin](v|q): log only if VERBOSE is set / QUIET is not set +# * log_push(level...): push levels on the log tree +# * log_pop(amount?): pop levels off the log tree; amount defaults to 1 +# * log_shift(level...): push levels on the log tree after popping that amount +# | equivalent to `log_pop $#; log_push "$@"` +# | with word splitting +# * log_reset(): pop all levels except the root +# * log_tree: initial state of the log tree, set this before sourcing this file +# | to set the default (not removable) log level +# * log_minlen: the minimum length of a log level. Levels that are too short +# | will be space-padded. Padding inserted to the right if negative +# * log_maxlen: the maximum length of a log level. Levels that are too long +# | will be truncated + +# you can initialize your "root" to anything by setting log_tree before +# you source this +# the default is "blog" - bunker log +: ${log_tree:=blog} +log_indent=0 + +# shadow tree, used to calculate indent +# if you push, don't log, and then pop, you shadow tree will = your tree +# => no need to recalculate indents +# in short, if shadow tree doesn't match the tree, +# the indent is recalculated at log-time +log_stree=$log_tree + +# this is a function that allows for selecting different styles of truncation +# $log_tstyles will be tried in a row until the result fits +# as such, it's highly recommended the final option be a terminal one +# available styles, with *s being terminal: +# * cut*: truncates rightwards +# * rcut*: truncates leftwards +# * vowels: removes all vowels +# * numbers: remove all numbers +# * noalnum: remove everything other than alphanumerics +: ${log_tstyles:=cut} +log_truncate() ( + set -- "$*" "$(printf "$*" | wc -c)" \ + "$(echo "$log_tstyles" | cut -d' ' -f1)" + if [ "${log_maxlen:-$2}" -ge "$2" ]; then + echo "$1" + return 0 + fi + case "$3" in + cut) + echo "$1" | cut -c 1-"${log_maxlen:-$2}" + return 0 + ;; + rcut) + start=$(( $2 - ${log_maxlen:-$2} + 1 )) + [ "$start" -lt 1 ] && start=1 + echo "$1" | cut -c "$start"- + return 0 + ;; + vowels) + set -- "$(echo "$1" | sed -e 's/[aeiou]//g')" + ;; + numbers) + set -- "$(echo "$1" | sed -e 's/[0-9]//g')" + ;; + noalnum) + set -- "$(echo "$1" | sed -e 's/[^0-9a-zA-Z]//g')" + ;; + *) return 1 ;; + esac + # goto next method + log_tstyles=$(echo "$log_tstyles" | cut -d' ' -f2-) + log_truncate "$1" +) + +# you can push multiple levels at once +# if a level has embedded spaces, it counts for multiple levels +# all words will be truncated to $log_maxlen, but only if it's set +log_push() { + # do word splitting in case of extra embedded " "s + set -- "$*" + for add in $1; do + # truncate to log_maxlen, if it's set + set -- "$@" "$(log_truncate "$add")" + done + shift + log_tree="$log_tree $@" +} + +# you can pop multiple levels at once, $1 is number to pop +log_pop() { + # default to popping 1 + # save the number of words in the log tree + set -- ${1:-1} $(echo "$log_tree" | wc -w) + + # never pop the last word + if [ $1 -ge $2 ]; then + set -- $(( $2 - 1 )) $2 + fi + log_tree=$(echo "$log_tree" | cut -d' ' -f 1-$(( $2 - $1 )) ) +} + +log_shift() { + # perform word splitting + set -- "$*" + set -- $1 + + log_pop $# + log_push "$@" +} + +log_reset() { + set -- $(echo "$log_tree" | wc -w) + log_pop $(( $1 - 1 )) +} + +# this will read stdin line by line +# to integrate external commands into the log +log_stdin() { + while read -r line; do + log "$line" + done +} + +# echo "$*", but with +# every level will be padded to $log_minlen, if it's set +# you can set it to a negative number to right-pad it +log() { + # we need to recalculate the indent + if [ "$log_indent" -eq 0 ] || [ "$log_tree" != "$log_stree" ]; then + # update the shadow tree and calculate the indentation level + log_stree=$log_tree + log_indent=$(printf "%${log_minlen}s/" $log_tree | wc -c) + + # prefix printing + printf "%${log_minlen}s/" $log_tree + printf "\b: " + else + # indent up to the indent, but print a | in place of the : + # the 2 is because the above has an off-by-one, and this is easier + printf ' %.0s' $(seq 2 "$log_indent") + printf '| ' + fi + echo "$*" +} >&2 + +# *v and *q variants +logv() { + [ -n "$VERBOSE" ] && log "$@" +} + +logq() { + [ -z "$QUIET" ] && log "$@" +} + +log_stdinv() { + [ -n "$VERBOSE" ] && log_stdin "$@" +} + +log_stdinq() { + [ -z "$QUIET" ] && log_stdin "$@" +} |
