#---------------------------------------------------------------------- # pooparse.py JJS 05/21/97 # # This module defines the "CmdDef" class, which defines a comman # format and the corresponding function to be invoked. It also # defines a few utility functions for resolving object references, # etc. # # Generally, the engine can simply call HandleCommand(caller,cmd). #---------------------------------------------------------------------- import poo from types import * from qsplit import * import string import regex import copy # A Qualifier is something that takes a string, and figures # out whether it meets certain criteria. For example, the # string may be required to evaluate to a positive number, # or be an object reference, etc. # global dictionary of qualifiers QUALS = {} # global dict mapping object names to objects gObjNames = {} # global variable used to define "this", i.e., the object # on which the currently considered CmdDef was found gThis = None class Qualifier: def __init__(self,name): global QUALS self.name = name self.specificity = 1 QUALS[name] = self def tagpat(self): return "\(.+\)" def matches(self,s,caller=None): # by default, anything matches (except a null string) return (s != "") def value(self,s,caller=None): # by default, just return the string as-is return s class QualStr(Qualifier): def value(self,s,caller=None): # strip quotes if found if s[0] == '"' or s[0] == "'": if s[-1] == s[0]: s = s[:-1] s = s[1:] return s QualStr("str") class QualPropname(QualStr): def tagpat(self): return "\([a-zA-Z0-9_]+\)" def value(self,s,caller=None): if s[0] == '@': return 'at_' + s[1:] return s QualPropname("propname") class QualFuncdef(QualStr): def tagpat(self): return "\([a-zA-Z0-9_]+(.*)\)" QualFuncdef("funcdef") class QualInteger(Qualifier): def __init__(self,name): Qualifier.__init__(self,name) self.specificity = self.specificity + 10 def tagpat(self): return "\(-?[0-9]+\)" def value(self,s,caller=None): return string.atoi(s) QualInteger("int") class QualObj(Qualifier): def __init__(self,name): Qualifier.__init__(self,name) self.specificity = self.specificity + 10 def tagpat(self): return "\(#[0-9\.]+\|$[a-zA-Z0-9_\.]+\|[a-zA-Z0-9_]+.*\)" def matches(self,s,caller): if s[0] == '#': return 1 if s[0] == '$': return 1 # next line is redundant, because these are in gObjNames, right? if s == 'me' or s == 'self' or s == 'caller' or s == 'here': return 1 if '.' in s: return 1 if s in gObjNames.keys(): return 1 pos = string.find(s, "'s") if pos > 0: # have something like "Bob's hat" objname = s[:pos] if objname in gObjNames.keys(): return 1 return 0 def value(self,s,caller): if s == 'me' or s == 'self' or s == 'caller': return caller if s == 'here': return caller.location if s == 'it': return caller.it if s[0] == '#' or s[0] == '$': ob = poo.getObj(s) return poo.getObj(s) if '.' in s: parts = string.split(s,'.') try: obj = gObjNames[parts[0]] except: return None for p in parts[1:]: try: obj = getattr(obj,p) except: return None return obj try: return gObjNames[s] except: pass pos = string.find(s, "'s") if pos > 0: # have something like "Bob's hat" try: return gObjNames[s[:pos]].findComponent(s[pos+3:]) except:pass return None QualObj("obj") class QualThis(QualObj): def matches(self,s,caller): # NOTE: this method assumes that gThis has been set to # the object on which this qualifier was found! return QualObj.matches(self,s,caller) and \ QualObj.value(self,s,caller) == gThis def value(self,s,caller): return gThis QualThis("this") class QualVal(Qualifier): def __init__(self,name): Qualifier.__init__(self,name) self.specificity = self.specificity + 5 def matches(self,s,caller): return 1 # ANYTHING will work as a <val> def value(self,s,caller): # try it as a number try: return string.atoi(s) except: pass # try it as an object out = QUALS['obj'].value(s,caller) if out: return out # check for special keywords if s == 'None' or s == 'none': return None # well, maybe it's a list if (s[0]=='(' and s[-1]==')') or (s[0]=='[' and s[-1]==']'): s = s[1:-1] listparts = qsplit_list(s) else: listparts = qsplit_list(s) if len(listparts) < 2: listparts = None if listparts: # break into pieces, and get the value of each one return map(lambda x,me=self,c=caller:me.value(x,c), listparts ) # see if it's a quoted string if s[0] == '"' or s[0] == "'": return QUALS['str'].value(s,caller) # none of the above? pass it as a string (for now!) return s QualVal("val") #---------------------------------------------------------------------- # CLASS: CmdDef # # CmdDef takes a pattern with literal words and tags of # the form "<tag>" (angle-brackets included). # It can then compare this to a string and if it matches, # return a list of words which fit in for each tag. #---------------------------------------------------------------------- class CmdDef: """CLASS CmdDef: This class implements a pattern-matcher, where the pattern is formed of words and tags. It can compare its pattern to a string and if it matches, retur a list of words which fit in for each tag. """ def __init__(self,owner=None,pat='',funcdef=''): self.__dict__['pat'] = pat self.__dict__['owner'] = owner if not owner: # no owner? either unpickling, or someone is hacking... if poo.gOwner: raise "NoWayJose", "use setCmdDef to create a command!" return # compile the given pattern self.compile() # store the function definition as well (sans any spaces) self.__dict__['funcdef'] = string.replace(funcdef,' ','') #------------------------------------------------------------------ # CmdDef method: compile # build an actual regular expression from the given pattern #------------------------------------------------------------------ def compile(self): pat = self.pat # first, escape any special-meaning characters if '.' in pat: pat = string.join(string.split(pat,'.'), '\.') if '$' in pat: pat = string.join(string.split(pat,'$'), '\$') if '*' in pat: pat = string.join(string.split(pat,'*'), '\*') charsdone = 0 inlen = len(pat) rexp = "" tags = [None] while charsdone < inlen: tagstart = string.find(pat, '<', charsdone) if tagstart >= 0: tagend = string.find(pat, '>', tagstart) if tagend < 0: raise "FormatError", "Unmatched '<' in: " + pat tag = pat[tagstart+1:tagend] tags.append( QUALS[tag] ) rexp = rexp + pat[charsdone:tagstart] \ + QUALS[tag].tagpat() charsdone = tagend + 1 else: rexp = rexp + pat[charsdone:] charsdone = inlen rexp = rexp + '$' # match end-of-line self.__dict__['rex'] = regex.compile(rexp) self.__dict__['tags'] = tuple(tags) #------------------------------------------------------------------ # CmdDef pickling support #------------------------------------------------------------------ def __getstate__(self): try: del self.__dict__['tagvals'] except: pass try: del self.__dict__['it'] except: pass temp = copy.copy(self.__dict__) del temp['rex'] del temp['tags'] try: del temp['boundTo'] except: pass return temp #------------------------------------------------------------------ # CmdDef unpickling support #------------------------------------------------------------------ def __setstate__(self,value): for key in value.keys(): self.__dict__[key] = value[key] self.compile() #------------------------------------------------------------------ # CmdDef attribute protection #------------------------------------------------------------------ def __setattr__(self,prop,value): gOwner = poo.gOwner if gOwner and gOwner != self.owner: raise "PermError", "Unable to modify " + str(self) self.__dict__[prop] = value #------------------------------------------------------------------ # CmdDef conversion to string #------------------------------------------------------------------ def __str__(self): return "CmdDef(" + self.pat + ")" #------------------------------------------------------------------ # CmdDef METHOD: matches # See if this CmdDef matches the given command string (considering # the current caller, and what we're bound to). If it does, # return a number indicating the specificity of the match. # If not, return 0. #------------------------------------------------------------------ def matches(self,s,caller): global gThis self.__dict__['it'] = None if self.rex.match(s) < 0: return 0 # pattern matches... now check each tag self.__dict__['tagvals'] = [None] specificity = len(self.pat) gThis = self.boundTo for i in range(1,len(self.tags)): s = self.rex.group(i) if not self.tags[i].matches(s,caller): return 0 val = self.tags[i].value(s,caller) if not self.__dict__['it'] and type(val) == InstanceType: self.__dict__['it'] = val self.tagvals.append( self.tags[i].value(s,caller) ) specificity = specificity + self.tags[i].specificity return specificity #------------------------------------------------------------------ # CmdDef METHOD: group #------------------------------------------------------------------ def group(self,*args): return apply(self.rex.group, args) #------------------------------------------------------------------ # CmdDef METHOD: value #------------------------------------------------------------------ def value(self,num): return self.tagvals[num] #------------------------------------------------------------------ # CmdDef METHOD: untag # given a tag string, e.g. "caller" or "%2", return an object # reference, or raise an exception. #------------------------------------------------------------------ def untag(self,tag,caller): if tag[0] == '%': return self.tagvals[string.atoi(tag[1:])] if tag == 'caller': return caller if tag == 'here': return caller.location if tag == 'None': return None # hmm, none of the above? Try int and string as well if tag[0] == '"' or tag[0] == "'": return tag[1:-1] try: return string.atoi(tag) except: raise "TagError", "Can't resolve tag value: " + tag #------------------------------------------------------------------ # CmdDef METHOD: callFunc # call the function associated with this CmdDef #------------------------------------------------------------------ def callFunc(self, caller): gThis = self.boundTo # Find the object on which the function was found. # This is set in GetCmdsToCheck. funcobj = self.boundTo periodpos = -1 #periodpos = string.find(self.funcdef,'.') parenpos = string.find(self.funcdef,'(') if parenpos < 0: parenpos = len(self.funcdef) #print "periodpos:", periodpos, " parenpos:",parenpos #if periodpos > 1 and periodpos < parenpos: # funcobj = self.untag(self.funcdef[:periodpos]) # find the name of the function funcname = self.funcdef[periodpos+1:parenpos] # make sure it exists and is callable func = getattr(funcobj,funcname) if not callable(func): raise "CmdError", funcobj.name + '.' + funcname + " is not callable." # now, parse each parameter args = [] if parenpos < len(self.funcdef)-2: for i in string.split(self.funcdef[parenpos+1:-1], ','): args.append( self.untag( i, caller ) ) # if 'x' flag is set, just call it the simple way if func.x: return apply(func, tuple(args)) # if 'x' flag is not set, temporarily set it just for this call func.__dict__['x'] = 1 try: out = apply(func, tuple(args)) finally: func.__dict__['x'] = 0 return out #---------------------------------------------------------------------- # FUNCTION: GatherObjNames # # This function stuffs gObjNames with all the objects local # to the caller. They are stuffed in an order which should # (hopefully) retain the most specific one in case of duplicates. #---------------------------------------------------------------------- def GatherObjNames(caller): global gObjNames objlists = [caller.contents(), caller.location.contents()] # first, stuff special words gObjNames = { 'me':caller, 'caller':caller, 'self':caller, 'here':caller.location } if caller.it: gObjNames['it'] = caller.it # then, stuff aliases for ol in objlists: for obj in ol: aliases = obj.aliases if aliases: for alias in obj.aliases: gObjNames[alias] = obj # then, stuff the actual names for ol in objlists: for obj in ol: gObjNames[obj.name] = obj #---------------------------------------------------------------------- # FUNCTION: ExtractObjects # # This function returns a list of objects which may be referred # to in the given string. It groks anything that starts with # '#' or '$', or that matches names in gObjNames. #---------------------------------------------------------------------- def ExtractObjects(caller, cmdstr, initialList=None): global gObjNames if initialList: out = initialList else: out = [] words = qsplit(cmdstr) for w in filter(lambda x:x,words): if w[0] == '#' or w[0] == '$': try: obj = poo.getObj(w) except: obj = None if type(obj) != InstanceType or \ (obj.__class__ != poo.Obj and \ obj.__class__ != poo.User and \ obj.__class__ != poo.Directory): obj = None elif w in gObjNames.keys(): obj = gObjNames[w] # LATER: handle multi-word object names here else: obj = None if obj and obj not in out: out.append(obj) return out #---------------------------------------------------------------------- # FUNCTION: HandleCommand # # This function takes a normal command (i.e., NOT an immediate-mode # line of Python code) and attempts to execute it. It will search # the local objects, etc., to match the object references. #---------------------------------------------------------------------- def HandleCommand(caller, cmdstr): global gCmdDefs, gObjNames # get local object names GatherObjNames(caller) # pre-processing... if cmdstr[0] == '"' or cmdstr[0] == "'": cmdstr = "say " + cmdstr elif cmdstr[0] == ':': cmdstr = "emote " + cmdstr[1:] # get list of referenced objects, i.e., # objects on which the commands might be defined objs = ExtractObjects( caller, cmdstr, [caller, caller.location] ) # find the best command match -- that is, the one that's most specific verb = string.split(cmdstr)[0] bestcmd = None bestspec = 0 for ob in objs: # for each object, we need to get the CmdDefs, and also # bind those CmdDefs to this object so we know what # object to treat as "self" when we invoke the function! defs = ob.getCmdDef(verb) if verb == ob.name or verb in ob.aliases: defs = defs + ob.getCmdDef('<this>') for c in defs: # bind the CmdDef to the object where it was found c.__dict__['boundTo'] = ob # check for a match -- keep only the best! matchspec = c.matches(cmdstr,caller) if matchspec and matchspec > bestspec: bestspec = matchspec bestcmd = c bestcmdobj = ob if bestcmd: bestcmd.__dict__['boundTo'] = bestcmdobj #bestcmd.matches(cmdstr) # call this again, to restore arg vals if bestcmd.it: caller.it = bestcmd.it return bestcmd.callFunc(caller) # if command doesn't match anything, suggest proper format pats = [] for ob in objs: for c in ob.getCmdDef(verb): p = string.join(string.split(c.pat,'<this>'),ob.name) if p not in pats: pats.append(p) if pats: print "Improper command. Try something like the following:" for p in pats: print " ", p else: print 'Huh? (Verb "' + verb + '" not found.)' return