Skip to content

Instantly share code, notes, and snippets.

@CosmicToast
Last active February 14, 2024 21:53
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save CosmicToast/64b22bebcd984078e6b7147827a331c1 to your computer and use it in GitHub Desktop.
Save CosmicToast/64b22bebcd984078e6b7147827a331c1 to your computer and use it in GitHub Desktop.
POSIX sh stack-based logging system.
#!/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 "$@"
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment