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

Mario Lobo mlobo at digiart.art.br
Sat Oct 9 22:26:07 UTC 2010


Forgive me for this top posting but I don't want to risk ruining the perfect 
flow of Garret's analysis. 

I read through it as if I was in a classroom lecture.

I have a special folder in kmail called "FreeBSD Tops" where I put mail that 
makes me discover something unknown to me about FBSD, and this one is marked 
as important.

Thank you both, Devin and Garret!. 

-- 
Mario Lobo
http://www.mallavoodoo.com.br
FreeBSD since 2.2.8 [not Pro-Audio.... YET!!] (99% winfoes FREE)


On Saturday 09 October 2010 17:25:55 Garrett Cooper wrote:
> 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
> _______________________________________________
> freebsd-hackers at freebsd.org mailing list
> http://lists.freebsd.org/mailman/listinfo/freebsd-hackers
> To unsubscribe, send any mail to "freebsd-hackers-unsubscribe at freebsd.org"


More information about the freebsd-hackers mailing list