xref: /freebsd/usr.sbin/bsdconfig/share/common.subr (revision 5686c6c38a3e1cc78804eaf5f880bda23dcf592f)
1if [ ! "$_COMMON_SUBR" ]; then _COMMON_SUBR=1
2#
3# Copyright (c) 2012 Ron McDowell
4# Copyright (c) 2012-2013 Devin Teske
5# All rights reserved.
6#
7# Redistribution and use in source and binary forms, with or without
8# modification, are permitted provided that the following conditions
9# are met:
10# 1. Redistributions of source code must retain the above copyright
11#    notice, this list of conditions and the following disclaimer.
12# 2. Redistributions in binary form must reproduce the above copyright
13#    notice, this list of conditions and the following disclaimer in the
14#    documentation and/or other materials provided with the distribution.
15#
16# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
17# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
19# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
20# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
21# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
22# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
23# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
24# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
25# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
26# SUCH DAMAGE.
27#
28# $FreeBSD$
29#
30############################################################ CONFIGURATION
31
32#
33# Default file descriptors to link to stdout/stderr for passthru allowing
34# redirection within a sub-shell to bypass directly to the terminal.
35#
36: ${TERMINAL_STDOUT_PASSTHRU:=3}}
37: ${TERMINAL_STDERR_PASSTHRU:=4}}
38
39############################################################ GLOBALS
40
41#
42# Program name
43#
44pgm="${0##*/}"
45
46#
47# Program arguments
48#
49ARGC="$#"
50ARGV="$@"
51
52#
53# Global exit status variables
54#
55SUCCESS=0
56FAILURE=1
57
58#
59# Operating environment details
60#
61export UNAME_S="$(uname -s)" # Operating System (i.e. FreeBSD)
62export UNAME_P="$(uname -p)" # Processor Architecture (i.e. i386)
63export UNAME_R="$(uname -r)" # Release Level (i.e. X.Y-RELEASE)
64
65#
66# Default behavior is to call f_debug_init() automatically when loaded.
67#
68: ${DEBUG_SELF_INITIALIZE=1}
69
70#
71# Define standard optstring arguments that should be supported by all programs
72# using this include (unless DEBUG_SELF_INITIALIZE is set to NULL to prevent
73# f_debug_init() from autamatically processing "$@" for the below arguments):
74#
75# 	d	Sets $debug to 1
76# 	D:	Sets $debugFile to $OPTARG
77#
78GETOPTS_STDARGS="dD:"
79
80############################################################ FUNCTIONS
81
82# f_dprintf $fmt [ $opts ... ]
83#
84# Sensible debug function. Override in ~/.bsdconfigrc if desired.
85# See /usr/share/examples/bsdconfig/bsdconfigrc for example.
86#
87# If $debug is set and non-NULL, prints DEBUG info using printf(1) syntax:
88# 	+ To $debugFile, if set and non-NULL
89# 	+ To standard output if $debugFile is either NULL or unset
90# 	+ To both if $debugFile begins with a single plus-sign (`+')
91#
92f_dprintf()
93{
94	[ "$debug" ] || return $SUCCESS
95	local fmt="$1"; shift
96	case "$debugFile" in ""|+*)
97	printf "DEBUG: $fmt${fmt:+\n}" "$@" >&${TERMINAL_STDOUT_PASSTHRU:-1}
98	esac
99	[ "${debugFile#+}" ] &&
100		printf "DEBUG: $fmt${fmt:+\n}" "$@" >> "${debugFile#+}"
101	return $SUCCESS
102}
103
104# f_debug_init
105#
106# Initialize debugging. Truncates $debugFile to zero bytes if set.
107#
108f_debug_init()
109{
110	#
111	# Process stored command-line arguments
112	#
113	set -- $ARGV
114	local OPTIND
115	f_dprintf "f_debug_init: ARGV=[%s] GETOPTS_STDARGS=[%s]" \
116	          "$ARGV" "$GETOPTS_STDARGS"
117	while getopts "$GETOPTS_STDARGS" flag > /dev/null; do
118		case "$flag" in
119		d) debug=1;;
120		D) debugFile="$OPTARG";;
121		\?) continue;;
122		esac
123	done
124	shift $(( $OPTIND - 1 ))
125	f_dprintf "f_debug_init: debug=[%s] debugFile=[%s]" \
126	          "$debug" "$debugFile"
127
128	#
129	# Automagically enable debugging if debugFile is set (and non-NULL)
130	#
131	[ "$debugFile" ] && { [ "${debug+set}" ] || debug=1; }
132
133	#
134	# Make debugging persistant if set
135	#
136	[ "$debug" ] && export debug
137	[ "$debugFile" ] && export debugFile
138
139	#
140	# Truncate the debug file upon. Note that we will trim a leading plus
141	# (`+') from the value of debugFile to support persistant meaning that
142	# f_dprintf() should print both to standard output and $debugFile
143	# (minus the leading plus, of course).
144	#
145	local _debug_file="${debugFile#+}"
146	if [ "$_debug_file" ]; then
147		if ( umask 022 && :> "$_debug_file" ); then
148			f_dprintf "Successfully initialized debugFile \`%s'" \
149			          "$_debug_file"
150			[ "${debug+set}" ] ||
151				debug=1 # turn debugging on if not set
152		else
153			unset debugFile
154			f_dprintf "Unable to initialize debugFile \`%s'" \
155			          "$_debug_file"
156		fi
157	fi
158}
159
160# f_err $fmt [ $opts ... ]
161#
162# Print a message to stderr (fd=2).
163#
164f_err()
165{
166	printf "$@" >&${TERMINAL_STDERR_PASSTHRU:-2}
167}
168
169# f_quietly $command [ $arguments ... ]
170#
171# Run a command quietly (quell any output to stdout or stderr)
172#
173f_quietly()
174{
175	"$@" > /dev/null 2>&1
176}
177
178# f_have $anything ...
179#
180# A wrapper to the `type' built-in. Returns true if argument is a valid shell
181# built-in, keyword, or externally-tracked binary, otherwise false.
182#
183f_have()
184{
185	f_quietly type "$@"
186}
187
188# f_getvar $var_to_get [$var_to_set]
189#
190# Utility function designed to go along with the already-builtin setvar.
191# Allows clean variable name indirection without forking or sub-shells.
192#
193# Returns error status if the requested variable ($var_to_get) is not set.
194#
195# If $var_to_set is missing or NULL, the value of $var_to_get is printed to
196# standard output for capturing in a sub-shell (which is less-recommended
197# because of performance degredation; for example, when called in a loop).
198#
199f_getvar()
200{
201	local __var_to_get="$1" __var_to_set="$2"
202	[ "$__var_to_set" ] || local value
203	eval ${__var_to_set:-value}=\"\${$__var_to_get}\"
204	eval [ \"\${$__var_to_get+set}\" ]
205	local __retval=$?
206	eval f_dprintf '"f_getvar: var=[%s] value=[%s] r=%u"' \
207		\"\$__var_to_get\" \"\$${__var_to_set:-value}\" \$__retval
208	[ "$__var_to_set" ] || { [ "$value" ] && echo "$value"; }
209	return $__retval
210}
211
212# f_isset $var
213#
214# Check if variable $var is set. Returns success if variable is set, otherwise
215# returns failure.
216#
217f_isset()
218{
219	eval [ \"\${${1%%[$IFS]*}+set}\" ]
220}
221
222# f_die [ $status [ $fmt [ $opts ... ]]]
223#
224# Abruptly terminate due to an error optionally displaying a message in a
225# dialog box using printf(1) syntax.
226#
227f_die()
228{
229	local status=$FAILURE
230
231	# If there is at least one argument, take it as the status
232	if [ $# -gt 0 ]; then
233		status=$1
234		shift 1 # status
235	fi
236
237	# If there are still arguments left, pass them to f_show_msg
238	[ $# -gt 0 ] && f_show_msg "$@"
239
240	# Optionally call f_clean_up() function if it exists
241	f_have f_clean_up && f_clean_up
242
243	exit $status
244}
245
246# f_interrupt
247#
248# Interrupt handler.
249#
250f_interrupt()
251{
252	exec 2>&1 # fix sh(1) bug where stderr gets lost within async-trap
253	f_die
254}
255
256# f_show_info $fmt [ $opts ... ]
257#
258# Display a message in a dialog infobox using printf(1) syntax.
259#
260f_show_info()
261{
262	local msg
263	msg=$( printf "$@" )
264
265	#
266	# Use f_dialog_infobox from dialog.subr if possible, otherwise fall
267	# back to dialog(1) (without options, making it obvious when using
268	# un-aided system dialog).
269	#
270	if f_have f_dialog_info; then
271		f_dialog_info "$msg"
272	else
273		dialog --infobox "$msg" 0 0
274	fi
275}
276
277# f_show_msg $fmt [ $opts ... ]
278#
279# Display a message in a dialog box using printf(1) syntax.
280#
281f_show_msg()
282{
283	local msg
284	msg=$( printf "$@" )
285
286	#
287	# Use f_dialog_msgbox from dialog.subr if possible, otherwise fall
288	# back to dialog(1) (without options, making it obvious when using
289	# un-aided system dialog).
290	#
291	if f_have f_dialog_msgbox; then
292		f_dialog_msgbox "$msg"
293	else
294		dialog --msgbox "$msg" 0 0
295	fi
296}
297
298
299# f_yesno $fmt [ $opts ... ]
300#
301# Display a message in a dialog yes/no box using printf(1) syntax.
302#
303f_yesno()
304{
305	local msg
306	msg=$( printf "$@" )
307
308	#
309	# Use f_dialog_yesno from dialog.subr if possible, otherwise fall
310	# back to dialog(1) (without options, making it obvious when using
311	# un-aided system dialog).
312	#
313	if f_have f_dialog_yesno; then
314		f_dialog_yesno "$msg"
315	else
316		dialog --yesno "$msg" 0 0
317	fi
318}
319
320# f_noyes $fmt [ $opts ... ]
321#
322# Display a message in a dialog yes/no box using printf(1) syntax.
323# NOTE: THis is just like the f_yesno function except "No" is default.
324#
325f_noyes()
326{
327	local msg
328	msg=$( printf "$@" )
329
330	#
331	# Use f_dialog_noyes from dialog.subr if possible, otherwise fall
332	# back to dialog(1) (without options, making it obvious when using
333	# un-aided system dialog).
334	#
335	if f_have f_dialog_noyes; then
336		f_dialog_noyes "$msg"
337	else
338		dialog --defaultno --yesno "$msg" 0 0
339	fi
340}
341
342# f_show_help $file
343#
344# Display a language help-file. Automatically takes $LANG and $LC_ALL into
345# consideration when displaying $file (suffix ".$LC_ALL" or ".$LANG" will
346# automatically be added prior to loading the language help-file).
347#
348# If a language has been requested by setting either $LANG or $LC_ALL in the
349# environment and the language-specific help-file does not exist we will fall
350# back to $file without-suffix.
351#
352# If the language help-file does not exist, an error is displayed instead.
353#
354f_show_help()
355{
356	local file="$1"
357	local lang="${LANG:-$LC_ALL}"
358
359	[ -f "$file.$lang" ] && file="$file.$lang"
360
361	#
362	# Use f_dialog_textbox from dialog.subr if possible, otherwise fall
363	# back to dialog(1) (without options, making it obvious when using
364	# un-aided system dialog).
365	#
366	if f_have f_dialog_textbox; then
367		f_dialog_textbox "$file"
368	else
369		dialog --msgbox "$( cat "$file" 2>&1 )" 0 0
370	fi
371}
372
373# f_include $file
374#
375# Include a shell subroutine file.
376#
377# If the subroutine file exists but returns error status during loading, exit
378# is called and execution is prematurely terminated with the same error status.
379#
380f_include()
381{
382	local file="$1"
383	f_dprintf "f_include: file=[%s]" "$file"
384	. "$file" || exit $?
385}
386
387# f_include_lang $file
388#
389# Include a language file. Automatically takes $LANG and $LC_ALL into
390# consideration when including $file (suffix ".$LC_ALL" or ".$LANG" will
391# automatically by added prior to loading the language file).
392#
393# No error is produced if (a) a language has been requested (by setting either
394# $LANG or $LC_ALL in the environment) and (b) the language file does not
395# exist -- in which case we will fall back to loading $file without-suffix.
396#
397# If the language file exists but returns error status during loading, exit
398# is called and execution is prematurely terminated with the same error status.
399#
400f_include_lang()
401{
402	local file="$1"
403	local lang="${LANG:-$LC_ALL}"
404
405	f_dprintf "f_include_lang: file=[%s] lang=[%s]" "$file" "$lang"
406	if [ -f "$file.$lang" ]; then
407		. "$file.$lang" || exit $?
408	else
409		. "$file" || exit $?
410	fi
411}
412
413# f_usage $file [ $key1 $value1 ... ]
414#
415# Display USAGE file with optional pre-processor macro definitions. The first
416# argument is the template file containing the usage text to be displayed. If
417# $LANG or $LC_ALL (in order of preference, respectively) is set, ".encoding"
418# will automatically be appended as a suffix to the provided $file pathname.
419#
420# When processing $file, output begins at the first line containing that is
421# (a) not a comment, (b) not empty, and (c) is not pure-whitespace. All lines
422# appearing after this first-line are output, including (a) comments (b) empty
423# lines, and (c) lines that are purely whitespace-only.
424#
425# If additional arguments appear after $file, substitutions are made while
426# printing the contents of the USAGE file. The pre-processor macro syntax is in
427# the style of autoconf(1), for example:
428#
429# 	f_usage $file "FOO" "BAR"
430#
431# Will cause instances of "@FOO@" appearing in $file to be replaced with the
432# text "BAR" before bering printed to the screen.
433#
434# This function is a two-parter. Below is the awk(1) portion of the function,
435# afterward is the sh(1) function which utilizes the below awk script.
436#
437f_usage_awk='
438BEGIN { found = 0 }
439{
440	if ( !found && $0 ~ /^[[:space:]]*($|#)/ ) next
441	found = 1
442	print
443}
444'
445f_usage()
446{
447	local file="$1"
448	local lang="${LANG:-$LC_ALL}"
449
450	f_dprintf "f_usage: file=[%s] lang=[%s]" "$file" "$lang"
451
452	shift 1 # file
453
454	local usage
455	if [ -f "$file.$lang" ]; then
456		usage=$( awk "$f_usage_awk" "$file.$lang" ) || exit $FAILURE
457	else
458		usage=$( awk "$f_usage_awk" "$file" ) || exit $FAILURE
459	fi
460
461	while [ $# -gt 0 ]; do
462		local key="$1"
463		export value="$2"
464		usage=$( echo "$usage" | awk \
465			"{ gsub(/@$key@/, ENVIRON[\"value\"]); print }" )
466		shift 2
467	done
468
469	f_err "%s\n" "$usage"
470
471	exit $FAILURE
472}
473
474# f_index_file $keyword
475#
476# Process all INDEX files known to bsdconfig and return the path to first file
477# containing a menu_selection line with a keyword portion matching $keyword.
478#
479# If $LANG or $LC_ALL (in order of preference, respectively) is set,
480# "INDEX.encoding" files will be searched first.
481#
482# If no file is found, error status is returned along with the NULL string.
483#
484# This function is a two-parter. Below is the awk(1) portion of the function,
485# afterward is the sh(1) function which utilizes the below awk script.
486#
487f_index_file_awk='
488# Variables that should be defined on the invocation line:
489# 	-v keyword="keyword"
490BEGIN { found = 0 }
491( $0 ~ "^menu_selection=\"" keyword "\\|" ) {
492	print FILENAME
493	found++
494	exit
495}
496END { exit ! found }
497'
498f_index_file()
499{
500	local keyword="$1"
501	local lang="${LANG:-$LC_ALL}"
502
503	f_dprintf "f_index_file: keyword=[%s] lang=[%s]" "$keyword" "$lang"
504
505	if [ "$lang" ]; then
506		awk -v keyword="$keyword" "$f_index_file_awk" \
507			$BSDCFG_LIBE${BSDCFG_LIBE:+/}*/INDEX.$lang &&
508			return
509		# No match, fall-thru to non-i18n sources
510	fi
511	awk -v keyword="$keyword" "$f_index_file_awk" \
512		$BSDCFG_LIBE${BSDCFG_LIBE:+/}*/INDEX
513}
514
515# f_index_menusel_keyword $indexfile $pgm
516#
517# Process $indexfile and return only the keyword portion of the menu_selection
518# line with a command portion matching $pgm.
519#
520# This function is for internationalization (i18n) mapping of the on-disk
521# scriptname ($pgm) into the localized language (given language-specific
522# $indexfile). If $LANG or $LC_ALL (in orderder of preference, respectively) is
523# set, ".encoding" will automatically be appended as a suffix to the provided
524# $indexfile pathname.
525#
526# If, within $indexfile, multiple $menu_selection values map to $pgm, only the
527# first one will be returned. If no mapping can be made, the NULL string is
528# returned.
529#
530# If $indexfile does not exist, error status is returned with NULL.
531#
532# This function is a two-parter. Below is the awk(1) portion of the function,
533# afterward is the sh(1) function which utilizes the below awk script.
534#
535f_index_menusel_keyword_awk='
536# Variables that should be defined on the invocation line:
537# 	-v pgm="program_name"
538#
539BEGIN {
540	prefix = "menu_selection=\""
541	plen = length(prefix)
542	found = 0
543}
544{
545	if (!match($0, "^" prefix ".*\\|.*\"")) next
546
547	keyword = command = substr($0, plen + 1, RLENGTH - plen - 1)
548	sub(/^.*\|/, "", command)
549	sub(/\|.*$/, "", keyword)
550
551	if ( command == pgm )
552	{
553		print keyword
554		found++
555		exit
556	}
557}
558END { exit ! found }
559'
560f_index_menusel_keyword()
561{
562	local indexfile="$1" pgm="$2"
563	local lang="${LANG:-$LC_ALL}"
564
565	f_dprintf "f_index_menusel_keyword: index=[%s] pgm=[%s] lang=[%s]" \
566	          "$indexfile" "$pgm" "$lang"
567
568	if [ -f "$indexfile.$lang" ]; then
569		awk -v pgm="$pgm" \
570			"$f_index_menusel_keyword_awk" \
571			"$indexfile.$lang"
572	elif [ -f "$indexfile" ]; then
573		awk -v pgm="$pgm" \
574			"$f_index_menusel_keyword_awk" \
575			"$indexfile"
576	fi
577}
578
579# f_index_menusel_command $indexfile $keyword
580#
581# Process $indexfile and return only the command portion of the menu_selection
582# line with a keyword portion matching $keyword.
583#
584# This function is for mapping [possibly international] keywords into the
585# command to be executed. If $LANG or $LC_ALL (order of preference) is set,
586# ".encoding" will automatically be appended as a suffix to the provided
587# $indexfile pathname.
588#
589# If, within $indexfile, multiple $menu_selection values map to $keyword, only
590# the first one will be returned. If no mapping can be made, the NULL string is
591# returned.
592#
593# If $indexfile doesn't exist, error status is returned with NULL.
594#
595# This function is a two-parter. Below is the awk(1) portion of the function,
596# afterward is the sh(1) function which utilizes the below awk script.
597#
598f_index_menusel_command_awk='
599# Variables that should be defined on the invocation line:
600# 	-v key="keyword"
601#
602BEGIN {
603	prefix = "menu_selection=\""
604	plen = length(prefix)
605	found = 0
606}
607{
608	if (!match($0, "^" prefix ".*\\|.*\"")) next
609
610	keyword = command = substr($0, plen + 1, RLENGTH - plen - 1)
611	sub(/^.*\|/, "", command)
612	sub(/\|.*$/, "", keyword)
613
614	if ( keyword == key )
615	{
616		print command
617		found++
618		exit
619	}
620}
621END { exit ! found }
622'
623f_index_menusel_command()
624{
625	local indexfile="$1" keyword="$2" command
626	local lang="${LANG:-$LC_ALL}"
627
628	f_dprintf "f_index_menusel_command: index=[%s] key=[%s] lang=[%s]" \
629	          "$indexfile" "$keyword" "$lang"
630
631	if [ -f "$indexfile.$lang" ]; then
632		command=$( awk -v key="$keyword" \
633				"$f_index_menusel_command_awk" \
634				"$indexfile.$lang" ) || return $FAILURE
635	elif [ -f "$indexfile" ]; then
636		command=$( awk -v key="$keyword" \
637				"$f_index_menusel_command_awk" \
638				"$indexfile" ) || return $FAILURE
639	else
640		return $FAILURE
641	fi
642
643	#
644	# If the command pathname is not fully qualified fix-up/force to be
645	# relative to the $indexfile directory.
646	#
647	case "$command" in
648	/*) : already fully qualified ;;
649	*)
650		local indexdir="${indexfile%/*}"
651		[ "$indexdir" != "$indexfile" ] || indexdir="."
652		command="$indexdir/$command"
653	esac
654
655	echo "$command"
656}
657
658# f_running_as_init
659#
660# Returns true if running as init(1).
661#
662f_running_as_init()
663{
664	#
665	# When a custom init(8) performs an exec(3) to invoke a shell script,
666	# PID 1 becomes sh(1) and $PPID is set to 1 in the executed script.
667	#
668	[ ${PPID:-0} -eq 1 ] # Return status
669}
670
671# f_mounted $local_directory
672#
673# Return success if a filesystem is mounted on a particular directory.
674#
675f_mounted()
676{
677	local dir="$1"
678	[ -d "$dir" ] || return $FAILURE
679	mount | grep -Eq " on $dir \([^)]+\)$"
680}
681
682############################################################ MAIN
683
684#
685# Trap signals so we can recover gracefully
686#
687trap 'f_interrupt' SIGINT
688trap 'f_die' SIGTERM SIGPIPE SIGXCPU SIGXFSZ \
689             SIGFPE SIGTRAP SIGABRT SIGSEGV
690trap '' SIGALRM SIGPROF SIGUSR1 SIGUSR2 SIGHUP SIGVTALRM
691
692#
693# Clone terminal stdout/stderr so we can redirect to it from within sub-shells
694#
695eval exec $TERMINAL_STDOUT_PASSTHRU\>\&1
696eval exec $TERMINAL_STDERR_PASSTHRU\>\&2
697
698#
699# Self-initialize unless requested otherwise
700#
701f_dprintf "%s: DEBUG_SELF_INITIALIZE=[%s]" \
702          dialog.subr "$DEBUG_SELF_INITIALIZE"
703case "$DEBUG_SELF_INITIALIZE" in
704""|0|[Nn][Oo]|[Oo][Ff][Ff]|[Ff][Aa][Ll][Ss][Ee]) : do nothing ;;
705*) f_debug_init
706esac
707
708#
709# Log our operating environment for debugging purposes
710#
711f_dprintf "UNAME_S=[%s] UNAME_P=[%s] UNAME_R=[%s]" \
712          "$UNAME_S" "$UNAME_P" "$UNAME_R"
713
714f_dprintf "%s: Successfully loaded." common.subr
715
716fi # ! $_COMMON_SUBR
717