menuparser.c code clean
[wmaker-crm.git] / WINGs / menuparser.c
blobc18f9b98b3f1d4704bc91beb81e8e75f96941f55
1 /*
2 * Window Maker window manager
4 * Copyright (c) 2012 Christophe Curis
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
16 * You should have received a copy of the GNU General Public License along
17 * with this program; if not, write to the Free Software Foundation, Inc.,
18 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20 #include "wconfig.h"
22 #include <stdio.h>
23 #include <string.h>
24 #include <ctype.h>
25 #include <stdarg.h>
27 #include <WINGs/WUtil.h>
29 #include "menuparser.h"
31 static WMenuParser menu_parser_create_new(const char *file_name, void *file,
32 const char *include_default_paths);
33 static char *menu_parser_isolate_token(WMenuParser parser,
34 WParserMacro *list_macros);
35 static void menu_parser_get_directive(WMenuParser parser);
36 static Bool menu_parser_include_file(WMenuParser parser);
37 static void menu_parser_condition_ifmacro(WMenuParser parser, Bool check_exists);
38 static void menu_parser_condition_else(WMenuParser parser);
39 static void menu_parser_condition_end(WMenuParser parser);
42 /* Constructor and Destructor for the Menu Parser object */
43 WMenuParser WMenuParserCreate(const char *file_name, void *file,
44 const char *include_default_paths)
46 WMenuParser parser;
48 parser = menu_parser_create_new(file_name, file, include_default_paths);
49 menu_parser_register_preset_macros(parser);
50 return parser;
53 void WMenuParserDelete(WMenuParser parser)
55 if (parser->include_file) {
56 /* Trick: the top parser's data are not wmalloc'd, we point on the
57 * provided data so we do not wfree it; however for include files
58 * we did wmalloc them.
59 * This code should not be used as the wfree is done when we reach
60 * the end of an include file; however this may not happen when an
61 * early exit occurs (typically when 'readMenuFile' does not find
62 * its expected command). */
63 fclose(parser->include_file->file_handle);
64 wfree((char *) parser->include_file->file_name);
65 WMenuParserDelete(parser->include_file);
68 if (parser->macros)
69 menu_parser_free_macros(parser);
71 wfree(parser);
74 static WMenuParser menu_parser_create_new(const char *file_name, void *file,
75 const char *include_default_paths)
77 WMenuParser parser;
79 parser = wmalloc(sizeof(*parser));
80 parser->include_default_paths = include_default_paths;
81 parser->file_name = file_name;
82 parser->file_handle = file;
83 parser->rd = parser->line_buffer;
85 return parser;
88 /* To report helpfull messages to user */
89 const char *WMenuParserGetFilename(WMenuParser parser)
91 return parser->file_name;
94 void WMenuParserError(WMenuParser parser, const char *msg, ...)
96 char buf[MAXLINE];
97 va_list args;
98 WMenuParser parent;
100 while (parser->include_file)
101 parser = parser->include_file;
103 va_start(args, msg);
104 vsnprintf(buf, sizeof(buf), msg, args);
105 va_end(args);
106 __wmessage("WMenuParser", parser->file_name, parser->line_number,
107 WMESSAGE_TYPE_WARNING, "%s", buf);
109 for (parent = parser->parent_file; parent != NULL; parent = parent->parent_file)
110 __wmessage("WMenuParser", parser->file_name, parser->line_number,
111 WMESSAGE_TYPE_WARNING, _(" included from file \"%s\" at line %d"),
112 parent->file_name, parent->line_number);
115 /* Read one line from file and split content
116 * The function returns False when the end of file is reached */
117 Bool WMenuParserGetLine(WMenuParser top_parser, char **title, char **command,
118 char **parameter, char **shortcut)
120 WMenuParser cur_parser;
121 enum { GET_TITLE, GET_COMMAND, GET_PARAMETERS, GET_SHORTCUT } scanmode;
122 char *token, *params = NULL;
123 char lineparam[MAXLINE];
125 lineparam[0] = '\0';
126 *title = NULL;
127 *command = NULL;
128 *parameter = NULL;
129 *shortcut = NULL;
130 scanmode = GET_TITLE;
132 read_next_line_with_filechange:
133 cur_parser = top_parser;
134 while (cur_parser->include_file)
135 cur_parser = cur_parser->include_file;
137 read_next_line:
138 if (fgets(cur_parser->line_buffer, sizeof(cur_parser->line_buffer), cur_parser->file_handle) == NULL) {
139 if (cur_parser->cond.depth > 0) {
140 int i;
142 for (i = 0; i < cur_parser->cond.depth; i++)
143 WMenuParserError(cur_parser, _("missing #endif to match #%s at line %d"),
144 cur_parser->cond.stack[i].name, cur_parser->cond.stack[i].line);
147 if (cur_parser->parent_file == NULL)
148 /* Not inside an included file -> we have reached the end */
149 return False;
151 /* We have only reached the end of an included file -> go back to calling file */
152 fclose(cur_parser->file_handle);
153 wfree((char *) cur_parser->file_name);
154 cur_parser = cur_parser->parent_file;
155 wfree(cur_parser->include_file);
156 cur_parser->include_file = NULL;
157 goto read_next_line_with_filechange;
160 cur_parser->line_number++;
161 cur_parser->rd = cur_parser->line_buffer;
163 for (;;) {
164 if (!menu_parser_skip_spaces_and_comments(cur_parser)) {
165 /* We reached the end of line */
166 if (scanmode == GET_TITLE)
167 goto read_next_line; /* Empty line -> skip */
168 else
169 break; /* Finished reading current line -> return it to caller */
172 if ((scanmode == GET_TITLE) && (*cur_parser->rd == '#')) {
173 cur_parser->rd++;
174 menu_parser_get_directive(cur_parser);
175 goto read_next_line_with_filechange;
178 if (cur_parser->cond.stack[0].skip)
179 goto read_next_line;
181 /* Found a word */
182 token = menu_parser_isolate_token(cur_parser, top_parser->macros);
183 switch (scanmode) {
184 case GET_TITLE:
185 *title = token;
186 scanmode = GET_COMMAND;
187 break;
189 case GET_COMMAND:
190 if (strcmp(token, "SHORTCUT") == 0) {
191 scanmode = GET_SHORTCUT;
192 wfree(token);
193 } else {
194 *command = token;
195 scanmode = GET_PARAMETERS;
197 break;
199 case GET_SHORTCUT:
200 if (*shortcut != NULL) {
201 WMenuParserError(top_parser, _("multiple SHORTCUT definition not valid") );
202 wfree(*shortcut);
204 *shortcut = token;
205 scanmode = GET_COMMAND;
206 break;
208 case GET_PARAMETERS:
210 char *src;
212 if (params == NULL) {
213 params = lineparam;
214 } else {
215 if ((params - lineparam) < sizeof(lineparam) - 1)
216 *params++ = ' ';
219 src = token;
220 while ((params - lineparam) < sizeof(lineparam) - 1)
221 if ( (*params = *src++) == '\0')
222 break;
223 else
224 params++;
225 wfree(token);
227 break;
231 if (title != NULL) {
232 char eot, *src, *dst;
234 src = *title;
235 eot = *src++;
236 if ((eot == '"') || (eot == '\'')) {
237 dst = *title;
239 while (*src != '\0')
240 *dst++ = *src++;
242 if ((dst > *title) && (dst[-1] == eot))
243 dst--;
245 *dst = '\0';
249 if (params != NULL) {
250 lineparam[sizeof(lineparam) - 1] = '\0';
251 *parameter = wstrdup(lineparam);
254 return True;
257 /* Return False when there's nothing left on the line,
258 otherwise increment parser's pointer to next token */
259 Bool menu_parser_skip_spaces_and_comments(WMenuParser parser)
261 for (;;) {
262 while (isspace(*parser->rd))
263 parser->rd++;
265 if (*parser->rd == '\0') {
266 return False; /* Found the end of current line */
267 } else if ((parser->rd[0] == '\\') &&
268 (parser->rd[1] == '\n') &&
269 (parser->rd[2] == '\0')) {
270 /* Means that the current line is expected to be continued on next line */
271 if (fgets(parser->line_buffer, sizeof(parser->line_buffer), parser->file_handle) == NULL) {
272 WMenuParserError(parser, _("premature end of file while expecting a new line after '\\'") );
273 return False;
275 parser->line_number++;
276 parser->rd = parser->line_buffer;
278 } else if (parser->rd[0] == '/') {
279 if (parser->rd[1] == '/') /* Single line C comment */
280 return False; /* Won't find anything more on this line */
282 if (parser->rd[1] == '*') {
283 int start_line;
285 start_line = parser->line_number;
286 parser->rd += 2;
287 for (;;) {
288 /* Search end-of-comment marker */
289 while (*parser->rd != '\0') {
290 if ((parser->rd[0] == '*') && (parser->rd[1] == '/'))
291 goto found_end_of_comment;
293 parser->rd++;
296 /* Marker not found -> load next line */
297 if (fgets(parser->line_buffer, sizeof(parser->line_buffer), parser->file_handle) == NULL) {
298 WMenuParserError(parser, _("reached end of file while searching '*/' for comment started at line %d"), start_line);
299 return False;
302 parser->line_number++;
303 parser->rd = parser->line_buffer;
306 found_end_of_comment:
307 parser->rd += 2; /* Skip closing mark */
308 continue; /* Because there may be spaces after the comment */
311 return True; /* the '/' was not a comment, treat it as user data */
312 } else {
313 return True; /* Found some data */
318 /* read a token (non-spaces suite of characters)
319 * the result is wmalloc's, so it needs to be free'd */
320 static char *menu_parser_isolate_token(WMenuParser parser, WParserMacro *list_macros)
322 char *start, *token;
323 int limit = MAX_NESTED_MACROS;
325 start = parser->rd;
326 restart_token_split:
328 while (*parser->rd != '\0')
329 if (isspace(*parser->rd)) {
330 break;
332 } else if ((parser->rd[0] == '/') &&
333 ((parser->rd[1] == '*') || (parser->rd[1] == '/'))) {
334 break;
336 } else if ((parser->rd[0] == '\\') && (parser->rd[1] == '\n')) {
337 break;
339 } else if ((*parser->rd == '"' ) || (*parser->rd == '\'')) {
340 char eot = *parser->rd++;
342 while ((*parser->rd != '\0') && (*parser->rd != '\n'))
343 if (*parser->rd++ == eot)
344 goto found_end_quote;
346 WMenuParserError(parser, _("missing closing quote or double-quote before end-of-line") );
348 found_end_quote:
350 } else if (isnamechr(*parser->rd)) {
351 WParserMacro *macro;
352 char *start_macro;
354 start_macro = parser->rd;
355 while (isnamechr(*parser->rd))
356 parser->rd++;
358 macro = menu_parser_find_macro(parser, start_macro);
359 if (macro != NULL) {
360 char *expand_there;
362 /* Copy the chars before the macro to the beginning of the buffer to
363 * leave as much room as possible for expansion */
364 expand_there = parser->line_buffer;
365 if (start != parser->line_buffer)
366 while (start < start_macro)
367 *expand_there++ = *start++;
369 start = parser->line_buffer;
371 /* Macro expansion will take care to keep the rest of the line after
372 * the macro to the end of the line for future token extraction */
373 menu_parser_expand_macro(parser, macro, expand_there,
374 sizeof(parser->line_buffer) - (start - parser->line_buffer) );
376 /* Restart parsing to allow expansion of sub macro calls */
377 parser->rd = expand_there;
378 if (limit-- > 0)
379 goto restart_token_split;
381 WMenuParserError(parser, _("too many nested macro expansion, breaking loop") );
383 while (isnamechr(*parser->rd))
384 parser->rd++;
386 break;
388 /* else: the text was passed over so it will be counted in the token from 'start' */
389 } else {
390 parser->rd++;
393 token = wmalloc(parser->rd - start + 1);
394 strncpy(token, start, parser->rd - start);
395 token[parser->rd - start] = '\0';
397 return token;
400 /* Processing of special # directives */
401 static void menu_parser_get_directive(WMenuParser parser)
403 char *command;
405 /* Isolate the command */
406 while (isspace(*parser->rd))
407 parser->rd++;
409 command = parser->rd;
410 while (*parser->rd)
411 if (isspace(*parser->rd)) {
412 *parser->rd++ = '\0';
413 break;
414 } else {
415 parser->rd++;
418 if (strcmp(command, "include") == 0) {
419 if (!menu_parser_include_file(parser))
420 return;
422 } else if (strcmp(command, "define") == 0) {
423 menu_parser_define_macro(parser);
425 } else if (strcmp(command, "ifdef") == 0) {
426 menu_parser_condition_ifmacro(parser, 1);
428 } else if (strcmp(command, "ifndef") == 0) {
429 menu_parser_condition_ifmacro(parser, 0);
431 } else if (strcmp(command, "else") == 0) {
432 menu_parser_condition_else(parser);
434 } else if (strcmp(command, "endif") == 0) {
435 menu_parser_condition_end(parser);
437 } else {
438 WMenuParserError(parser, _("unknow directive '#%s'"), command);
439 return;
442 if (menu_parser_skip_spaces_and_comments(parser))
443 WMenuParserError(parser, _("extra text after '#' command is ignored: \"%.16s...\""),
444 parser->rd);
447 /* Extract the file name, search for it in known directories
448 * and create a sub-parser to handle it.
449 * Returns False if the file could not be found */
450 static Bool menu_parser_include_file(WMenuParser parser)
452 char buffer[MAXLINE];
453 char *req_filename, *fullfilename, *p;
454 char eot;
455 FILE *fh;
457 if (!menu_parser_skip_spaces_and_comments(parser)) {
458 WMenuParserError(parser, _("no file name found for #include") );
459 return False;
462 switch (*parser->rd++) {
463 case '<':
464 eot = '>';
465 break;
466 case '"':
467 eot = '"';
468 break;
469 default:
470 WMenuParserError(parser, _("file name must be enclosed in brackets or double-quotes for #define") );
471 return False;
474 req_filename = parser->rd;
475 while (*parser->rd) {
476 if (*parser->rd == eot) {
477 *parser->rd++ = '\0';
478 goto found_end_define_fname;
479 } else {
480 parser->rd++;
484 WMenuParserError(parser, _("missing closing '%c' in filename specification"), eot);
485 return False;
487 found_end_define_fname:
488 /* If we're inside a #if sequence, we abort now, but not sooner in
489 * order to keep the syntax check */
490 if (parser->cond.stack[0].skip)
491 return False;
493 { /* Check we are not nesting too many includes */
494 WMenuParser p;
495 int count;
497 count = 0;
498 for (p = parser; p->parent_file; p = p->parent_file)
499 count++;
501 if (count > MAX_NESTED_INCLUDES) {
502 WMenuParserError(parser, _("too many nested includes") );
503 return False;
507 /* Absolute paths */
508 fullfilename = req_filename;
509 if (req_filename[0] != '/') {
510 /* Search first in the same directory as the current file */
511 p = strrchr(parser->file_name, '/');
512 if (p != NULL) {
513 int len;
515 len = p - parser->file_name + 1;
516 if (len > sizeof(buffer) - 1)
517 len = sizeof(buffer) - 1;
519 strncpy(buffer, parser->file_name, len);
520 strncpy(buffer+len, req_filename, sizeof(buffer) - len - 1);
521 buffer[sizeof(buffer) - 1] = '\0';
522 fullfilename = buffer;
525 fh = fopen(fullfilename, "rb");
527 /* Not found? Search in wmaker's known places */
528 if (fh == NULL) {
529 if (req_filename[0] != '/') {
530 const char *src;
531 int idx;
533 fullfilename = buffer;
534 src = parser->include_default_paths;
535 while (*src != '\0') {
536 idx = 0;
537 if (*src == '~') {
538 char *home = wgethomedir();
539 while (*home != '\0') {
540 if (idx < sizeof(buffer) - 2)
541 buffer[idx++] = *home;
542 home++;
544 src++;
547 while ((*src != '\0') && (*src != ':')) {
548 if (idx < sizeof(buffer) - 2)
549 buffer[idx++] = *src;
550 src++;
553 buffer[idx++] = '/';
554 for (p = req_filename; *p != '\0'; p++)
555 if (idx < sizeof(buffer) - 1)
556 buffer[idx++] = *p;
557 buffer[idx] = '\0';
559 fh = fopen(fullfilename, "rb");
560 if (fh != NULL)
561 goto found_valid_file;
563 if (*src == ':')
564 src++;
567 WMenuParserError(parser, _("could not find file \"%s\" for include"), req_filename);
568 return False;
571 /* Found the file, make it our new source */
572 found_valid_file:
573 parser->include_file = menu_parser_create_new(wstrdup(req_filename), fh, parser->include_default_paths);
574 parser->include_file->parent_file = parser;
575 return True;
578 /* Check wether a macro exists or not, and marks the parser to ignore the
579 * following data accordingly */
580 static void menu_parser_condition_ifmacro(WMenuParser parser, Bool check_exists)
582 WParserMacro *macro;
583 int idx;
584 const char *cmd_name, *macro_name;
586 cmd_name = check_exists?"ifdef":"ifndef";
587 if (!menu_parser_skip_spaces_and_comments(parser)) {
588 WMenuParserError(parser, _("missing macro name argument to #%s"), cmd_name);
589 return;
592 /* jump to end of provided name for later checks that no extra stuff is following */
593 macro_name = parser->rd;
594 while (isnamechr(*parser->rd))
595 parser->rd++;
597 /* Add this condition to the stack of conditions */
598 if (parser->cond.depth >= sizeof(parser->cond.stack) / sizeof(parser->cond.stack[0])) {
599 WMenuParserError(parser, _("too many nested #if sequences") );
600 return;
603 for (idx = parser->cond.depth - 1; idx >= 0; idx--)
604 parser->cond.stack[idx + 1] = parser->cond.stack[idx];
606 parser->cond.depth++;
608 if (parser->cond.stack[1].skip) {
609 parser->cond.stack[0].skip = True;
610 } else {
611 macro = menu_parser_find_macro(parser, macro_name);
612 parser->cond.stack[0].skip =
613 ((check_exists) && (macro == NULL)) ||
614 ((!check_exists) && (macro != NULL)) ;
617 strcpy(parser->cond.stack[0].name, cmd_name);
618 parser->cond.stack[0].line = parser->line_number;
621 /* Swap the 'data ignore' flag because a #else condition was found */
622 static void menu_parser_condition_else(WMenuParser parser)
624 if (parser->cond.depth <= 0) {
625 WMenuParserError(parser, _("found #%s but have no matching #if"), "else" );
626 return;
629 if ((parser->cond.depth > 1) && (parser->cond.stack[1].skip))
630 /* The containing #if is false, so we continue skipping anyway */
631 parser->cond.stack[0].skip = True;
632 else
633 parser->cond.stack[0].skip = !parser->cond.stack[0].skip;
636 /* Closes the current conditional, removing it from the stack */
637 static void menu_parser_condition_end(WMenuParser parser)
639 int idx;
641 if (parser->cond.depth <= 0) {
642 WMenuParserError(parser, _("found #%s but have no matching #if"), "endif" );
643 return;
646 if (--parser->cond.depth > 0)
647 for (idx = 0; idx < parser->cond.depth; idx++)
648 parser->cond.stack[idx] = parser->cond.stack[idx + 1];
649 else
650 parser->cond.stack[0].skip = False;