sysrc -- a sysctl(8)-like utility for managing /etc/rc.conf et. al.

Garrett Cooper gcooper at FreeBSD.org
Sat Oct 9 20:25:56 UTC 2010


On Wed, Oct 6, 2010 at 8:29 PM, Devin Teske <dteske at vicor.com> wrote:
>
> On Oct 6, 2010, at 4:09 PM, Brandon Gooch wrote:
>
>> On Wed, Oct 6, 2010 at 3:45 PM, Devin Teske <dteske at vicor.com> wrote:
>>> Hello fellow freebsd-hackers,
>>>
>>> Long-time hacker, first-time poster.
>>>
>>> I'd like to share a shell script that I wrote for FreeBSD system
>>> administration.
>>>
>>
>> It seems the list ate the attachment :(
>
>
> Here she is ^_^ Comments welcome.

Hah. More nuclear reactor than bikeshed :D!

> #!/bin/sh
> # -*- tab-width:  4 -*- ;; Emacs
> # vi: set tabstop=4     :: Vi/ViM

> #
> # Default setting whether to dump a list of internal dependencies upon exit
> #
> : ${SYSRC_SHOW_DEPS:=0}
>
> ############################################################ GLOBALS
>
> # Global exit status variables
> : ${SUCCESS:=0}
> : ${FAILURE:=1}

Should this really be set to something other than 0 or 1 by the
end-user's environment? This would simplify a lot of return/exit
calls...

> #
> # Program name
> #
> progname="${0##*/}"
>
> #
> # Options
> #
> SHOW_EQUALS=
> SHOW_NAME=1
>
> # Reserved for internal use
> _depend=

When documenting arguments passed to functions, I usually do something like:

# 1 - a var
# 2 - another var
#
# ... etc

because it's easier to follow for me at least.

Various spots in the codebase have differing styles though (and it
would be better to follow the style in /etc/rc.subr, et all for
consistency, because this tool is a consumer of those APIs).

> ############################################################ 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.
> #
> : dependency checks performed after depend-function declaration
> : function ; fprintf ( ) # $fd $fmt [ $opts ... ]

Dumb question. Does declaring `: dependency checks performed after
depend-function declaration' and `: function' buy you anything other
than readability via comments with syntax checking?

> {
>        local fd=$1
>        [ $# -gt 1 ] || return ${FAILURE-1}

While working at IronPort, Doug (my tech lead) has convinced me that
constructs like:

if [ $# -le 1 ]
then
    return ${FAILURE-1}
fi

Are a little more consistent and easier to follow than:

[ $# -gt 1 ] || return ${FAILURE-1}

Because some folks have a tendency to chain shell expressions, i.e.

expr1 || expr2 && expr3

Instead of:

if expr1 || expr2
then
    expr3
fi

or...

if ! expr1
then
    expr2
fi
if [ $? -eq 0 ]
then
    expr3
fi

I've caught myself chaining 3 expressions together, and I broke that
down into a simpler (but more longhand format), but I've caught people
chaining 4+ expressions together, which just becomes unmanageable to
follow (and sometimes bugs creep in because of operator ordering and
expression evaluation and subshells, etc, but that's another topic for
another time :)..).

>        shift 1
>        printf "$@" >&$fd
> }
>
> # eprintf $fmt [ $opts ... ]
> #
> # Print a message to stderr (fd=2).
> #
> : dependency checks performed after depend-function declaration
> : function ; eprintf ( ) # $fmt [ $opts ... ]
> {
>        fprintf 2 "$@"
> }
>
> # show_deps
> #
> # Print the current list of dependencies.
> #
> : dependency checks performed after depend-function declaration
> : function ; show_deps ( ) #
> {
>        if [ "$SYSRC_SHOW_DEPS" = "1" ]; then
>                eprintf "Running internal dependency list:\n"
>
>                local d
>                for d in $_depend; do
>                        eprintf "\t%-15ss%s\n" "$d" "$( type "$d" )"

The command(1) -v builtin is more portable than the type(1) builtin
for command existence lookups (it just doesn't tell you what the
particular item is that you're dealing with like type(1) does).

I just learned that it also handles other builtin lexicon like if,
for, while, then, do, done, etc on FreeBSD at least; POSIX also
declares that it needs to support that though, so I think it's a safe
assumption to state that command -v will provide you with what you
need.

>                done
>        fi
> }
>
> # die [ $err_msg ... ]
> #
> # Optionally print a message to stderr before exiting with failure status.
> #
> : dependency checks performed after depend-function declaration
> : function ; die ( ) # [ $err_msg ... ]
> {
>        local fmt="$1"
>        [ $# -gt 0 ] && shift 1
>        [  "$fmt"  ] && eprintf "$fmt\n" "$@"

"x$fmt" != x ? It seems like it could be simplified to:

if [ $# -gt 0 ]
then
    local fmt=$1
    shift 1
    eprintf "$fmt\n" "$@"
fi

>        show_deps
>        exit ${FAILURE-1}
> }
>
> # have $anything
> #
> # Used for dependency calculations. Arguments are passed to the `type' built-in
> # to determine if a given command is available (either as a shell built-in or
> # as an external binary). The return status is true if the given argument is
> # for an existing built-in or executable, otherwise false.
> #
> # This is a convenient method for building dependency lists and it aids in the
> # readability of a script. For example,
> #
> #       Example 1: have sed || die "sed is missing"
> #       Example 2: if have awk; then
> #                       # We have awk...
> #                  else
> #                       # We DON'T have awk...
> #                  fi
> #       Example 3: have reboot && reboot
> #
> : dependency checks performed after depend-function declaration
> : function ; have ( ) # $anything
> {
>        type "$@" > /dev/null 2>&1
> }
>
> # depend $name [ $dependency ... ]
> #
> # Add a dependency. Die with error if dependency is not met.
> #
> : dependency checks performed after depend-function declaration
> : function ; depend ( ) # $name [ $dependency ... ]
> {
>        local by="$1" arg
>        shift 1
>
>        for arg in "$@"; do
>                local d

Wouldn't it be better to declare this outside of the loop (I'm not
sure how optimal it is to place it inside the loop)?

>                for d in $_depend ""; do
>                        [ "$d" = "$arg" ] && break
>                done
>                if [ ! "$d" ]; then

Could you make this ` "x$d" = x ' instead?

>                        have "$arg" || die \
>                                "%s: Missing dependency '%s' required by %s" \
>                                "${progname:-$0}" "$arg" "$by"

The $0 substitution is unnecessary based on how you set progname above:

$ foo=yadda
$ echo ${foo##*/}
yadda
$ foo=yadda/badda/bing/bang
$ echo ${foo##*/}
bang

>                        _depend="$_depend${_depend:+ }$arg"
>                fi
>        done
> }
>
> #
> # Perform dependency calculations for above rudimentary functions.
> # NOTE: Beyond this point, use the depend-function BEFORE dependency-use
> #
> depend fprintf   'local' '[' 'return' 'shift' 'printf'
> depend eprintf   'fprintf'
> depend show_deps 'if' '[' 'then' 'eprintf' 'local' 'for' 'do' 'done' 'fi'
> depend die       'local' '[' 'shift' 'eprintf' 'show_deps' 'exit'
> depend have      'local' 'type' 'return'
> depend depend    'local' 'shift' 'for' 'do' '[' 'break' 'done' 'if' 'then' \
>                 'have' 'die' 'fi'

I'd say that you have bigger fish to try if your shell lacks the
needed lexicon to parse built-ins like for, do, local, etc :)...

> # usage
> #
> # Prints a short syntax statement and exits.
> #
> depend usage 'local' 'eprintf' 'die'
> : function ; usage ( ) #
> {
>        local optfmt="\t%-12s%s\n"
>        local envfmt="\t%-22s%s\n"
>
>        eprintf "Usage: %s [OPTIONS] name[=value] ...\n" "${progname:-$0}"
>
>        eprintf "OPTIONS:\n"
>        eprintf "$optfmt" "-h --help" \
>                "Print this message to stderr and exit."
>        eprintf "$optfmt" "-d" \
>                "Print list of internal dependencies before exit."
>        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" "-n" \
>                "Show only variable values, not their names."
>        eprintf "\n"
>
>        eprintf "ENVIRONMENT:\n"
>        eprintf "$envfmt" "SYSRC_SHOW_DEPS" \
>                "Dump list of dependencies. Must be zero or one"
>        eprintf "$envfmt" "" \
>                "(default: \`0')"
>        eprintf "$envfmt" "RC_DEFAULTS" \
>                "Location of \`/etc/defaults/rc.conf' file."
>
>        die
> }
>
> # sysrc $setting
> #
> # 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).
> #
> # Examples:
> #
> #       sysrc sshd_enable
> #               returns YES or NO
> #       sysrc defaultrouter
> #               returns IP address of default router (if configured)
> #       sysrc 'hostname%%.*'
> #               returns $hostname up to (but not including) first `.'
> #       sysrc 'network_interfaces%%[$IFS]*'
> #               returns first word of $network_interfaces
> #       sysrc 'ntpdate_flags##*[$IFS]'
> #               returns last word of $ntpdate_flags (time server address)
> #       sysrc usbd_flags-"default"
> #               returns $usbd_flags or "default" if unset
> #       sysrc usbd_flags:-"default"
> #               returns $usbd_flags or "default" if unset or NULL
> #       sysrc cloned_interfaces+"alternate"
> #               returns "alternate" if $cloned_interfaces is set
> #       sysrc cloned_interfaces:+"alternate"
> #               returns "alternate" if $cloned_interfaces is set and non-NULL
> #       sysrc '#kern_securelevel'
> #               returns length in characters of $kern_securelevel
> #       sysrc 'hostname?'
> #               returns NULL and error status 2 if $hostname is unset (or if
> #               set, returns the value of $hostname with no error status)
> #       sysrc 'hostname:?'
> #               returns NULL and error status 2 if $hostname is unset or NULL
> #               (or if set and non-NULL, returns value without error status)
> #

I would probably just point someone to a shell manual, as available
options and behavior may change, and behavior shouldn't (but
potentially could) vary between versions of FreeBSD.

> depend sysrc 'local' '[' 'return' '.' 'have' 'eval' 'echo'
> : function ; sysrc ( ) # $varname
> {
>        : ${RC_DEFAULTS:="/etc/defaults/rc.conf"}
>
>        local defaults="$RC_DEFAULTS"
>        local varname="$1"
>
>        # Check arguments
>        [ -r "$defaults" ] || return
>        [ "$varname" ] || return
>
>        ( # Execute within sub-shell to protect parent environment
>                [ -f "$defaults" -a -r "$defaults" ] && . "$defaults"
>                have source_rc_confs && source_rc_confs
>                eval echo '"${'"$varname"'}"' 2> /dev/null
>        )
> }
>
> # ... | 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
> #
> depend lrev 'local' 'if' '[' 'then' 'while' 'do' 'shift' 'done' 'else' 'read' \
>            'fi' 'echo'
> : function ; lrev ( ) # $file ...
> {
>        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 $setting $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'.
> #
> depend sysrc_set 'local' 'sysrc' '[' 'return' 'for' 'do' 'done' 'if' 'have' \
>                 'then' 'else' 'while' 'read' 'case' 'esac' 'fi' 'break' \
>                 'eprintf' 'echo' 'lrev'
> : function ; sysrc_set ( ) # $varname $new_value
> {
>        local rc_conf_files="$( sysrc rc_conf_files )"
>        local varname="$1" new_value="$2"

IIRC I've run into issues doing something similar to this in the past,
so I broke up the local declarations on 2+ lines.

>        local file conf_files=
>
>        # Check arguments
>        [ "$rc_conf_files" ] || return ${FAILURE-1}
>        [ "$varname" ] || return ${FAILURE-1}
>

Why not just do...

if [ "x$rc_conf_files" = x -o "x$varname" = x ]
then
    return ${FAILURE-1}
fi

...?

>        # Reverse the order of files in rc_conf_files
>        for file in $rc_conf_files; do
>                conf_files="$file${conf_files:+ }$conf_files"
>        done
>
>        #
>        # Determine which file we are to operate on. If no files match, we'll
>        # simply append to the last file in the list (`/etc/rc.conf').
>        #
>        local found=
>        local regex="^[[:space:]]*$varname="
>        for file in $conf_files; do
>                #if have grep; then
>                if false; then
>                        grep -q "$regex" $file && found=1

Probably want to redirect stderr for the grep output to /dev/null, or
test for the file's existence first, because rc_conf_files doesn't
check for whether or not the file exists which would result in noise
from your script:

$ . /etc/defaults/rc.conf
$ echo $rc_conf_files
/etc/rc.conf /etc/rc.conf.local
$ grep -q foo /etc/rc.local
grep: /etc/rc.local: No such file or directory

>                else
>                        while read LINE; do \
>                                case "$LINE" in \
>                                $varname=*) found=1;; \
>                                esac; \
>                        done < $file
>                fi
>                [ "$found" ] && break
>        done
>
>        #
>        # Perform sanity checks.
>        #
>        if [ ! -w $file ]; then
>                eprintf "\n%s: cannot create %s: permission denied\n" \

Being pedantic, I would capitalize the P in permission to match
EACCES's output string.

>                        "${progname:-$0}" "$file"
>                return ${FAILURE-1}
>        fi
>
>        #
>        # If not found, append new value to last file and return.
>        #
>        if [ ! "$found" ]; then
>                echo "$varname=\"$new_value\"" >> $file
>                return ${SUCCESS-0}
>        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
>                        #if have grep; then
>                        if false; then
>                                match="$( echo "$LINE" | grep "$regex" )"
>                        else
>                                case "$LINE" in
>                                $varname=*) match=1;;
>                                         *) match=;;
>                                esac
>                        fi
>                        if [ "$match" ]; then
>                                LINE="$varname"'="'"$new_value"'"'
>                                found=1
>                        fi
>                fi
>                echo "$LINE"
>          done
>        ) | lrev`"
>
>        [ "$new_contents" ] \
>                && echo "$new_contents" > $file

What if this write fails, or worse, 2+ people were modifying the file
using different means at the same time? You could potentially
lose/corrupt your data and your system is potentially hosed, is it
not? Why not write the contents out to a [sort of?] temporary file
(even $progname.$$ would suffice probably, but that would have
potential security implications so mktemp(1) might be the way to go),
then move the temporary file to $file? You might also want to use
lockf to lock the file.

> }
>
> ############################################################ MAIN SOURCE
>
> #
> # Perform sanity checks
> #
> depend main '[' 'usage'
> [ $# -gt 0 ] || usage
>
> #
> # Process command-line options
> #
> depend main 'while' '[' 'do' 'case' 'usage' 'eprintf' \
>            'break' 'esac' 'shift' 'done'
> while [ $# -gt 0 ]; do

Why not just use the getopts shell built-in and shift $(( $OPTIND - 1
)) at the end?

>        case "$1" in
>        -h|--help) usage;;
>        -d) SYSRC_SHOW_DEPS=1;;
>        -e) SHOW_EQUALS=1;;
>        -n) SHOW_NAME=;;
>        -*) eprintf "%s: unrecognized option \`$1'\n" "${progname:-$0}"
>            usage;;
>         *) # Since it is impossible (in many shells, including bourne, c,
>            # tennex-c, and bourne-again) to name a variable beginning with a
>            # dash/hyphen [-], we will terminate the option-list at the first
>            # item that doesn't begin with a dash.
>            break;;
>        esac
>        shift 1
> done
> [ "$SHOW_NAME" ] || SHOW_EQUALS=
>
> #
> # Process command-line arguments
> #
> depend main '[' 'while' 'do' 'case' 'echo' 'sysrc' 'if' 'sysrc_set' 'then' \
>            'fi' 'esac' 'shift' 'done'
> SEP=': '
> [ "$SHOW_EQUALS" ] && SEP='="'
> while [ $# -gt 0 ]; do
>        NAME="${1%%=*}"
>        case "$1" in
>        *=*)
>                echo -n "${SHOW_NAME:+$NAME$SEP}$(
>                         sysrc "$1" )${SHOW_EQUALS:+\"}"
>                if sysrc_set "$NAME" "${1#*=}"; then
>                        echo " -> $( sysrc "$NAME" )"
>                fi

What happens if this set fails :)? It would be confusing to end users
if you print out the value (and they expected it to be set), but it
failed for some particular reason.

>                ;;
>        *)
>                if ! IGNORED="$( sysrc "$NAME?" )"; then
>                        echo "${progname:-$0}: unknown variable '$NAME'"
>                else
>                        echo "${SHOW_NAME:+$NAME$SEP}$(
>                              sysrc "$1" )${SHOW_EQUALS:+\"}"

    Not sure if it's a gmail screwup or not, but is there supposed to
be a newline between `$(' and `sysrc' ?
    And now some more important questions:

    1. What if I do: sysrc PS1 :) (hint: variables inherited from the
shell really shouldn't end up in the output / be queried)?
    2. Could you add an analog for sysctl -a and sysctl -n ?
    3. There are some more complicated scenarios that unfortunately
this might not pass when setting variables (concerns that come to mind
deal with user-set $rc_conf_files where values could be spread out
amongst different rc.conf's, and where more complicated shell syntax
would become a slippery slope for this utility, because one of the
lesser used features within rc.conf is that it's nothing more than
sourceable bourne shell script :)...). I would definitely test the
following scenarios:

#/etc/rc.conf-1:
foo=baz

#/etc/rc.conf-2:
foo=bar

#/etc/rc.conf-3:
foo="$foo zanzibar"

Scenario A:

#/etc/rc.conf:
rc_conf_files="/etc/rc.conf-1 /etc/rc.conf-2"

    The value of foo should be set to bar; ideally the value of foo in
/etc/rc.conf-2 should be set to a new value by the end user.

Scenario B:

#/etc/rc.conf:
rc_conf_files="/etc/rc.conf-2 /etc/rc.conf-1"

    The value of foo should be set to baz; ideally the value of foo in
/etc/rc.conf-1 should be set to a new value by the end user.

Scenario C:

#/etc/rc.conf:
rc_conf_files="/etc/rc.conf-1 /etc/rc.conf-2 /etc/rc.conf-3"

    The value of foo should be set to `bar zanzibar'; ideally the
value of foo in /etc/rc.conf-3 should be set to a new value by the end
user (but that will affect the expected output potentially).

Scenario D:

#/etc/rc.conf:
rc_conf_files="/etc/rc.conf-2 /etc/rc.conf-1 /etc/rc.conf-3"

    The value of foo should be set to `baz zanzibar'; ideally the
value of foo in /etc/rc.conf-3 should be set to a new value by the end
user (but that will affect the expected output potentially).

    I'll probably think up some more scenarios later that should be
tested... the easy way out is to state that the tool does a best
effort at overwriting the last evaluated value.
    Overall, awesome looking tool and I'll be happy to test it
Thanks!
-Garrett


More information about the freebsd-hackers mailing list