Code for barn-growl utility for OS X.
[snippets/.git] / barn-growl / sexpr.py
1 #!/usr/bin/env python
2 ##
3 ##  sexpr.py - by Yusuke Shinyama
4 ##
5 ##  * public domain *
6 ##
7
8 from abstfilter import AbstractFeeder, AbstractFilter, AbstractConsumer
9
10
11 ##  SExprReader
12 ##
13 class SExprReader(AbstractFilter):
14   """Usage:
15   
16   reader = SExprReader(consumer)
17   reader.feed("(this is (sexpr))")
18   reader.close()
19   """
20   
21   COMMENT_BEGIN = ";"
22   COMMENT_END = "\n"
23   SEPARATOR = " \t\n"
24   PAREN_BEGIN = "("
25   PAREN_END = ")"
26   QUOTE = '"'
27   ESCAPE = "\\"
28
29   def __init__(self, next_filter,
30                comment_begin=COMMENT_BEGIN,
31                comment_end=COMMENT_END,
32                separator=SEPARATOR,
33                paren_begin=PAREN_BEGIN,
34                paren_end=PAREN_END,
35                quote=QUOTE,
36                escape=ESCAPE):
37     AbstractFilter.__init__(self, next_filter)
38     self.comment_begin = comment_begin
39     self.comment_end = comment_end
40     self.separator = separator
41     self.paren_begin = paren_begin
42     self.paren_end = paren_end
43     self.quote = quote
44     self.escape = escape
45     self.special = comment_begin + separator + paren_begin + paren_end + quote + escape
46     self.reset()
47     return
48
49   # SExprReader ignores any error and
50   # try to continue as long as possible.
51   # if you want to throw exception however,
52   # please modify these methods.
53   
54   # called if redundant parantheses are found.
55   def illegal_close_paren(self, i):
56     print "Ignore a close parenthesis: %d" % i
57     return
58   # called if it reaches the end-of-file while the stack is not empty.
59   def premature_eof(self, i, x):
60     print "Premature end of file: %d parens left, partial=%s" % (i, x)
61     return
62
63   # reset the internal states.
64   def reset(self):
65     self.incomment = False              # if within a comment.
66     self.inquote = False                # if within a quote.
67     self.inescape = False               # if within a escape.
68     self.sym = ''                       # partially constructed symbol.
69     # NOTICE: None != nil (an empty list)
70     self.build = None                   # partially constructed list.
71     self.build_stack = []     # to store a chain of partial lists.
72     return self
73
74   # analyze strings
75   def feed(self, tokens):
76     for (i,c) in enumerate(tokens):
77       if self.incomment:
78         # within a comment - skip
79         self.incomment = (c not in self.comment_end)
80       elif self.inescape or (c not in self.special):
81         # add to the current working symbol
82         self.sym += c
83         self.inescape = False
84       elif c in self.escape:
85         # escape
86         self.inescape = True
87       elif self.inquote and (c not in self.quote):
88         self.sym += c
89       else:
90         # special character (blanks, parentheses, or comment)
91         if self.sym:
92           # close the current symbol
93           if self.build == None:
94             self.feed_next(self.sym)
95           else:
96             self.build.append(self.sym)
97           self.sym = ''
98         if c in self.comment_begin:
99           # comment
100           self.incomment = True
101         elif c in self.quote:
102           # quote
103           self.inquote = not self.inquote
104         elif c in self.paren_begin:
105           # beginning a new list.
106           self.build_stack.append(self.build)
107           empty = []
108           if self.build == None:
109             # begin from a scratch.
110             self.build = empty
111           else:
112             # begin from the end of the current list.
113             self.build.append(empty)
114             self.build = empty
115         elif c in self.paren_end:
116           # terminating the current list
117           if self.build == None:
118             # there must be a working list.
119             self.illegal_close_paren(i)
120           else:
121             if len(self.build_stack) == 1:
122               # current working list is the last one in the stack.
123               self.feed_next(self.build)
124             self.build = self.build_stack.pop()
125     return self
126
127   # terminate
128   def terminate(self):
129     # a working list should not exist.
130     if self.build != None:
131       # error - still try to construct a partial structure.
132       if self.sym:
133         self.build.append(self.sym)
134         self.sym = ''
135       if len(self.build_stack) == 1:
136         x = self.build
137       else:
138         x = self.build_stack[1]
139       self.build = None
140       self.build_stack = []
141       self.premature_eof(len(self.build_stack), x)
142     elif self.sym:
143       # flush the current working symbol.
144       self.feed_next(self.sym)
145     self.sym = ''
146     return self
147
148   # closing.
149   def close(self):
150     AbstractFilter.close(self)
151     self.terminate()
152     return
153
154
155 ##  StrictSExprReader
156 ##
157 class SExprIllegalClosingParenError(ValueError):
158   """It throws an exception with an ill-structured input."""
159   pass
160 class SExprPrematureEOFError(ValueError):
161   pass
162 class StrictSExprReader(SExprReader):
163   def illegal_close_paren(self, i):
164     raise SExprIllegalClosingParenError(i)
165   def premature_eof(self, i, x):
166     raise SExprPrematureEOFError(i, x)
167   
168
169 ##  str2sexpr
170 ##
171 class _SExprStrConverter(AbstractConsumer):
172   results = []
173   def feed(self, s):
174     _SExprStrConverter.results.append(s)
175     return
176 _str_converter = SExprReader(_SExprStrConverter())
177 _str_converter_strict = StrictSExprReader(_SExprStrConverter())
178
179 def str2sexpr(s):
180   """parse a string as a sexpr."""
181   _SExprStrConverter.results = []
182   _str_converter.reset().feed(s).terminate()
183   return _SExprStrConverter.results
184 def str2sexpr_strict(s):
185   """parse a string as a sexpr."""
186   _SExprStrConverter.results = []
187   _str_converter_strict.reset().feed(s).terminate()
188   return _SExprStrConverter.results
189
190
191 ##  sexpr2str
192 ##
193 def sexpr2str(e):
194   """convert a sexpr into Lisp-like representation."""
195   if not isinstance(e, list):
196     return e
197   return "("+" ".join(map(sexpr2str, e))+")"
198
199
200 # test stuff
201 def test():
202   assert str2sexpr("(this ;comment\n is (a test (sentences) (des()) (yo)))") == \
203          [["this", "is", ["a", "test", ["sentences"], ["des", []], ["yo"]]]]
204   assert str2sexpr('''(paren\\(\\)theses_in\\#symbol "space in \nsymbol"
205                    this\\ way\\ also. "escape is \\"better than\\" quote")''') == \
206          [['paren()theses_in#symbol', 'space in \nsymbol', 'this way also.', 'escape is "better than" quote']]
207   str2sexpr("(this (is (a (parial (sentence")
208   return  
209
210
211 # main
212 if __name__ == "__main__":
213   test()