# PSPScriptParse.py...  This file does the parsing for the script editor.
#

import sys, re, string
import PSPScriptMsgs
import PSPScriptPyBlocks


# * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
#
#   this section contains the re pattern matching statements...
#
# * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *

PattImportPSP = re.compile(r"\bfrom\s*PSPApp\s*import\s*\*")
PattImportJasc = re.compile(r"\bfrom\s*JascApp\s*import\s*\*")
PattImportGeneral = re.compile(r"\bfrom\s*\w*\s*import\s*\*")
PattDefDo = re.compile(r"\bdef\s*Do\(Environment\)\:")
PattScriptProp = re.compile(r"\bdef\s*ScriptProperties\(\)\:\s*")
PattScriptPropLine2 = re.compile(r"\s*\breturn\s*{\s*")
PattBlankLine = re.compile(r"\s*\n")
PattCommentLine = re.compile(r"\s*#")
PattCloseParen = re.compile(r"\s*\)")
PattDocBlock = re.compile(r"\s*'''")
PattCloseBracket = re.compile(r"\s*\}")
PattBlankCmtLine = re.compile("\s*#\s*\n")
PattCloseBracketCmtLine = re.compile("\s*#\s*}\)")
#   match PSPCommand.
#       group(0) -> entire string
#       group(1) -> App.do(
#       group(2) -> quoted PSP Command Name
#       group(3) -> {
PattPSPCmd = re.compile(r"(\s*App\.Do\(\s*Environment,\s*)" +
						r"(\'[\w*\s]+\')+\," +
						r"(\s*{\s*)")
PattCR = re.compile(r"\s*\n")
PattBracketParen = re.compile(r"\s*}\)")

#   match simple PSP parameter.  Match through the ':'
#       group(1) -> parameter name
#  PattPSPParmSimple = re.compile(r"(\s*\'\s??\w*\s??\w*\')+\s*:\s*")
'''
PattPSPParmSimple = re.compile(r"(\s*\')" +
								r"(\s??\w*\s??\w*)" +
								r"\'+\s*:\s*")
'''

PattPSPParmSimple = re.compile(r"(\s*\')" +
							   r"([\w*\s]+)" +
							   r"\'+\s*:\s*")

#   match PSP parameter Start Dictionary.  Find parm name and match
#       up through the opening brace
#       group(1) -> parameter name
PattPSPParmStrDict = re.compile(r"(\s*\'\w*\')+\s*:\s*{")


# * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
# Class: ParseScript
# Author: Carl Lestor
# Purpose: This class handles the parsing of the python script file.  Its main job
#   is to parse the file and build up 'pyBlocks'.  These objects represent blocks
#   of python code, psp commands, script descriptions and the like.  
# Returns: none
# Parameters:   fp - file handle for the file to parse.
#               pyBlockPtr = place to store the python blocks parsed   
# Notes: Errors are sent to std out; the script output window.
# * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
class ParseScript:
	def __init__(self):
		self.pyBlocks = []      # a place to pack hold the python blocks representing the script
		self.fp = 0             # handle of the file
		self.line = ""          # line being parsed
		self.block = ""         # blocks of code built up during parse
		self.linepos = 0        # line position during parse
		#print 'IN PARSE SCRIPT'

		# following data is used for understanding parse failures
		self.prevLine = ""      # previous line
		self.prevPrevLine = ""  # previous previous line
		self.lineNum = 0        # current line being parsed
		self.errorString = ""   # set when a parse fails
		self.filename = ""      # store the file name in case we need to invoke a text editor

# * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
# Class: ParseScript::openFile
# Author: Steve Neumeyer
# Purpose: initialize the file pointer for reading
# Returns: none
# Parameters:   name - the filename to open
# Notes: Simply opens the file and stores the reference in the file member
# * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
	def openFile(self, name):
		try:
			self.filename = name
			self.fp = open(name, 'r')
			return 1
		except:
			return 0        

# * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
# Class: ParseScript::getLine
# Author: Carl Lestor
# Purpose: get a new line of text from the file
# Returns: none
# Parameters:   cmtflag - indicates if we are handling a commented psp command
# Notes: Much of the parsing shares the logic if we are parsing a commented psp
#       command or a non-commented one; hence the input flag.  This method
#       silently skips the splat if we are doing comments.
# * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
	def getLine(self, cmtFlag):
		# if requested, handle comment lines by moving the line position past the opening comment char
		self.prevPrevLine = self.prevLine       # save prev lines for diagnosis...
		self.prevLine = self.line
		self.line = self.fp.readline()
		self.linepos = 0
		self.lineNum = self.lineNum + 1
		if cmtFlag:
			# caller indicated we are in a state parsing commented material.  skip over the comment
			# portion of the line.  Return 0 if we do not find a comment....
			match = PattCommentLine.match(self.line)
			if match:
				self.linepos = match.end(0)
				return (self.line)
			else:
				return 0
		else:
			return (self.line)
		

# * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
# Class: ParseScript::addPyBlock
# Author: Carl Lestor
# Purpose: get a new line of text from the file
# Returns: none
# Parameters:   type - type of block to add.
# Notes: 
# * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
	def addPyBlock(self, type):
		# set up the block that was found.
		pyb = PSPScriptPyBlocks.PyBlock(type, self.block)
		self.pyBlocks = self.pyBlocks + [pyb]
		self.block = ""


# * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
# Class: ParseScript::parseFail
# Author: Carl Lestor
# Purpose: simply a place to handle parse errors consistently.
# Returns: none
# Parameters:   error.  String describing the type of error ecountered.  Abstracted to
#       a unique file for translation ease.
# Notes: 
# * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
	def parseFail(self, error):
		# the parse failed...
##		print 'In parseFail, error = %s' % error
		self.errorString = PSPScriptMsgs.PSPSEmsgs(error)

# * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
# Class: ParseScript::getNextNonEscapedQuoteOld
# Author: Steve Neumeyer
# Purpose: get the next quote that is not escaped with a \ preceeding it
# Returns: the position of the next non-escaped quote mark
#			-1 for error condition
# Parameters:   currentPosition - the current position in the string
#				string - the line of text being searched
# Notes: this could be moved to PSPTools
#			this version only handles single quote marks '
# * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
	def getNextNonEscapedQuoteOld(self, currentPosition, str):
		escapedQuote = "\\'"

		while 1:
			nextQuotePos = string.find(str, "'", currentPosition)
			nextEscapedQuotePos = string.find(str, escapedQuote, currentPosition)
			if nextEscapedQuotePos != -1:
				nextEscapedQuotePos += 1 

			# if the next quote is escaped, search forward from that position until
			# we find a non-escaped quote
			if nextQuotePos == nextEscapedQuotePos:
				currentPosition = 1+nextQuotePos # now we are searching from the last found quote pos
				continue
			else:
				return nextQuotePos

# * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
# Class: ParseScript::getNextNonEscapedQuote
# Author: Steve Neumeyer
# Purpose: get the next quote mark ' or " that is not escaped with a \ preceeding it
# Returns: the position of the next non-escaped quote mark
#			-1 for error condition
# Parameters:   currentPosition - the current position in the string
#				string - the line of text being searched
#				doublequote - 1 or nonzero - we are dealing with a double quote
#							- 0 - single quote
# Notes: updated version to handle ' and " quotes
# * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
	def getNextNonEscapedQuote(self, currentPosition, str, doublequote):
		if doublequote:
			escapedQuote = r'\"'
		else:
			escapedQuote = r"\'"

		while 1:
			if doublequote:
				nextQuotePos = string.find(str, '"', currentPosition)				
			else:
				nextQuotePos = string.find(str, "'", currentPosition)


			nextEscapedQuotePos = string.find(str, escapedQuote, currentPosition)
			if nextEscapedQuotePos != -1:
				nextEscapedQuotePos += 1 

			# if the next quote is escaped, search forward from that position until
			# we find a non-escaped quote
			if nextQuotePos == nextEscapedQuotePos:
				currentPosition = 1+nextQuotePos # now we are searching from the last found quote pos
				continue
			else:
				return nextQuotePos

# * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
# Class: ParseScript::EatValue
# Author: Carl Lestor
# Purpose: eats the value portion of a dictionary
#       entry.  We simply consume all characters until the
#       comma indicating the value has ended.  We must ignore
#       commas embedded in tuples, dicts, and lists....  When
#       successful, we add the value to the codeblock.
# Returns:  success - 1 if good, 0 if parse fails
#           moreLeft - 1 if true, 0 if end of dictionary
#           Value - value parsed
# Parameters:  cmtFlag - indicates if we are parsing a comment. 
# Notes: 
# * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
	def EatValue(self, cmtFlag):

		startoff = self.linepos
		fragToAdd = self.line[startoff:len(self.line)]
		Value = ''

		embedded = 0
		stringEating = 0
		skipNextStrQuotePos = 0
		blobEating = 0
		if self.line[self.linepos] == "'":
			doubleQuote = 0
		elif self.line[self.linepos] == '"':
			doubleQuote = 1
		else:
			doubleQuote = 0
			
		# store the position of the final quote mark in a string
		# this is an index to the last ' or " in a string, depending
		# on what type of quote we started with.  default index is
		# the position of the last ' mark
		#finalQuotePos = string.rfind(self.line, "'")
		nextQuotePos = self.getNextNonEscapedQuote(self.linepos+1, self.line, doubleQuote)
		while 1:
			# get the character and process it....                
			ch = self.line[self.linepos]

			if blobEating:
				# Blobs start and end with triple single quotes - everything else is part of the value
				if ch == '\0' or ch == '\n':
					# New Line in BLOB
					Value = Value + fragToAdd[0:self.linepos + 1 - startoff]
					self.getLine(cmtFlag)
					if not self.line:
						# we hit eof in error. incomplete blob
						self.block = self.block + Value
						return (0, 0, Value)
					fragToAdd = self.line
					self.linepos = 0
					startoff = self.linepos
				elif ch =="\'": 
					nextQuotePos = self.getNextNonEscapedQuote(self.linepos+1, self.line, 0)
					self.linepos = self.linepos + 1
					if nextQuotePos == self.linepos and self.linepos + 1 == self.getNextNonEscapedQuote(self.linepos+1, self.line, 0):
						# Three quotes in a row - End of the blob
						self.linepos = self.linepos + 2
						nextQuotePos = self.getNextNonEscapedQuote(self.linepos, self.line, 0)
						blobEating = 0
				else:
					self.linepos = self.linepos + 1
			elif stringEating:
				# handle strings... end chars in strings must be ignored...            
				if ch == '\0' or ch == '\n':				
					# New Line in String
					Value = Value + fragToAdd[0:self.linepos - 2 - startoff]
					self.getLine(cmtFlag)
					if not self.line:
						# we hit eof in error. incomplete string
						self.block = self.block + Value
						return (0, 0, Value)
					self.linepos = 0
					skipNextStrQuotePos = 1
				elif skipNextStrQuotePos and (ch == "\'" or ch == "\""):
						skipNextStrQuotePos = 0
						startoff = self.linepos + 1
						nextQuotePos = self.getNextNonEscapedQuote(self.linepos+1, self.line, not singleQuote)
						fragToAdd = self.line[startoff:len(self.line)]
				elif singleQuote and ch == "\'" and self.linepos == nextQuotePos: #finalQuotePos:
						stringEating = 0
						singleQuote = 0
				elif not singleQuote and ch == '\"' and self.linepos == nextQuotePos: #finalQuotePos:
						stringEating = 0
				self.linepos = self.linepos + 1
			elif ch =="\'": 
				stringEating = 1
				singleQuote = 1
				#finalQuotePos = string.rfind(self.line, "'")
				nextQuotePos = self.getNextNonEscapedQuote(self.linepos+1, self.line, 0)
				self.linepos = self.linepos + 1
				if nextQuotePos == self.linepos and self.linepos + 1 == self.getNextNonEscapedQuote(self.linepos+1, self.line, 0):
					# Three quotes in a row - Start of a blob
					self.linepos = self.linepos + 2
					nextQuotePos = self.getNextNonEscapedQuote(self.linepos, self.line, 0)
					stringEating = 0
					singleQuote = 0
					blobEating = 1
			elif ch =='\"': 
				stringEating = 1
				singleQuote = 0
				#finalQuotePos = string.rfind(self.line, '"')
				nextQuotePos = self.getNextNonEscapedQuote(self.linepos+1, self.line, 1)
				self.linepos = self.linepos + 1
			# check for ending characters                                    
			elif ch =="," and embedded == 0:
				# end of value, more dictionary to handle....
				Value = Value + fragToAdd[0:self.linepos - startoff]
				self.block = self.block + Value + ch
				self.linepos = self.linepos + 1
				self.chkEOLN(cmtFlag)
				return (1, 1, Value)
			elif ch == "}" and embedded == 0:
				# end of value, end of dictionary....
				Value = Value + fragToAdd[0:self.linepos - startoff]
				self.block = self.block + Value + ch
				self.linepos = self.linepos + 1
				self.chkEOLN(cmtFlag)
				return (1, 0, Value)

			# handle being imbedded in dicts, tuples, or lists...            
			elif (ch == '(' or ch =='{' or ch == '['):
				embedded = embedded + 1
				self.linepos = self.linepos + 1
			elif ch == ')' or ch =='}' or ch == ']':
				embedded = embedded - 1
				self.linepos = self.linepos + 1
 				
			# go right through EOLN for longer parms...
			elif ch == '\0' or ch == '\n':
				# we hit end of line
				Value = Value + fragToAdd[0:self.linepos + 1 - startoff]
				self.getLine(cmtFlag)
				if not self.line:
					# we hit eof in error. incomplete dict or imbalanced parens
					self.block = self.block + Value
					return (0, 0, Value)
				fragToAdd = self.line
				self.linepos = 0
				startoff = self.linepos
			else:
				# any old character
				self.linepos = self.linepos + 1


# * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
# Class: ParseScript::EatDocBlock
# Author: Carl Lestor
# Purpose: eats the contents of a triple quoted documentation block
#       The first triple quoted chars have been found..  When
#       successful, we add the value to the codeblock.
# Returns: success - 1 if good, 0 if parse failure
# Parameters:   none
# Notes: 
# * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
	def eatDocBlock(self):

		startoff = self.linepos
		fragToAdd = self.line[startoff:len(self.line)]
		singleQuoteDepth = 0
	  
		while singleQuoteDepth < 3:
			ch = self.line[self.linepos]
			self.linepos = self.linepos + 1

			if ch == "\'":
				singleQuoteDepth = singleQuoteDepth + 1

			# handle multi line doc blocks...
			elif ch == '\0' or ch == '\n':
				# we hit end of line
				self.block = self.block + fragToAdd[0:self.linepos + 1 - startoff]
				self.getLine(0)
				if not self.line:
					# we hit eof in error. incomplete doc block.
					return (0)
				fragToAdd = self.line
				self.linepos = 0
				startoff = 0
				singleQuoteDepth = 0
			else:
				# any old character
				singleQuoteDepth = 0
		
		# end of doc block found....
		self.block = self.block + fragToAdd[0:self.linepos + 1 - startoff]
		self.chkEOLN(0)
		return (1)
 

# * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
# Class: ParseScript::EatDocBlock
# Author: Carl Lestor
# Purpose: check for end of line.  consume white space as we go...
# Returns: 
# Parameters: chkCmtFlag - indicates if we are parsing a comment or not.
# Notes: 
# * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
	def chkEOLN(self, chkCmtFlag):
		
		while 1:
			ch = self.line[self.linepos]
			if ch == " " or ch == "\n":
				# consume it
				self.linepos = self.linepos + 1
				self.block = self.block + ch
				if self.linepos == len(self.line):
					# we have reached the end of line.
					self.getLine(chkCmtFlag)
					if (chkCmtFlag):
						# we found a new commented line. need to pick up the splat...
						self.block = self.block + self.line[0:self.linepos]
					return (1)
			else: 
				return (1)
	

# * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
# Class: ParseScript::ParseDictionary
# Author: Carl Lestor
# Purpose: parse a dictionary.  
# Returns: success - 1 if good, 0 if parse failure
# Parameters:   OurParms - dictionary or parms being built.  
#               cmtFlag - indicates if we are parsing a comment or not.
# Notes: Finds a dictionary entry and adds it to OurParms
# * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
	def parseDictionary(self, OurParms, cmtFlag):

		# assumes open brace has been consumed. match all
		# and consume the end brace.
		while 1:
			# we either have consumed the line with the re match or there are some
			#   parms also on that line.  if at the end, get a new one....
			if self.linepos >= len(self.line):
				self.getLine(cmtFlag)
				if not self.line:
					return 0
				if (cmtFlag):
					# we found a new commented line. need to pick up the splat...
					self.block = self.block + self.line[0:self.linepos]
			# find a parmName:value pair.  first the name.
			match = PattPSPParmSimple.match(self.line, self.linepos)
			if match:
				# parmName found.  update block and get value.  
				self.block = self.block + match.group(0)
				self.linepos = match.end(0)                
				self.chkEOLN(cmtFlag)
				ok, moreLeft, Value = self.EatValue(cmtFlag)
				if not ok:
					return 0

				# need to trim the spaces from match group1
				OurParms.append([match.group(2), Value])    
				if not moreLeft:
					return 1
			else:
				return 0

			
# * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
# Class: ParseScript::checkState
# Author: Carl Lestor
# Purpose: checks to see if the statement found is valid in the current state
# Returns: 
# Parameters:   state - current state of the parse
#               StatementFound - indicates what type of statement was discovered.
# Notes: The purpose of this method is to provide the user with a more meaningful
#   error msg if the script obviously will not run.  Hopefully this will aid users
#   new to hand editing scripts.  When they invoke the Script Editor, some simple
#   error will be discovered.  This is light and not thorough syntax checking.
# * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
	def checkState(self, state, StatementFound):

		#
		#   state tables.  for each state, the dictionary contains the pair of 'received statement type' and
		#       'new state'.  If the new type contains 'ERROR:' in it. the parse failed.  Error message text
		#       is isloated in PSPScriptMsgs for translation....
		#
		ExpectImport = {"Import":"DefDo",
						"Comment":"Import",
						"Script":"ERROR:Parse11",
						"CmtPSPCmd":"ERROR:Parse12",
						"DefDo":"ERROR:Parse13",
						"PSPCmd":"ERROR:Parse14"}

		ExpectDefDo = {"Import":"ERROR:Parse21",
						"Comment":"DefDo",
						"Script":"DefDo",
						"CmtPSPCmd":"ERROR:Parse22",
						"DefDo":"PSPCmd",
						"PSPCmd":"ERROR:Parse23"}
		
		ExpectPSPCmd = {"Import":"ERROR:Parse31",
						"Comment":"PSPCmd",
						"Script":"PSPCmd",
						"CmtPSPCmd":"PSPCmd",
						"DefDo":"ERROR:Parse32",
						"PSPCmd":"PSPCmd"}

		if state == "Import":
			newstate = ExpectImport[StatementFound]
		elif state == "DefDo":
			newstate = ExpectDefDo[StatementFound]
		else:
			newstate = ExpectPSPCmd[StatementFound]
							
		if newstate.count("ERROR"):
			return (0, newstate)
		else:
			return (1, newstate)
			

	# * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
	# Class: ParseScript::parseScript
	# Author: Carl Lestor
	# Purpose: This is the main method that parses the script.
	# Returns:
	#           success - 1 if good, 0 if bad
	#           PSPCmdCount - number of psp commands found
	# Parameters:   none
	# Notes: The method is a basic loop processing a line at a time of the input file.  Top
	#   level line recognition is done with re pattern matching.  Outer loop looks for
	#   basic types of statements and then parses the details.
	# * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
	def parseScript(self):

		PSPCmdCount = 0
		ExpectedStmt = "Import"
		
		while 1:
			self.getLine(0)
			if not self.line:
				return (1, PSPCmdCount)
			#
			# try to match our line to one of our patterns....  first the import statement
			#
			match = PattImportPSP.match(self.line)
			if match:
				parseOK, ExpectedStmt = self.checkState(ExpectedStmt, "Import")
				if not parseOK:
					self.parseFail(ExpectedStmt)
					return (0, PSPCmdCount)
				self.block = self.line
				self.addPyBlock ("ImportPSP")
				continue
			match = PattImportJasc.match(self.line)
			if match:
				parseOK, ExpectedStmt = self.checkState(ExpectedStmt, "Import")
				if not parseOK:
					self.parseFail(ExpectedStmt)
					return (0, PSPCmdCount)
				self.block = self.line
				self.addPyBlock ("ImportPSP")
				continue
			#
			# match a def do statement
			#
			match = PattDefDo.match(self.line)
			if match:
				parseOK, ExpectedStmt = self.checkState(ExpectedStmt, "DefDo")
				if not parseOK:
					self.parseFail(ExpectedStmt)
					return (0, PSPCmdCount)
				self.block = self.line
				self.addPyBlock ("DefDo Match")
				continue
			#
			# match the script properties
			#
			match = PattScriptProp.match(self.line)
			if match:
				parseOK, ExpectedStmt = self.checkState(ExpectedStmt, "Script")
				if not parseOK:
					self.parseFail(ExpectedStmt)
					return (0, PSPCmdCount)
				self.block = self.line
				# line 1 matches.  match line2.
				self.getLine(0)
				self.linepos = 0
				match = PattScriptPropLine2.match(self.line)
				if match:
#                    self.block = self.line
					self.block = self.block + match.group(0)
					self.linepos = match.end(0)
					parmList = []
					if self.parseDictionary(parmList, 0):
						self.addPyBlock ("Script Properties")
#                        print "parmList = ", parmList      # may not need parmList of dict items...
					else:
						# failed to parse script properties parameters
						self.parseFail("ERROR:Parse41")
						return (0, PSPCmdCount)
				else:
					# failed to parse script properties return statement
					self.parseFail("ERROR:Parse42")
					return (0, PSPCmdCount)
			#
			# match a psp command
			#
			match = PattPSPCmd.match(self.line)
			if match:
				parseOK, ExpectedStmt = self.checkState(ExpectedStmt, "PSPCmd")
				if not parseOK:
					self.parseFail(ExpectedStmt)
					return (0, PSPCmdCount)
				# set the end to the last match position.  group 3....
				self.block = match.group(0)
				self.linepos = match.end(0)
				parmList = []
				if self.parseDictionary(parmList, 0):
					# we have parsed the dictionary. pick up the trailing paren
					match = PattCloseParen.match(self.line, self.linepos)
					if match:
						self.block = self.block + match.group(0)
						self.linepos = match.end(0)
					self.addPyBlock ("PSPCmd")
					PSPCmdCount = PSPCmdCount + 1
				else:
					# need to handle the case of a command with no dict/repository
					# match = PattCloseBracket.match(self.line, self.linepos)
					match = PattCR.match(self.line, self.linepos)
					# the command has no dict/repository
					if match: 
						self.block = self.block + match.group(0)
						self.linepos = match.end(0)
						self.getLine(0)
						match = PattBracketParen.match(self.line, self.linepos)
						if match:
							self.block = self.block + match.group(0)
							self.linepos = match.end(0)
						else:	# error
							self.parseFail("ERROR:Parse51")
							return (0, PSPCmdCount)
						self.addPyBlock("PSPCmd")
						PSPCmdCount += 1
					else:   
						# failed to parse psp command parameter dictionary
						
						# csl...  this error msg is non optimal.  mismatched parens can cause the parse
						#       to go deep into the file.  line shown in error obj could be far away from bad
						#       parm list.  (looking for ending paren down the file.)  improvement to add pspcmd to
						#       error msg....
					
						self.parseFail("ERROR:Parse51")
						return (0, PSPCmdCount)
				continue

			#
			# match a commented line or commented psp command
			#
			match = PattCommentLine.match(self.line)
			if match:
				self.linepos = match.end(0)
				if ExpectedStmt == "PSPCmd":
					# could be a commented psp command.  better check for that.  else just a cmt....
					self.block = self.block + match.group(0)
					keepLookingFrom = match.end(0)
					match = PattPSPCmd.match(self.line, keepLookingFrom)
					if match:
						# we have found a comment that starts off like a psp command.  check the parms.
						self.block = self.block + match.group(0)
						self.linepos = match.end(0)
						parmList = []
						if self.parseDictionary(parmList, 1):
							# we have parsed the dictionary. pick up the trailing paren
							match = PattCloseParen.match(self.line, self.linepos)
							if match:
								self.block = self.block + match.group(0)
								self.linepos = match.end(0)
								self.addPyBlock ("CmtedPSPCmd")
								PSPCmdCount = PSPCmdCount + 1
								continue
						else: # commented command with empty dict/repository (ex: UndoLastCmd)
							match = PattBlankCmtLine.match(self.line, 0)
							if match:
								self.block = self.block + match.group(0)[1:]
								self.linepos = match.end(0)
								self.getLine(1)
								match = PattCloseBracketCmtLine.match(self.line, 0)
								if match:
									self.block = self.block + match.group(0)
									self.linepos = match.end(0)
									self.addPyBlock("CmtedPSPCmd")
									PSPCmdCount += 1
									continue
								else:
									self.parseFail("ERROR:Parse51")
									return (0, PSPCmdCount)
							else:
								self.parseFail("ERROR:Parse51")
								return (0, PSPCmdCount)
					else:
						self.block = ''

				# csl.... there are several ways to fall through to this.  UT all...        
				# ended up being a typical comment
				# self.block = self.block + self.line[self.linepos:len(self.line)]
				self.block = self.block + self.line[0:len(self.line)]
				self.addPyBlock ("PythonCmt")
				continue

			#
			# match a doc block (python within a triple quoted block..)
			#
			match = PattDocBlock.match(self.line)
			if match:
				self.block = match.group(0)
				self.linepos = match.end(0)
				if self.eatDocBlock():
					self.addPyBlock ("DocBlock")
				else:
					# failed to find the end of doc block
					self.parseFail("ERROR:Parse52")
					return (0, PSPCmdCount)
				continue

			#
			# match a blank or new line.  these are allowed anywhere...
			#
			match = PattBlankLine.match(self.line)
			if match:
				self.block = self.line
				self.addPyBlock ("BlankLine")
				continue
			
			else:
				#
				# everything else is a general python code block
				self.block = self.line
				self.addPyBlock ("PyCode")


# * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
# Function: DoParse
# Author: Steve Neumeyer
# Purpose: Do a parse and return the result
# Returns: the result of the parse
# Parameters: fname - file to parse
# Notes: 
# * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
def DoParse(fname, sp):

	parseResult = 0
	if sp.openFile(fname):
		parseResult = sp.parseScript()

	return sp, parseResult        
				

# * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
# Function: DriveParse
# Author: Carl Lestor
# Purpose: This is a utility function to test the parse.  
# Returns: 
# Parameters:
#       fname - file to parse
#       printIt - true if we should print to stdout the parse details in addition to logging to the file
# Notes: This is not part of the production product.  Drive UT from external script.  Write basic results
#       to std out and details to output file name derived from input file name...
# * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
def DriveParse(fname, printIt, sp):

	# instance declared in calling function -- sp = ParseScript()
	printString = ""
	if sp.openFile(fname):
		
		# create output file name and set up for logging....
		nameParts = string.split(fname, '.')
		outName = nameParts[0] + "OUT.txt"
		outf = open(outName, 'w')
		
		parseResult = sp.parseScript()

		# print the summary result to stdout and the details to the output file....        
		if parseResult:
			# parse was good
			print "filename:" + fname + "  Parse Result:good"
			outf.write("filename:" + fname + "  Parse Result:good\n\n\n")
			for pyblock in sp.pyBlocks:
				block, cmd = pyblock.dump()
				outf.write("Pyblock:" + cmd + "\n" + block + "\n")
				printString = printString + "Pyblock:" + cmd + "\n" + block + "\n"
		else:
			# parse failure
			print "filename:" + fname + "  " + sp.errorString
			outf.write("filename:" + fname + "  " + sp.errorString + "\n")
			outf.write(sp.prevPrevLine + sp.prevLine + sp.line)
			outf.write("Failing line number:" + str(sp.lineNum) + "    Failing Char Position:" + str(sp.linepos))
			printString = printString + sp.prevPrevLine + sp.prevLine + sp.line
			printString = printString + "Failing line number:" + str(sp.lineNum) + "    Failing Char Position:" + str(sp.linepos)

		if printIt == "dump":
			print printString
																													  
		outf.close()

	else:
		print "failed to open file:", fname

	return sp        
	
######################
if __name__ == '__main__':

	sp = ParseScript()

	DriveParse(fname, "dump", sp)

##    if len(sys.argv) == 3:
##        DriveParse(sys.argv[1], sys.argv[2])
##    elif len(sys.argv) == 2:
##        # assume no stdout dumping is wanted
##        DriveParse(sys.argv[1], "NoDump")
##    else:
##        print "Needs 2 parameters: filename and dump option.  Second parm = 'dump' to send details to stdout."
