WPrefs: Fixed handling of options which default to true
[wmaker-crm.git] / WINGs / menuparser.c
blobe812ba37f6f6d34aa872fd9af6532e7e9be50b1e
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 (params != NULL) {
232 lineparam[sizeof(lineparam) - 1] = '\0';
233 *parameter = wstrdup(lineparam);
236 return True;
239 /* Return False when there's nothing left on the line,
240 otherwise increment parser's pointer to next token */
241 Bool menu_parser_skip_spaces_and_comments(WMenuParser parser)
243 for (;;) {
244 while (isspace(*parser->rd))
245 parser->rd++;
247 if (*parser->rd == '\0') {
248 return False; /* Found the end of current line */
249 } else if ((parser->rd[0] == '\\') &&
250 (parser->rd[1] == '\n') &&
251 (parser->rd[2] == '\0')) {
252 /* Means that the current line is expected to be continued on next line */
253 if (fgets(parser->line_buffer, sizeof(parser->line_buffer), parser->file_handle) == NULL) {
254 WMenuParserError(parser, _("premature end of file while expecting a new line after '\\'") );
255 return False;
257 parser->line_number++;
258 parser->rd = parser->line_buffer;
260 } else if (parser->rd[0] == '/') {
261 if (parser->rd[1] == '/') /* Single line C comment */
262 return False; /* Won't find anything more on this line */
264 if (parser->rd[1] == '*') {
265 int start_line;
267 start_line = parser->line_number;
268 parser->rd += 2;
269 for (;;) {
270 /* Search end-of-comment marker */
271 while (*parser->rd != '\0') {
272 if ((parser->rd[0] == '*') && (parser->rd[1] == '/'))
273 goto found_end_of_comment;
275 parser->rd++;
278 /* Marker not found -> load next line */
279 if (fgets(parser->line_buffer, sizeof(parser->line_buffer), parser->file_handle) == NULL) {
280 WMenuParserError(parser, _("reached end of file while searching '*/' for comment started at line %d"), start_line);
281 return False;
284 parser->line_number++;
285 parser->rd = parser->line_buffer;
288 found_end_of_comment:
289 parser->rd += 2; /* Skip closing mark */
290 continue; /* Because there may be spaces after the comment */
293 return True; /* the '/' was not a comment, treat it as user data */
294 } else {
295 return True; /* Found some data */
300 /* read a token (non-spaces suite of characters)
301 * the result is wmalloc's, so it needs to be free'd */
302 static char *menu_parser_isolate_token(WMenuParser parser, WParserMacro *list_macros)
304 char buffer_token[sizeof(parser->line_buffer)];
305 char *token;
306 int limit = MAX_NESTED_MACROS;
308 token = buffer_token;
310 restart_token_split:
312 while (*parser->rd != '\0')
313 if (isspace(*parser->rd)) {
314 break;
316 } else if ((parser->rd[0] == '/') &&
317 ((parser->rd[1] == '*') || (parser->rd[1] == '/'))) {
318 break;
320 } else if (parser->rd[0] == '\\') {
321 if ((parser->rd[1] == '\n') || (parser->rd[1] == '\0'))
322 break;
324 parser->rd++;
325 *token++ = *parser->rd++;
327 } else if (*parser->rd == '"' ) {
328 char ch;
330 /* Double-quoted string deserve special processing because macros are not expanded
331 inside. We also remove the double quotes. */
332 parser->rd++;
333 while ((*parser->rd != '\0') && (*parser->rd != '\n')) {
334 ch = *parser->rd++;
335 if (ch == '\\') {
336 if ((*parser->rd == '\0') || (*parser->rd == '\n'))
337 break;
338 *token++ = *parser->rd++;
339 } else if (ch == '"')
340 goto found_end_dquote;
341 else
342 *token++ = ch;
345 WMenuParserError(parser, _("missing closing double-quote before end-of-line") );
347 found_end_dquote:
350 } else if (*parser->rd == '\'') {
351 char ch;
353 /* Simple-quoted string deserve special processing because we keep their content
354 as-is, including the quotes and the \-escaped text */
355 *token++ = *parser->rd++;
356 while ((*parser->rd != '\0') && (*parser->rd != '\n')) {
357 ch = *parser->rd++;
358 *token++ = ch;
359 if (ch == '\'')
360 goto found_end_squote;
363 WMenuParserError(parser, _("missing closing simple-quote before end-of-line") );
365 found_end_squote:
368 } else if (isnamechr(*parser->rd)) {
369 WParserMacro *macro;
371 macro = menu_parser_find_macro(parser, parser->rd);
372 if (macro != NULL) {
373 /* The expansion is done inside the parser's buffer
374 this is needed to allow sub macro calls */
375 menu_parser_expand_macro(parser, macro);
377 /* Restart parsing to allow expansion of sub macro calls */
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;
387 } else {
388 while (isnamechr(*parser->rd))
389 *token++ = *parser->rd++;
391 } else {
392 *token++ = *parser->rd++;
395 *token++ = '\0';
396 token = wmalloc(token - buffer_token);
397 strcpy(token, buffer_token);
399 return token;
402 /* Processing of special # directives */
403 static void menu_parser_get_directive(WMenuParser parser)
405 char *command;
407 /* Isolate the command */
408 while (isspace(*parser->rd))
409 parser->rd++;
411 command = parser->rd;
412 while (*parser->rd)
413 if (isspace(*parser->rd)) {
414 *parser->rd++ = '\0';
415 break;
416 } else {
417 parser->rd++;
420 if (strcmp(command, "include") == 0) {
421 if (!menu_parser_include_file(parser))
422 return;
424 } else if (strcmp(command, "define") == 0) {
425 menu_parser_define_macro(parser);
427 } else if (strcmp(command, "ifdef") == 0) {
428 menu_parser_condition_ifmacro(parser, 1);
430 } else if (strcmp(command, "ifndef") == 0) {
431 menu_parser_condition_ifmacro(parser, 0);
433 } else if (strcmp(command, "else") == 0) {
434 menu_parser_condition_else(parser);
436 } else if (strcmp(command, "endif") == 0) {
437 menu_parser_condition_end(parser);
439 } else {
440 WMenuParserError(parser, _("unknow directive '#%s'"), command);
441 return;
444 if (menu_parser_skip_spaces_and_comments(parser))
445 WMenuParserError(parser, _("extra text after '#' command is ignored: \"%.16s...\""),
446 parser->rd);
449 /* Extract the file name, search for it in known directories
450 * and create a sub-parser to handle it.
451 * Returns False if the file could not be found */
452 static Bool menu_parser_include_file(WMenuParser parser)
454 char buffer[MAXLINE];
455 char *req_filename, *fullfilename, *p;
456 char eot;
457 FILE *fh;
459 if (!menu_parser_skip_spaces_and_comments(parser)) {
460 WMenuParserError(parser, _("no file name found for #include") );
461 return False;
464 switch (*parser->rd++) {
465 case '<':
466 eot = '>';
467 break;
468 case '"':
469 eot = '"';
470 break;
471 default:
472 WMenuParserError(parser, _("file name must be enclosed in brackets or double-quotes for #define") );
473 return False;
476 req_filename = parser->rd;
477 while (*parser->rd) {
478 if (*parser->rd == eot) {
479 *parser->rd++ = '\0';
480 goto found_end_define_fname;
481 } else {
482 parser->rd++;
486 WMenuParserError(parser, _("missing closing '%c' in filename specification"), eot);
487 return False;
489 found_end_define_fname:
490 /* If we're inside a #if sequence, we abort now, but not sooner in
491 * order to keep the syntax check */
492 if (parser->cond.stack[0].skip)
493 return False;
495 { /* Check we are not nesting too many includes */
496 WMenuParser p;
497 int count;
499 count = 0;
500 for (p = parser; p->parent_file; p = p->parent_file)
501 count++;
503 if (count > MAX_NESTED_INCLUDES) {
504 WMenuParserError(parser, _("too many nested includes") );
505 return False;
509 /* Absolute paths */
510 fullfilename = req_filename;
511 if (req_filename[0] != '/') {
512 /* Search first in the same directory as the current file */
513 p = strrchr(parser->file_name, '/');
514 if (p != NULL) {
515 int len;
517 len = p - parser->file_name + 1;
518 if (len > sizeof(buffer) - 1)
519 len = sizeof(buffer) - 1;
521 strncpy(buffer, parser->file_name, len);
522 strncpy(buffer+len, req_filename, sizeof(buffer) - len - 1);
523 buffer[sizeof(buffer) - 1] = '\0';
524 fullfilename = buffer;
527 fh = fopen(fullfilename, "rb");
529 /* Not found? Search in wmaker's known places */
530 if (fh == NULL) {
531 if (req_filename[0] != '/') {
532 const char *src;
533 int idx;
535 fullfilename = buffer;
536 src = parser->include_default_paths;
537 while (*src != '\0') {
538 idx = 0;
539 if (*src == '~') {
540 char *home = wgethomedir();
541 while (*home != '\0') {
542 if (idx < sizeof(buffer) - 2)
543 buffer[idx++] = *home;
544 home++;
546 src++;
549 while ((*src != '\0') && (*src != ':')) {
550 if (idx < sizeof(buffer) - 2)
551 buffer[idx++] = *src;
552 src++;
555 buffer[idx++] = '/';
556 for (p = req_filename; *p != '\0'; p++)
557 if (idx < sizeof(buffer) - 1)
558 buffer[idx++] = *p;
559 buffer[idx] = '\0';
561 fh = fopen(fullfilename, "rb");
562 if (fh != NULL)
563 goto found_valid_file;
565 if (*src == ':')
566 src++;
569 WMenuParserError(parser, _("could not find file \"%s\" for include"), req_filename);
570 return False;
573 /* Found the file, make it our new source */
574 found_valid_file:
575 parser->include_file = menu_parser_create_new(wstrdup(req_filename), fh, parser->include_default_paths);
576 parser->include_file->parent_file = parser;
577 return True;
580 /* Check wether a macro exists or not, and marks the parser to ignore the
581 * following data accordingly */
582 static void menu_parser_condition_ifmacro(WMenuParser parser, Bool check_exists)
584 WParserMacro *macro;
585 int idx;
586 const char *cmd_name, *macro_name;
588 cmd_name = check_exists?"ifdef":"ifndef";
589 if (!menu_parser_skip_spaces_and_comments(parser)) {
590 WMenuParserError(parser, _("missing macro name argument to #%s"), cmd_name);
591 return;
594 /* jump to end of provided name for later checks that no extra stuff is following */
595 macro_name = parser->rd;
596 while (isnamechr(*parser->rd))
597 parser->rd++;
599 /* Add this condition to the stack of conditions */
600 if (parser->cond.depth >= sizeof(parser->cond.stack) / sizeof(parser->cond.stack[0])) {
601 WMenuParserError(parser, _("too many nested #if sequences") );
602 return;
605 for (idx = parser->cond.depth - 1; idx >= 0; idx--)
606 parser->cond.stack[idx + 1] = parser->cond.stack[idx];
608 parser->cond.depth++;
610 if (parser->cond.stack[1].skip) {
611 parser->cond.stack[0].skip = True;
612 } else {
613 macro = menu_parser_find_macro(parser, macro_name);
614 parser->cond.stack[0].skip =
615 ((check_exists) && (macro == NULL)) ||
616 ((!check_exists) && (macro != NULL)) ;
619 strcpy(parser->cond.stack[0].name, cmd_name);
620 parser->cond.stack[0].line = parser->line_number;
623 /* Swap the 'data ignore' flag because a #else condition was found */
624 static void menu_parser_condition_else(WMenuParser parser)
626 if (parser->cond.depth <= 0) {
627 WMenuParserError(parser, _("found #%s but have no matching #if"), "else" );
628 return;
631 if ((parser->cond.depth > 1) && (parser->cond.stack[1].skip))
632 /* The containing #if is false, so we continue skipping anyway */
633 parser->cond.stack[0].skip = True;
634 else
635 parser->cond.stack[0].skip = !parser->cond.stack[0].skip;
638 /* Closes the current conditional, removing it from the stack */
639 static void menu_parser_condition_end(WMenuParser parser)
641 int idx;
643 if (parser->cond.depth <= 0) {
644 WMenuParserError(parser, _("found #%s but have no matching #if"), "endif" );
645 return;
648 if (--parser->cond.depth > 0)
649 for (idx = 0; idx < parser->cond.depth; idx++)
650 parser->cond.stack[idx] = parser->cond.stack[idx + 1];
651 else
652 parser->cond.stack[0].skip = False;