aboutsummaryrefslogtreecommitdiff
path: root/log.sh
blob: bb979b5fa165c424f4ff5e3571dea5a9c5bbcfc1 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
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 "$@"
}