Commit | Line | Data |
---|---|---|
1dcdf455 RK |
1 | /* |
2 | * This file is part of DisOrder | |
3 | * Copyright (C) 2008 Richard Kettlewell | |
4 | * | |
5 | * This program is free software; you can redistribute it and/or modify | |
6 | * it under the terms of the GNU General Public License as published by | |
7 | * the Free Software Foundation; either version 2 of the License, or | |
8 | * (at your option) any later version. | |
9 | * | |
10 | * This program is distributed in the hope that it will be useful, but | |
11 | * WITHOUT ANY WARRANTY; without even the implied warranty of | |
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |
13 | * General Public License for more details. | |
14 | * | |
15 | * You should have received a copy of the GNU General Public License | |
16 | * along with this program; if not, write to the Free Software | |
17 | * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 | |
18 | * USA | |
19 | */ | |
20 | ||
21 | /** @file lib/macros.c | |
22 | * @brief Macro expansion | |
23 | */ | |
24 | ||
25 | #include <config.h> | |
26 | #include "types.h" | |
27 | ||
28 | #include <string.h> | |
29 | #include <ctype.h> | |
30 | #include <assert.h> | |
f640bcb3 RK |
31 | #include <stdio.h> |
32 | #include <sys/stat.h> | |
33 | #include <fcntl.h> | |
34 | #include <unistd.h> | |
35 | #include <errno.h> | |
1dcdf455 RK |
36 | |
37 | #include "macros.h" | |
38 | #include "mem.h" | |
39 | #include "vector.h" | |
40 | #include "log.h" | |
f640bcb3 RK |
41 | #include "hash.h" |
42 | #include "sink.h" | |
43 | #include "syscalls.h" | |
1dcdf455 RK |
44 | |
45 | VECTOR_TYPE(mx_node_vector, const struct mx_node *, xrealloc); | |
46 | ||
f640bcb3 RK |
47 | /** @brief Definition of an expansion */ |
48 | struct expansion { | |
49 | /** @brief Minimum permitted arguments */ | |
50 | int min; | |
51 | ||
52 | /** @brief Maximum permitted arguments */ | |
53 | int max; | |
54 | ||
55 | /** @brief Flags */ | |
56 | unsigned flags; | |
57 | ||
58 | /** @brief Callback (cast to appropriate type) */ | |
59 | void (*callback)(); | |
60 | }; | |
61 | ||
62 | /** @brief Expansion takes parsed templates, not strings */ | |
63 | #define EXP_MAGIC 0x0001 | |
64 | ||
65 | static hash *expansions; | |
66 | ||
67 | /* Parsing ------------------------------------------------------------------ */ | |
68 | ||
1dcdf455 RK |
69 | /** @brief Parse a template |
70 | * @param filename Input filename (for diagnostics) | |
71 | * @param line Line number (use 1 on initial call) | |
72 | * @param input Start of text to parse | |
73 | * @param end End of text to parse or NULL | |
74 | * @return Pointer to parse tree root node | |
75 | * | |
76 | * Parses the text in [start, end) and returns an (immutable) parse | |
77 | * tree representing it. | |
78 | * | |
79 | * If @p end is NULL then the whole string is parsed. | |
80 | * | |
81 | * Note that the @p filename value stored in the parse tree is @p filename, | |
82 | * i.e. it is not copied. | |
83 | */ | |
84 | const struct mx_node *mx_parse(const char *filename, | |
85 | int line, | |
86 | const char *input, | |
87 | const char *end) { | |
88 | int braces, expansion_start_line, argument_start_line; | |
89 | const char *argument_start, *argument_end, *p; | |
90 | struct mx_node_vector v[1]; | |
91 | struct dynstr d[1]; | |
92 | struct mx_node *head = 0, **tailp = &head, *e; | |
93 | int omitted_terminator; | |
94 | ||
95 | if(!end) | |
96 | end = input + strlen(input); | |
97 | while(input < end) { | |
98 | if(*input != '@') { | |
99 | expansion_start_line = line; | |
100 | dynstr_init(d); | |
101 | /* Gather up text without any expansions in. */ | |
102 | while(input < end && *input != '@') { | |
103 | if(*input == '\n') | |
104 | ++line; | |
105 | dynstr_append(d, *input++); | |
106 | } | |
107 | dynstr_terminate(d); | |
108 | e = xmalloc(sizeof *e); | |
109 | e->next = 0; | |
110 | e->filename = filename; | |
111 | e->line = expansion_start_line; | |
112 | e->type = MX_TEXT; | |
113 | e->text = d->vec; | |
114 | *tailp = e; | |
115 | tailp = &e->next; | |
116 | continue; | |
117 | } | |
118 | mx_node_vector_init(v); | |
119 | braces = 0; | |
120 | p = input; | |
121 | ++input; | |
122 | expansion_start_line = line; | |
123 | omitted_terminator = 0; | |
124 | while(!omitted_terminator && input < end && *input != '@') { | |
125 | /* Skip whitespace */ | |
126 | if(isspace((unsigned char)*input)) { | |
127 | if(*input == '\n') | |
128 | ++line; | |
129 | ++input; | |
130 | continue; | |
131 | } | |
132 | if(*input == '{') { | |
133 | /* This is a bracketed argument. We'll walk over it counting | |
134 | * braces to figure out where the end is. */ | |
135 | ++input; | |
136 | argument_start = input; | |
137 | argument_start_line = line; | |
138 | while(input < end && (*input != '}' || braces > 0)) { | |
139 | switch(*input++) { | |
140 | case '{': ++braces; break; | |
141 | case '}': --braces; break; | |
142 | case '\n': ++line; break; | |
143 | } | |
144 | } | |
145 | /* If we run out of input without seeing a '}' that's an error */ | |
146 | if(input >= end) | |
147 | fatal(0, "%s:%d: unterminated expansion '%.*s'", | |
148 | filename, argument_start_line, | |
149 | (int)(input - argument_start), argument_start); | |
150 | /* Consistency check */ | |
151 | assert(*input == '}'); | |
152 | /* Record the end of the argument */ | |
153 | argument_end = input; | |
154 | /* Step over the '}' */ | |
155 | ++input; | |
156 | if(input < end && isspace((unsigned char)*input)) { | |
157 | /* There is at least some whitespace after the '}'. Look | |
158 | * ahead and see what is after all the whitespace. */ | |
159 | for(p = input; p < end && isspace((unsigned char)*p); ++p) | |
160 | ; | |
161 | /* Now we are looking after the whitespace. If it's | |
162 | * anything other than '{', including the end of the input, | |
163 | * then we infer that this expansion finished at the '}' we | |
164 | * just saw. (NB that we don't move input forward to p - | |
165 | * the whitespace is NOT part of the expansion.) */ | |
166 | if(p == end || *p != '{') | |
167 | omitted_terminator = 1; | |
168 | } | |
169 | } else { | |
170 | /* We are looking at an unbracketed argument. (A common example would | |
171 | * be the expansion or macro name.) This is terminated by an '@' | |
172 | * (indicating the end of the expansion), a ':' (allowing a subsequent | |
173 | * unbracketed argument) or a '{' (allowing a bracketed argument). The | |
174 | * end of the input will also do. */ | |
175 | argument_start = input; | |
176 | argument_start_line = line; | |
177 | while(input < end | |
178 | && *input != '@' && *input != '{' && *input != ':') { | |
179 | if(*input == '\n') ++line; | |
180 | ++input; | |
181 | } | |
182 | argument_end = input; | |
183 | /* Trailing whitespace is not significant in unquoted arguments (and | |
184 | * leading whitespace is eliminated by the whitespace skip above). */ | |
185 | while(argument_end > argument_start | |
186 | && isspace((unsigned char)argument_end[-1])) | |
187 | --argument_end; | |
188 | /* Step over the ':' if that's what we see */ | |
189 | if(input < end && *input == ':') | |
190 | ++input; | |
191 | } | |
192 | /* Now we have an argument in [argument_start, argument_end), and we know | |
193 | * its filename and initial line number. This is sufficient to parse | |
194 | * it. */ | |
195 | mx_node_vector_append(v, mx_parse(filename, argument_start_line, | |
196 | argument_start, argument_end)); | |
197 | } | |
198 | /* We're at the end of an expansion. We might have hit the end of the | |
199 | * input, we might have hit an '@' or we might have matched the | |
200 | * omitted_terminator criteria. */ | |
201 | if(input < end) { | |
202 | if(!omitted_terminator) { | |
203 | assert(*input == '@'); | |
204 | ++input; | |
205 | } | |
206 | } | |
207 | /* @@ terminates this file */ | |
208 | if(v->nvec == 0) | |
209 | break; | |
210 | /* Currently we require that the first element, the expansion name, is | |
211 | * always plain text. Removing this restriction would raise some | |
212 | * interesting possibilities but for the time being it is considered an | |
213 | * error. */ | |
214 | if(v->vec[0]->type != MX_TEXT) | |
215 | fatal(0, "%s:%d: expansion names may not themselves contain expansions", | |
216 | v->vec[0]->filename, v->vec[0]->line); | |
217 | /* Guarantee a NULL terminator (for the case where there's more than one | |
218 | * argument) */ | |
219 | mx_node_vector_terminate(v); | |
220 | e = xmalloc(sizeof *e); | |
221 | e->next = 0; | |
222 | e->filename = filename; | |
223 | e->line = expansion_start_line; | |
224 | e->type = MX_EXPANSION; | |
225 | e->name = v->vec[0]->text; | |
226 | e->nargs = v->nvec - 1; | |
227 | e->args = v->nvec > 1 ? &v->vec[1] : 0; | |
228 | *tailp = e; | |
229 | tailp = &e->next; | |
230 | } | |
231 | return head; | |
232 | } | |
233 | ||
9dbb630e RK |
234 | static void mx__dump(struct dynstr *d, const struct mx_node *m) { |
235 | int n; | |
236 | ||
237 | if(!m) | |
238 | return; | |
239 | switch(m->type) { | |
240 | case MX_TEXT: | |
241 | dynstr_append_string(d, m->text); | |
242 | break; | |
243 | case MX_EXPANSION: | |
244 | dynstr_append(d, '@'); | |
245 | dynstr_append_string(d, m->name); | |
246 | for(n = 0; n < m->nargs; ++n) { | |
247 | dynstr_append(d, '{'); | |
248 | mx__dump(d, m->args[n]); | |
249 | dynstr_append(d, '}'); | |
250 | } | |
251 | dynstr_append(d, '@'); | |
252 | break; | |
253 | default: | |
254 | assert(!"invalid m->type"); | |
255 | } | |
256 | mx__dump(d, m->next); | |
257 | } | |
258 | ||
259 | /** @brief Dump a parse macro expansion to a string */ | |
260 | char *mx_dump(const struct mx_node *m) { | |
261 | struct dynstr d[1]; | |
262 | ||
263 | dynstr_init(d); | |
264 | mx__dump(d, m); | |
265 | dynstr_terminate(d); | |
266 | return d->vec; | |
267 | } | |
268 | ||
f640bcb3 RK |
269 | /* Expansion registration --------------------------------------------------- */ |
270 | ||
271 | static void mx__register(unsigned flags, | |
272 | const char *name, | |
273 | int min, | |
274 | int max, | |
275 | void (*callback)()) { | |
276 | struct expansion e[1]; | |
277 | ||
278 | if(!expansions) | |
279 | expansions = hash_new(sizeof(struct expansion)); | |
280 | e->min = min; | |
281 | e->max = max; | |
282 | e->flags = flags; | |
283 | e->callback = callback; | |
284 | hash_add(expansions, name, &e, HASH_INSERT_OR_REPLACE); | |
285 | } | |
286 | ||
287 | /** @brief Register a simple expansion rule | |
288 | * @param name Name | |
289 | * @param min Minimum number of arguments | |
290 | * @param max Maximum number of arguments | |
291 | * @param callback Callback to write output | |
292 | */ | |
293 | void mx_register(const char *name, | |
294 | int min, | |
295 | int max, | |
296 | mx_simple_callback *callback) { | |
297 | mx__register(0, name, min, max, (void (*)())callback); | |
298 | } | |
299 | ||
300 | /** @brief Register a magic expansion rule | |
301 | * @param name Name | |
302 | * @param min Minimum number of arguments | |
303 | * @param max Maximum number of arguments | |
304 | * @param callback Callback to write output | |
305 | */ | |
306 | void mx_magic_register(const char *name, | |
307 | int min, | |
308 | int max, | |
309 | mx_magic_callback *callback) { | |
310 | mx__register(EXP_MAGIC, name, min, max, (void (*)())callback); | |
311 | } | |
312 | ||
313 | /* Expansion ---------------------------------------------------------------- */ | |
314 | ||
315 | /** @brief Expand a template | |
316 | * @param m Where to start | |
317 | * @param output Where to send output | |
318 | * @param u User data | |
319 | * @return 0 on success, non-0 on error∑ | |
320 | * | |
321 | * If a sink write fails then -1 is returned. If any callback returns non-zero | |
322 | * then that value is returned. It is suggested that callbacks adopt this | |
323 | * policy too and use positive values to mean other kinds of fatal error. | |
324 | */ | |
325 | int mx_expand(const struct mx_node *m, | |
326 | struct sink *output, | |
327 | void *u) { | |
328 | const struct expansion *e; | |
329 | int rc; | |
330 | ||
331 | if(!m) | |
332 | return 0; | |
333 | switch(m->type) { | |
334 | case MX_TEXT: | |
335 | if(sink_writes(output, m->text) < 0) | |
336 | return -1; | |
337 | break; | |
338 | case MX_EXPANSION: | |
339 | if(!(e = hash_find(expansions, m->name))) { | |
340 | error(0, "%s:%d: unknown expansion name '%s'", | |
341 | m->filename, m->line, m->name); | |
342 | if(sink_printf(output, "[[%s unknown]]", m->name) < 0) | |
343 | return -1; | |
344 | } else if(m->nargs < e->min) { | |
345 | error(0, "%s:%d: expansion '%s' requires %d args, only %d given", | |
346 | m->filename, m->line, m->name, e->min, m->nargs); | |
347 | if(sink_printf(output, "[[%s too few args]]", m->name) < 0) | |
348 | return -1; | |
349 | } else if(m->nargs > e->max) { | |
350 | error(0, "%s:%d: expansion '%s' takes at most %d args, but %d given", | |
351 | m->filename, m->line, m->name, e->max, m->nargs); | |
352 | if(sink_printf(output, "[[%s too many args]]", m->name) < 0) | |
353 | return -1; | |
354 | } else if(e->flags & EXP_MAGIC) { | |
355 | /* Magic callbacks we can call directly */ | |
356 | if((rc = ((mx_magic_callback *)e->callback)(m->nargs, | |
357 | m->args, | |
358 | output, | |
359 | u))) | |
360 | return rc; | |
361 | } else { | |
362 | /* For simple callbacks we expand their arguments for them */ | |
363 | char **args = xcalloc(1 + m->nargs, sizeof (char *)); | |
364 | int n; | |
365 | ||
366 | for(n = 0; n < m->nargs; ++n) | |
367 | if((rc = mx_expandstr(m->args[n], &args[n], u))) | |
368 | return rc; | |
369 | args[n] = NULL; | |
370 | if((rc = ((mx_simple_callback *)e->callback)(m->nargs, | |
371 | args, | |
372 | output, | |
373 | u))) | |
374 | return rc; | |
375 | } | |
376 | break; | |
377 | default: | |
378 | assert(!"invalid m->type"); | |
379 | } | |
380 | return mx_expand(m, output, u); | |
381 | } | |
382 | ||
383 | /** @brief Expand a template storing the result in a string | |
384 | * @param m Where to start | |
385 | * @param sp Where to store string | |
386 | * @param u User data | |
387 | * @return 0 on success, non-0 on error | |
388 | * | |
389 | * Same return conventions as mx_expand(). This wrapper is slightly more | |
390 | * convenient to use from 'magic' expansions. | |
391 | */ | |
392 | int mx_expandstr(const struct mx_node *m, | |
393 | char **sp, | |
394 | void *u) { | |
395 | struct dynstr d[1]; | |
396 | int rc; | |
397 | ||
398 | if(!(rc = mx_expand(m, sink_dynstr(d), u))) { | |
399 | dynstr_terminate(d); | |
400 | *sp = d->vec; | |
401 | } | |
402 | return rc; | |
403 | } | |
404 | ||
405 | /** @brief Expand a template file | |
406 | * @param path Filename | |
407 | * @param output Where to send output | |
408 | * @param u User data | |
409 | * @return 0 on success, non-0 on error | |
410 | * | |
411 | * Same return conventions as mx_expand(). | |
412 | */ | |
413 | int mx_expand_file(const char *path, | |
414 | struct sink *output, | |
415 | void *u) { | |
416 | int fd, n; | |
417 | struct stat sb; | |
418 | char *b; | |
419 | off_t sofar; | |
420 | ||
421 | if((fd = open(path, O_RDONLY)) < 0) | |
422 | fatal(errno, "error opening %s", path); | |
423 | if(fstat(fd, &sb) < 0) | |
424 | fatal(errno, "error statting %s", path); | |
425 | if(!S_ISREG(sb.st_mode)) | |
426 | fatal(0, "%s: not a regular file", path); | |
427 | sofar = 0; | |
428 | b = xmalloc_noptr(sb.st_size); | |
429 | while(sofar < sb.st_size) { | |
430 | n = read(fd, b + sofar, sb.st_size - sofar); | |
431 | if(n > 0) | |
432 | sofar += n; | |
433 | else if(n == 0) | |
434 | fatal(0, "unexpected EOF reading %s", path); | |
435 | else if(errno != EINTR) | |
436 | fatal(errno, "error reading %s", path); | |
437 | } | |
438 | xclose(fd); | |
439 | return mx_expand(mx_parse(path, 1, b, b + sb.st_size), | |
440 | output, | |
441 | u); | |
442 | } | |
443 | ||
1dcdf455 RK |
444 | /* |
445 | Local Variables: | |
446 | c-basic-offset:2 | |
447 | comment-column:40 | |
448 | fill-column:79 | |
449 | indent-tabs-mode:nil | |
450 | End: | |
451 | */ |