aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChloƩ Vulquin <code@toast.bunkerlabs.net>2026-01-14 22:22:22 +0100
committerChloƩ Vulquin <code@toast.bunkerlabs.net>2026-01-15 06:37:04 +0100
commit74e70ef6e33cc57a1077892deccf6424199ac7ab (patch)
tree3ba24598a6bb4226b188f060154092d0db4313b0
initial importshlib
-rw-r--r--README.md6
-rw-r--r--args.bash166
-rw-r--r--fetch.sh128
-rw-r--r--log.sh204
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>//'
+}
diff --git a/log.sh b/log.sh
new file mode 100644
index 0000000..bb979b5
--- /dev/null
+++ b/log.sh
@@ -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 "$@"
+}