#!/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! ## 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 "$@" }