Scott M. Mcdermott

UNIX Systems & Network Administrator
available for contract or salaried positions

rcinit

#!/bin/bash
#
# Set up shell variables from an optional rcfile, with
# override capability from the command line, supporting both
# long and short options.
#
# USAGE:
#
#       0. "include rc" from the source file to get these functions
#       1. call rcset() for each option to be available in rcfile or on CL
#       2. run rcinit() passing $@ and (optionally) with $RCFILE set
#       3. optionally call rc_export_all() to set them for export
#       4. optionally call rcsetusage() and add fourth arg to rcset()
#       5. optionally call rcusage() to generate usage from (4)
#
#       (all other functions herein are private)
#
# EXAMPLE:
#
#   in script do:
#
#       rcset   n newest_only           0
#       rcset   z no_stdout             1
#       rcset   f filename              :/path/to/file
#       rcset   o outfile               :
#
#   makes options available:
#
#       short '-n', long '--newest-only', rcfile 'newest_only'
#
#               - variable name $cf_newest_only in script
#               - defaults to 0 if not given
#               - sets value to 1 if specified
#               - will not allow set besides 0 or 1 in rcfile
#               - will not allow set at all on command line
#
#       short '-f', long '--filename', rcfile 'filename'
#
#               - variable name $cf_filename in script
#               - defaults to "/path/to/file" if not given
#               - override default with string if specified
#               - will not allow set at all on command line
#               - requires argument to option on command line
#
#       short '-o', long '--outfile', rcfile 'outfile'
#
#               - variable name $cf_outfile in script
#               - default value null/empty (will be set)
#               - override default with string if specified
#               - requires argument to option on command line
#
# NOTES:
#
#    - provide additional arguments to rcset() for usage info
#    - use $RCPREFIX to change variable name in script
#    - does not work with args containing spaces or commas
#    - rcfile and command line parsing should use same code
#

require bomb
require get_invocation_name
require strip_file_stdout

RCPREFIX=${RCPREFIX:-'cf_'} # default variable name prefix

##############################################################################

rc_get_varname ()
{
        local idx=$1
        local var=$RCPREFIX

        var=${RCPREFIX}
        var=${var}${LIBSH_RC_LONGS[idx]}
        var=${var%:}
        var=${var//-/_} # command line uses dash, rcfile uses underscore

        printf $var
}

rc_get_index ()
{
        local what=$1

        local -i idx
        local -i nopts=$LIBSH_RC_NUMOPTS

        for ((idx = 0; idx < nopts; idx++))
        do
                if [[ $what == "-${LIBSH_RC_SHORTS[idx]%:}" ||
                      $what == "--${LIBSH_RC_LONGS[idx]%:}" ]]
                then
                        printf $idx
                        return 0
                fi
        done

        return 1
}

rc_get_longest_option_length ()
{
        local i
        local longest

        local nopts=$LIBSH_RC_NUMOPTS

        for ((i = 0; i < nopts; i++)); do
                var=${LIBSH_RC_LONGS[i]%:}
                ((longest < ${#var})) && longest=${#var}
        done

        printf %u $longest
}

rc_set_defaults ()
{
        local nopts=$LIBSH_RC_NUMOPTS
        local i

        for ((i = 0; i < nopts; i++)); do
                var=`rc_get_varname $i`
                val=${LIBSH_RC_DEFAULTS[i]}
                eval export $var=$val
        done
}

rc_parse_clopts ()
{
        local args
        local var

        local -i idx
        local -i nopts=$LIBSH_RC_NUMOPTS

        rc_getshorts_nullsep () { local IFS=; printf "${LIBSH_RC_SHORTS[*]}"; }
        rc_getlongs_commasep () { local IFS=,; printf "${LIBSH_RC_LONGS[*]}"; }

        if ! args=$(getopt                                      \
                --name          $(get_invocation_name)          \
                --options       $(rc_getshorts_nullsep)         \
                --long          $(rc_getlongs_commasep)         \
                --                                              \
                "$@"
        ); then
                errstr="argument parse failure"

                if ((LIBSH_RC_USAGE_SET))
                then warn "$errstr"; rcusage_exit 1
                else bomb "$errstr"
                fi
        fi

        # iterate through options prepared by getopt(1)
        #
        eval set -- "$args"
        while true
        do
                # test for end of options, which getopt(1)
                # *always* outputs for us, so this serves as
                # a reliable stop-loop condition
                #
                [[ $1 == '--' ]] && {
                        shift
                        break
                }

                # if we haven't reached the end yet, go
                # through each of our configured options
                # from rcset() and see if this is it
                #
                idx=$(rc_get_index $1) ||
                        bomb "$1: impossible no index"

                # this is our option, prepare variable name
                #
                var=`rc_get_varname $idx`

                # see if rcset() told us that this option
                # had an argument
                #
                if ((LIBSH_RC_HASARG[idx]))
                then
                        # if so, shift it off the list and
                        # get the value ready for set in the
                        # environment
                        #
                        shift
                        val="$1"
                else
                        # otherwise, invert the already set
                        # default (it's a flag, presence of
                        # which inverts the rcset()
                        # specified default)
                        #
                        val=$((var ^ 1))
                fi

                [[ $var && $val ]] ||
                        bomb "var set error"

                # we now have variable name and value, so
                # instantiate in the caller's environment
                #
                eval export $var="$val"

                # XXX maybe support comma-separated lists
                # that get inserted into an array instead?
                # would work from calling code since $array
                # is the same as ${array[0]}, but arrays
                # cannot be exported, a significant
                # limitation
                #
                #eval $var="(${val/,/\ })"

                # now move on to the next one
                #
                shift
        done

        # remaining arguments are returned to the caller in
        # an array, so it doesn't have to use a subshell to
        # get them via stdout, which would mean bomb()
        # herein wouldn't really exit (lame bash limitation)
        #
        RCARGS=("$@")
}

# - takes filename as argument
# - reads data from file into variables (with RCPREFIX)
# - configure recognized variables with rcset()
# - bombs on unknown variables
# - supports blank lines and comments
# - no support (yet) for multival, multiword or multiline
#
rc_parse_rcfile ()
{
        local var val idx

        # do nothing if user didn't set the variable for
        # rcfile location (expanded and passed to us)
        #
        [[ $1 ]] || return 0

        # rcfile just contains var val pairs, which we set
        # in the environment (prepending varname prefix)
        # after stripping out decoration
        #
        while read var val
        do
                [[ $var && $val ]] ||
                        bomb "${var:-empty}: bad syntax"

                idx=$(rc_get_index "--${var//_/-}") ||
                        bomb "$var: unknown option specified"

                ((LIBSH_RC_HASARG[idx])) ||
                        [[ $val == 0 || $val == 1 ]] ||
                                bomb "$var: is a flag option"

                eval export ${RCPREFIX}${var}="$val"

        done <<< "$(strip_file_stdout $1)"
}

##############################################################################

# Each invocation sets up a new shell script variable with
# optional default.  After setting these up, call rcinit()
# with the script argument vector.
#
# arg1 (char)
#
#       - command-line short option
#
# arg2 (string)
#
#       - exact name of option in rcfile
#       - is command-line long option after underscore -> dash
#       - instantiates variable by this name in script (prefixed)
#
# arg3
#
#       - first char ':' then takes an argument (is not a flag)
#       - otherwise does not take an argument (is a flag)
#       - no such thing as "optional" arguments, either does or not
#       - default value follows (non-optional for flags)
#       - if no default specified for opt with arg, default null
#       - flags (don't require arg) *must* have defaults
#
# arg4
#
#       - string containing the description, for usage information
#
rcset ()
{
        local short=$1
        local long=$2
        local value=$3

        local -a details=($4)
        local    detail="${details[@]}"

        (($# == 3 || $# == 4)) ||
                bomb "bad usage"

        # long options use dashes instead of underscores by
        # convention, but we expect the caller to use
        # underscores, since they aren't valid variable
        # names
        #
        long=${long//_/-}

        # start where we left off last call, and increment
        # the counter for total number of seen options
        # (which will be index+1, so use postfix)
        #
        local -i i=$((LIBSH_RC_NUMOPTS++))

        if [[ ${value:0:1} == ':' ]]
        then
                # these will end up passed to getopt(1),
                # which takes a trailing colon to indicate
                # required arguments
                #
                short+=:
                long+=:

                # later we'll want to know if we should
                # shift off an argument from the vector
                #
                LIBSH_RC_HASARG[i]=1
        else
                # if the option does not take an argument,
                # then it must give a boolean describing its
                # default value, which the flag will invert
                # if set
                #
                [[ $value == 0 || $value == 1 ]] ||
                        bomb "-$short: must give 0 or 1 (default to invert)"
        fi

        LIBSH_RC_SHORTS[i]=$short
        LIBSH_RC_LONGS[i]=$long
        LIBSH_RC_DEFAULTS[i]=${value#:}
        LIBSH_RC_DETAILS[i]="$detail"
}

# set name, synopsis, and long description for later
# automatic generation of usage data using rcusage()
#
rcsetusage ()
{
        (($# == 3)) ||
                bomb "must specify all usage parameters"

        LIBSH_RC_NAME=$1
        LIBSH_RC_SUMMARY=($2)

        # remaining args are blank-line separated paragraphs
        # comprising the description
        #
        local IFS=$'\n'
        LIBSH_RC_DESCRIPTION=($(
                gawk \
                        --re-interval \
                        --assign RS='\n{2,}' \
                '{
                        gsub("\n", " ");
                        printf("%s\n", $0);
                }' \
                <<< "$3"
        ))

        LIBSH_RC_USAGE_SET=1
}

# user calls us with (1) optional $RCFILE set to a
# name/value pair file and also (2) passes in the argument
# vector, which we parse to override defaults set with
# rcset() and in $RCFILE
#
rcinit ()
{
        ((LIBSH_RC_NUMOPTS)) ||
                bomb "rcset() must be called prior to rcinit()"

        rc_set_defaults
        rc_parse_rcfile $RCFILE
        rc_parse_clopts "$@"

        rc_unexport_all # default is not to export
}

# call this to generate usage information on stdout from the
# options configured with rcset()
#
rcusage ()
{
        local indent=4
        local maxwidth=78
        local maxindented=$((maxwidth - indent))

        ###

        # print a string, wrapped and indented
        #
        usage_string ()
        {
                local -i i
                local +i s
                local indent=$((indent - 1)) # 'echo' adds a space

                for ((i = 0; i < indent; i++))
                do s+=$'\x20'
                done

                echo "$s" $@ |
                fmt -uw $maxindented
        }

        # emit all options, one per line, with defaults,
        # indented
        #
        usage_emit_options ()
        {
                local nopts=$LIBSH_RC_NUMOPTS

                local -i i

                local +i short long

                for ((i = 0; i < nopts; i++))
                do
                        short=${LIBSH_RC_SHORTS[i]%:}

                        if ((LIBSH_RC_HASARG[i]))
                        then long="${LIBSH_RC_LONGS[i]%:}=<arg>"
                        else long="${LIBSH_RC_LONGS[i]%:}"
                        fi

                        default=${LIBSH_RC_DEFAULTS[i]}

                        printf -- "-%c, --%s" $short $long
                        [[ $default ]] && printf -- "\t(%s)" $default
                        printf \\n

                done | {
                        column -ts $'\t' |
                        pr -To $indent
                }
        }

        usage_emit_description ()
        {
                local i
                local ndescs=${#LIBSH_RC_DESCRIPTION[@]}

                for ((i = 0; i < ndescs; i++)); do
                        printf "%s\n\n" \
                                "$(usage_string "${LIBSH_RC_DESCRIPTION[i]}")"
                done
        }

        usage_emit_details ()
        {
                local var detail
                local i

                local nopts=$LIBSH_RC_NUMOPTS
                local longest=`rc_get_longest_option_length`

                for ((i = 0; i < nopts; i++))
                do
                        var=${LIBSH_RC_LONGS[i]%:}
                        var=${var//-/_}
                        detail="${LIBSH_RC_DETAILS[i]}"

                        # if the descriptions make the lines
                        # too long, format them differently
                        #
                        if (((indent + longest + ${#detail} + 3) > maxwidth))
                        then
                                printf --                       \
                                        "\n%s: %s\n"            \
                                        $var "$detail"          |
                                fmt -utw $maxindented           |
                                pr -To $indent
                        else
                                printf --                       \
                                        "%$((longest+1))s %s\n" \
                                        ${var}: "$detail"       |
                                fmt -utw $maxindented           |
                                pr -To $indent
                        fi
                done
        }

        ###

        cat <<- HERE
        $LIBSH_RC_NAME:

                $(usage_string "${LIBSH_RC_SUMMARY[@]}")

        HERE
        ((LIBSH_RC_NUMOPTS)) && cat <<-HERE
        USAGE

                $(usage_emit_options)

        HERE
        ((LIBSH_RC_NUMOPTS)) && cat <<- HERE
        OPTIONS

                $(usage_emit_details)

        HERE

        # this one is always displayed, but might be the last one
        cat <<- HERE
        DESCRIPTION

                $(usage_emit_description)
        HERE
        ((LIBSH_RC_NUMOPTS)) && cat <<- HERE

        RCFILE

                $(usage_string "
                        Specify the options in var val
                        format, one per line, in an rcfile,
                        default \"${RCFILE:-'none'}\",
                        which can be specified in \$RCFILE.
                        Comments are '#' until end of line,
                        and blank lines are ignored.  The
                        variable names correspond to long
                        options, but with underscores
                        instead of dashes.
                ")
        HERE
}

rcusage_exit ()
{
        rcusage
        exit ${1:-0}
}

# mark all rcset() variables as exported
# XXX TODO how do we handle this once we use arrays?
# XXX this is unnecessary; already exported at set-time
#
rc_export_all ()
{
        export $(compgen -A variable $RCPREFIX)
}

# this only works because we already exported them, so the
# subshell can generate a list
#
rc_unexport_all ()
{
        export -n $(compgen -A variable $RCPREFIX)
}
# vim:syn=sh:ft=sh