xref: /freebsd/libexec/rc/debug.sh (revision c4fde87a3236b500246d4fce76dbca2a3d64f73a)
1:
2# NAME:
3#	debug.sh - selectively debug scripts
4#
5# SYNOPSIS:
6#	$_DEBUG_SH . debug.sh
7#	DebugOn [-eo] "tag" ...
8#	DebugOff [-eo] [rc="rc"] "tag" ...
9#	Debugging
10#	DebugAdd "tag"
11#	DebugEcho ...
12#	DebugLog ...
13#	DebugShell "tag" ...
14#	DebugTrace ...
15#	Debug "tag" ...
16#
17#	$DEBUG_SKIP echo skipped when Debug "tag" is true.
18#	$DEBUG_DO echo only done when Debug "tag" is true.
19#
20# DESCRIPTION:
21#	debug.sh provides the following functions to facilitate
22#	flexible run-time tracing of complicated shell scripts.
23#
24#	DebugOn turns tracing on if any "tag" is found in "DEBUG_SH".
25#	It turns tracing off if "!tag" is found in "DEBUG_SH".
26#	It also sets "DEBUG_ON" to the "tag" that caused tracing to be
27#	enabled, or "DEBUG_OFF" if we matched "!tag".
28#	If '-e' option given returns 1 if no "tag" matched.
29#	If the '-o' flag is given, tracing is turned off unless there
30#	was a matched "tag", useful for functions too noisy to tace.
31#
32#	Further; when we set "DEBUG_ON" if we find
33#	"$DEBUG_ON:debug_add:tag" in "DEBUG_SH" we will
34#	add the new "tag" to "DEBUG_SH" so it only has effect after that
35#	point.
36#
37#	DebugOff turns tracing on if any "tag" matches "DEBUG_OFF" or
38#	off if any "tag" matches "DEBUG_ON". This allows nested
39#	functions to not interfere with each other.
40#
41#	DebugOff accepts but ignores the '-e' and '-o' options.
42#	The optional "rc" value will be returned rather than the
43#	default of 0. Thus if DebugOff is the last operation in a
44#	function, "rc" will be the return code of that function.
45#
46#	DebugAdd allows adding a "tag" to "DEBUG_SH" to influence
47#	later events, possibly in a child process.
48#
49#	DebugEcho is just shorthand for:
50#.nf
51#	$DEBUG_DO echo "$@"
52#.fi
53#
54#	Debugging returns true if tracing is enabled.
55#	It is useful for bounding complex debug actions, rather than
56#	using lots of "DEBUG_DO" lines.
57#
58#	DebugShell runs an interactive shell if any "tag" is found in
59#	"DEBUG_INTERACTIVE", and there is a tty available.
60#	The shell used is defined by "DEBUG_SHELL" or "SHELL" and
61#	defaults to '/bin/sh'.
62#
63#	Debug calls DebugOn and if that does not turn tracing on, it
64#	calls DebugOff to turn it off.
65#
66#	The variables "DEBUG_SKIP" and "DEBUG_DO" are set so as to
67#	enable/disable code that should be skipped/run when debugging
68#	is turned on. "DEBUGGING" is the same as "DEBUG_SKIP" for
69#	backwards compatability.
70#
71#	The use of $_DEBUG_SH is to prevent multiple inclusion, though
72#	it does no harm in this case.
73#
74# BUGS:
75#	Does not work with some versions of ksh.
76#	If a function turns tracing on, ksh turns it off when the
77#	function returns - useless.
78#	PD ksh works ok ;-)
79#
80# AUTHOR:
81#	Simon J. Gerraty <sjg@crufty.net>
82
83# RCSid:
84#	$Id: debug.sh,v 1.54 2026/10/13 23:00:48 sjg Exp $
85#
86#	@(#) Copyright (c) 1994-2024 Simon J. Gerraty
87#
88#	SPDX-License-Identifier: BSD-2-Clause
89#
90#	Please send copies of changes and bug-fixes to:
91#	sjg@crufty.net
92#
93
94_DEBUG_SH=:
95
96case $0 in
97-*) ;;
98*) Myname=${Myname:-`basename $0 .sh`};;
99esac
100
101DEBUGGING=
102DEBUG_DO=:
103DEBUG_SKIP=
104export DEBUGGING DEBUG_DO DEBUG_SKIP
105
106# have is handy
107if test -z "$_HAVE_SH"; then
108	_HAVE_SH=:
109
110	##
111	# have that does not rely on return code of type
112	#
113	have() {
114		case `(type "$1") 2>&1` in
115		*" found") return 1;;
116		esac
117		return 0
118	}
119fi
120
121# does local *actually* work?
122local_works() {
123    local _fu
124}
125
126if local_works > /dev/null 2>&1; then
127    _local=local
128else
129    _local=:
130fi
131# for backwards compatability
132local=$_local
133
134if test -z "$isPOSIX_SHELL"; then
135	if (echo ${PATH%:*}) > /dev/null 2>&1; then
136		# true should be a builtin, : certainly is
137		isPOSIX_SHELL=:
138	else
139		isPOSIX_SHELL=false
140		case `(type false) 2>&1` in
141		*builtin*) ;;
142		*)
143			false() {
144				return 1
145			}
146			;;
147		esac
148	fi
149fi
150
151is_posix_shell() {
152	$isPOSIX_SHELL
153	return
154}
155
156
157##
158# _debugAdd match
159#
160# Called from _debugOn when $match also appears in $DEBUG_SH with
161# a suffix of :debug_add:tag we will add tag to DEBUG_SH
162#
163_debugAdd() {
164	eval $_local tag
165
166	for tag in `IFS=,; echo $DEBUG_SH`
167	do
168		: tag=$tag
169		case "$tag" in
170		$1:debug_add:*)
171			if is_posix_shell; then
172				tag=${tag#$1:debug_add:}
173			else
174				tag=`expr $tag : '.*:debug_add:\(.*\)'`
175			fi
176			case ",$DEBUG_SH," in
177			*,$tag,*) ;;
178			*)	set -x
179				: _debugAdd $1
180				DEBUG_SH=$DEBUG_SH,$tag
181				set +x
182				;;
183			esac
184			;;
185		esac
186	done
187	export DEBUG_SH
188}
189
190
191##
192# _debugOn match first
193#
194# Actually turn on tracing, set $DEBUG_ON=$match
195#
196# Check if $DEBUG_SH contains $match:debug_add:* and call _debugAdd
197# to add the suffix to DEBUG_SH.  This useful when we only want
198# to trace some script when run under specific circumstances.
199#
200# If we have included hooks.sh $_HOOKS_SH will be set
201# and if $first (the first arg to DebugOn) is suitable as a variable
202# name we will run ${first}_debugOn_hooks.
203#
204# We disable tracing for hooks_run itself but functions can trace
205# if they want based on DEBUG_DO
206#
207_debugOn() {
208	DEBUG_OFF=
209	DEBUG_DO=
210	DEBUG_SKIP=:
211	DEBUG_X=-x
212	# do this first to reduce noise
213	case ",$DEBUG_SH," in
214	*,$1:debug_add:*) _debugAdd $1;;
215	*,$2:debug_add:*) _debugAdd $2;;
216	esac
217	set -x
218	DEBUG_ON=$1
219	case "$_HOOKS_SH,$2" in
220	,*|:,|:,*[${CASE_CLASS_NEG:-!}A-Za-z0-9_]*) ;;
221	*)	# avoid noise from hooks_run
222		set +x
223		hooks_run ${2}_debugOn_hooks
224		set -x
225		;;
226	esac
227}
228
229##
230# _debugOff match $DEBUG_ON $first
231#
232# Actually turn off tracing, set $DEBUG_OFF=$match
233#
234# If we have included hooks.sh $_HOOKS_SH will be set
235# and if $first (the first arg to DebugOff) is suitable as a variable
236# name we will run ${first}_debugOff_hooks.
237#
238# We do hooks_run after turning off tracing, but before resetting
239# DEBUG_DO so functions can trace if they want
240#
241_debugOff() {
242	$DEBUG_DO set -x	# so we see DEBUG_OFF=something in the trace
243	DEBUG_OFF=${1:-$DEBUG_ON}
244	set +x
245	DEBUG_OFF=$1
246	case "$_HOOKS_SH,$3" in
247	,*|:,|:,*[${CASE_CLASS_NEG:-!}A-Za-z0-9_]*) ;;
248	*)	hooks_run ${3}_debugOff_hooks;;
249	esac
250	set +x			# just to be sure
251	DEBUG_ON=$2
252	DEBUG_DO=:
253	DEBUG_SKIP=
254	DEBUG_X=
255}
256
257##
258# DebugAdd tag
259#
260# Add tag to DEBUG_SH
261#
262DebugAdd() {
263        DEBUG_SH=${DEBUG_SH:+$DEBUG_SH,}$1
264        export DEBUG_SH
265}
266
267##
268# DebugEcho message
269#
270# Output message if we are debugging
271#
272DebugEcho() {
273	$DEBUG_DO echo "$@"
274}
275
276##
277# Debugging
278#
279# return 0 if we are debugging.
280#
281Debugging() {
282	test "$DEBUG_SKIP"
283}
284
285##
286# DebugLog message
287#
288# Outout message with timestamp if we are debugging
289#
290DebugLog() {
291	$DEBUG_SKIP return 0
292	echo `date '+@ %s [%Y-%m-%d %H:%M:%S %Z]'` "$@"
293}
294
295##
296# DebugTrace message
297#
298# Something hard to miss when wading through huge -x output
299#
300DebugTrace() {
301	$DEBUG_SKIP return 0
302	set +x
303	echo "@ ==================== [ $DEBUG_ON ] ===================="
304	DebugLog "$@"
305	echo "@ ==================== [ $DEBUG_ON ] ===================="
306	set -x
307}
308
309##
310# DebugOn [-e] [-o] match ...
311#
312# Turn on debugging if any $match is found in $DEBUG_SH.
313#
314DebugOn() {
315	eval ${local:-:} _e _match _off _rc
316	_rc=0			# avoid problems with set -e
317	_off=:
318	while :
319	do
320		case "$1" in
321		-e) _rc=1; shift;; # caller ok with return 1
322		-o) _off=; shift;; # off unless we have a match
323		*) break;;
324		esac
325	done
326	case ",${DEBUG_SH:-$DEBUG}," in
327	,,)	return $_rc;;
328	*,[Dd]ebug,*) ;;
329	*) $DEBUG_DO set +x;;		# reduce the noise
330	esac
331	_match=
332	# if debugging is off because of a !e
333	# don't add 'all' to the On list.
334	case "$_off$DEBUG_OFF" in
335	:)	_e=all;;
336	*)	_e=;;
337	esac
338	for _e in ${*:-$Myname} $_e
339	do
340		: $_e in ,${DEBUG_SH:-$DEBUG},
341		case ",${DEBUG_SH:-$DEBUG}," in
342		*,!$_e,*|*,!$Myname:$_e,*)
343			# only turn it off if it was on
344			_rc=0
345			$DEBUG_DO _debugOff $_e $DEBUG_ON $1
346			break
347			;;
348		*,$_e,*|*,$Myname:$_e,*)
349			# only turn it on if it was off
350			_rc=0
351			_match=$_e
352			$DEBUG_SKIP _debugOn $_e $1
353			break
354			;;
355		esac
356	done
357	if test -z "$_off$_match"; then
358		# off unless explicit match, but
359		# only turn it off if it was on
360		$DEBUG_DO _debugOff $_e $DEBUG_ON $1
361	fi
362	DEBUGGING=$DEBUG_SKIP	# backwards compatability
363	$DEBUG_DO set -x	# back on if needed
364	$DEBUG_DO set -x	# make sure we see it in trace
365	return $_rc
366}
367
368##
369# DebugOff [-e] [-o] [rc=$?] match ...
370#
371# Only turn debugging off if one of our args was the reason it
372# was turned on.
373#
374# We normally return 0, but caller can pass rc=$? as first arg
375# so that we preserve the status of last statement.
376#
377# The options '-e' and '-o' are ignored, they just make it easier to
378# keep DebugOn and DebugOff lines in sync.
379#
380DebugOff() {
381	eval ${local:-:} _e _rc
382	case ",${DEBUG_SH:-$DEBUG}," in
383	*,[Dd]ebug,*) ;;
384	*) $DEBUG_DO set +x;;		# reduce the noise
385	esac
386	_rc=0			# always happy
387	while :
388	do
389		case "$1" in
390		-[eo]) shift;;	# ignore it
391		rc=*) eval "_$1"; shift;;
392		*) break;;
393		esac
394	done
395	for _e in $*
396	do
397		: $_e==$DEBUG_OFF DEBUG_OFF
398		case "$DEBUG_OFF" in
399		"")	break;;
400		$_e)	_debugOn $DEBUG_ON $1; return $_rc;;
401		esac
402	done
403	for _e in $*
404	do
405		: $_e==$DEBUG_ON DEBUG_ON
406		case "$DEBUG_ON" in
407		"")	break;;
408		$_e)	_debugOff "" "" $1; return $_rc;;
409		esac
410	done
411	DEBUGGING=$DEBUG_SKIP	# backwards compatability
412	$DEBUG_DO set -x	# back on if needed
413	$DEBUG_DO set -x	# make sure we see it in trace
414	return $_rc
415}
416
417_TTY=${_TTY:-`test -t 0 && tty || :`}; export _TTY
418
419# override this if you like
420_debugShell() {
421	test "x$_TTY" != x || return 0
422	{
423		echo DebugShell "$@"
424		echo "Type 'exit' to continue..."
425	} > $_TTY
426	${DEBUG_SHELL:-${SHELL:-/bin/sh}} < $_TTY > $_TTY 2>&1
427}
428
429# Run an interactive shell if appropriate
430# Note: you can use $DEBUG_SKIP DebugShell ... to skip unless debugOn
431DebugShell() {
432	eval ${local:-:} _e
433	case "$_TTY%${DEBUG_INTERACTIVE}" in
434	*%|%*) return 0;;	# no tty or no spec
435	esac
436	for _e in ${*:-$Myname} all
437	do
438		case ",${DEBUG_INTERACTIVE}," in
439		*,!$_e,*|*,!$Myname:$_e,*)
440			return 0
441			;;
442		*,$_e,*|*,$Myname:$_e,*)
443			# Provide clues as to why/where
444			_debugShell "$_e: $@"
445			return $?
446			;;
447		esac
448	done
449	return 0
450}
451
452# For backwards compatibility
453Debug() {
454	case "${DEBUG_SH:-$DEBUG}" in
455	"")	;;
456	*)	DEBUG_ON=${DEBUG_ON:-_Debug}
457		DebugOn -e $* || DebugOff $DEBUG_LAST
458		DEBUGGING=$DEBUG_SKIP
459		;;
460	esac
461}
462