#!/usr/bin/python

#########################################################################
# This file is part of Lyntin.
#
# Lyntin is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# Lyntin is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
# copyright (c) Free Software Foundation 2001-2007
#
# $Id: argparser.py,v 1.7 2007/07/24 00:39:03 willhelm Exp $
#########################################################################
"""
This provides the ArgumentParser class which parses X{command argument}s
automatically into a dictionary as well as a series of Parser classes
which handle single arguments and TypeCheckers which handle validating
user input values and converting them into other types (string -> int...).

It's very complex, however, it allows us to nicely centralize a lot
of the duties involved in parsing user input into one place rather than
doing it in every single command over and over again.  This way commands
can specify their syntax line and the ArgParser handles the rest returning
to the commands a dict with the name/value pairs including argument 
defaults.

When building an arg spec, you can pass into the ArgumentParser several
options which change the way it parses the user input.

Supported options::

  stripBraces (default=on): whether all arguments should have 
            braces stripped before being parsed

  noparsing (default=off): doesn't insure that all arguments are
            parsed.  works well when matched with limitparsing=0 to 
            provide a syntax line for commands that parse their own 
            input

  limitparsing:int (default=-1): only parse this number of tokens 
            into dict, the rest of the input line goes into 
            argdict["input"]

  nodefaults:bool (default=off): turn off default lookups through variables.

Refer to the modules.lyntincmds and modules.tintincmds for examples
on arg specs and commands and how it all intertwines.
"""
import re, time
from lyntin import utils

defaultOptions={ "stripBraces": 1, "noparsing": 0, "limitparsing": -1, "nodefaults":0 }
optionParser = None

class ParserException(Exception):
  pass

class ArgumentParser:
  """
  This is the actual ArgumentParser class.  Each command gets its
  own ArgumentParser.  It handles taking in user input and parsing
  it into a series of arguments which are then placed in a dict and
  passed to the command function.  This centralizes parsing user
  input into one place and also enables us to centralize type checking,
  argument checking, and conversion in one place as well.
  """
  def __init__(self, argspec, argoptions=None):
    """
    Initializes the ArgParser instance so it can then parse user
    input for arguments.  It also generates the syntaxline which
    is used by the help system to tell the user what the command
    syntax is.

    @param argspec: the argument specification for the command
    @type  argspec: string

    @param argoptions: the options to use to parse the argspec
    @type  argoptions: string
    """
    # the syntax line is automatically generated from the argspec.
    # we print it out whenever we have a ParserException in the user input.
    self.syntaxline = ""

    if argoptions:
      self.buildOptions(argoptions)
    else:
      self.options = defaultOptions.copy()
    self.buildParsers(argspec)
    return

  def getOption(self, optionname):
    """
    Gets a specific option from the arg options hash as passed into the
    __init__.

    @param optionname: the name of the option
    @type  optionname: string

    @return: the value of the option or None
    @rtype: varies (string or int)
    """
    if optionname in self.options:
      return self.options[optionname]
    else:
      return None

  def buildOptions(self, argoptions):
    """
    Set the options for this ArgumentParser
    key=value values in argoptions get put directly into self.options
    other values in argoptions get set to 1 in self.options.

    example 1::

      argoptions="ignorall loud:boolean=true"
      self.options={"ignorall":1,"loud":1}

    example 2::

      argoptions="lalala wewewewe=hahaha"
      self.options={"lalala":1,"wewewewe":"hahaha"}

    @param argoptions: the argoptions as passed into __init__.
    @type  argoptions: string
    """
    # FIXME - this should operate on a locally created dict and pass
    # that dict back to __init__ which should set self.options and
    # self.argoptions
    global optionParser
    self.argoptions=argoptions
    self.options=defaultOptions.copy()

    if optionParser == None:
      optionParser = ArgumentParser("otherOptions* otherValuedOptions**")
    argdict = optionParser.parse(argoptions)

    for key in argdict:
      if key == "otherOptions":
        for otherOption in argdict[key]:
          self.options[otherOption] = 1
          if len(otherOption)>3 and otherOption[0:2] == "no":
            self.options[otherOption[2:]] = 0
      elif key == "otherValuedOptions":
        self.options.update(argdict[key])
      else:
        self.options[key] = argdict[key]

    # set types for certain options
    self.options["limitparsing"] = int(self.options["limitparsing"])

  def buildParsers(self, argspec):
    """
    Build up the set of parsers to be used for argument parsing.

    The argspec follows the following format:

      1. [argname[:argtype]]+ 
      2. [argname[:argtype]=defaultval]+ 
      3. [argname:argtype*] 
      4. [argname[:argtype]]+ 
      5. [argname[:argtype]=defaultval]+ 
      6. [argname:argtype**]

    Any of the arguments can be specified either by name or populated
    by position, except for arguments after the index collector
    argument.  Those must be specified by name only.

    Once one default value is given all further arguments must have
    default values (except collector arguments, which have implicit
    default arguments of the empty list and the empty map).

    For examples see the test code at the end of argparser.py.

    @param argspec: the argument specification to use to parse future
        user input
    @type  argspec: string
    """
    self.parsers = {}
    self.indexparsers = []
    self.extraindexparser = None
    self.extranamedparser = None

    self.argspec = self.split(argspec, buildsyntaxline=1)

    parsedspec = self.argspec

    doneWithIndices = 0
    defaultSeen = 0
    for i in range(0,len(parsedspec)):
      namedCollector = 0
      indexCollector = 0
      argname, argdef = parsedspec[i]
      if argname.find(":") > -1:
        argname,typespec = argname.split(":",1)
      else:
        # extra argname assignment here is just for consistency
        argname, typespec = argname, "string"

      if len(argname) >= 1 and argname.endswith("*"):
        if argdef != None:
          raise ParserException("cannot specify a default value for a collection argument (%s=%s)" % (argname, argdef))

        if len(argname) >= 2 and argname.endswith("**"):
          argname = argname[:-2]
          if i < len(parsedspec) -1:
            raise ParserException("named collection argument must be the last argument (%s)" % (argname))
          parser = ExtraNamedParser(self, argname)
          namedCollector = 1

        else: #this is an index collection argument
          argname = argname[:-1]
          parser = ExtraIndexParser(self, argname)
          indexCollector = 1
          doneWithIndices = 1
          
      else:
        parser = Parser(self,argname)

      typechecker = _createTypeChecker(typespec)
      if not typechecker:
        raise ParserException("Unknown type specifier: %s" % (typespec))

      parser.setTypeChecker(typechecker)

      if argdef != None:
        parser.setDefault(parser.parse(argdef))
        defaultSeen = 1

      if defaultSeen and not parser.defaultset:
        raise ParserException("Argument without default value (%s) seen after default values already specified" % (argname))
      
      if not namedCollector and not indexCollector:
        if not doneWithIndices:
          self.indexparsers.append(parser)
        if argname in self.parsers:
          raise ParserException("Multiple argument named %s specified." % (argname))
        self.parsers[argname] = parser
      elif namedCollector:
        self.extranamedparser = parser
        self.parsers[argname] = parser
      elif indexCollector:
        self.extraindexparser = parser
        self.parsers[argname] = parser
    
  def parse(self, input, defaultresolver=None):
    """
    Takes an input string and produces the populated dictionary
    matching self's argspec.  

    @param input: the user input string
    @type  input: string

    @param defaultresolver: A function that will take an argument 
        name and return a default value (None if there should be none) 
        to override builtin defaults.
    @type  defaultresolver: function

    @return: the populated dictionary of all the args and values
    @rtype: dict

    @raise ParserException: if extra arguments are encountered without
        appropriate collection arguments specified, or if required arguments
        are missing, or if arguments passed in aren't valid
    """    
    argdict = {}

    arguments = self.split(input, self.getOption("limitparsing"))

    foundNamedArg = 0
    for i in range(0,len(arguments)):
        key,val = arguments[i]

        if val == None:
          if foundNamedArg and not self.extraindexparser:
            raise ParserException("Non-named argument (%s) found after Named argument" % (key))
          if i < len(self.indexparsers):
            parser = self.indexparsers[i]
          elif self.extraindexparser:
            parser = self.extraindexparser
          else:
            raise ParserException(f"Unexpected argument received {key} by the argument parser.")
          parser.parseInto(i, key, argdict)
        else:
          foundNamedArg = 1
          if key in self.parsers:
            parser = self.parsers[key]
          else:
            matchedkeys = [arg for arg in self.parsers if arg.startswith(key)]

            if len(matchedkeys) == 1:
              parser = self.parsers[matchedkeys[0]]
            elif self.extranamedparser:
              parser = self.extranamedparser
            else:
              if len(matchedkeys) == 0:
                raise ParserException("Invalid named argument: %s=%s" % (key,val))
              else:
                raise ParserException("Ambiguous named argument: %s=%s %s" % (key, val, matchedkeys))
            
          parser.parseInto(key, val, argdict)


    # now check that everything has been specified, putting in defaults 
    # where available
    for key in self.parsers:
      if key not in argdict:
        # gotta be careful here with the extra defaultset value since
        # the parser may parse a string into None, or anything really
        default = None
        defaultset = 0
        parser = self.parsers[key]

        if not self.getOption("nodefaults") and defaultresolver:
          default = defaultresolver(key)
          if default != None:
            default = parser.parse(default)
            defaultset = 1
            
        if not defaultset and parser.defaultset:
          default = parser.default
          defaultset = 1
        
        if not defaultset and not self.getOption("noparsing"):
          raise ParserException("Must specify a value for argument %s" % (key))
        else:
          argdict[key] = default
          
    return argdict

  def split(self, input, maxsplit=-1, buildsyntaxline=0):
    """
    Take an input string and tokenizes it into a list of pairs.
    Tokens with equal signs come back as (key,value) pairs, those
    without come back as (argument,None)

    {}s are treated like quotes, and everything between the {}s is
    ignored.

    Any amount of white space between arguments is ignored.  (No empty
    arguments are returned.)

    "\\" (single backslash) escapes anything, including =, {, }, 
    \\ (single backslash), and any character, so "\\a" (single 
    backslash then a) becomes "a" in the argument, and
    "\\n\\o\\t\\a\\d\\r\\a\\g\\o\\n" (a series of single backslashes 
    followed by a letter) is the same as "notadragon".

    After maxplit arguments are parsed (all of the arguments if maxsplit 
    < 0), we'll stop and return the rest of input as the final item.

    @param input: the user input to tokenize
    @type  input: string

    @param maxsplit: the maximum number of pairs to split
    @type  maxsplit: int (-1 if no maxplit)

    @param buildsyntaxline: whether or not to build the syntax
        line as we're going along (this method is used both when
        the ArgParser is initialized as well as for parsing
        user input)
    @type  buildsyntaxline: int (0 or 1)

    @return: the split input
    @rtype: list of 2-length-tuples

    @raises ParserException: if \\ (single backslash) is found at the end 
        of the line or if mismatched { or } are found
    """
    bracketdepth = 0
    arg = ""
    val = None
    equalsign = 0
    arguments = []
    while input and (maxsplit < 0 or len(arguments) < maxsplit):
      nextchar = input[0:1]
      input = input[1:]

      if nextchar == " " or nextchar == "\t":
        if not bracketdepth:
          # We've completed a full argument
          if arg!="":
            arguments.append( (arg,val) )
            if buildsyntaxline:
              synarg = arg.upper()
              if synarg.endswith("*"):
                synarg = synarg[:-1] + "..."

              if val and len(val) > 0:
                synarg = synarg + "=" + val

              if equalsign == 1:
                self.syntaxline += "[<%s>] " % synarg
              else:
                self.syntaxline += "[%s] " % synarg

          arg = ""
          val = None
          equalsign = 0
        else:
          if val != None:
            val = val + nextchar
          else:
            arg = arg + nextchar
      elif nextchar == "\\":
        if input == "":
          raise ParserException("\\ at end of line.")
        else:
          nextchar = input[0:1]
          input = input[1:]
          if val != None:
            val = val + nextchar
          else:
            arg = arg + nextchar
      elif nextchar == "}":
        bracketdepth = bracketdepth - 1
        if bracketdepth < 0:
          raise ParserException("mismatched }")
        if val != None:
          val = val + nextchar
        else:
          arg = arg + nextchar
      elif nextchar == "{":
        bracketdepth = bracketdepth + 1
        if val != None:
          val = val + nextchar
        else:
          arg = arg + nextchar
      elif val == None and bracketdepth == 0 and nextchar == "=":
        val = ""
        equalsign = 1
      else:
        if val != None:
          val = val + nextchar
        else:
          arg = arg + nextchar

    if bracketdepth:
      raise ParserException("Mismatched {")

    if arg != "":
      arguments.append( (arg, val) )
      if buildsyntaxline:
        synarg = arg.upper()
        if synarg.endswith("*"):
          synarg = synarg[:-1] + "..."

        if val and len(val) > 0:
          synarg = synarg + "=" + val

        if equalsign == 1:
          self.syntaxline += "[<%s>] " % synarg
        else:
          self.syntaxline += "[%s] " % synarg

      arg = ""
      val = ""

    if input:
      arguments.append( ("input", input) )
    
    return arguments

class Parser:
  """
  This is the base class for the parsers that argumentparser uses to
  actually populate the dictionary with each argument.
  """
  def __init__(self, argparser, argname):    
    """
    Initializes the Parser.

    @param argparser: the argparser instance this belongs to
    @type  argparser: argparser.ArgParser

    @param argname: the name of the argument this parser handles type
        and value checking for.
    @type  argname: string
    """
    self.argname = argname
    self.default = None
    self.defaultset = 0
    self.typechecker = None
    self.argparser = argparser

  def setTypeChecker(self, typechecker):
    """
    Sets the typechecker.  The Parser then uses this to check the
    values coming in before setting them on the name/value dict.

    @param typechecker: the typechecker to set
    @type  typechecker: argparser.TypeChecker
    """
    self.typechecker = typechecker

  def parseInto(self, key, val, argdict):
    """
    Populates the argument dictionary with the appropriate value.

    @param key: the argument name
    @type  key: string

    @param val: the argument value
    @type  val: string

    @param argdict: the argument dictionary to populate
    @type  argdict: dict

    @raises ParserException: if multiple values were given for the argument
    """
    if self.argname in argdict:
      raise ParserException("Multiple values for argument %s given" % (self.argname))
    else:
      argdict[self.argname] = self.parse(val)

  def parse(self, val):
    """
    Parses the value according to this parser and its associated 
    Typechecker.  We both typecheck it here (making sure it's valid)
    as well as convert it (i.e. strings -> ints, strings -> regexps...).

    @param val: the value to parse
    @type  val: string

    @return: the newly adjusted value
    @rtype: varies
    """
    if self.argparser.getOption("stripBraces"):
      val = utils.strip_braces(val)
    if self.typechecker:
      return self.typechecker.check(val)
    else:
      return val

  def setDefault(self, val):
    """
    Sets the default value for this Parser which is used when no
    other value is given.  Also sets the "defaultset" member which
    tells the Parser that it has a default value.

    @param val: the default value
    @type  val: string or int
    """
    self.default = val
    self.defaultset = 1
      
class ExtraIndexParser(Parser):
  """
  This class captures the parsing behaviour for an index collector.
  for each call to parseInto an entry is put into the list value in
  the argument dictionary.
  """
  def __init__(self, argparser, argname):
    """
    Initializes the Parser.

    @param argparser: the argparser instance this belongs to
    @type  argparser: argparser.ArgParser

    @param argname: the name of the argument this parser handles type
        and value checking for.
    @type  argname: string
    """
    Parser.__init__(self,argparser,argname)
    self.default = []
    self.defaultset = 1
    
  def parseInto(self, key, val, argdict):
    """
    Populates the argument dictionary with the appropriate value.

    @param key: the argument name
    @type  key: string

    @param val: the argument value
    @type  val: string

    @param argdict: the argument dictionary to populate
    @type  argdict: dict

    @raises ParserException: if multiple values were given for the argument
    """
    val = self.parse(val)
    if self.argname in argdict:
      argdict[self.argname].append(val)
    else:
      argdict[self.argname] = [val]

class ExtraNamedParser(Parser):
  """
  This class captures the parsing behaviour for a named value collector.
  for each call to parseInto a new key=value pair is put into a map
  in the argument dictionary.
  """
  def __init__(self,argparser,argname):
    """
    Initializes the Parser.

    @param argparser: the argparser instance this belongs to
    @type  argparser: argparser.ArgParser

    @param argname: the name of the argument this parser handles type
        and value checking for.
    @type  argname: string
    """
    Parser.__init__(self,argparser,argname)
    self.default = {}
    self.defaultset = 1
    
  def parseInto(self, key, val, argdict):
    """
    Populates the argument dictionary with the appropriate value.

    @param key: the argument name
    @type  key: string

    @param val: the argument value
    @type  val: string

    @param argdict: the argument dictionary to populate
    @type  argdict: dict

    @raises ParserException: if multiple values were given for the argument
    """
    val=self.parse(val)
    if self.argname in argdict:
      if key in argdict or key in argdict[self.argname]:
        raise ParserException("Multiple values given for argument %s" % (key))
      argdict[self.argname][key] = (val)
    else:
      argdict[self.argname] = {key:val}


typecheckers = {}

def _createTypeChecker(typespec):
  """
  Factory method for instantiating TypeChecker subclasses based on the
  type being passed in.

  First the typespec is split at its first colon.

  The first element (the typename) of the typespec is used as a key
  into typecheckers to find the function/class object to call to
  create the typechecker. 

  The rest of the typespec (the typeargs) is used with the function to
  construct the typechecker desired.

  @param typespec: the type that we're checking.  i.e. "string", "int", ...
      This translates directly into a specific TypeChecker.
  @type  typespec: string

  @return: the TypeChecker subclass created
  @rtype: argparser.TypeChecker
  """
  typespec = typespec.split(":",1)
  if len(typespec) == 1:
    typename, typeargs = typespec[0],None
  else:
    typename, typeargs = typespec

  if typename not in typecheckers:
    return None
  typechecker = typecheckers[typename](typename,typeargs)

  return typechecker

class TypeChecker:
  """
  Trivial base class for argument checkers
  """
  def __init__(self, typename, typeargs):
    """
    Initializes the TypeChecker.  Over-ridden by all the TypeChecker 
    subclasses.

    @param typename: the name of the type
    @type  typename: string

    @param typeargs: the arguments passed to the TypeChecker to initialize
        it
    @type  typeargs: tuple
    """
    return

  def check(self, arg):
    """
    Over-ridden by all the TypeChecker subclasses.

    @param arg: the argument to check the type-hood and convert
    @type  arg: string

    @returns: the converted argument
    @rtype: varies
    """
    return arg

class StringChecker(TypeChecker):
  """
  Essentiallly the same as the trivial base class, but it's explicit
  that we just return the string we take in without any transformation.
  """
  def __init__(self, typename, typeargs):
    if typeargs:
      raise ParserException("TypeArgs (%s) specified for non-configurable type (%s)" % (typeargs, typename))
    return

  def check(self, arg):
    """
    Returns the string we're looking at without any transformation.

    @param arg: the argument to pass back
    @type  arg: string

    @return: the argument
    @rtype:  string
    """
    return arg

typecheckers["string"] = StringChecker

class IntChecker(TypeChecker):
  """
  Accept only integer values and return integer objects.
  """
  def __init__(self, typename, typeargs):
    """
    Initializes the TypeChecker.  Over-ridden by all the TypeChecker 
    subclasses.

    @param typename: the name of the type
    @type  typename: string

    @param typeargs: the arguments passed to the TypeChecker to initialize
        it
    @type  typeargs: tuple

    @raises ParserException: If typeargs are passed in--this is 
        non-configurable.
    """
    if typeargs:
      raise ParserException("TypeArgs (%s) specified for non-configurable type (%s)" % (typeargs, typename))

  def check(self,arg):
    """
    Verifies the arg is of type int and converts it to an int.

    @param arg: the argument to check the type-hood and convert
    @type  arg: string

    @returns: the converted argument from string to int
    @rtype: int
    """
    return int(arg)

typecheckers["int"] = IntChecker

class BooleanChecker(TypeChecker):
  """
  Accept only boolean values using the utils.convert_boolean function.
  """
  def __init__(self, typename, typeargs):
    if typeargs:
      raise ParserException("TypeArgs (%s) specified for non-configurable type (%s)" % (typeargs, typename))

  def check(self,arg):
    """
    Verfies the argument is a boolean (using utils.convert_boolean function)
    and converts it to a boolean value (0 or 1).

    @param arg: the argument to check the type-hood and convert
    @type  arg: string

    @returns: the converted argument
    @rtype: int (0 or 1)

    @raises ParserException: if the arg is not a valid boolean
    """
    ret = utils.convert_boolean(arg)
    if ret == 1 or ret == 0:
      return ret

    raise ParserException("Invalid boolean value specified: %s" % (arg))

typecheckers["boolean"] = BooleanChecker

class BooleanOrNoneChecker(TypeChecker):
  """
  Accept only boolean values or special "not specified" values.  Booleans
  are handled by utils.convert_boolean.  Values not handled by that
  function will throw an exception.
  """
  def __init__(self, typename, typeargs):
    if typeargs:
      raise ParserException("TypeArgs (%s) specified for non-configurable type (%s)" % (typeargs, typename))

  def check(self,arg):
    """
    Verfies the argument is a boolean (using utils.convert_boolean function)
    and converts it to a boolean value (0 or 1).  Also handles None, "-"
    and the empty string as None.

    @param arg: the argument to check the type-hood and convert
    @type  arg: string

    @returns: the converted argument
    @rtype: int (0 or 1) or None

    @raises ParserException: if the arg is not a valid boolean
    """
    ret = utils.convert_boolean(arg)
    if ret == 1 or ret == 0:
      return ret
    elif arg == "None" or arg == "-" or arg == "":
      return None
    else:
      raise ParserException("Invalid boolean value specified: %s" % (arg))

typecheckers["booleanornone"] = BooleanOrNoneChecker

class EvalChecker(TypeChecker):
  """
  Evaluate its input argument as python code and return the resulting object.
  """
  def __init__(self, typename, typeargs):
    if typeargs:
      raise ParserException("TypeArgs (%s) specified for non-configurable type (%s)" % (typeargs, typename))

  def check(self,arg):
    """
    Assumes the argument is valid python code.  It evaluates the code
    and returns the result.

    @param arg: the argument to evaluate
    @type  arg: string

    @returns: the result of eval(arg)
    @rtype: varies

    @raises ParserException: if we can't eval the argument
    """
    try:
      return eval(arg)
    except Exception as e:
      raise ParserException("Error eval-ing argument (%s): %s" % (arg, e))

typecheckers["eval"] = EvalChecker 

class TimeSpanChecker(TypeChecker):
  """
  Accepts an amount of time and converts it to a number of seconds.
  """
  def __init__(self, typename, typeargs):
    if typeargs:
      raise ParserException("TypeArgs (%s) specified for non-configurable type (%s)" % (typeargs, typename))

  def check(self, arg):
    """
    Assumes the argument is a time span.

    @param arg: the timespan
    @type  arg: string

    @returns: the number of seconds in the timespan
    @rtype: int

    @raise ParserException: if the timespan is invalid
    """
    try:
      time = utils.parse_timespan(arg)
      return time
    except:
      raise ParserException("Invalid timespan specified %s" % (arg,))

typecheckers["timespan"] = TimeSpanChecker
  
class TimeChecker(TypeChecker):
  """
  Accepts a date specification.

  Will also accept a time specification and apply it as a delta from
  _now_.  converts to the standard seconds-from_epoch. 
  """
  def __init__(self, typename, typeargs):
    if typeargs:
      raise ParserException("TypeArgs (%s) specified for non-configurable type (%s)" % (typeargs, typename))

  def check(self, arg):
    """
    Assumes the argument is valid python code.  It evaluates the code
    and returns the result.

    @param arg: the time argument
    @type  arg: string

    @returns: the number of seconds since the epoch
    @rtype: int

    @raise ParserException: if the arg is not a valid time
    """
    time = utils.parse_time(arg)
    if time != None:
      return time
    else:
      raise ParserException("Invalid time specified %s" % (arg,))

typecheckers["time"] = TimeChecker
  
class ChoiceChecker(TypeChecker):
  """
  Allows for a value to come from a selection of different strings.
  Automatically expands to one of them if it is uniquely specified.

  Typeargs should be a |-delimitted list of possibly values
  """
  def __init__(self, typename, typeargs):
    """
    Uses the typeargs to set the choices availble.
    """
    if not typeargs:
      raise ParserException("TypeArgs (%s) not specified for %s type - must allow at least one choice." % (typeargs, typename) )
    self._choices = typeargs.split("|")

  def check(self, arg):
    """
    Checks the arg against a series of choices that were presented
    when this checker was instantiated.  So you could say that valid
    values of this argument are "high" and "low" and if the argument
    value the user passed in was "blue", then we would raise a 
    ParserException since that's not a valid value.

    @param arg: the argument
    @type  arg: string

    @raise ParserException: if the arg is not a valid choice
    """
    possibilities = []
    for item in self._choices:
      if item.find(arg) == 0:
        possibilities.append(item)
        if len(item) == len(arg):
          return item
    if len(possibilities) == 0 or len(possibilities) > 1:
      raise ParserException("Invalid argument, must be one of %s." % (self._choices,))
    else:
      return possibilities[0]

typecheckers["choice"] = ChoiceChecker

class ReChecker(TypeChecker):
  """
  Compiles the incoming argument as a regular expression.
  """
  def __init__(self, typename, typeargs):
    if typeargs:
      raise ParserException("TypeArgs (%s) specified for non-configurable type (%s)" % (typeargs, typename))

  def check(self, arg):
    """
    Converts the string into a regular expression.  Raises whatever
    exceptions re.compile raises.

    @param arg: the argument
    @type  arg: string

    @return: the compiled regular expression
    @rtype: Re
    """
    return re.compile(arg)

typecheckers["re"] = ReChecker

if __name__ == '__main__':
  testargs = {
    ("arg1 arg2 arg3* arg4**",None):["test1 test3 test5 test7 help=wahoo woo=weewee"],
    ("mapname*",None):["3k mapper by notadragon","lalala"],
    ("mapname*","noparsing"):["3k mapper by notadragon"],
    ("option* quiet:boolean=true",None):["a b c quiet=false d","a b c quiet=true","x b c"]} 

  for argspec,argoptions in testargs.items():
    argparser = ArgumentParser(argspec,argoptions)
    print(f"Argspec: {argspec}")
    if argoptions: print(f"Argopts: {argoptions}")
    for args in testargs[(argspec,argoptions)]:
      print(f"Args   : {args}")
      print(f"Dict   : {argparser.parse(args)}")

# Local variables:
# mode:python
# py-indent-offset:2
# tab-width:2
# End:
