# optdefn.sh
# -----------------------------------------------------------------------------
#
# Infrastructure module, implementing the handlers for defining and parsing
# of command line options and variable assignments.
#
# -----------------------------------------------------------------------------
#
# $Id: optdefn.sh,v a57bbf0187da 2020/08/20 16:29:01 keith $
#
# Written by Keith Marshall <keithmarshall@users.sourceforge.net>
# Copyright (C) 2011, 2013, 2018, 2020, MinGW.org Project
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# -----------------------------------------------------------------------------
#
  if ( foo=foo=bar=baz; test "${foo%%=*}" = foo ) > /dev/null 2>&1
  then
  # Option string manipulator (helper) functions, for separation of
  # option name and optarg value parts of compound option strings.
  #
  # To avoid forking, prefer POSIX.1 variable substring extractors,
  # when they are supported; for long form option parsing, and for
  # variable assignment...
  #
    optstring_get_name() { echo "${1%%=*}"; }
    optstring_get_value() { echo "${1#*=}"; }

  # ...and for short form option string interpretation.
  #
    optstring_get_optchr() { echo ${1%"${1#?}"}; }
    optstring_get_remnant() { echo "${1#?}"; }

  else
  # Ultimately, more portable, but may require forking "sed"
  # for long form manipulations...
  #
    optstring_get_name() { ( IFS='='; set -- $1; echo $1 ); }
    optstring_get_value() { echo "$1" | sed 's,^[^=]*=,,'; }

  # ...and "cut", for short form.
  #
    optstring_get_optchr() { echo "$1" | cut -c1; }
    optstring_get_remnant() { echo "$1" | cut -c2-; }
  fi

  optdefine() {
  # Usage: shortname [longname] [argument_property]
  #    or: longname [shortname] [argument_property]
  #
  # Register either long or short option names, or both, such that they
  # will be subsequently recognized by the option interpreter.  A short
  # option name is exactly one character; a long name is two characters
  # or more.  If both long and short names are specified, they will be
  # interpreted as aliases for the same option.
  #
  # If specified, argument_property MUST appear as the last argument,
  # and MUST be one of "no_argument" (the default), "optional_argument",
  # or "requires_argument".
  #
  # Each option definition must be accompanied by an implementation for
  # an interpretive function, named opteval_${longname}, or in the case
  # of an option with only a short name, opteval_${shortname}; (if both
  # long and short names are specified, the interpretave function should
  # be named in terms of the long name, and the short name alias will be
  # mapped automatically).
  #
    local optname longname shortname argmode
    test $# -gt 3 && die "optdefine: too many arguments"
    for optname
      do test x$argmode = x || {
	 # If specified, the argument_property specification MUST be
	 # given as the last argument.
	 #
	   die "optdefine: '$optname' not allowed after '$argmode'"
	 }
	 test x`optstring_get_optchr "$optname"` = "x$optname" && {
	 # At most, one short option name argument is permitted, and
	 # it may not collide with any already defined.
	 #
	   test x$shortname = x || optdefine_too_many "short names"
	   for shortname in $shortopts
	     do test "x$optname" = "x$shortname" && optdefine_duplicate
	     done
	   shortname=$optname shortopts=${shortopts+"$shortopts "}$optname
	 } || {
	 # Anything, other than a short option name, must be one of
	 # argument_property specifiers, or a long option name.
	 #
	   case $optname in
	      no_argument)
		 argmode=$optname
		 ;;
	      requires_argument | optional_argument)
		 argmode=$optname
		 test x$longname = x || {
		   eval if_${longname}_$optname=true
		   eval if_${longname}_optional_argument=true
		 }
		 test x$shortname = x || {
		   eval if_${shortname}_$optname=true
		   eval if_${shortname}_optional_argument=true
		 } ;;

	      # As with a short option name, at most one long name is
	      # permitted, and it may not collide with any which has
	      # already been defined.
	      #
	      *) test x$longname = x || optdefine_too_many "long names"
		 longname=`sanitized_name $optname`
		 for optname in $longopts
		   do test "x$optname" = "x$longname" && optdefine_duplicate
		   done
		 longopts=${longopts+"$longopts "}$longname
		 ;;
	   esac
	 }
      done

    # Set up fallback interpreters; (user should then override these, to
    # provide an opteval_${optname} interpretive function).
    #
    test "x$longname$shortname" = x || {
      test "x$longname" = x && {
      # There is no long name; map a fallback function for only the
      # short option name.
      #
	eval opteval_$shortname'() { optunhandled "$@"; }'
      } || {
      # There IS a long name; map a fallback function for it...
      #
	eval opteval_$longname'() { optunhandled "$@"; }'
	test "x$shortname" = x || {
	# ...and, when there is also a short name, alias a short
	# name handler to whatever function is ultimately mapped
	# to interpret the long option.
	#
	  eval opteval_$shortname'() { opteval_'$longname' "$@"; }'
	}
      }
    }
  }
  # Diagnostic helpers, for "optdefine" usage errors.
  #
  optdefine_too_many() { die "optdefine: '$optname': too many $@"; }
  optdefine_duplicate() { die "optdefine: option '$optname' already defined"; }

  # Provide a mechanism for interpreting arbitrary alternative spellings,
  # as may be specified by any of a sequence of "replace=alternative" name
  # pairs, for long option names which would introduce ambiguity between
  # otherwise legitimate abbreviations of equivalent names.
  #
  optalias() { longopt_aliases=`echo $longopt_aliases "$@"`; }
  optalias_translate() {
  # Usage: optalias_translate name replace=alternative ...
  #
  # If "name", excluding any "-" or "--" prefix, matches any initial
  # substring of the "replace" part of any of the "replace=alternative"
  # arguments, which is not also an initial of the "alternative" within
  # the same argument, the replace "name" by "alternative", preserving
  # any "-" or "--" prefix which may be specified.
  #
  # Typically, the list of "replace=alternative" arguments will have been
  # constructed by prior use of the "optalias replace=alternative" command;
  # however, this is not mandatory; "optalias_translate" may be used to map
  # aliases for arbitrary option, or action names.
  #
    local optalias_prefix="" optalias_lookup="$1"
    case $1 in --*) optalias_prefix="--" ;; -*) optalias_prefix="-" ;; esac
    while test $# -gt 1
    do shift
       local optalias_replace=`optstring_get_name "$1"`
       local optalias_alternative=`optstring_get_value "$1"`
       case $optalias_prefix$optalias_replace in $optalias_lookup*)
	 case $optalias_prefix$optalias_alternative in $optalias_lookup*) ;;
	   *) local optalias_substitute=$optalias_prefix$optalias_alternative
	 esac
       esac
    done
    echo ${optalias_substitute-$optalias_lookup}
  }

  optunhandled() {
  # Default "opteval" interpreter; if the user neglects to define an
  # interpreter for any defined option, this will diagnose the omission,
  # at eventual point of use.
  #
    die "'$1':option '$optmatch' declared but no handler implemented"
  }

  optchk() {
  # Typical usage:
  #   while optchk "$@"
  #	do shift $argshift
  #	   test "x${optmatch}x" = "x--x" && break
  #	done
  #
  # Scan a vector of arguments, checking if each, in turn, represents
  # either a long or a short form option; sets $optmatch to each matched
  # option, in turn, and $argshift to the number of arguments consumed,
  # (one or two), by each such matched option.  Stop at first unmatched
  # argument, (setting $argshift to zero), or at exact "--" match, (in
  # which case, setting $argshift to one).
  #
    optmatch="" argshift=1
    optmatch_unknown() { die "unknown option: '$1'"; }
    case $1 in
      -)   # A bare hyphen is NOT an option; leave it for subsequent
	   # processing, and set the termination condition for option
	   # scanning, just as for any regular argument.
	   #
	   argshift=0; false
	   ;;
      --)  # Conversely, bare double-hyphen is neither an option, nor an
	   # argument; leave it behind, processing in the scanning loop,
	   # which will normally discard it and terminate the scan.
	   #
	   optmatch="--"
	   ;;
      --*) # Looks like a long form option; parse it as such, ensuring
	   # that the short form fall-back mechanism is disabled.
	   #
	   local optchk_getopt_long_only=false;
	   arg="$1"; optchk_getopt_long "$@"
	   ;;
      -*)  # Looks like a short form option, but consider the possibility
	   # that it may be a long form option, which should be interpreted
	   # in getopt_long_only() mode.
	   #
	   ${optchk_getopt_long_only-false} && {
	     arg="-$1"; optchk_getopt_long "$@"
	   }
	   # If getopt_long_only() matching was attempted, confirm whether an
	   # unambiguous long form interpretation has been identified; if not,
	   # prepare $arg for short form interpretation...
	   #
	   test "x$optmatch" = x && arg=`optstring_get_remnant $1` || {
	   # ...whereas, in the case of successful long form identification,
	   # we clear $arg, to indicate that no further option identification
	   # is required, and complete long form option evaluation, (which is
	   # deferred by optchk_getopt_long() when $optchk_getopt_long_only
	   # has been set to "true", as it is in this case).
	   #
	     arg=""; opteval "$@"
	   }

	   # When a long option match has been successfully identified, $arg
	   # will have been set to the empty string, and we will not enter the
	   # following loop; otherwise, $arg will have been set to a potential
	   # sequence of short form options, which this loop will evaluate.
	   #
	   until test "x$arg" = x
	   # Scan option string, attempting to match each character in turn
	   # as a short option; (this approximates the behaviour of "getopts",
	   # or the external "getopt" program, neither of which can be relied
	   # upon to support optional option-argument processing).
	   #
	     do opt=`optstring_get_optchr "$arg"`
	        arg=`optstring_get_remnant "$arg"`
		have_optmatch=false argshift=0
		for optchr in $shortopts '??'
		  do $have_optmatch || {
		     # We haven't found a match for the current character...
		     #
		       test x$optchr = 'x??' && {
		       # ...and we've run out of possibilities; if we haven't
		       # yet matched ANY character in the current group, then
		       # it may still be possible to find a long option match;
		       # otherwise, this is a fatal matching failure.
		       #
			 test "x$optmatch" = x || optmatch_unknown "-$opt"
		       }
		       # When there is still a matching possibility, (either
		       # short form, or long form)...
		       #
		       test $opt = $optchr && {
		       # ...and we've identified a short form match, then we
		       # record it as such; also, if the matched short option
		       # accepts an argument, propagate any residual of this
		       # option string as an argument...
		       #
			 test "x$arg" = x || argshift=1
			 have_optmatch=true optmatch=$opt
			 eval '${if_'$opt'_optional_argument-false}' &&
			   optarg=$arg arg=""

		       # ...before evaluating this option.
		       #
			 opteval "$@"
		       }
		     }
		  done

		# Confirm that we matched at least one option, otherwise we
		# must generate a matching failure abort, to avoid endlessly
		# looping over the same unmatched argument.
		#
		test -n "$optmatch" || optmatch_unknown "-$opt"
	     done
	   ;;
      *=*) # This is a variable assignment, rather than an option; separate
	   # the variable name and value components, complete the assignment,
	   # then continue scanning for further options, or assignments.
	   #
	   arg=`optstring_get_name "$1"`
	   optarg=`optstring_get_value "$1"`
	   eval $arg='"$optarg"'
	   ;;
      *)   # Anything else is just a regular argument; leave it for subsequent
	   # processing, and set the termination condition for option scanning,
	   # just as we did for the bare hyphen argument above.
	   #
	   argshift=0; false
    esac
  }

  optchk_getopt_long() {
  # Usage: arg=possible_match; optchk_getopt_long "$@"
  #
  # "$@" is the entire argument vector, inherited from the caller.
  # For regular getopt_long() matching, $arg should match "--*", and
  # should have been preset to be identical to "$1"; however, in the
  # case of getopt_long_only() matching, when the original "$1" has
  # only one leading hyphen, $arg should be passed as "-$1".
  #
    case $arg in
      # Any option with an assigned value: separate the
      # name and value parts.
      #
      *=*) optarg=`optstring_get_value "$arg"`
	   arg=`optstring_get_name "$arg"`
	   argshift=1
	   ;;
      # For any other possible match, force a $argshift adjustment,
      # to accommodate a possibly detached mandatory argument.
      #
      *) argshift=0
    esac

    # Although "mingw-pkg" syntax conforms, generally, to "World English"
    # spelling conventions, (as defined by the Oxford English Dictionary),
    # North American users may be inclined to adopt alternative spellings,
    # as promoted by Noah Webster, (e.g. "license" instead of "licence";
    # "color" instead of "colour"); accept such alternative spellings.
    #
    arg=`optalias_translate $arg $longopt_aliases`

    # Initialize, to ensure that we match the sanitized form of $arg,
    # and that we are equipped to detect, and handle, any ambiguously
    # matched abbreviations, or to override ambiguities on detection
    # of an exact match.
    #
    arg=`sanitized_name "$arg"`
    local optchk_ambiguous_match=false
    local optchk_exact_match=false

    for opt in $longopts
    # Compare $arg with each and every entry in $longopts, considering
    # possible expansions of abbreviated forms, to identify all possible
    # matches; (note the "__" prefix applied to $opt, which will match
    # the sanitized form of the "--" prefix to $arg).
    #
      do $optchk_exact_match || {
	   case __$opt in $arg)
	   # This represents an exact long option match, which takes
	   # priority over any possibly ambiguous abbreviation; record
	   # it as the only possible (unambiguous) match.
	   #
	     optmatch=$opt
	     optchk_ambiguous_match=false
	     optchk_exact_match=true
	     ;;
	   $arg*)
	   # This represents a plausible matching abbreviation; provided
	   # it is the first such, record it as a viable match, otherwise
	   # mark the match state as "ambiguous".
	   #
	     test "x$optmatch" = x && optmatch=$opt ||
	       optchk_ambiguous_match=true
	   esac
	 }
      done

    # Finally, hand off any unambiguously matched option for evaluation,
    # or handle an ambiguously matched abbreviation, and abort...
    #
    if ${optchk_getopt_long_only-false}
    then
    # ...noting that, in the $optchk_getopt_long_only case, an abort is
    # reported by simply denying any optmatch, and all abort diagnostics,
    # or evaluation of a successful optmatch, are deferred to caller...
    #
      $optchk_ambiguous_match && optmatch=""

    else
    # ...whereas, in the regular optchk_getopt_long() case, an abort is
    # diagnosed, and actioned immediately...
    #
      $optchk_ambiguous_match && die "option '$1' is ambiguous"

    # ...otherwise, a successfully matched option is evaluated here,
    # (subject to checking that no unexpected optarg was attached).
    #
      case $1 in *=*)
	eval '${if_'$optmatch'_optional_argument-false}' ||
	  die "option '`optstring_get_name $1`' does not accept an argument" ;;
      esac
      opteval "$@"
    fi
  }

  opteval() {
  # Usage: opteval "$@"
  #
  # Generic handler, ultimately invoked by "optchk", to redirect any
  # identified option match to its user defined interpreter, together
  # with the original "$@" argument vector from "optchk" itself.
  #
    test "x$optmatch" = x && optmatch_unknown "$1"
    test $argshift -eq 0 && eval '${if_'$optmatch'_requires_argument-false}' && {
      test $# -ge 2 || die "option '$optmatch' requires an argument"
      optarg="$2" argshift=2
    } || argshift=1
    opteval_$optmatch "$@"
  }

  option() {
  # Usage: option context "argument ..."
  #
  # Common handler for the --option="spec" command line option, and for
  # the "option" directive, which may be specified in any "pkgspec" file.
  #
    var=`sanitized_name $1`_options; shift
    eval $var'="`echo $'$var' "$@"`"'
  }

  sanitized_name() {
  # Usage: sanitized_name $var
  #
  # Replace any and all non-alphanumeric characters which may appear in
  # the expansion of $var, to ensure that this is suitable for use as a
  # shell variable or function name.
  #
    echo "$@" | sed 's,[^0-9A-Za-z],_,g'
  }
#
# -----------------------------------------------------------------------------
# $RCSfile: optdefn.sh,v $: end of file
