]> CyberLeo.Net >> Repos - FreeBSD/FreeBSD.git/blob - usr.sbin/bsdconfig/share/common.subr
Add support for scripting (sysinstall style).
[FreeBSD/FreeBSD.git] / usr.sbin / bsdconfig / share / common.subr
1 if [ ! "$_COMMON_SUBR" ]; then _COMMON_SUBR=1
2 #
3 # Copyright (c) 2012 Ron McDowell
4 # Copyright (c) 2012 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 #
44 pgm="${0##*/}"
45
46 #
47 # Program arguments
48 #
49 ARGC="$#"
50 ARGV="$@"
51
52 #
53 # Global exit status variables
54 #
55 SUCCESS=0
56 FAILURE=1
57
58 #
59 # Operating environment details
60 #
61 export UNAME_S="$(uname -s)" # Operating System (i.e. FreeBSD)
62 export UNAME_P="$(uname -p)" # Processor Architecture (i.e. i386)
63 export UNAME_R="$(uname -r)" # Release Level (i.e. X.Y-RELEASE)
64
65 ############################################################ FUNCTIONS
66
67 # f_dprintf $fmt [ $opts ... ]
68 #
69 # Sensible debug function. Override in ~/.bsdconfigrc if desired.
70 # See /usr/share/examples/bsdconfig/bsdconfigrc for example.
71 #
72 # If $debug is set and non-NULL, prints DEBUG info using printf(1) syntax:
73 #       + To $debugFile, if set and non-NULL
74 #       + To standard output if $debugFile is either NULL or unset
75 #       + To both if $debugFile begins with a single plus-sign (`+')
76 #
77 f_dprintf()
78 {
79         [ "$debug" ] || return $SUCCESS
80         local fmt="$1"; shift
81         case "$debugFile" in ""|+*)
82         printf "DEBUG: $fmt${fmt:+\n}" "$@" >&${TERMINAL_STDOUT_PASSTHRU:-1}
83         esac
84         [ "${debugFile#+}" ] &&
85                 printf "DEBUG: $fmt${fmt:+\n}" "$@" >> "${debugFile#+}"
86         return $SUCCESS
87 }
88
89 # f_err $fmt [ $opts ... ]
90 #
91 # Print a message to stderr (fd=2).
92 #
93 f_err()
94 {
95         printf "$@" >&${TERMINAL_STDERR_PASSTHRU:-2}
96 }
97
98 # f_quietly $command [ $arguments ... ]
99 #
100 # Run a command quietly (quell any output to stdout or stderr)
101 #
102 f_quietly()
103 {
104         "$@" > /dev/null 2>&1
105 }
106
107 # f_have $anything ...
108 #
109 # A wrapper to the `type' built-in. Returns true if argument is a valid shell
110 # built-in, keyword, or externally-tracked binary, otherwise false.
111 #
112 f_have()
113 {
114         f_quietly type "$@"
115 }
116
117 # f_getvar $var_to_get [$var_to_set]
118 #
119 # Utility function designed to go along with the already-builtin setvar.
120 # Allows clean variable name indirection without forking or sub-shells.
121 #
122 # Returns error status if the requested variable ($var_to_get) is not set.
123 #
124 # If $var_to_set is missing or NULL, the value of $var_to_get is printed to
125 # standard output for capturing in a sub-shell (which is less-recommended
126 # because of performance degredation; for example, when called in a loop).
127 #
128 f_getvar()
129 {
130         local var_to_get="$1" var_to_set="$2"
131         [ "$var_to_set" ] || local value
132         eval ${var_to_set:-value}=\"\${$var_to_get}\"
133         eval [ \"\${$var_to_get+set}\" ]
134         local retval=$?
135         eval f_dprintf '"f_getvar: var=[%s] value=[%s] r=%u"' \
136                        \"\$var_to_get\" \"\$${var_to_set:-value}\" \$retval
137         [ "$var_to_set" ] || { [ "$value" ] && echo "$value"; }
138         return $retval
139 }
140
141 # f_die [ $status [ $fmt [ $opts ... ]]]
142 #
143 # Abruptly terminate due to an error optionally displaying a message in a
144 # dialog box using printf(1) syntax.
145 #
146 f_die()
147 {
148         local status=$FAILURE
149
150         # If there is at least one argument, take it as the status
151         if [ $# -gt 0 ]; then
152                 status=$1
153                 shift 1 # status
154         fi
155
156         # If there are still arguments left, pass them to f_show_msg
157         [ $# -gt 0 ] && f_show_msg "$@"
158
159         # Optionally call f_clean_up() function if it exists
160         f_have f_clean_up && f_clean_up
161
162         exit $status
163 }
164
165 # f_interrupt
166 #
167 # Interrupt handler.
168 #
169 f_interrupt()
170 {
171         exec 2>&1 # fix sh(1) bug where stderr gets lost within async-trap
172         f_die
173 }
174
175 # f_show_info $fmt [ $opts ... ]
176 #
177 # Display a message in a dialog infobox using printf(1) syntax.
178 #
179 f_show_info()
180 {
181         local msg
182         msg=$( printf "$@" )
183
184         #
185         # Use f_dialog_infobox from dialog.subr if possible, otherwise fall
186         # back to dialog(1) (without options, making it obvious when using
187         # un-aided system dialog).
188         #
189         if f_have f_dialog_info; then
190                 f_dialog_info "$msg"
191         else
192                 dialog --infobox "$msg" 0 0
193         fi
194 }
195
196 # f_show_msg $fmt [ $opts ... ]
197 #
198 # Display a message in a dialog box using printf(1) syntax.
199 #
200 f_show_msg()
201 {
202         local msg
203         msg=$( printf "$@" )
204
205         #
206         # Use f_dialog_msgbox from dialog.subr if possible, otherwise fall
207         # back to dialog(1) (without options, making it obvious when using
208         # un-aided system dialog).
209         #
210         if f_have f_dialog_msgbox; then
211                 f_dialog_msgbox "$msg"
212         else
213                 dialog --msgbox "$msg" 0 0
214         fi
215 }
216
217 # f_show_help $file
218 #
219 # Display a language help-file. Automatically takes $LANG and $LC_ALL into
220 # consideration when displaying $file (suffix ".$LC_ALL" or ".$LANG" will
221 # automatically be added prior to loading the language help-file).
222 #
223 # If a language has been requested by setting either $LANG or $LC_ALL in the
224 # environment and the language-specific help-file does not exist we will fall
225 # back to $file without-suffix.
226 #
227 # If the language help-file does not exist, an error is displayed instead.
228 #
229 f_show_help()
230 {
231         local file="$1"
232         local lang="${LANG:-$LC_ALL}"
233
234         [ -f "$file.$lang" ] && file="$file.$lang"
235
236         #
237         # Use f_dialog_textbox from dialog.subr if possible, otherwise fall
238         # back to dialog(1) (without options, making it obvious when using
239         # un-aided system dialog).
240         #
241         if f_have f_dialog_textbox; then
242                 f_dialog_textbox "$file"
243         else
244                 dialog --msgbox "$( cat "$file" 2>&1 )" 0 0
245         fi
246 }
247
248 # f_include $file
249 #
250 # Include a shell subroutine file.
251 #
252 # If the subroutine file exists but returns error status during loading, exit
253 # is called and execution is prematurely terminated with the same error status.
254 #
255 f_include()
256 {
257         local file="$1"
258         f_dprintf "f_include: file=[%s]" "$file"
259         . "$file" || exit $?
260 }
261
262 # f_include_lang $file
263 #
264 # Include a language file. Automatically takes $LANG and $LC_ALL into
265 # consideration when including $file (suffix ".$LC_ALL" or ".$LANG" will
266 # automatically by added prior to loading the language file).
267 #
268 # No error is produced if (a) a language has been requested (by setting either
269 # $LANG or $LC_ALL in the environment) and (b) the language file does not
270 # exist -- in which case we will fall back to loading $file without-suffix.
271 #
272 # If the language file exists but returns error status during loading, exit
273 # is called and execution is prematurely terminated with the same error status.
274 #
275 f_include_lang()
276 {
277         local file="$1"
278         local lang="${LANG:-$LC_ALL}"
279
280         f_dprintf "f_include_lang: file=[%s] lang=[%s]" "$file" "$lang"
281         if [ -f "$file.$lang" ]; then
282                 . "$file.$lang" || exit $?
283         else
284                 . "$file" || exit $?
285         fi
286 }
287
288 # f_usage $file [ $key1 $value1 ... ]
289 #
290 # Display USAGE file with optional pre-processor macro definitions. The first
291 # argument is the template file containing the usage text to be displayed. If
292 # $LANG or $LC_ALL (in order of preference, respectively) is set, ".encoding"
293 # will automatically be appended as a suffix to the provided $file pathname.
294 #
295 # When processing $file, output begins at the first line containing that is
296 # (a) not a comment, (b) not empty, and (c) is not pure-whitespace. All lines
297 # appearing after this first-line are output, including (a) comments (b) empty
298 # lines, and (c) lines that are purely whitespace-only.
299 #
300 # If additional arguments appear after $file, substitutions are made while
301 # printing the contents of the USAGE file. The pre-processor macro syntax is in
302 # the style of autoconf(1), for example:
303 #
304 #       f_usage $file "FOO" "BAR"
305 #
306 # Will cause instances of "@FOO@" appearing in $file to be replaced with the
307 # text "BAR" before bering printed to the screen.
308 #
309 # This function is a two-parter. Below is the awk(1) portion of the function,
310 # afterward is the sh(1) function which utilizes the below awk script.
311 #
312 f_usage_awk='
313 BEGIN { found = 0 }
314 {
315         if ( !found && $0 ~ /^[[:space:]]*($|#)/ ) next
316         found = 1
317         print
318 }
319 '
320 f_usage()
321 {
322         local file="$1"
323         local lang="${LANG:-$LC_ALL}"
324
325         f_dprintf "f_usage: file=[%s] lang=[%s]" "$file" "$lang"
326
327         shift 1 # file
328
329         local usage
330         if [ -f "$file.$lang" ]; then
331                 usage=$( awk "$f_usage_awk" "$file.$lang" ) || exit $FAILURE
332         else
333                 usage=$( awk "$f_usage_awk" "$file" ) || exit $FAILURE
334         fi
335
336         while [ $# -gt 0 ]; do
337                 local key="$1"
338                 export value="$2"
339                 usage=$( echo "$usage" | awk \
340                         "{ gsub(/@$key@/, ENVIRON[\"value\"]); print }" )
341                 shift 2
342         done
343
344         f_err "%s\n" "$usage"
345
346         exit $FAILURE
347 }
348
349 # f_index_file $keyword
350 #
351 # Process all INDEX files known to bsdconfig and return the path to first file
352 # containing a menu_selection line with a keyword portion matching $keyword.
353 #
354 # If $LANG or $LC_ALL (in order of preference, respectively) is set,
355 # "INDEX.encoding" files will be searched first.
356 #
357 # If no file is found, error status is returned along with the NULL string.
358 #
359 # This function is a two-parter. Below is the awk(1) portion of the function,
360 # afterward is the sh(1) function which utilizes the below awk script.
361 #
362 f_index_file_awk='
363 # Variables that should be defined on the invocation line:
364 #       -v keyword="keyword"
365 BEGIN { found = 0 }
366 ( $0 ~ "^menu_selection=\"" keyword "\\|" ) {
367         print FILENAME
368         found++
369         exit
370 }
371 END { exit ! found }
372 '
373 f_index_file()
374 {
375         local keyword="$1"
376         local lang="${LANG:-$LC_ALL}"
377
378         f_dprintf "f_index_file: keyword=[%s] lang=[%s]" "$keyword" "$lang"
379
380         if [ "$lang" ]; then
381                 awk -v keyword="$keyword" "$f_index_file_awk" \
382                         $BSDCFG_LIBE${BSDCFG_LIBE:+/}*/INDEX.$lang &&
383                         return
384                 # No match, fall-thru to non-i18n sources
385         fi
386         awk -v keyword="$keyword" "$f_index_file_awk" \
387                 $BSDCFG_LIBE${BSDCFG_LIBE:+/}*/INDEX
388 }
389
390 # f_index_menusel_keyword $indexfile $pgm
391 #
392 # Process $indexfile and return only the keyword portion of the menu_selection
393 # line with a command portion matching $pgm.
394 #
395 # This function is for internationalization (i18n) mapping of the on-disk
396 # scriptname ($pgm) into the localized language (given language-specific
397 # $indexfile). If $LANG or $LC_ALL (in orderder of preference, respectively) is
398 # set, ".encoding" will automatically be appended as a suffix to the provided
399 # $indexfile pathname.
400 #
401 # If, within $indexfile, multiple $menu_selection values map to $pgm, only the
402 # first one will be returned. If no mapping can be made, the NULL string is
403 # returned.
404 #
405 # If $indexfile does not exist, error status is returned with NULL.
406 #
407 # This function is a two-parter. Below is the awk(1) portion of the function,
408 # afterward is the sh(1) function which utilizes the below awk script.
409 #
410 f_index_menusel_keyword_awk='
411 # Variables that should be defined on the invocation line:
412 #       -v pgm="program_name"
413 #
414 BEGIN {
415         prefix = "menu_selection=\""
416         plen = length(prefix)
417         found = 0
418 }
419 {
420         if (!match($0, "^" prefix ".*\\|.*\"")) next
421
422         keyword = command = substr($0, plen + 1, RLENGTH - plen - 1)
423         sub(/^.*\|/, "", command)
424         sub(/\|.*$/, "", keyword)
425
426         if ( command == pgm )
427         {
428                 print keyword
429                 found++
430                 exit
431         }
432 }
433 END { exit ! found }
434 '
435 f_index_menusel_keyword()
436 {
437         local indexfile="$1" pgm="$2"
438         local lang="${LANG:-$LC_ALL}"
439
440         f_dprintf "f_index_menusel_keyword: index=[%s] pgm=[%s] lang=[%s]" \
441                   "$indexfile" "$pgm" "$lang"
442
443         if [ -f "$indexfile.$lang" ]; then
444                 awk -v pgm="$pgm" \
445                         "$f_index_menusel_keyword_awk" \
446                         "$indexfile.$lang"
447         elif [ -f "$indexfile" ]; then
448                 awk -v pgm="$pgm" \
449                         "$f_index_menusel_keyword_awk" \
450                         "$indexfile"
451         fi
452 }
453
454 # f_index_menusel_command $indexfile $keyword
455 #
456 # Process $indexfile and return only the command portion of the menu_selection
457 # line with a keyword portion matching $keyword.
458 #
459 # This function is for mapping [possibly international] keywords into the
460 # command to be executed. If $LANG or $LC_ALL (order of preference) is set,
461 # ".encoding" will automatically be appended as a suffix to the provided
462 # $indexfile pathname.
463 #
464 # If, within $indexfile, multiple $menu_selection values map to $keyword, only
465 # the first one will be returned. If no mapping can be made, the NULL string is
466 # returned.
467 #
468 # If $indexfile doesn't exist, error status is returned with NULL.
469 #
470 # This function is a two-parter. Below is the awk(1) portion of the function,
471 # afterward is the sh(1) function which utilizes the below awk script.
472 #
473 f_index_menusel_command_awk='
474 # Variables that should be defined on the invocation line:
475 #       -v key="keyword"
476 #
477 BEGIN {
478         prefix = "menu_selection=\""
479         plen = length(prefix)
480         found = 0
481 }
482 {
483         if (!match($0, "^" prefix ".*\\|.*\"")) next
484
485         keyword = command = substr($0, plen + 1, RLENGTH - plen - 1)
486         sub(/^.*\|/, "", command)
487         sub(/\|.*$/, "", keyword)
488
489         if ( keyword == key )
490         {
491                 print command
492                 found++
493                 exit
494         }
495 }
496 END { exit ! found }
497 '
498 f_index_menusel_command()
499 {
500         local indexfile="$1" keyword="$2" command
501         local lang="${LANG:-$LC_ALL}"
502
503         f_dprintf "f_index_menusel_command: index=[%s] key=[%s] lang=[%s]" \
504                   "$indexfile" "$keyword" "$lang"
505
506         if [ -f "$indexfile.$lang" ]; then
507                 command=$( awk -v key="$keyword" \
508                                 "$f_index_menusel_command_awk" \
509                                 "$indexfile.$lang" ) || return $FAILURE
510         elif [ -f "$indexfile" ]; then
511                 command=$( awk -v key="$keyword" \
512                                 "$f_index_menusel_command_awk" \
513                                 "$indexfile" ) || return $FAILURE
514         else
515                 return $FAILURE
516         fi
517
518         #
519         # If the command pathname is not fully qualified fix-up/force to be
520         # relative to the $indexfile directory.
521         #
522         case "$command" in
523         /*) : already fully qualified ;;
524         *)
525                 local indexdir="${indexfile%/*}"
526                 [ "$indexdir" != "$indexfile" ] || indexdir="."
527                 command="$indexdir/$command"
528         esac
529
530         echo "$command"
531 }
532
533 ############################################################ MAIN
534
535 #
536 # Trap signals so we can recover gracefully
537 #
538 trap 'f_interrupt' SIGINT
539 trap 'f_die' SIGTERM SIGPIPE SIGXCPU SIGXFSZ \
540              SIGFPE SIGTRAP SIGABRT SIGSEGV
541 trap '' SIGALRM SIGPROF SIGUSR1 SIGUSR2 SIGHUP SIGVTALRM
542
543 #
544 # Clone terminal stdout/stderr so we can redirect to it from within sub-shells
545 #
546 eval exec $TERMINAL_STDOUT_PASSTHRU\>\&1
547 eval exec $TERMINAL_STDERR_PASSTHRU\>\&2
548
549 #
550 # Make debugging persistant if set
551 #
552 [ "$debug" ] && export debug
553
554 #
555 # Truncate the debug file upon initialization (now). Note that we will trim a
556 # leading plus (`+') from the value of debugFile to support persistant meaning
557 # that f_dprintf() should print both to standard output and $debugFile (minus
558 # the leading plus, of course).
559 #
560 _debug_file="${debugFile#+}"
561 if [ "$_debug_file" ]; then
562         if ( umask 022 && :> "$_debug_file" ); then
563                 f_dprintf "Successfully initialized debugFile \`%s'" \
564                           "$_debug_file"
565         else
566                 unset debugFile
567                 f_dprintf "Unable to initialize debugFile \`%s'" \
568                           "$_debug_file"
569         fi
570 fi
571 unset _debug_file
572
573 #
574 # Log our operating environment for debugging purposes
575 #
576 f_dprintf "UNAME_S=[%s] UNAME_P=[%s] UNAME_R=[%s]" \
577           "$UNAME_S" "$UNAME_P" "$UNAME_R"
578
579 f_dprintf "%s: Successfully loaded." common.subr
580
581 fi # ! $_COMMON_SUBR