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