############################################################################
#
#	File:     options.icn
#
#	Subject:  Procedure to get command-line options
#
#	Authors:  Robert J. Alexander and Gregg M. Townsend
#
#	Date:     May 5, 2000
#             Nov 20 2019   Don Ward    Add --options-with-dashes
#
############################################################################
#
#   This file is in the public domain.
#
############################################################################
#
#	options(arg, optstring,errproc) removes command options from the
#	argument list of an Icon main procedure, returning a table of
#	option values.
#
############################################################################
#  
#     options(arg,optstring,errproc) -- Get command line options.
#
#     This procedure separates and interprets command options included in 
#  the main program argument list.  Option names and values are removed
#  from the argument list and returned in a table.
#
# -----------------------------------------------------------------------
# [Pre Nov 2019 version description: see additional comments at the end]
# 
#     On the command line, options are introduced by a "-" character.  An
#  option name is either a single printable character, as in "-n" or "-?",
#  or a string of letters, numbers, and underscores, as in "-geometry".
#  Valueless single-character options may appear in combination, for
#  example as "-qtv".
#
#     Some options require values.  Generally, the option name is one
#  argument and the value appears as the next argument, for example
#  "-F file.txt".   However, with a single-character argument name
#  (as in that example), the value may be concatenated: "-Ffile.txt"
#  is accepted as equivalent.
#  
#     Options may be freely interspersed with non-option arguments.
#  An argument of "-" is treated as a non-option.  The special argument
#  "--" terminates option processing.  Non-option arguments are returned
#  in the original argument list for interpretation by the caller.
#
#     An argument of the form @filename (a "@" immediately followed
#  by a file name) causes options() to replace that argument with
#  arguments retrieved from the file "filename".  Each line of the file
#  is taken as a separate argument, exactly as it appears in the file.
#  Arguments beginning with - are processed as options, and those
#  starting with @ are processed as nested argument files.  An argument
#  of "--" causes all remaining arguments IN THAT FILE ONLY to be
#  treated as non-options (including @filename arguments).
#
#     The parameters of options(arg,optstring,errproc) are:
#  
#       arg         the argument list as passed to the main procedure.
#
#       optstring   a string specifying the allowable options.  This is
#		    a concatenation, with optional spaces between, of
#		    one or more option specs of the form
#			-name%
#		    where
#			-	introduces the option
#			name	is either a string of alphanumerics
#				(any of a-z, A-Z, 0-9, and _)
#				or any single printable character
#			%	is one of the following flag characters:
#				!	No value is required or allowed
#			 	:	A string value is required
#				+	An integer value is required
#				.	A real value is required
#
#		    The leading "-" may be omitted for a single-character
#		    option.  The "!" flag may be omitted except when
#		    needed to terminate a multi-character name.
#		    Thus, the following optstrings are equivalent:
#			"-n+ -t -v -q -F: -geometry: -silent"
#			"n+tvqF:-geometry:-silent"
#			"-silent!n+tvqF:-geometry:"
#
#		    If "optstring" is omitted any single letter is
#		    assumed to be valid and require no data.
#
#	errproc	    a procedure which will be called if an error is
#		    is detected in the command line options.  The
#		    procedure is called with one argument:  a string
#		    describing the error that occurred.  After errproc()
#		    is called, options() immediately returns the outcome
#		    of errproc(), without processing further arguments.
#		    Already processed arguments will have been removed
#		    from "arg".  If "errproc" is omitted, stop() is
#		    called if an error is detected.
#  
#     A table is returned containing the options that were specified.
#  The keys are the specified option names.  The assigned values are the
#  data values following the options converted to the specified type.
#  A value of 1 is stored for options that accept no values.
#  The table's default value is &null.
# 
#
#     Upon return, the option arguments are removed from arg, leaving
#  only the non-option arguments.
# 
# ---------------------------------------------------------------------------
# [Nov 2019 enhancements]
# 
#   Options may begin with "--" and, if they do, may contain "-" characters.  
# So options like --dry-run will be parsed correctly and will result in an
# entry in the options table with a key of "-dry-run" (note the leading
# minus in the key).
# 
#   To remove the leading minus from such keys, make the first character
# of the option string an equals character: then, a supplied argument of
# either --opt or -opt would result in a key of "opt" in the table. When the
# first character is an equals character, the procedure ensures that only
# one of "--opt" or "-opt" may be specified in the option string. The only
# way to allow (for example) both --run and -run as options is to miss out
# the leading equals and to treat option["-run"] and option["run"]
# separately in the calling program.
# e.g.
#   "-bish-bash-bosh--bash"   is ok.
#   "=-bish-bash-bosh--bash"  results in a "duplicate option --bash" error.
#  
#   "=-bish-bash-bosh--bish-bash-bosh" is also ok because, in this case,
#   the options are
#          -bish -bash -bosh and  --bish-bash-bosh
# Because the -- options can swallow succeeding minus characters, it's
# probably better to use spaces to separate arguments, even when not
# needed. In the preceding example, it's much clearer to write
#    "= -bish -bash -bosh --bish-bash-bosh"
# 
#   This version will also accept parameters with an equal sign in them. e.g.
# instead of
#     progname -time 45
# you can write
#     progname -time=45
# A similar extension is available in the @ file. You can now write
#     -time 45
# or
#     -time = 45
# instead of two lines
#     -time
#     45
# as previously.
# 
############################################################################

procedure options(arg,optstring,errproc)
   local f,fList,fileArg,fn,ignore,optname,opttable,opttype,p,x,option,optcs
   local minusoptcs, stripm
   #
   #  Initialize.
   #
   /optstring := string(&letters++"_")
   /errproc := stop
   option := table()
   fList := []
   opttable := table()
   optcs := &letters ++ &digits ++ '_'
   minusoptcs := optcs ++ '-'
   #
   #  Scan the option specification string.
   #
   optstring ? {
      stripm := ="="
      until pos(0) do {
         tab(many(' '))
         if ="--" then {
            if any(optcs) then {
               optname := ( "-" || tab(many(minusoptcs)) | break)
               if \stripm & \opttable[optname[2:0]] then {
                  return errproc("duplicate option -" || optname)
               }
            }
         } else if ="-" then {
            if any(optcs) then {
               optname := (tab(many(optcs)) | break)
               if \stripm & \opttable["-" || optname] then {
                  return errproc("duplicate option -" || optname)
               }
            } else { optname := move(1) | break }
         } else { optname := move(1) | break }

         tab(many(' '))
         opttype := tab(any('!:+.')) | "!"
         opttable[optname] := opttype
      }
   }

   #
   #  Iterate over program invocation argument words.
   #
   while x := get(arg) do {
      if /x then ignore := &null     # if end of args from file, stop ignoring
      else x ? {
         if pos(-2) & ="--"
         then { ignore := 1 }  # ignore following args if --
         else if ="-" & not pos(0) & /ignore then {
            tab(0) ? until pos(0) do {
               if opttype := \opttable[optname := ((pos(1),(tab(upto(' \t=')|0) )) | move(1))]
               then {
                  option[optname] :=
                     if any(':+.',opttype) then {
                        tab(many(' \t')); tab(any('=')); tab(many(' \t'));
                        p := "" ~== tab(0) | get(arg) |
                           return errproc("No parameter following -" || optname)
                        case opttype of {
                           ":": p
                           "+": integer(p) |
                              return errproc("-" || optname ||
                                             " needs numeric parameter")
                           ".": real(p) |
                              return errproc("-" || optname ||
                                             " needs numeric parameter")
                        }
                     }
                     else 1
               }
               else return errproc("Unrecognized option: -" || optname)
            }
         } else if ="@" & not pos(0) & /ignore then {
            #
            #  The argument begins with the character "@", fetch option
            #  words from lines of a text file.
            #
            f := open(fn := tab(0)) | return errproc("Can't open " || fn)
            fileArg := []
            while put(fileArg,read(f))
            close(f)
            push(arg)   # push null to signal end of args from file
            while push(arg,pull(fileArg))
         } else put(fList,x)
      }
   }

   while push(arg,pull(fList))  # restore unprocessed arguments

   if \stripm then { # replace option["-opt"] with option["opt"]
      every x := key(option) do {
         if "-"  == x[1] then {
            option[x[2:0]] := option[x]
            push(fList,x)       # reuse fList for options to be removed
         }
      }
      every delete(option, !fList)
   }
   
   return option
end
