sysrc(8) -- a sysctl(8)-like utility for managing rc.conf(5)

Devin Teske devin.teske at fisglobal.com
Tue Oct 12 23:27:51 UTC 2010


Hey all,

Long-time user, first-time poster (to this list at least).

Over on the -hackers@ mailing-list, I posted the first version of a
script I've written named sysrc(8), designed to ease management of the
rc.conf(5) files. There were a lot of great discussions about this over
on -hackers@: http://lists.freebsd.org/pipermail/freebsd-hackers/2010-
October/thread.html

I'm now happy to post the second version of this script which attempts
to address all of the concerns that -hackers@ brought-up.

Behold... sysrc(8) v2.0

NOTE: just scroll down a little to the INFORMATION section for an easy-
to-read usage statement.

#!/bin/sh
# -*- tab-width:  4 -*- ;; Emacs
# vi: set tabstop=4     :: Vi/ViM
#
# Revision: 2.0
# Last Modified: October 12th, 2010
############################################################ COPYRIGHT
#
# (c)2010. Devin Teske. All Rights Reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#
# AUTHOR      DATE      DESCRIPTION
# dteske   2010.10.12   Updates per freebsd-hackers thread.
# dteske   2010.09.29   Initial version.
#
############################################################ INFORMATION
#
# Command Usage:
#
#   sysrc [OPTIONS] name[=value] ...
#
#   OPTIONS:
#   	-h         Print this message to stderr and exit.
#   	-f file    Operate on the specified file(s) instead of rc_conf_files.
#   	-a         Dump a list of non-default configuration variables.
#   	-A         Dump a list of all configuration variables (incl. defaults).
#   	-d         Print a description of the given variable.
#   	-e         Print query results as `var=value' (useful for producing
#   	           output to be fed back in). Ignored if -n is specified.
#   	-v         Verbose. Print the pathname of the specific rc.conf(5)
#   	           file where the directive was found.
#   	-i         Ignore unknown variables.
#   	-n         Show only variable values, not their names.
#   	-N         Show only variable names, not their values.
# 
#   ENVIRONMENT:
#   	RC_DEFAULTS      Location of `/etc/defaults/rc.conf' file.
#   	SYSRC_VERBOSE    Default verbosity. Set to non-NULL to enable.
#
############################################################ CONFIGURATION

#
# Default verbosity.
#
: ${SYSRC_VERBOSE:=}

#
# Default location of the rc.conf(5) defaults configuration file.
#
: ${RC_DEFAULTS:="/etc/defaults/rc.conf"}

############################################################ GLOBALS

#
# Global exit status variables
#
SUCCESS=0
FAILURE=1

#
# Program name
#
progname="${0##*/}"

#
# Options
#
DESCRIBE=
RC_CONFS=
IGNORE_UNKNOWNS=
SHOW_ALL=
SHOW_EQUALS=
SHOW_NAME=1
SHOW_VALUE=1

############################################################ FUNCTION

# fprintf $fd $fmt [ $opts ... ]
#
# Like printf, except allows you to print to a specific file-descriptor. Useful
# for printing to stderr (fd=2) or some other known file-descriptor.
#
fprintf()
{
	local fd=$1
	[ $# -gt 1 ] || return $FAILURE
	shift 1
	printf "$@" >&$fd
}

# eprintf $fmt [ $opts ... ]
#
# Print a message to stderr (fd=2).
#
eprintf()
{
	fprintf 2 "$@"
}

# die [ $fmt [ $opts ... ]]
#
# Optionally print a message to stderr before exiting with failure status.
#
die()
{
	local fmt="$1"
	[ $# -gt 0 ] && shift 1
	[  "$fmt"  ] && eprintf "$fmt\n" "$@"

	exit $FAILURE
}

# usage
#
# Prints a short syntax statement and exits.
#
usage()
{
	local optfmt="\t%-11s%s\n"
	local envfmt="\t%-17s%s\n"

	eprintf "Usage: %s [OPTIONS] name[=value] ...\n" "$progname"

	eprintf "OPTIONS:\n"
	eprintf "$optfmt" "-h" \
	        "Print this message to stderr and exit."
	eprintf "$optfmt" "-f file" \
	        "Operate on the specified file(s) instead of rc_conf_files."
	eprintf "$optfmt" "-a" \
	        "Dump a list of non-default configuration variables."
	eprintf "$optfmt" "-A" \
	        "Dump a list of all configuration variables (incl. defaults)."
	eprintf "$optfmt" "-d" \
	        "Print a description of the given variable."
	eprintf "$optfmt" "-e" \
	        "Print query results as \`var=value' (useful for producing"
	eprintf "$optfmt" "" \
	        "output to be fed back in). Ignored if -n is specified."
	eprintf "$optfmt" "-v" \
	        "Verbose. Print the pathname of the specific rc.conf(5)"
	eprintf "$optfmt" "" \
	        "file where the directive was found."
	eprintf "$optfmt" "-i" \
	        "Ignore unknown variables."
	eprintf "$optfmt" "-n" \
	        "Show only variable values, not their names."
	eprintf "$optfmt" "-N" \
	        "Show only variable names, not their values."
	eprintf "\n"

	eprintf "ENVIRONMENT:\n"
	eprintf "$envfmt" "RC_DEFAULTS" \
	        "Location of \`/etc/defaults/rc.conf' file."
	eprintf "$envfmt" "SYSRC_VERBOSE" \
	        "Default verbosity. Set to non-NULL to enable."

	die
}

# clean_env [ --except $varname ... ]
#
# Unset all environment variables in the current scope. An optional list of
# arguments can be passed, indicating which variables to avoid unsetting; the
# `--except' is required to enabled the exclusion-list as the remainder of
# positional arguments.
#
# Be careful not to call this in a shell that you still except to perform
# $PATH expansion in, because this will blow $PATH away. This is best used
# within a sub-shell block "(...)" or "$(...)" or "`...`".
#
clean_env()
{
	local var arg except=

	#
	# Should we process an exclusion-list?
	#
	if [ "$1" = "--except" ]; then
		except=1
		shift 1
	fi

	#
	# Loop over a list of variable names from set(1) built-in.
	#
	for var in $( set | awk -F= \
		'/^[[:alpha:]_][[:alnum:]_]*=/ {print $1}' \
		| grep -v '^except$'
	); do
		#
		# In POSIX bourne-shell, attempting to unset(1) OPTIND results
		# in "unset: Illegal number:" and causes abrupt termination.
		#
		[ "$var" = OPTIND ] && continue

		#
		# Process the exclusion-list?
		#
		if [ "$except" ]; then
			for arg in "$@" ""; do
				[ "$var" = "$arg" ] && break
			done
			[ "$arg" ] && continue
		fi

		unset "$var"
	done
}

# sysrc_get $varname
#
# Get a system configuration setting from the collection of system-
# configuration files (in order: /etc/defaults/rc.conf /etc/rc.conf
# and /etc/rc.conf).
#
# NOTE: Additional shell parameter-expansion formats are supported. For
# example, passing an argument of "hostname%%.*" (properly quoted) will
# return the hostname up to (but not including) the first `.' (see sh(1),
# "Parameter Expansion" for more information on additional formats).
#
sysrc_get()
{
	# Sanity check
	[ -f "$RC_DEFAULTS" -a -r "$RC_DEFAULTS" ] || return $FAILURE

	# Taint-check variable name
	case "$1" in
	[0-9]*)
		# Don't expand possible positional parameters
		return $FAILURE;;
	*)
		[ "$1" ] || return $FAILURE
	esac

	( # Execute within sub-shell to protect parent environment

		#
		# Clear the environment of all variables, preventing the
		# expansion of normals such as `PS1', `TERM', etc.
		#
		clean_env --except RC_CONFS RC_DEFAULTS

		. "$RC_DEFAULTS"

		#
		# If `-f file' was passed, set $rc_conf_files to an explicit
		# value, modifying the default behavior of source_rc_confs().
		#
		[ "$RC_CONFS" ] && rc_conf_files="$RC_CONFS"

		unset RC_CONFS
			# no longer needed

		source_rc_confs

		#
		# This must be the last functional line for both the sub-shell,
		# and the function to preserve the return status from formats
		# such as "${varname?}" and "${varname:?}" (see "Parameter
		# Expansion" in sh(1) for more information).
		#
		eval echo '"${'"$1"'}"' 2> /dev/null
	)
}

# sysrc_find $varname
#
# Find which file holds the effective last-assignment to a given variable
# within the rc.conf(5) file(s).
#
# If the variable is found in any of the rc.conf(5) files, the function prints
# the filename it was found in and then returns success. Otherwise output is
# NULL and the function returns with error status.
#
sysrc_find()
{
	local varname="$1"
	local rc_conf_files="$( sysrc_get rc_conf_files )"
	local conf_files=
	local file

	# Check parameters
	[ "$varname" ] || return $FAILURE

	#
	# If `-f file' was passed, set $rc_conf_files to an explicit
	# value, modifying the default behavior of source_rc_confs().
	#
	[ "$RC_CONFS" ] && rc_conf_files="$RC_CONFS"

	#
	# Reverse the order of files in rc_conf_files (the boot process sources
	# these in order, so we will search them in reverse-order to find the
	# last-assignment -- the one that ultimately effects the environment).
	#
	for file in $rc_conf_files; do
		conf_files="$file${conf_files:+ }$conf_files"
	done

	#
	# Append the defaults file (since directives in the defaults file
	# indeed affect the boot process, we'll want to know when a directive
	# is found there).
	#
	conf_files="$conf_files${conf_files:+ }$RC_DEFAULTS"

	#
	# Find which file matches assignment to the given variable name.
	#
	for file in $conf_files; do
		if grep -q "^[[:space:]]*$varname=" $file; then
			echo $file
			return $SUCCESS
		fi
	done

	return $FAILURE # Not found
}

# ... | lrev
# lrev $file ...
#
# Reverse lines of input. Unlike rev(1) which reverses the ordering of
# characters on a single line, this function instead reverses the line
# sequencing.
#
# For example, the following input:
#
# 	Line 1
# 	Line 2
# 	Line 3
#
# Becomes reversed in the following manner:
#
# 	Line 3
# 	Line 2
# 	Line 1
#
lrev()
{
	local stdin_rev=
	if [ $# -gt 0 ]; then
		#
		# Reverse lines from files passed as positional arguments.
		#
		while [ $# -gt 0 ]; do
			local file="$1"
			[ -f "$file" ] && lrev < "$file"
			shift 1
		done
	else
		#
		# Reverse lines from standard input
		#
		while read -r LINE; do
			stdin_rev="$LINE
$stdin_rev"
		done
	fi

	echo -n "$stdin_rev"
}

# sysrc_set $varname $new_value
#
# Change a setting in the system configuration files (edits the files in-place
# to change the value in the last assignment to the variable). If the variable
# does not appear in the source file, it is appended to the end of the primary
# system configuration file `/etc/rc.conf'.
#
sysrc_set()
{
	local varname="$1" new_value="$2"

	# Check arguments
	[ "$varname" ] || return $FAILURE

	#
	# Find which rc.conf(5) file contains the last-assignment
	#
	local not_found=
	local file="$( sysrc_find "$varname" )"
	if [ "$file" = "$RC_DEFAULTS" -o ! "$file" ]; then
		#
		# We either got a null response (not found) or the variable
		# was only found in the rc.conf(5) defaults. In either case,
		# let's instead modify the first file from $rc_conf_files.
		#

		not_found=1

		#
		# If `-f file' was passed, use $RC_CONFS
		# rather than $rc_conf_files.
		#
		if [ "$RC_CONFS" ]; then
			file="${RC_CONFS%%[$IFS]*}"
		else
			file="$( sysrc_get "rc_conf_files%%[$IFS]*" )"
		fi
	fi

	#
	# Perform sanity checks.
	#
	if [ ! -w $file ]; then
		eprintf "\n%s: cannot create %s: Permission denied\n" \
		        "$progname" "$file"
		return $FAILURE
	fi

	#
	# If not found, append new value to last file and return.
	#
	if [ "$not_found" ]; then
		echo "$varname=\"$new_value\"" >> "$file"
		return $SUCCESS
	fi

	#
	# Operate on the matching file, replacing only the last occurrence.
	#
	local new_contents="`lrev $file 2> /dev/null | \
	( found=
	  while read -r LINE; do
	  	if [ ! "$found" ]; then
	  		match="$( echo "$LINE" | grep "$regex" )"
	  		if [ "$match" ]; then
	  			LINE="$varname"'="'"$new_value"'"'
	  			found=1
	  		fi
	  	fi
	  	echo "$LINE"
	  done
	) | lrev`"

	[ "$new_contents" ] || return $FAILURE

	#
	# Create a new temporary file to write to.
	#
	local tmpfile="$( mktemp -t "$progname" )"
	[ "$tmpfile" ] || return $FAILURE

	#
	# Fixup permissions (else we're in for a surprise, as mktemp(1) creates
	# the temporary file with 0600 permissions, and if we simply mv(1) the
	# temporary file over the destination, the destination will inherit the
	# permissions from the temporary file).
	#
	chmod $( stat -f '%#Lp' "$file" ) "$tmpfile" 2> /dev/null

	#
	# Fixup ownerhsip. The destination file _is_ writable (we tested
	# earlier above). However, this will fail if we don't have sufficient
	# permissions (so we throw stderr into the bit-bucket).
	#
	chown $( stat -f '%u:%g' "$file" ) "$tmpfile" 2> /dev/null

	#
	# Write the temporary file contents and move it into place.
	#
	echo "$new_contents" > "$tmpfile" || return $FAILURE
	mv "$tmpfile" "$file"
}

# sysrc_desc $varname
#
# Attempts to return the comments associated with varname from the rc.conf(5)
# defaults file `/etc/defaults/rc.conf' (or whatever RC_DEFAULTS points to).
#
# Multi-line comments are joined together. Results are NULL if no description
# could be found.
#
sysrc_desc()
{
	local varname="$1"

	(
		buffer=
		while read LINE; do
			case "$LINE" in
			$varname=*)
				buffer="$LINE"
				break
			esac
		done

		# Return if the variable wasn't found
		[ "$buffer" ] || return $FAILURE

		regex='[[:alpha:]_][[:alnum:]_]*='
		while read LINE; do
			#
			# Stop reading comments if we reach a new assignment
			# directive or if the line contains only whitespace
			#
			echo "$LINE" | grep -q "^[[:space:]]*$regex" && break
			echo "$LINE" | grep -q "^[[:space:]]*#$regex" && break
			echo "$LINE" | grep -q "^[[:space:]]*$" && break

			# Append new line to buffer
			buffer="$buffer
$LINE"
		done

		# Return if the buffer is empty
		[ "$buffer" ] || return $FAILURE

		#
		# Clean up the buffer.
		#
		regex='^[^#]*\(#[[:space:]]*\)\{0,1\}'
		buffer="$( echo "$buffer" | sed -e "s/$regex//" )"
		buffer="$( echo "$buffer" | tr '\n' ' ' \
			| sed -e 's/^[[:space:]]*//;s/[[:space:]]*$//' )"

		echo "$buffer"

	) < "$RC_DEFAULTS"
}

############################################################ MAIN SOURCE

#
# Perform sanity checks
#
[ $# -gt 0 ] || usage

#
# Process command-line options
#
while getopts hf:aAdevinN flag; do
	case "$flag" in
	h) usage;;
	f) RC_CONFS="$OPTARG";;
	a) SHOW_ALL=1;;
	A) SHOW_ALL=2;;
	d) DESCRIBE=1;;
	e) SHOW_EQUALS=1;;
	v) SYSRC_VERBOSE=1;;
	i) IGNORE_UNKNOWNS=1;;
	n) SHOW_NAME=;;
	N) SHOW_VALUE=;;
	\?) usage;;
	esac
done
shift $(( $OPTIND - 1 ))

#
# Process command-line options
#
SEP=': '
[ "$SHOW_EQUALS" ] && SEP='="'
[ "$SHOW_NAME" ] || SHOW_EQUALS=
[ "$SYSRC_VERBOSE" = "0" ] && SYSRC_VERBOSE=
if [ ! "$SHOW_VALUE" ]; then
	SHOW_NAME=1
	SHOW_EQUALS=
fi

if [ "$SHOW_ALL" ]; then
	#
	# Get a list of variables that are currently set in the rc.conf(5)
	# files (included `/etc/defaults/rc.conf') by performing a call to
	# source_rc_confs() in a clean environment.
	#
	(
		#
		# Set which variables we want to preserve in the environment.
		# Append the pipe-character (|) to the list of internal field
		# separation (IFS) characters, allowing us to use the below
		# list both as an extended grep (-E) pattern and argument list
		# (required to first get clean_env() to preserve these in the
		# environment and then later to prune them from the list of
		# variables produced by set(1)).
		#
		IFS="$IFS|"
		EXCEPT="IFS|EXCEPT|PATH|RC_DEFAULTS|OPTIND|DESCRIBE|SEP"
		EXCEPT="$EXCEPT|SHOW_ALL|SHOW_EQUALS|SHOW_NAME|SHOW_VALUE"
		EXCEPT="$EXCEPT|SYSRC_VERBOSE|RC_CONFS"

		#
		# Clean the environment (except for our required variables)
		# and then source the required files.
		#
		clean_env --except $EXCEPT
		if [ -f "$RC_DEFAULTS" -a -r "$RC_DEFAULTS" ]; then
			. "$RC_DEFAULTS"

			#
			# If passed `-a' (rather than `-A'), re-purge the
			# environment, removing the rc.conf(5) defaults.
			#
			[ "$SHOW_ALL" = "1" ] \
				&& clean_env --except rc_conf_files $EXCEPT

			#
			# If `-f file' was passed, set $rc_conf_files to an
			# explicit value, modifying the default behavior of
			# source_rc_confs().
			#
			[ "$RC_CONFS" ] && rc_conf_files="$RC_CONFS"

			source_rc_confs

			#
			# If passed `-a' (rather than `-A'), remove
			# `rc_conf_files' unless it was defined somewhere
			# other than rc.conf(5) defaults.
			#
			[ "$SHOW_ALL" = "1" -a \
			  "$( sysrc_find rc_conf_files )" = "$RC_DEFAULTS" \
			] \
			&& unset rc_conf_files
		fi

		for NAME in $( set | awk -F= \
			'/^[[:alpha:]_][[:alnum:]_]*=/ {print $1}' \
			| grep -Ev "^($EXCEPT)$"
		); do
			#
			# If enabled, describe rather than expand value
			#
			if [ "$DESCRIBE" ]; then
				echo "$NAME: $( sysrc_desc "$NAME" )"
				continue
			fi

			[ "$SYSRC_VERBOSE" ] && \
				echo -n "$( sysrc_find "$NAME" ): "

			#
			# If `-N' is passed, simplify the output
			#
			if [ ! "$SHOW_VALUE" ]; then
				echo "$NAME"
				continue
			fi

			echo "${SHOW_NAME:+$NAME$SEP}$(
			      sysrc_get "$NAME" )${SHOW_EQUALS:+\"}"
		done
	)

	#
	# Ignore the remainder of positional arguments.
	#
	exit $SUCCESS
fi

#
# Process command-line arguments
#
while [ $# -gt 0 ]; do
	NAME="${1%%=*}"

	[ "$DESCRIBE" ] && \
		echo "$NAME: $( sysrc_desc "$NAME" )"

	case "$1" in
	*=*)
		#
		# Like sysctl(8), if both `-d' AND "name=value" is passed,
		# first describe, then attempt to set
		#

		if [ "$SYSRC_VERBOSE" ]; then
			file="$( sysrc_find "$NAME" )"
			[ "$file" = "$RC_DEFAULTS" -o ! "$file" ] && \
				file="$( sysrc_get "rc_conf_files%%[$IFS]*" )"
			echo -n "$file: "
		fi

		#
		# If `-N' is passed, simplify the output
		#
		if [ ! "$SHOW_VALUE" ]; then
			echo "$NAME"
			sysrc_set "$NAME" "${1#*}"
		else
			echo -n "${SHOW_NAME:+$NAME$SEP}$(
			         sysrc_get "$NAME" )${SHOW_EQUALS:+\"}"
			if sysrc_set "$NAME" "${1#*=}"; then
				echo " -> $( sysrc_get "$NAME" )"
			fi
		fi
		;;
	*)
		if ! IGNORED="$( sysrc_get "$NAME?" )"; then
			[ "$IGNORE_UNKNOWNS" ] \
				|| echo "$progname: unknown variable '$NAME'"
			shift 1
			continue
		fi

		#
		# Like sysctl(8), when `-d' is passed,
		# desribe it rather than expanding it
		#

		if [ "$DESCRIBE" ]; then
			shift 1
			continue
		fi

		[ "$SYSRC_VERBOSE" ] && \
			echo -n "$( sysrc_find "$NAME" ): "

		#
		# If `-N' is passed, simplify the output
		#
		if [ ! "$SHOW_VALUE" ]; then
			echo "$NAME"
		else
			echo "${SHOW_NAME:+$NAME$SEP}$(
			      sysrc_get "$NAME" )${SHOW_EQUALS:+\"}"
		fi
	esac
	shift 1
done



-- 
Cheers,
Devin Teske

-> CONTACT INFORMATION <-
Business Solutions Consultant II
FIS - fisglobal.com
510-735-5650 Mobile
510-621-2038 Office
510-621-2020 Office Fax
909-477-4578 Home/Fax
devin.teske at fisglobal.com

-> LEGAL DISCLAIMER <-
This message  contains confidential  and proprietary  information
of the sender,  and is intended only for the person(s) to whom it
is addressed. Any use, distribution, copying or disclosure by any
other person  is strictly prohibited.  If you have  received this
message in error,  please notify  the e-mail sender  immediately,
and delete the original message without making a copy.

-> FUN STUFF <-
-----BEGIN GEEK CODE BLOCK-----
Version 3.1
GAT/CS d(+) s: a- C++(++++) UB++++$ P++(++++) L++(++++) !E--- W++ N? o? K- w O
M+ V- PS+ PE Y+ PGP- t(+) 5? X+(++) R>++ tv(+) b+(++) DI+(++) D(+) G+>++ e>+ h
r>++ y+ 
------END GEEK CODE BLOCK------
http://www.geekcode.com/

-> END TRANSMISSION <-

_____________

The information contained in this message is proprietary and/or confidential. If you are not the intended recipient, please: (i) delete the message and all copies; (ii) do not disclose, distribute or use the message in any manner; and (iii) notify the sender immediately. In addition, please be aware that any message addressed to our domain is subject to archiving and review by persons other than the intended recipient. Thank you.
_____________


More information about the freebsd-rc mailing list