前のエントリー で簡易S式パーサを re.Scanner
で作ったのですが、まぁ個人的にまとめておいたほうが後々使えるだろう、ということでライブラリにまとめました。ダンパもついているので、S式の読み込みの他、PythonオブジェクトをS式で出力することができます。
実装には引き続き re.Scanner
を活用しています。おかげで短い行数でキレイにかけているのではないかと。
ダウンロード
ソースはこんな感じ(テストのぞく)。
1import re, sys
2from unicodedata import east_asian_width
3
4try:
5 from re import Scanner
6except ImportError:
7 from sre import Scanner
8
9class ParseError(StandardError): pass
10
11class Ident(unicode):
12 def __repr__(self):
13 return "Ident(%s)"%unicode.__repr__(self)
14
15class Symbol(unicode):
16 def __repr__(self):
17 return "Symbol(%s)"%unicode.__repr__(self)
18
19class Pair(list):
20 def __repr__(self):
21 return "Pair(%s)"%list.__repr__(self)
22
23class Token(object):
24 def __init__(self, value, pos):
25 self.value = value
26 self.pos = pos
27 def __repr__(self):
28 return repr(self.value)
29
30class Binding(object):
31 def __init__(self, dct):
32 self.dct = dict(((k, k.__class__), v) for k,v in dct.iteritems())
33 __contains__ = lambda self, key: (key, key.__class__) in self.dct
34 __getitem__ = lambda self,key: self.dct[(key, key.__class__)]
35
36default_binding = {"#t":True, "true":True, "#f":False, "false":False, "nil":None, "dict":Ident(u'alist->hash-table')}
37
38class Reader(object):
39 PAREN = {"]":"[", ")":"("}
40 def __init__(self, binding=None, symbol_marker="'", use_dict=True):
41 self.binding = binding or default_binding
42 self.symbol_marker = symbol_marker
43 self.use_dict = use_dict
44
45 def read(self, value):
46 self.result = []
47 self.paren_stack = []
48 self.source = value
49 self.pos = 0
50 self.scanner = Scanner([
51 (r"\s+", self("skip")),
52 (r";[^\n]*\n", self("skip")),
53 (r""""(((?<=\\)")|[^"])*((?<!\\)")""", self("str")),
54 (r"(\(|\[)", self("open")),
55 (r"(\)|\])", self("close")),
56 (r"(([\d]+|(((\d+)?\.[\d]+)|([\d]+\.)))e[\+\-]?[\d]+)|(((\d+)?\.[\d]+)|([\d]+\.))", self("number")),
57 (r"\-?((0x[\da-f]+)|(0[0-7]+)|([1-9][\d]*)|0)[l]?", self("number")),
58 (r"""%s([^\(\[\)\]\s"]+)"""%self.symbol_marker, self("symbol")),
59 (r"""([^\(\[\)\]\s"]+)""", self("ident")),
60 (r"""".*""", self("unterm_str")),
61 (r".*", self("unknown_token"))
62 ], re.M|re.S|re.I)
63 self.scanner.scan(self.source)
64 if self.paren_stack:
65 self.raise_error("missing closing parenthesis.")
66 return self.parse(self.result)
67
68 def append(self, v):
69 self.last().append(Token(v, self.pos))
70
71 def __call__(self, name):
72 def _(scanner, s):
73 self.pos += len(s)
74 return getattr(self, name)(s)
75 return _
76
77 def unknown_token(self,s): self.raise_error("unknown token: %s"%s)
78 def skip(self, _): pass
79 def open(self, s):
80 new_lst = []
81 self.last().append(new_lst)
82 self.paren_stack.append([s, new_lst])
83 def close(self, s):
84 if not self.paren_stack:
85 self.raise_error("missing opening parenthesis.")
86 if self.PAREN[s] != self.paren_stack.pop()[0]:
87 self.raise_error("missing closing parenthesis.")
88 def str(self, s): self.append(eval('u""'+s+'""'))
89 def unterm_str(self, s): self.raise_error("unterminated string literal.")
90 def number(self, s): self.append(eval(s))
91 def symbol(self, s): self.append(Symbol(s[1:]))
92 def ident(self, s):
93 if s in self.binding:
94 self.append(self.binding[s])
95 else:
96 self.append(Ident(s))
97
98 def last(self):
99 if self.paren_stack:
100 return self.paren_stack[-1][1]
101 else:
102 return self.result
103
104 def parse(self, rs):
105 def is_ident(value, expected):
106 return getattr(value,"value", None) == Ident(expected)
107 def is_pair(rs):
108 return getattr(rs, "__len__", lambda :0)()==3 and is_ident(rs[1], u".")
109
110 if isinstance(rs, list):
111 if not len(rs):
112 return []
113 elif self.use_dict and is_ident(rs[0], u"alist->hash-table"):
114 if len(rs) != 2:
115 self.raise_error("alist->hash-table: expected 1 arguments, got %d."%(len(rs)-1), rs[0].pos)
116 if not all(is_pair(a) for a in rs[1]):
117 self.raise_error("alist->hash-table: aruguments must be alist", rs[0].pos)
118 return dict((self.parse(i[0]), self.parse(i[2])) for i in rs[1])
119 elif len(rs)!=3 and any(is_ident(t, u".") for t in rs):
120 self.raise_error('illegal use of "."', rs[0].pos)
121 elif is_pair(rs):
122 parsed = self.parse(rs[2])
123 if not isinstance(rs[2], list):
124 return Pair([rs[0].value, parsed])
125 if isinstance(parsed, Pair):
126 return Pair([rs[0].value, parsed])
127 elif isinstance(parsed, list):
128 return [rs[0].value]+parsed
129 else:
130 return [rs[0].value, parsed]
131 else:
132 return map(self.parse, rs)
133 else:
134 return rs.value
135
136 def raise_error(self, msg="parse error", pos=None, range=3):
137 pos = pos or self.pos
138 lines = self.source.split("\n")
139 curline = self.source[:pos].count("\n")
140 linepos = pos - len("\n".join(lines[:curline]))
141 buf = ["\n"]
142 for i in xrange(max(0, curline-range), curline+1):
143 buf.append("% 5d: %s"%(i+1, lines[i]))
144 width = 7 + sum(east_asian_width(c) == 'W' and 2 or 1 for c in unicode(lines[i]))
145 buf.append("%s~"%(" "*width))
146 buf.append("line %d, %d: %s"%(curline+1,linepos, msg))
147 raise ParseError(("\n".join(buf)).encode(sys.stderr.encoding))
148
149class Dumper(object):
150 def __init__(self, binding=None ,symbol_marker="'"):
151 binding = binding or default_binding
152 self.binding = Binding(dict(zip(binding.values(), binding)))
153 self.symbol_marker = symbol_marker
154
155 def dump(self, obj):
156 result = self.to_sexp(obj, [])
157 if isinstance(result, list) and len(result) and result[0]=="(":
158 result = result[1:-1]
159 return u" ".join(result)
160
161 def to_sexp(self, obj, result):
162 ap = result.append
163 tos = lambda v: self.to_sexp(v, result)
164 if isinstance(obj, Pair):
165 ap("(")
166 tos(obj[0])
167 ap(" . ")
168 tos(obj[1])
169 ap(")")
170 elif isinstance(obj, (tuple, list)):
171 ap("(")
172 map(tos, obj)
173 ap(")")
174 else:
175 if isinstance(obj, dict):
176 ap("( alist->hash-table ")
177 tos([(k, Ident(u"."), v) for k,v in obj.items()])
178 ap(" ) ")
179 elif obj in self.binding:
180 ap(unicode(Ident(self.binding[obj])))
181 elif isinstance(obj, Symbol):
182 ap(u"'%s"%unicode(obj))
183 elif isinstance(obj, (Ident,int, float, long)):
184 ap(unicode(obj))
185 else:
186 s = unicode(repr(obj)).decode("unicode_escape")
187 m = re.match(r"""^[u|r]?["|'](.*)["|']$""", s, re.M|re.S)
188 if m:
189 s = m.group(1)
190 ap("\"%s\""%s.replace('"','\\"').replace("\\'","'"))
191 return result
192
193dumper = Dumper()
194read = Reader().read
195dump = dumper.dump
概要
特徴は
- 辞書を定義できる
- ドット対に対応
- 識別子に対して、任意のバインディングを指定できる
- わりとちゃんとエラー表示される
- シンボル表記、数値表記(python表記)に対応
といった当たりでしょうか。具体的にはテストコードを見てもらうと分かるかと。
1(あああ hoge->fuga123 (1 . (2 . 3)) "hoge\\"hoge" ;comment2
2foo "aaa" #t <= 'foo
3"hogehoge
4foo
5" (5 . (6 .()))
6)
7(dict (
8 ("いいい" .
9 (alist->hash-table (
10 ("a-1" . "vvv")
11 ("a-2" . (
12 hoge foo bar
13 ))
14 )))
15))
16(10 1L -45 010 0x10 -10 -0x10 3.14 10. .001 1e100 3.14e-10 0e0)
17; comment3 ()(
18
19""")
という感じのS式が
1[
2 [Ident(u'あああ'), Ident(u'hoge->fuga123'), Pair([1, Pair([2, 3])]), u'hoge"hoge',
3 Ident(u'foo'), u'aaa', True, Ident(u'<='), Symbol(u'foo'),
4 u'hogehoge\nfoo\n', [5,6]],
5 {u'いいい':
6 {u'a-1': u'vvv',
7 u'a-2': [Ident(u'hoge'), Ident(u'foo'), Ident(u'bar')]}},
8 [10, 1L, -45, 010, 0x10, -10, -0x10, 3.14, 10., .001, 1e100, 3.14e-10, 0e0]
9]
となります。 Ident
や Symbol
はunicodeのサブクラス、 Pair
はリストのサブクラスになっているので、違和感なく使えると思います。
また、 alist->hash-table
で辞書が作れます。デフォルトで dict
に alist->hash-table
をバインドしてますので、 dict
でも辞書が作れます。この機能はオンオフ切り替えも可能です。
その他、 #t
を True
などSchemeっぽくデフォルトバインディングが用意してあります。もちろん、バインディングは変更可能ですのでCLっぽくもできます。
と、こんな感じです。一番便利なのはやっぱり辞書ですかねえ。なので、YAML,JSONで書いてる設定ファイルをS式で置き換え・・・なんてことができるかもしれません。