1 """!ATParser is a text parser that replaces strings with variables and
4 import sys, os, re, StringIO, logging
9 functions=dict(lc=
lambda x:str(x).lower(),
10 uc=
lambda x:str(x).upper(),
11 len=
lambda x:str(len(x)),
12 trim=
lambda x:str(x).strip())
15 """!Raised when the parser encounters a syntax error."""
17 """!Raised when a script @[VARNAME:?message] is encountered, and
18 the variable does not exist."""
20 """!Raised when an "@** abort" directive is reached in a script."""
22 """!Raised when a script requests an unknown variable."""
24 """!NoSuchVariable constructor
25 @param infile the input file that caused problems
26 @param varname the variable that does not exist
27 @param line the line number of the problematic line"""
37 '%s:%s: undefined variable %s'%(infile,line,varname))
48 """!Turns \\t to tab, \\n to end of line, \\r to carriage return, \\b to
49 backspace and \\(octal) to other characters.
50 @param text the text to scan"""
51 if '0123456789'.find(text[1])>=0:
52 return chr(int(text[1:],8))
68 outer=dict(active=
True,in_if_block=
False,in_ifelse_block=
False,used_if=
False,ignore=
False)
72 if_unused_if=dict(active=
False,in_if_block=
True,in_ifelse_block=
False,used_if=
False,ignore=
False)
76 if_active_if=dict(active=
True,in_if_block=
True,in_ifelse_block=
False,used_if=
True,ignore=
False)
80 if_used_if=dict(active=
False,in_if_block=
True,in_ifelse_block=
True,used_if=
True,ignore=
False)
84 if_active_else=dict(active=
True,in_if_block=
False,in_ifelse_block=
True,used_if=
True,ignore=
False)
88 if_inactive_else=dict(active=
False,in_if_block=
False,in_ifelse_block=
True,used_if=
True,ignore=
False)
92 ignore_if_block=dict(active=
False,in_if_block=
True,in_ifelse_block=
False,used_if=
False,ignore=
True)
96 ignore_else_block=dict(active=
False,in_if_block=
False,in_ifelse_block=
True,used_if=
False,ignore=
True)
99 """!Takes input files or other data, and replaces certain strings
100 with variables or functions.
102 The calling convention is quite simple:
104 ap=ATParser(varhash={"NAME":"Katrina", "STID":"12L"})
105 ap.parse_file("input-file.txt")
106 lines="line 1\nline 2\nline 3 of @[NAME]"
107 ap.parse_lines(lines,"(string-data)")
108 ap.parse_stream(sys.stdin,"(stdin)")
111 Inputs are general strings with @@[...] and @@** escape sequences which
112 follow familiar shell syntax (but with @@[...] instead of ${...}):
114 My storm is @[NAME] and the RSMC is @[RSMC:-${center:-unknown}].
116 In this case, it would print:
118 My storm is Katrina and the RSMC is unknown.
120 since NAME is set, but RSMC and center are unset.
122 There are also block if statements:
126 @** elseif name==KATRINA
133 and a variety of other things:
135 @[<anotherfile.txt] # read another file
136 @[var=value] # assign a variable
137 @[var:=value] # assign a variable and insert the value in the output stream
138 @[var2:?] # abort if var2 is not assigned, otherwise insert var2's contents
139 @[var3==BLAH?thencondition:elsecondition] # if-then-else substitution
140 @[var3!=BLAH?thencondition:elsecondition] # same, but with a "not equal"
141 @[var4:-substitution] # insert var4, or this substitution if var4 is unset
142 @[var5:+text] # insert text if var5 is set
145 There are also a small number of functions that modify text before
146 it is sent to stdout. (The original variable is unmodified, only
147 the output text is changed.)
149 @[var1.uc] # uppercase value of var1
150 @[var1.lc] # lowercase value of var1
151 @[var1.len] # length of var1
152 @[var1.trim] # var1 with leading and trailing whitespace removed
155 def __init__(self,stream=sys.stdout,varhash=None,logger=None,
157 """!ATParser constructor
158 @param stream the output stream
159 @param varhash a dict of variables. All values must be strings.
160 If this is unspecified, os.environ will be used.
161 @param logger the logging.Logger to read.
162 @param max_lines the maximum number of lines to read"""
178 """!Print a warning to the logger, if we have a logger.
180 @param text the warning text."""
182 self.__logger.warn(text)
185 """!The maximum number of lines to read."""
189 """!The current input file name."""
191 def _write(self,data):
192 """!Write data to the output stream
193 @param data the data to write."""
194 self.__stream.write(data)
196 """!Applies a function to text.
198 @param fun1 the function to apply
199 @param morefun more functions to apply
201 runme=functions.get(fun1,
None)
202 if runme
is not None:
204 if val
is None: val=
''
207 'Ignoring unknown function \"%s\" -- I only know these: %s'
208 %(fun1,
' '.join(functions.keys())))
209 m=re.match(
'\.([A-Za-z0-9_]+)(.*)',morefun)
211 (fun2,morefun2)=m.groups()
212 return self.
applyfun(val,fun2,morefun2)
216 """!Return the value of a variable with functions applied.
217 @param varname the variable name, including functions
218 @param optional if False, raise an exception if the variable is
219 unset. If True, return '' for unset variables.
221 m=re.match(
'([A-Za-z0-9_]+)\.([A-Za-z0-9_]+)(.*)',varname)
223 (varname,fun1,morefun)=m.groups()
224 val=self.
from_var(varname,optional=optional)
225 return self.
applyfun(val,fun1,morefun)
234 """!Return the value of a variable with functions applied, or
235 '' if the variable is unset.
236 @param varname the name of the variable.
238 return self.
from_var(varname,optional=
True)
241 """!Return the value of a variable with functions applied,
242 raising an exception if the variable is unset.
243 @param varname the name of the variable.
245 return self.
from_var(varname,optional=
False)
248 """!Expand @@[...] blocks in a string.
249 @param text the string
250 @returns a new string with expansions performed
252 (text,n) = re.subn(
r'(?<!\\)\$[a-zA-Z_0-9.]+',
255 (text,n) = re.subn(
r'(?<!\\)\$\{[^{}]*\}',
258 (text,n) = re.subn(
r'\\([0-9]{3}|.)',
262 """!Read a stream and parse its contents
263 @param stream the stream (an opened file)
264 @param streamname a name for this stream for error messages"""
271 """!Read a file and parse its contents.
272 @param filename the name of this file for error messages"""
274 with open(filename,
'rt')
as f:
280 """!Read the contents of a file and return it.
281 @param filename_pattern a filename with ${} or @@[] blocks in it.
284 with open(filename,
'rt')
as f:
288 """!Return the value of a variable, or None if it is unset."""
292 """!Expand one ${...} or @@[...] block
293 @param data the contents of the block
295 m=re.match(
r'(?ms)\A([a-z_A-Z][a-zA-Z_0-9]*)'
296 r'((?:\.[A-Za-z0-9.]+)?)'
297 r'(?:(==|!=|:\+|:-|=|:=|:\?|<|:<|:)(.*))?\Z',
301 (varname,functions,operator,operand)=m.groups()
303 if operand
is None: operand=
''
304 vartext=self.
getvar(varname)
305 varset = vartext
is not None and vartext!=
''
307 if vartext
is None: varetext=
''
308 mf=re.match(
r'\A\.([A-Z0-9a-z_]+)(.*)\Z',functions)
309 (fun,morefun)=mf.groups()
310 vartext=self.
applyfun(vartext,fun,morefun)
318 if val
is None: val=
''
319 mo=re.match(
r'\A([0-9]+)(?::([0-9]+))?',operand)
322 (start,count)=mo.groups()
324 if start
is None or start==
'':
330 if count
is None or count==
'':
334 if start+count>length:
336 return val[ start : (start+count) ]
340 elif operator==
'==' or operator==
'!=':
343 mo=re.match(
r'(?ms)\A((?:[^\\\?]|(?:\\\\)*|(?:\\\\)*\\.)*)\?(.*?):((?:[^\\:]|(?:\\\\)*|(?:\\\\)*\\.)*)\Z',operand)
345 (test,thendo,elsedo)=(
'',
'',
'')
347 (test,thendo,elsedo)=mo.groups()
351 thendo
if (val==test)
else elsedo)
354 thendo
if (val!=test)
else elsedo)
364 'variable. Aborting.'%(varname,))
367 elif varname
is not None and varname!=
'':
371 "Don't know what to do with text \"%s\""%(data,))
374 """!Expand text within an @@[...] block.
375 @param data the contents of the block
383 if data.find(
'@[')>=0:
384 raise ParserSyntaxError(
'Found a @[ construct nested within a comment (@[#...])')
391 """!Return a string description of the parser stack for debugging."""
392 out=StringIO.StringIO()
393 out.write(
'STATE STACK: \n')
397 out.write(
'ignoring block: ')
398 out.write(
'active ' if(state[
'active'])
else 'inactive ')
399 if state[
'in_if_block']:
400 out.write(
'in if block, before else ')
401 if state[
'in_ifelse_block']:
402 out.write(
'in if block, after else ')
403 if not state[
'in_if_block']
and not state[
'in_ifelse_block']:
404 out.write(
'not if or else')
406 out.write(
'(have activated a past if/elseif/else) ')
415 """!Is the current block active?
419 if not state[
'active']:
424 """!Return the top parser state without removing it
425 @param what why the state is being examined. This is for
430 raise AssertionError(
'Internal error: no state to search when looking for %s in top state.'%(what,))
431 elif what
not in self.
_states[-1]:
432 raise AssertionError(
'Internal error: cannot find %s in top state.'%(what,))
433 return bool(self.
_states[-1][what])
438 """!Push a new state to the top of the parser state stack
440 self._states.append(state)
443 """!Remove and return the top parser state
445 return self._states.pop()
448 """!Replace the top parser state.
450 @param state the new parser state"""
454 """!Given a multi-line string, parse the contents line-by-line
455 @param lines the multi-line string
456 @param filename the name of the file it was from, for error messages"""
458 for line
in lines.splitlines():
463 """!Parses one line of text.
464 @param line the line of text.
465 @param filename the name of the source file, for error messages
466 @param lineno the line number within the source file, for
471 m=re.match(
r'^\s*\@\*\*\s*if\s+([A-Za-z_][A-Za-z_0-9.]*)\s*([!=])=\s*(.*?)\s*$',line)
479 (left,comp,right)=m.groups()
496 m=re.match(
r'^\s*\@\*\*\s*abort\s+(.*)$',line)
499 raise ScriptAbort(
'Found an abort directive on line %d: %s'%(
503 m=re.match(
r'^\s*\@\*\*\s*warn\s+(.*)$',line)
509 m=re.match(
'^\s*\@\*\*\s*else\s*if\s+([A-Za-z_][A-Za-z_0-9.]*)\s*([!=])=\s*(.*?)\s*\Z',line)
512 (left, comp, right) = m.groups()
517 'Found an elseif without a matching if at line %d'%lineno)
521 'Unexpected elseif after an else at line %d'%lineno)
524 'Unexpected elseif at line %d'%lineno)
533 activate = 3
if (comp==
'=')
else 0
535 activate = 0
if (comp==
'=')
else 3
540 m=re.match(
r'^\s*\@\*\*\s*else\s*(?:\#.*)?$',line)
558 m=re.match(
r'^\s*\@\*\*\s*endif\s*(?:\#.*)?$',line)
566 m=re.match(
r'^\s*\@\*\*\s*insert\s*(\S.*?)\s*$',line)
573 m=re.match(
r'^\s*\@\*\*\s*include\s*(\S.*?)\s*$',line)
581 m=re.match(
r'^\s*\@\*\*.*',line)
583 raise ParserSyntaxError(
'Invalid \@** directive in line \"%s\". Ignoring line.\n'%(line,))
590 (outline,n)=re.subn(
r'\@\[((?:\n|[^\]])*)\]',
593 if not isinstance(outline,basestring):
594 raise TypeError(
'The re.subn returned a %s %s instead of a basestring.'%(type(outline).__name__,repr(outline)))
597 raise ParserLineLimit(
'Read past max_lines=%d lines from input file. Something is probably wrong.'%self.
max_lines)
def from_var(self, varname, optional)
Return the value of a variable with functions applied.
def __init__
NoSuchVariable constructor.
Raised when a script @[VARNAME:?message] is encountered, and the variable does not exist...
def replace_state(self, state)
Replace the top parser state.
def applyfun(self, val, fun1, morefun)
Applies a function to text.
def warn(self, text)
Print a warning to the logger, if we have a logger.
Raised when an "@** abort" directive is reached in a script.
def require_data(self, data)
Expand text within an @[...] block.
def max_lines(self)
The maximum number of lines to read.
def require_var(self, varname)
Return the value of a variable with functions applied, raising an exception if the variable is unset...
def replace_vars(self, text)
Expand @[...] blocks in a string.
def var_or_command(self, data)
Expand one ${...} or @[...] block.
line
The line number that caused the problem.
def parse_file(self, filename)
Read a file and parse its contents.
def parse_lines(self, lines, filename)
Given a multi-line string, parse the contents line-by-line.
def __init__
ATParser constructor.
infile
The file that caused the problem.
Raised when a script requests an unknown variable.
def infile(self)
The current input file name.
def replace_backslashed(text)
Turns \t to tab, \n to end of line, \r to carriage return, \b to backspace and \(octal) to other char...
def parse_line(self, line, filename, lineno)
Parses one line of text.
def _write(self, data)
Write data to the output stream.
Raised when the parser encounters a syntax error.
def str_state(self)
Return a string description of the parser stack for debugging.
def top_state
Return the top parser state without removing it.
def require_file(self, filename_pattern)
Read the contents of a file and return it.
def pop_state(self)
Remove and return the top parser state.
def parse_stream(self, stream, streamname)
Read a stream and parse its contents.
varhash
The dict of variables.
Takes input files or other data, and replaces certain strings with variables or functions.
def optional_var(self, varname)
Return the value of a variable with functions applied, or '' if the variable is unset.
def active(self)
Is the current block active?
varname
The problematic variable name.
def push_state(self, state)
Push a new state to the top of the parser state stack.
def getvar(self, varname)
Return the value of a variable, or None if it is unset.