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