xref: /freebsd/usr.sbin/bsdconfig/share/sysrc.subr (revision 1f843bc05983a51b6f19350bbc09b1f0b2b34312)
1if [ ! "$_SYSRC_SUBR" ]; then _SYSRC_SUBR=1
2#
3# Copyright (c) 2006-2012 Devin Teske
4# All Rights Reserved.
5#
6# Redistribution and use in source and binary forms, with or without
7# modification, are permitted provided that the following conditions
8# are met:
9# 1. Redistributions of source code must retain the above copyright
10#    notice, this list of conditions and the following disclaimer.
11# 2. Redistributions in binary form must reproduce the above copyright
12#    notice, this list of conditions and the following disclaimer in the
13#    documentation and/or other materials provided with the distribution.
14#
15# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
16# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING BUT NOT LIMITED TO, THE
17# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
18# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
19# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
21# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
22# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
23# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
24# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
25# SUCH DAMAGE.
26#
27# $FreeBSD$
28#
29############################################################ INCLUDES
30
31BSDCFG_SHARE="/usr/share/bsdconfig"
32. $BSDCFG_SHARE/common.subr || exit 1
33
34BSDCFG_LIBE="/usr/libexec/bsdconfig"
35f_include_lang $BSDCFG_LIBE/include/messages.subr
36
37############################################################ CONFIGURATION
38
39#
40# Standard pathnames (inherit values from shell if available)
41#
42: ${RC_DEFAULTS:="/etc/defaults/rc.conf"}
43
44############################################################ GLOBALS
45
46#
47# Global exit status variables
48#
49SUCCESS=0
50FAILURE=1
51
52#
53# Valid characters that can appear in an sh(1) variable name
54#
55# Please note that the character ranges A-Z and a-z should be avoided because
56# these can include accent characters (which are not valid in a variable name).
57# For example, A-Z matches any character that sorts after A but before Z,
58# including A and Z. Although ASCII order would make more sense, that is not
59# how it works.
60#
61VALID_VARNAME_CHARS="0-9ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_"
62
63############################################################ FUNCTIONS
64
65# f_clean_env [ --except $varname ... ]
66#
67# Unset all environment variables in the current scope. An optional list of
68# arguments can be passed, indicating which variables to avoid unsetting; the
69# `--except' is required to enable the exclusion-list as the remainder of
70# positional arguments.
71#
72# Be careful not to call this in a shell that you still expect to perform
73# $PATH expansion in, because this will blow $PATH away. This is best used
74# within a sub-shell block "(...)" or "$(...)" or "`...`".
75#
76f_clean_env()
77{
78	local var arg except=
79
80	#
81	# Should we process an exclusion-list?
82	#
83	if [ "$1" = "--except" ]; then
84		except=1
85		shift 1
86	fi
87
88	#
89	# Loop over a list of variable names from set(1) built-in.
90	#
91	for var in $( set | awk -F= \
92		'/^[[:alpha:]_][[:alnum:]_]*=/ {print $1}' \
93		| grep -v '^except$'
94	); do
95		#
96		# In POSIX bourne-shell, attempting to unset(1) OPTIND results
97		# in "unset: Illegal number:" and causes abrupt termination.
98		#
99		[ "$var" = OPTIND ] && continue
100
101		#
102		# Process the exclusion-list?
103		#
104		if [ "$except" ]; then
105			for arg in "$@" ""; do
106				[ "$var" = "$arg" ] && break
107			done
108			[ "$arg" ] && continue
109		fi
110
111		unset "$var"
112	done
113}
114
115# f_sysrc_get $varname
116#
117# Get a system configuration setting from the collection of system-
118# configuration files (in order: /etc/defaults/rc.conf /etc/rc.conf
119# and /etc/rc.conf).
120#
121# NOTE: Additional shell parameter-expansion formats are supported. For
122# example, passing an argument of "hostname%%.*" (properly quoted) will
123# return the hostname up to (but not including) the first `.' (see sh(1),
124# "Parameter Expansion" for more information on additional formats).
125#
126f_sysrc_get()
127{
128	# Sanity check
129	[ -f "$RC_DEFAULTS" -a -r "$RC_DEFAULTS" ] || return $FAILURE
130
131	# Taint-check variable name
132	case "$1" in
133	[0-9]*)
134		# Don't expand possible positional parameters
135		return $FAILURE;;
136	*)
137		[ "$1" ] || return $FAILURE
138	esac
139
140	( # Execute within sub-shell to protect parent environment
141
142		#
143		# Clear the environment of all variables, preventing the
144		# expansion of normals such as `PS1', `TERM', etc.
145		#
146		f_clean_env --except IFS RC_CONFS RC_DEFAULTS
147
148		. "$RC_DEFAULTS" > /dev/null 2>&1
149
150		unset RC_DEFAULTS
151			# no longer needed
152
153		#
154		# If the query is for `rc_conf_files' then store the value that
155		# we inherited from sourcing RC_DEFAULTS (above) so that we may
156		# conditionally restore this value after source_rc_confs in the
157		# event that RC_CONFS does not customize the value.
158		#
159		if [ "$1" = "rc_conf_files" ]; then
160			_rc_conf_files="$rc_conf_files"
161		fi
162
163		#
164		# If RC_CONFS is defined, set $rc_conf_files to an explicit
165		# value, modifying the default behavior of source_rc_confs().
166		#
167		if [ "${RC_CONFS+set}" ]; then
168			rc_conf_files="$RC_CONFS"
169			_rc_confs_set=1
170		fi
171
172		source_rc_confs > /dev/null 2>&1
173
174		#
175		# If the query was for `rc_conf_files' AND after calling
176		# source_rc_confs the value has not changed, then we should
177		# restore the value to the one inherited from RC_DEFAULTS
178		# before performing the final query (preventing us from
179		# returning what was set via RC_CONFS when the intent was
180		# instead to query the value from the file(s) specified).
181		#
182		if [ "$1" = "rc_conf_files" -a \
183		     "$_rc_confs_set" -a \
184		     "$rc_conf_files" = "$RC_CONFS" \
185		]; then
186			rc_conf_files="$_rc_conf_files"
187			unset _rc_conf_files
188			unset _rc_confs_set
189		fi
190
191		unset RC_CONFS
192			# no longer needed
193
194		#
195		# This must be the last functional line for both the sub-shell
196		# and the function to preserve the return status from formats
197		# such as "${varname?}" and "${varname:?}" (see "Parameter
198		# Expansion" in sh(1) for more information).
199		#
200		eval echo '"${'"$1"'}"' 2> /dev/null
201	)
202}
203
204# f_sysrc_get_default $varname
205#
206# Get a system configuration default setting from the default rc.conf(5) file
207# (or whatever RC_DEFAULTS points at).
208#
209f_sysrc_get_default()
210{
211	# Sanity check
212	[ -f "$RC_DEFAULTS" -a -r "$RC_DEFAULTS" ] || return $FAILURE
213
214	# Taint-check variable name
215	case "$1" in
216	[0-9]*)
217		# Don't expand possible positional parameters
218		return $FAILURE;;
219	*)
220		[ "$1" ] || return $FAILURE
221	esac
222
223	( # Execute within sub-shell to protect parent environment
224
225		#
226		# Clear the environment of all variables, preventing the
227		# expansion of normals such as `PS1', `TERM', etc.
228		#
229		f_clean_env --except RC_DEFAULTS
230
231		. "$RC_DEFAULTS" > /dev/null 2>&1
232
233		unset RC_DEFAULTS
234			# no longer needed
235
236		#
237		# This must be the last functional line for both the sub-shell
238		# and the function to preserve the return status from formats
239		# such as "${varname?}" and "${varname:?}" (see "Parameter
240		# Expansion" in sh(1) for more information).
241		#
242		eval echo '"${'"$1"'}"' 2> /dev/null
243	)
244}
245
246# f_sysrc_find $varname
247#
248# Find which file holds the effective last-assignment to a given variable
249# within the rc.conf(5) file(s).
250#
251# If the variable is found in any of the rc.conf(5) files, the function prints
252# the filename it was found in and then returns success. Otherwise output is
253# NULL and the function returns with error status.
254#
255f_sysrc_find()
256{
257	local varname="${1%%[!$VALID_VARNAME_CHARS]*}"
258	local regex="^[[:space:]]*$varname="
259	local rc_conf_files="$( f_sysrc_get rc_conf_files )"
260	local conf_files=
261	local file
262
263	# Check parameters
264	case "$varname" in
265	""|[0-9]*) return $FAILURE
266	esac
267
268	#
269	# If RC_CONFS is defined, set $rc_conf_files to an explicit
270	# value, modifying the default behavior of source_rc_confs().
271	#
272	[ "${RC_CONFS+set}" ] && rc_conf_files="$RC_CONFS"
273
274	#
275	# Reverse the order of files in rc_conf_files (the boot process sources
276	# these in order, so we will search them in reverse-order to find the
277	# last-assignment -- the one that ultimately effects the environment).
278	#
279	for file in $rc_conf_files; do
280		conf_files="$file${conf_files:+ }$conf_files"
281	done
282
283	#
284	# Append the defaults file (since directives in the defaults file
285	# indeed affect the boot process, we'll want to know when a directive
286	# is found there).
287	#
288	conf_files="$conf_files${conf_files:+ }$RC_DEFAULTS"
289
290	#
291	# Find which file matches assignment to the given variable name.
292	#
293	for file in $conf_files; do
294		[ -f "$file" -a -r "$file" ] || continue
295		if grep -Eq "$regex" $file; then
296			echo $file
297			return $SUCCESS
298		fi
299	done
300
301	return $FAILURE # Not found
302}
303
304# f_sysrc_desc $varname
305#
306# Attempts to return the comments associated with varname from the rc.conf(5)
307# defaults file `/etc/defaults/rc.conf' (or whatever RC_DEFAULTS points to).
308#
309# Multi-line comments are joined together. Results are NULL if no description
310# could be found.
311#
312# This function is a two-parter. Below is the awk(1) portion of the function,
313# afterward is the sh(1) function which utilizes the below awk script.
314#
315f_sysrc_desc_awk='
316# Variables that should be defined on the invocation line:
317# 	-v varname="varname"
318#
319BEGIN {
320	regex = "^[[:space:]]*"varname"="
321	found = 0
322	buffer = ""
323}
324{
325	if ( ! found )
326	{
327		if ( ! match($0, regex) ) next
328
329		found = 1
330		sub(/^[^#]*(#[[:space:]]*)?/, "")
331		buffer = $0
332		next
333	}
334
335	if ( !/^[[:space:]]*#/ ||
336	      /^[[:space:]]*[[:alpha:]_][[:alnum:]_]*=/ ||
337	      /^[[:space:]]*#[[:alpha:]_][[:alnum:]_]*=/ ||
338	      /^[[:space:]]*$/ ) exit
339
340	sub(/(.*#)*[[:space:]]*/, "")
341	buffer = buffer" "$0
342}
343END {
344	# Clean up the buffer
345	sub(/^[[:space:]]*/, "", buffer)
346	sub(/[[:space:]]*$/, "", buffer)
347
348	print buffer
349	exit ! found
350}
351'
352f_sysrc_desc()
353{
354	awk -v varname="$1" "$f_sysrc_desc_awk" < "$RC_DEFAULTS"
355}
356
357# f_sysrc_set $varname $new_value
358#
359# Change a setting in the system configuration files (edits the files in-place
360# to change the value in the last assignment to the variable). If the variable
361# does not appear in the source file, it is appended to the end of the primary
362# system configuration file `/etc/rc.conf'.
363#
364# This function is a two-parter. Below is the awk(1) portion of the function,
365# afterward is the sh(1) function which utilizes the below awk script.
366#
367f_sysrc_set_awk='
368# Variables that should be defined on the invocation line:
369# 	-v varname="varname"
370# 	-v new_value="new_value"
371#
372BEGIN {
373	regex = "^[[:space:]]*"varname"="
374	found = retval = 0
375}
376{
377	# If already found... just spew
378	if ( found ) { print; next }
379
380	# Does this line match an assignment to our variable?
381	if ( ! match($0, regex) ) { print; next }
382
383	# Save important match information
384	found = 1
385	matchlen = RSTART + RLENGTH - 1
386
387	# Store the value text for later munging
388	value = substr($0, matchlen + 1, length($0) - matchlen)
389
390	# Store the first character of the value
391	t1 = t2 = substr(value, 0, 1)
392
393	# Assignment w/ back-ticks, expression, or misc.
394	# We ignore these since we did not generate them
395	#
396	if ( t1 ~ /[`$\\]/ ) { retval = 1; print; next }
397
398	# Assignment w/ single-quoted value
399	else if ( t1 == "'\''" ) {
400		sub(/^'\''[^'\'']*/, "", value)
401		if ( length(value) == 0 ) t2 = ""
402		sub(/^'\''/, "", value)
403	}
404
405	# Assignment w/ double-quoted value
406	else if ( t1 == "\"" ) {
407		sub(/^"(.*\\\\+")*[^"]*/, "", value)
408		if ( length(value) == 0 ) t2 = ""
409		sub(/^"/, "", value)
410	}
411
412	# Assignment w/ non-quoted value
413	else if ( t1 ~ /[^[:space:];]/ ) {
414		t1 = t2 = "\""
415		sub(/^[^[:space:]]*/, "", value)
416	}
417
418	# Null-assignment
419	else if ( t1 ~ /[[:space:];]/ ) { t1 = t2 = "\"" }
420
421	printf "%s%c%s%c%s\n", substr($0, 0, matchlen), \
422		t1, new_value, t2, value
423}
424END { exit retval }
425'
426f_sysrc_set()
427{
428	local varname="$1" new_value="$2"
429
430	# Check arguments
431	[ "$varname" ] || return $FAILURE
432
433	#
434	# Find which rc.conf(5) file contains the last-assignment
435	#
436	local not_found=
437	local file="$( f_sysrc_find "$varname" )"
438	if [ "$file" = "$RC_DEFAULTS" -o ! "$file" ]; then
439		#
440		# We either got a null response (not found) or the variable
441		# was only found in the rc.conf(5) defaults. In either case,
442		# let's instead modify the first file from $rc_conf_files.
443		#
444
445		not_found=1
446
447		#
448		# If RC_CONFS is defined, use $RC_CONFS
449		# rather than $rc_conf_files.
450		#
451		if [ "${RC_CONFS+set}" ]; then
452			file="${RC_CONFS%%[$IFS]*}"
453		else
454			file=$( f_sysrc_get 'rc_conf_files%%[$IFS]*' )
455		fi
456	fi
457
458	#
459	# If not found, append new value to last file and return.
460	#
461	if [ "$not_found" ]; then
462		echo "$varname=\"$new_value\"" >> "$file"
463		return $?
464	fi
465
466	#
467	# Perform sanity checks.
468	#
469	if [ ! -w "$file" ]; then
470		f_err "$msg_cannot_create_permission_denied\n" \
471		      "$pgm" "$file"
472		return $FAILURE
473	fi
474
475	#
476	# Create a new temporary file to write to.
477	#
478	local tmpfile="$( mktemp -t "$pgm" )"
479	[ "$tmpfile" ] || return $FAILURE
480
481	#
482	# Fixup permissions (else we're in for a surprise, as mktemp(1) creates
483	# the temporary file with 0600 permissions, and if we simply mv(1) the
484	# temporary file over the destination, the destination will inherit the
485	# permissions from the temporary file).
486	#
487	local mode
488	mode=$( stat -f '%#Lp' "$file" 2> /dev/null )
489	f_quietly chmod "${mode:-0644}" "$tmpfile"
490
491	#
492	# Fixup ownership. The destination file _is_ writable (we tested
493	# earlier above). However, this will fail if we don't have sufficient
494	# permissions (so we throw stderr into the bit-bucket).
495	#
496	local owner
497	owner=$( stat -f '%u:%g' "$file" 2> /dev/null )
498	f_quietly chown "${owner:-root:wheel}" "$tmpfile"
499
500	#
501	# Operate on the matching file, replacing only the last occurrence.
502	#
503	local new_contents retval
504	new_contents=$( tail -r $file 2> /dev/null )
505	new_contents=$( echo "$new_contents" | awk -v varname="$varname" \
506		-v new_value="$new_value" "$f_sysrc_set_awk" )
507	retval=$?
508
509	#
510	# Write the temporary file contents.
511	#
512	echo "$new_contents" | tail -r > "$tmpfile" || return $FAILURE
513	if [ $retval -ne $SUCCESS ]; then
514		echo "$varname=\"$new_value\"" >> "$tmpfile"
515	fi
516
517	#
518	# Taint-check our results.
519	#
520	if ! /bin/sh -n "$tmpfile"; then
521		f_err "$msg_previous_syntax_errors\n" "$pgm" "$file"
522		rm -f "$tmpfile"
523		return $FAILURE
524	fi
525
526	#
527	# Finally, move the temporary file into place.
528	#
529	mv "$tmpfile" "$file"
530}
531
532# f_sysrc_delete $varname
533#
534# Remove a setting from the system configuration files (edits files in-place).
535# Deletes all assignments to the given variable in all config files. If the
536# `-f file' option is passed, the removal is restricted to only those files
537# specified, otherwise the system collection of rc_conf_files is used.
538#
539# This function is a two-parter. Below is the awk(1) portion of the function,
540# afterward is the sh(1) function which utilizes the below awk script.
541#
542f_sysrc_delete_awk='
543# Variables that should be defined on the invocation line:
544# 	-v varname="varname"
545#
546BEGIN {
547	regex = "^[[:space:]]*"varname"="
548	found = 0
549}
550{
551	if ( $0 ~ regex )
552		found = 1
553	else
554		print
555}
556END { exit ! found }
557'
558f_sysrc_delete()
559{
560	local varname="$1"
561	local file
562
563	# Check arguments
564	[ "$varname" ] || return $FAILURE
565
566	#
567	# Operate on each of the specified files
568	#
569	for file in ${RC_CONFS-$( f_sysrc_get rc_conf_files )}; do
570		[ -e "$file" ] || continue
571
572		#
573		# Create a new temporary file to write to.
574		#
575		local tmpfile="$( mktemp -t "$pgm" )"
576		[ "$tmpfile" ] || return $FAILURE
577
578		#
579		# Fixup permissions and ownership (mktemp(1) defaults to 0600
580		# permissions) to instead match the destination file.
581		#
582		local mode owner
583		mode=$( stat -f '%#Lp' "$file" 2> /dev/null )
584		owner=$( stat -f '%u:%g' "$file" 2> /dev/null )
585		f_quietly chmod "${mode:-0644}" "$tmpfile"
586		f_quietly chown "${owner:-root:wheel}" "$tmpfile"
587
588		#
589		# Operate on the file, removing all occurrences, saving the
590		# output in our temporary file.
591		#
592		awk -v varname="$varname" "$f_sysrc_delete_awk" "$file" \
593			> "$tmpfile"
594		if [ $? -ne $SUCCESS ]; then
595			# The file didn't contain any assignments
596			rm -f "$tmpfile"
597			continue
598		fi
599
600		#
601		# Taint-check our results.
602		#
603		if ! /bin/sh -n "$tmpfile"; then
604			f_err "$msg_previous_syntax_errors\n" \
605			      "$pgm" "$file"
606			rm -f "$tmpfile"
607			return $FAILURE
608		fi
609
610		#
611		# Perform sanity checks
612		#
613		if [ ! -w "$file" ]; then
614			f_err "$msg_permission_denied\n" "$pgm" "$file"
615			rm -f "$tmpfile"
616			return $FAILURE
617		fi
618
619		#
620		# Finally, move the temporary file into place.
621		#
622		mv "$tmpfile" "$file"
623	done
624}
625
626fi # ! $_SYSRC_SUBR
627