-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathexpander.py
232 lines (197 loc) · 5.88 KB
/
expander.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
#!/usr/bin/env python
'''
Given a file or directory, expands "macros" using a given language.
Macro definition ("hello" is the name, and "python" is the language):
??defm:hello:python(arg1, arg2){{
print "puts 'Hello, %s!!!' \n%s" % (arg1, arg2)
}}
Macro call (eg, in ruby code):
??hello??'Joe'??'puts "Why, dude?"'??
Result:
puts 'Hello, Joe!!!'
puts "Why, dude?"
'''
import sys
import os
import re
import tempfile
import subprocess
from glob import iglob
from pprint import pprint
# CONSTANTS
# define the special char that is used in the syntax.
# probably use something that's no where else in the code.
special_char_escaped = '\?\?'
special_char = '??'
end_char = ';'
start_def = '{{'
end_def = '}}'
# REGEXES
# the regexes to match varius parts of the code
re_func_def = re.compile(
r'%(sc)sdefm:(\w+):(\w+)\(\s*([^\)]*)\s*\)\s*%(st)s(.*?)%(en)s'\
% {
'sc': special_char_escaped,
'st': start_def,
'en': end_def,
},
re.I | re.S)
re_func_call = re.compile(
r'%(sc)s(.*?)%(sc)s((?:.*?%(sc)s)*?)%(end)s'\
% {
'sc': special_char_escaped,
'end': end_char,
},
re.I | re.S)
def indent(code, indent_str=None, ignore_first_line=False):
if indent_str is None:
indent_str = ' '
out = []
first = True
for line in code.split('\n'):
if first and ignore_first_line:
out.append(line)
else:
out.append(indent_str + line)
first = False
return '\n'.join(out)
# LANGUAGES
class Lang(object):
functions = ''
functions_declared = set()
class Python(Lang):
def add_function(self, name, args, code):
self.functions += '\ndef %s(%s):\n%s' %\
(name,
', '.join(args),
indent(code))
self.functions_declared.add(name)
def run_function(self, func, args):
if func in self.functions_declared:
code = "%s\n%s(%s)\n" % (self.functions, func, ','.join(args))
# write out the file and return the result
_, t_name = tempfile.mkstemp('.py')
f = open(t_name, 'wb')
if f:
f.write(code)
f.close()
else:
raise Exception("Can't write to temp file %s." % t_name)
# run the script
result = subprocess.check_output(['python', t_name])
# delete the file
os.unlink(t_name)
# return the result
return result.rstrip('\n')
else:
raise Exception("Unknown function %s" % func)
# defines the languages we can use for function calls
langs = {
'python': Python(),
}
# DATA STRUCTURES
# stores all the files
files = {}
# stores how to call the functions
function_calls = {}
# FUNCTIONS
def define_functions(f):
"""
Takes a file and finds all function definitions.
"""
def define_func(m):
"""
Takes a function match and saves it off.
"""
name, lang, args, f_code = m.groups()
args = map(str.strip, args.split(','))
if name in function_calls:
raise Exception("Trying to redeclare function %s." % name)
l = langs.get(lang)
if l is not None:
l.add_function(name, args, f_code)
function_calls[name] = l
return ''
code = files.get(f)
if code is not None:
files[f] = re_func_def.sub(define_func, code)
def str_reverse(s):
"""
Reverses a string.
"""
s = list(s)
s.reverse()
return ''.join(s)
def expand_functions():
"""
Loop through all the files and expand the functions.
"""
current_code = ''
def expand(m):
"""
Takes a match object and expands the function calls.
"""
# get the peices
name, args = m.groups()
args = args.split(special_char)[:-1]
# get the left indent.
left_indent = ''
i = m.start() - 1
while i >= 0 and (current_code[i] == ' ' or current_code[i] == '\t'):
left_indent += current_code[i]
i -= 1
left_indent = str_reverse(left_indent)
# get the function result
l = function_calls.get(name)
if l is None:
raise Exception("Undeclared function %s" % name)
result = l.run_function(name, args)
# return the result
if result is None:
raise Exception("Error running function")
else:
return indent(result, left_indent, True)
for f, code in files.items():
current_code = code
files[f] = re_func_call.sub(expand, current_code)
def expand_dir(path):
# loop through the files in the path and add them to the files structure
for f in iglob(os.path.join(path, '*.exp.*')):
expand_file(f)
def expand_file(fname):
# open the file and save it's contents
with open(fname) as f:
files[fname] = '\n'.join([x.rstrip('\n').rstrip('\r') for x in f])
define_functions(fname)
def write_out_files():
for name, code in files.iteritems():
out_name = name.replace('.exp.', '.')
f = open(out_name, 'wb')
if not f:
raise IOError("Can't output the processed file %s" % name)
f.write(code)
f.close()
def main():
# get the arguments
if len(sys.argv) == 1:
# use the current directory
path = os.getcwd()
elif len(sys.argv) == 2:
path = sys.argv[1]
else:
sys.stderr.write("USAGE: expander [PATH]\n")
sys.exit(1)
# do the right thing based on the type of argument.
if os.path.isfile(path):
expand_file(path)
expand_functions()
sys.stdout.write(files[path])
elif os.path.isdir(path):
expand_dir(path)
expand_functions()
write_out_files()
else:
sys.stderr.write("Couldn't find the given path '%s'." % path)
sys.exit(1)
if __name__ == "__main__":
main()