Skip to content

Instantly share code, notes, and snippets.

@hercynium
Created November 16, 2012 17:34
Show Gist options
  • Select an option

  • Save hercynium/4089250 to your computer and use it in GitHub Desktop.

Select an option

Save hercynium/4089250 to your computer and use it in GitHub Desktop.

Revisions

  1. hercynium revised this gist Nov 16, 2012. 2 changed files with 114 additions and 28 deletions.
    6 changes: 4 additions & 2 deletions bash-getopt-example.sh
    Original file line number Diff line number Diff line change
    @@ -5,9 +5,11 @@ set -o pipefail

    source /bath/to/bash-getopt

    foo_dflt="wibble"

    bash-getopt "$@" <<END_OPTS
    FOO=f|foo:NAME "$foo" "this is a description for the option. it can span
    FOO=f|foo:NAME "$foo_dflt" "this is a description for the option. it can span
    multiple lines. foo has a single : so it requires a value when used."
    BAR=b|bar::PATH "" "bar has :: after it, so passing a value is optional"
    @@ -24,5 +26,5 @@ echo "[$FOO] [$BAR] [$ZIP]"
    for a in "${ARGV[@]}"; do echo "[$a]"; done

    # a function is created to display usage info:
    usage
    _usage

    136 changes: 110 additions & 26 deletions gistfile1.sh
    Original file line number Diff line number Diff line change
    @@ -9,11 +9,12 @@
    #
    # Source this file from any bash script, then process your options like this:
    #
    # # assume $foo is set to some sane default earlier in the script
    # # assume $foo_dflt is set to some sane default earlier in the script
    # bash-getopt "$@" <<END_OPTS
    #
    # FOO=f|foo:NAME "$foo" "this is a description for the option. it can span
    # multiple lines. foo has a single : so it requires a value when used."
    # FOO=f|foo:NAME "$foo_dflt" "this is a description for the option. it can
    # span multiple lines. foo has a single : so a value is required when
    # the option is used"
    #
    # BAR=b|bar::PATH "" "bar has :: after it, so passing a value is optional"
    #
    @@ -33,16 +34,23 @@
    # STDERR and the script will exit. -h and --help options are automatically
    # generated and display help on STDOUT when used.
    #
    # The behavior/semantics of the option processing is identical to GNU getopt,
    # The behavior/semantics of the option processing is identical* to GNU getopt,
    # as that is actually what is called to finally do the option processing.
    #
    # * identical except for one thing. a when GNU getopt gets the value of a
    # short-opt, like -f, it does something annoying. -f'wibble' and -fwibble
    # do what you expect, but -f="wibble" gets the value "=wibble" which is
    # *not* what I expect. There is code in here to "fix" this, so you do get
    # "wibble" from -f="wibble", even though it's not 100% identical behavior.
    #
    # COMPATIBILITY:
    #
    # This has currently been tested and is known to work on the following
    # OS/Bash/getopt configurations:
    #
    # Mac OS X 10.7.5, bash 3.2.48, GNU getopt (enhanced) 1.1.4 (from homebrew)
    # CentOS 6.3, bash 4.1.2, GNU getopt (enhanced) 1.1.4
    # CentOS 5.5, bash 3.2.25, GNU getopt (enhanced) 1.1.4
    #
    # It will not work with the BSD-style getopt shipped with Mac OS X. You'll
    # need to install GNU getopt and put it in your path before the system
    @@ -89,7 +97,9 @@
    #
    # boolean/flag options do not have a field for setting a default because
    # it's simply not necessary. If the option was used, the value is 1. If
    # the option was not used, the value is the empty string, "".
    # the option was not used, the value is the empty string, "". Note that
    # the variable still gets created and set, so you can check it when the
    # nounset shell option is in effect.
    #
    # If a default or description field will contain spaces, it must be quoted.
    # Standard shell quoting rules apply. Because of the way the definitions are
    @@ -109,21 +119,28 @@
    #
    # FOO=f|foo:NAME
    # | | | | |
    # var name ---+ | | | |
    # short name ----+ | | |
    # long name --------+ | |
    # type indicator -----+ |
    # value unit -----------+
    # var name ---+ | | | | required
    # short name ----+ | | | optional*
    # long name --------+ | | optional*
    # type indicator -----+ | can be :,:: or not present
    # value unit -----------+ optional, only valid if type indicator is present
    #
    # * you may omit a short name or long name, but never both.
    # if one is omitted, the | separator must also be omitted.
    #
    # In this case, the value of this option will be assigned to a variable named
    # $FOO, you can use this option with either -f or --foo, the type indicates
    # that this is a value-required option, and in the help text the value will
    # referred to as NAME.
    #
    # Var Name:
    #
    # The variable name can be composed of any characters that are typically
    # valid for a variable name, [_0-9a-zA-Z]. The variable name is required
    # because making it optional just doesn't seem worth the effort and code.
    #
    # Short and Long Names:
    #
    # The short and long names for the option are separated by a pipe ("|").
    # There can only be one of each type of name for the option but you can omit
    # one or the other if you wish (examples: FOO=f:NAME or FOO=foo:NAME)
    @@ -135,20 +152,32 @@
    # recognized as valid in an option name, [-_0-9a-zA-Z] but it must begin
    # with a letter or number and must not end with a dash ("-")
    #
    # Type Indicator
    #
    # The type indicator can be zero, one, or two colons (":"). The indicator
    # has the same semantics as it does with GNU getopt: A single colon indicates
    # that if the option is used, a value *must* be supplied. Two indicates that
    # if the option is used, a value *may* be supplied. No colons indicate that
    # the option is a boolean flag, and using it results in a value of "1" and
    # not using it results in a value of the empty string ("").
    #
    # Value Unit:
    #
    # The "value unit" is used when showing usage or help text to indicate to the
    # user what the value represents. For example, the usage will show this for
    # foo: "--foo=NAME". The value unit can only be specified when the option
    # type is one that takes a value; It can't be used with boolean/flag options
    # because it makes no sense. (ok, very *little* sense). The value unit is
    # optional. Simply omit it if you do not want one. (example: FOO=f|foo:)
    #
    # ADDITIONAL NOTES
    #
    # If there is an environment var named DEBUG present and it is set to a true
    # value (anything but 0,'', and unset), various bits of debugging info will
    # be displayed. This can come in handy when filing a bug report, or if you
    # just plain like that sort of thing.
    #
    #
    ################################################################################

    # this file should only ever be sourced.
    @@ -201,8 +230,11 @@ warn () {
    }

    # returns true if all arguments evaluate to true.
    # if no arguments, returns false.
    is-true () {
    # (eg, not 0 or ''). if any arguments are false, returns
    # false. if no arguments are supplied, returns false.
    # (therefore, this function considers an empty arglist
    # to be false)
    all-true () {
    local rc=1
    while [[ $# -gt 0 ]]; do
    case "$1" in
    @@ -213,9 +245,14 @@ is-true () {
    return $rc
    }

    # same as all-true, but for a single argument (or no arguments)
    is-true () { [[ $# -gt 0 ]] || return 1; all-true "$1"; }

    # returns true if all arguments evaluate to false.
    # if no arguments, returns true
    is-false () {
    # (eg, 0 or ''). if any arguments are true, returns false.
    # if no arguments are supplied, returns true (therefore,
    # this function considers an empty arglist to be false)
    all-false () {
    local rc=0
    while [[ $# -gt 0 ]]; do
    case "$1" in
    @@ -226,14 +263,20 @@ is-false () {
    return $rc
    }

    # same as all-false but for a single argument (or no arguments)
    is-false () { [[ $# -gt 0 ]] || return 0; all-false "$1"; }


    # equivalient to warn if $DEBUG is true.
    debug () {
    is-false "${DEBUG-}" || warn "$@"
    }

    # returns true if all arguments are integers.
    # if no arguments, returns false
    is-int () {
    # returns false if any argument is not an integer.
    # if no arguments supplied, returns false (because
    # nothing is not an integer)
    all-int () {
    local rc=1
    while [[ $# -gt 0 ]]; do
    if ! printf '%i' "$1" &>/dev/null; then
    @@ -247,23 +290,37 @@ is-int () {
    return $rc
    }

    # Just like perl's croak, report an error from the caller and exit. The exit
    # code will be one of the following (in order of precedence)
    # - if the first argument is an integer, use that.
    # - if the return code of the last command was not 0, use that.
    # same as all-int but for a single argument (or no arguments)
    is-int () { all-int "$1"; }


    # Just like perl's croak, report an error from the caller and exit with
    # a code indicating an error. The exit code will be one of the following
    # (in order of precedence):
    # - the value of the first argument, if it is an integer.
    # - the return code of the last command, it it was not 0.
    # - 1
    # You can pass as many arguments to this as you want and they will
    # be output as text to STDERR. You can also pipe in text as well.
    # If the first argument is an integer, it will not be printed, but
    # will be used as the exit code.
    croak () {
    local rc=$?
    [[ $rc -ne 0 ]] || rc=1
    if is-int "$1"; then
    rc="$1"
    shift
    fi
    read line sub file <<< $(caller 1)
    # NOTE: I should check to see if the $(caller 1) should be quoted.
    # the cat with heredoc is there to fix vim's broken syntax hilighting.
    read line sub file <<< $(caller 1); cat <<FIXVIM >/dev/null
    <
    FIXVIM
    warn "ERROR [$file:$line]: $@"
    exit $rc
    }


    # returns true if the first argument matches any of the subsequent arguments
    in-list () {
    local want="$1"
    @@ -275,12 +332,36 @@ in-list () {
    }

    # this version avoids calling external programs, and also supports multi-line
    # option definitions
    # option definitions.
    # This is the main function that users will use.
    #
    # The arguments to this function are eventually passed to GNU getopt for
    # processing just as you would normally use getopt. However, there's a
    # mechanism to pass options to this function that are *not* processed by
    # getopt, and instead have other effects. Currently, any of these extra
    # options are just spliced into the call to GNU getopt, but this function
    # might someday have a few options of its own.
    #
    # Normally, you would just call bash-getopt like this (just imagine the option
    # specs are there): bash-getopt "$@"
    #
    # But when an error occurs, getopt's error message says "getopt: error ..."
    #
    # If you want that error to look like it came from your program, you can call
    # bash-getopt like this: bash-getopt -n "$0" -- "$@"
    #
    # Note that the program options are separated from the getopt options by a --.
    # This is the same thing getopt itself does, so I think it makes sense here.
    #
    bash-getopt () {

    # if there's a -- in the arglist, everything before should be
    # passed-thru as an arg to getopt
    local -a go_opts=("") # init the array to avoid errors under nounset
    ## if there's a -- in the arglist, everything before will be used as
    ## an option for this function or passed-thru as an option for GNU
    ## getopt. Everything after the -- is an option to be processed.

    # collect opts for GNU getopt in this array, initializing it
    # with a single null string to avoid errors under nounset
    local -a go_opts=("")
    if in-list '--' "$@"; then
    for (( x=0; x<$#; x++ )); do
    if [[ "$1" == '--' ]]; then
    @@ -291,9 +372,12 @@ bash-getopt () {
    done
    fi

    # the remaining options are the ones we want to parse
    # the remaining options are the ones we want to process
    local -a opts=( "$@" )

    # there currently aren't any options for this function but if there were,
    # we'd find them by processing ${go_opts[@]} here.

    # these vars will collect pieces of info: short option names, long names,
    # case statement clauses, and variable initializations
    local go_short='' go_long='' go_cases='' go_vars=''
  2. hercynium created this gist Nov 16, 2012.
    28 changes: 28 additions & 0 deletions bash-getopt-example.sh
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,28 @@
    #!/bin/bash
    set -o errexit
    set -o nounset
    set -o pipefail

    source /bath/to/bash-getopt

    bash-getopt "$@" <<END_OPTS
    FOO=f|foo:NAME "$foo" "this is a description for the option. it can span
    multiple lines. foo has a single : so it requires a value when used."
    BAR=b|bar::PATH "" "bar has :: after it, so passing a value is optional"
    ZIP=z|zip "zip has no :, so it is a boolean flag. flag options only need
    a description; the default is *always* the empty string"
    END_OPTS

    # option values are in the specified vars
    echo "[$FOO] [$BAR] [$ZIP]"

    # leftover/positional args are in an ARGV array
    for a in "${ARGV[@]}"; do echo "[$a]"; done

    # a function is created to display usage info:
    usage

    489 changes: 489 additions & 0 deletions gistfile1.sh
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,489 @@
    # vim: set ts=4 sw=4 tw=0 ft=sh et :
    ################################################################################
    #
    # NAME: bash-getopt (aka better-bash-getopt)
    #
    # This is (IMO) a better way to do option processing from bash.
    #
    # USAGE:
    #
    # Source this file from any bash script, then process your options like this:
    #
    # # assume $foo is set to some sane default earlier in the script
    # bash-getopt "$@" <<END_OPTS
    #
    # FOO=f|foo:NAME "$foo" "this is a description for the option. it can span
    # multiple lines. foo has a single : so it requires a value when used."
    #
    # BAR=b|bar::PATH "" "bar has :: after it, so passing a value is optional"
    #
    # ZIP=z|zip "zip has no :, so it is a boolean flag. flag options only need
    # a description; the default is *always* the empty string"
    #
    # END_OPTS
    #
    # Upon successful processing of the above options, the calling script will
    # have the variables $FOO, $BAR, and $ZIP available. Any positional arguments
    # found will be put into an array, ${ARGV[@]}. The original $@ will be
    # unmodified. In addition, there will be a _usage function available which
    # the caller may use if, for example, they do additional validation of the
    # values in the option variables (say, checking that $BAR is a valid path)
    #
    # If there is an error, a well-formatted usage message will be displayed on
    # STDERR and the script will exit. -h and --help options are automatically
    # generated and display help on STDOUT when used.
    #
    # The behavior/semantics of the option processing is identical to GNU getopt,
    # as that is actually what is called to finally do the option processing.
    #
    # COMPATIBILITY:
    #
    # This has currently been tested and is known to work on the following
    # OS/Bash/getopt configurations:
    #
    # Mac OS X 10.7.5, bash 3.2.48, GNU getopt (enhanced) 1.1.4 (from homebrew)
    # CentOS 6.3, bash 4.1.2, GNU getopt (enhanced) 1.1.4
    #
    # It will not work with the BSD-style getopt shipped with Mac OS X. You'll
    # need to install GNU getopt and put it in your path before the system
    # getopt. I doubt it will work with versions of bash prior to 3.x
    #
    # Except GNU getopt, no other external programs are used.
    #
    # DESCRIPTION:
    #
    # This is an implementation of an idea I've had kicking around for a while.
    #
    # Using named (vs positional) options in shell scripts involves writing way
    # too much boilerplate, repetition of information, and is just an overall
    # PITA.
    #
    # Combine that with the limitations of bash's builtin getopts, and the
    # shortcomings of GNU getopt, having to edit multiple places in a script
    # when adding or changing options, and the fact that people regularly take
    # shortcuts and/or make mistakes... you get the idea.
    #
    # bash-getopt is designed to be easy to use, relatively intuitive, and
    # to eliminate repeated information and unnecessary boilerplate as much
    # as possible.
    #
    # It is still being fleshed out, but the current implementation is reasonably
    # complete, and seems to be quite stable and fast.
    #
    # OPTION DEFINITIONS
    #
    # The lines in the "heredoc" in the example above are called "Option
    # Definitions". An option definition has either two or three fields,
    # depending on the type of option (which is defined in the first field)
    #
    # The first field is the "Option Specification" and has a format and syntax
    # described in more detail below. The specification defines, among other
    # things, the "type" of the option - boolean flag, value required, or value
    # optional.
    #
    # After the specification field, boolean flag options only have one
    # additional field, a description for including in usage/help output.
    # The two value option types have two more fields, the first being the
    # default value for when the option is not used by the caller of the
    # script, the second being the afore-mentioned description field.
    #
    # boolean/flag options do not have a field for setting a default because
    # it's simply not necessary. If the option was used, the value is 1. If
    # the option was not used, the value is the empty string, "".
    #
    # If a default or description field will contain spaces, it must be quoted.
    # Standard shell quoting rules apply. Because of the way the definitions are
    # processed, a description can span multiple lines, but the extra whitespace
    # will get collapsed. Also, there can be blank-lines between option
    # definitions for better readability.
    #
    # A previous version of bash-getopt even supported bash-style comments in
    # between the definitions, but the code to do that properly has been deemed
    # more trouble than the feature is worth, for now.
    #
    # OPTION SPECIFICATIONS
    #
    # An "option specification" allows you to succinctly express a lot of
    # information about your program's command-line options. Using one of the
    # examples above, the format for an option specification is parsed like this:
    #
    # FOO=f|foo:NAME
    # | | | | |
    # var name ---+ | | | |
    # short name ----+ | | |
    # long name --------+ | |
    # type indicator -----+ |
    # value unit -----------+
    #
    # In this case, the value of this option will be assigned to a variable named
    # $FOO, you can use this option with either -f or --foo, the type indicates
    # that this is a value-required option, and in the help text the value will
    # referred to as NAME.
    #
    # The variable name can be composed of any characters that are typically
    # valid for a variable name, [_0-9a-zA-Z]. The variable name is required
    # because making it optional just doesn't seem worth the effort and code.
    #
    # The short and long names for the option are separated by a pipe ("|").
    # There can only be one of each type of name for the option but you can omit
    # one or the other if you wish (examples: FOO=f:NAME or FOO=foo:NAME)
    #
    # The short name can be any character in [0-9a-zA-Z]. I might be able to
    # allow a single dash as well, but I haven't yet tested it.
    #
    # The long name can be composed of any characters that are typically
    # recognized as valid in an option name, [-_0-9a-zA-Z] but it must begin
    # with a letter or number and must not end with a dash ("-")
    #
    # The type indicator can be zero, one, or two colons (":"). The indicator
    # has the same semantics as it does with GNU getopt: A single colon indicates
    # that if the option is used, a value *must* be supplied. Two indicates that
    # if the option is used, a value *may* be supplied. No colons indicate that
    # the option is a boolean flag, and using it results in a value of "1" and
    # not using it results in a value of the empty string ("").
    #
    # The "value unit" is used when showing usage or help text to indicate to the
    # user what the value represents. For example, the usage will show this for
    # foo: "--foo=NAME". The value unit can only be specified when the option
    # type is one that takes a value; It can't be used with boolean/flag options
    # because it makes no sense. (ok, very *little* sense). The value unit is
    # optional. Simply omit it if you do not want one. (example: FOO=f|foo:)
    #
    ################################################################################

    # this file should only ever be sourced.
    # But... if it's executed directly, let's try to helpful!
    if [[ "$0" == "${BASH_SOURCE[0]}" ]]; then
    _show_help () {
    echo -n "SHOWING HELP FOR ${BASH_SOURCE[0]}..."
    sed -nr '/^########/,/^########/{s/^#*//;p}' "${BASH_SOURCE[0]}"
    }

    if [[ -t 1 ]]; then
    # if STDOUT is connected to a terminal, show help using a pager
    _show_help | "${PAGER-less}"
    else
    # if it isn't a terminal, just spew to STDERR
    _show_help >&2
    fi
    exit 1
    fi

    ###
    ### TODO: see if the utility functions in this script (warn,debug,is-true,etc)
    ### can either be used to augment Ed's utils, or be replaced by them.
    ###

    # the stuff below, I'm just thinking about, for now. I'm not certain there
    # won't be unexpected difficulties with changing shell opts in sourced code
    # and trying to restore them *correctly* before returning to the caller.
    # I wonder if something like this could work:
    # manage-settings () {
    # get-settings () { set +o; }
    # local _saved_settings="$(get-settings)"
    # restore-settings () { eval $_saved_settings; }
    # return-handler () { trap - RETURN; restore-settings; }
    # echo trap return-handler RETURN;
    # }
    # # within a function in the sourced script
    # eval manage-settings
    # use-strict () { set -o errexit -o nounset -o pipefail; }
    # # and the return handler would restore the settings (maybe)

    # Output the args or piped STDIN to STDERR. For args, behaves just like
    # echo, so for example, you can use the -n and -e flags.
    warn () {
    if [[ $# -ne 0 ]]; then
    echo >&2 "$@"
    else
    cat >&2
    fi
    }

    # returns true if all arguments evaluate to true.
    # if no arguments, returns false.
    is-true () {
    local rc=1
    while [[ $# -gt 0 ]]; do
    case "$1" in
    0|'') rc=1; break;;
    *) rc=0; shift;;
    esac
    done
    return $rc
    }

    # returns true if all arguments evaluate to false.
    # if no arguments, returns true
    is-false () {
    local rc=0
    while [[ $# -gt 0 ]]; do
    case "$1" in
    0|'') shift;;
    *) rc=1; break;;
    esac
    done
    return $rc
    }

    # equivalient to warn if $DEBUG is true.
    debug () {
    is-false "${DEBUG-}" || warn "$@"
    }

    # returns true if all arguments are integers.
    # if no arguments, returns false
    is-int () {
    local rc=1
    while [[ $# -gt 0 ]]; do
    if ! printf '%i' "$1" &>/dev/null; then
    rc=1
    break
    else
    rc=0
    fi
    shift
    done
    return $rc
    }

    # Just like perl's croak, report an error from the caller and exit. The exit
    # code will be one of the following (in order of precedence)
    # - if the first argument is an integer, use that.
    # - if the return code of the last command was not 0, use that.
    # - 1
    croak () {
    local rc=$?
    [[ $rc -ne 0 ]] || rc=1
    if is-int "$1"; then
    rc="$1"
    shift
    fi
    read line sub file <<< $(caller 1)
    warn "ERROR [$file:$line]: $@"
    exit $rc
    }

    # returns true if the first argument matches any of the subsequent arguments
    in-list () {
    local want="$1"
    shift;
    for x in "$@"; do
    [[ "$x" != "$want" ]] || return 0
    done
    return 1
    }

    # this version avoids calling external programs, and also supports multi-line
    # option definitions
    bash-getopt () {

    # if there's a -- in the arglist, everything before should be
    # passed-thru as an arg to getopt
    local -a go_opts=("") # init the array to avoid errors under nounset
    if in-list '--' "$@"; then
    for (( x=0; x<$#; x++ )); do
    if [[ "$1" == '--' ]]; then
    shift; break
    fi
    go_opts[$x]="'$1'"
    shift
    done
    fi

    # the remaining options are the ones we want to parse
    local -a opts=( "$@" )

    # these vars will collect pieces of info: short option names, long names,
    # case statement clauses, and variable initializations
    local go_short='' go_long='' go_cases='' go_vars=''
    local usage_txt='' help_txt=''

    # newline in a variable is useful
    local nl="
    "

    # if nothing's been piped into this command, there's an error
    [[ ! -t 0 ]] || croak "Expected option definitions piped on STDIN"

    # read all input into one string. note that this causes read
    # to exit with code 1, hence the || true.
    read -d '' optdefs || true

    # certain characters need to be escaped for the next step
    optdefs="${optdefs//|/\|}"
    optdefs="${optdefs//=/\=}"

    # set the definitions as positional parameters
    eval set -- $optdefs

    # now process these to build the necessary code for processing the
    # actual program options
    while [[ $# -gt 0 ]]; do

    local raw_spec="$1"; shift
    local opt_spec="$raw_spec"; # this one will be edited along the way

    # extract the var, if any, this opt's value will be assigned to
    local opt_var='' rx_varname='^([_0-9a-zA-Z]+)='
    if [[ "$opt_spec" =~ $rx_varname ]]; then
    opt_var="${BASH_REMATCH[1]}"
    opt_spec="${opt_spec##*=}"
    fi

    # extract the "option type indicator" and "unit", if any. (type
    # indicator determines if the option takes parameters and if a value
    # is required, and unit is shown for help & usage output.
    local opt_type='' opt_unit='' rx_typeunit='([:]+)(.*)$'
    if [[ "$opt_spec" =~ $rx_typeunit ]]; then
    opt_type="${BASH_REMATCH[1]}"
    opt_unit="${BASH_REMATCH[2]}"
    opt_spec="${opt_spec%%:*}"
    fi

    # extract short name, long name, and opt type
    local opt_short='' opt_long=''

    local rx_short='^([0-9a-zA-Z])$' # only short name
    local rx_long='^([0-9a-zA-Z][-_0-9a-zA-Z]*[0-9a-zA-Z])$' # only long name
    local rx_both='^([0-9a-zA-Z])[|]([0-9a-zA-Z][-_0-9a-zA-Z]*[0-9a-zA-Z])?$' # both
    if [[ "$opt_spec" =~ $rx_both ]]; then
    opt_short=${BASH_REMATCH[1]}
    opt_long=${BASH_REMATCH[2]}
    elif [[ "$opt_spec" =~ $rx_long ]]; then
    opt_long=${BASH_REMATCH[1]}
    elif [[ "$opt_spec" =~ $rx_short ]]; then
    opt_short=${BASH_REMATCH[1]}
    else
    croak "Option specification [$raw_spec] is invalid"
    fi

    # determine the default value and description
    local opt_default='' opt_descr=''

    if [[ -n "$opt_type" ]]; then
    opt_default="$1"
    opt_descr="$2"
    shift 2
    else
    opt_descr="$1"
    shift
    fi

    # kept this since it's useful for debugging.
    debug <<END
    Option definition info:
    raw_spec="$raw_spec"
    opt_var="$opt_var"
    opt_spec="$opt_spec"
    opt_short="$opt_short"
    opt_long="$opt_long"
    opt_type="$opt_type"
    opt_unit="$opt_unit"
    opt_default="$opt_default"
    opt_descr="$opt_descr"
    END
    ### done parsing the option definition. now generate pieces of code
    ### for actually processing the caller's options and/or outputting
    ### help and usage


    # short & long opts gor gnu getopt
    go_short+="$opt_short$opt_type"
    go_long+="${go_long:+,}$opt_long$opt_type"

    # declare and initialize the exported vars
    go_vars+="export $opt_var='$opt_default'$nl"

    ### build the case clause
    local opt_case_match="$opt_short" # the match part of the clause
    opt_case_match+="${opt_case_match:+|}$opt_long"

    local opt_case_code="" # the code in the case clause
    if [[ -z "$opt_type" ]]; then
    # boolean flag type
    opt_case_code="export $opt_var=1"
    else
    # value type
    # because of a quirk when using a short-opt like "-f=foo" the
    # value ends up as "=foo". this is "fixed" by the ${1#=} below
    opt_case_code="export $opt_var=\${1#=}; shift"
    fi
    go_cases+="${go_cases:+ }$opt_case_match) $opt_case_code;;$nl"

    ### help/usage text
    local opt_help="${opt_short+-$opt_short}"
    opt_help="${opt_help- } ${opt_long+--$opt_long}"
    [[ -z "$opt_type" ]] || opt_help+="=${opt_unit-VALUE}"
    opt_help+="$opt_descr"
    [[ -z "$opt_type" ]] || opt_help+=" ['$opt_default']"
    help_txt+=" $opt_help$nl"

    done

    # remove extraneous newlines from generated code
    go_vars="${go_vars%
    }"
    go_cases="${go_cases%
    }"
    help_txt="${help_txt%
    }"

    # add options for help output
    go_short+="h"
    go_long+="${go_long:+,}help"

    ### TODO: build up usage & help text
    help_text+=" -h --helpdisplay this help text"
    help_text="$( column -s '' -t <<< "$help_txt" )"



    ### all the necessary pieces are ready! Assemble the getopt
    ### code and stuff it in $go_code
    read -d '' go_code <<END_GETOPT_CODE || true
    $go_vars
    _usage () {
    cat <<END_USAGE
    usage: \$0 [options]
    $help_text
    END_USAGE
    }
    local gotopts
    if ! gotopts=\$(getopt -o '$go_short' -l '$go_long' ${go_opts[@]} -- \"\${opts[@]}\"); then
    rc=\$?
    _usage 1>&2
    exit \$rc
    fi
    eval set -- "\$gotopts"
    while [[ \$# -gt 0 ]]; do
    local opt="\${1##-}"; opt="\${opt##-}"
    shift
    case "\$opt" in
    $go_cases
    h|help) _usage; exit 0;;
    '') break;; # end of options
    *) croak "Unrecognized option: [\$1]";;
    esac
    done
    # put remaining arguments here. done in two statements like this
    # because no other way worked for some reason.
    export ARGV=""; [[ \$# -eq 0 ]] || ARGV=( "\$@" )
    END_GETOPT_CODE



    debug "$go_code"
    eval "$go_code"
    }

    true # I just felt like putting this here, no real reason.