WINGs: Added 'const' attribute to function 'WMCreateHashTable'
[wmaker-crm.git] / WINGs / menuparser.c
blobdb0225bf9d61a832f4dcfb4a06ecc969b2e97169
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 static void menu_parser_get_directive(WMenuParser parser);
35 static Bool menu_parser_include_file(WMenuParser parser);
36 static void menu_parser_condition_ifmacro(WMenuParser parser, Bool check_exists);
37 static void menu_parser_condition_else(WMenuParser parser);
38 static void menu_parser_condition_end(WMenuParser parser);
41 /* Constructor and Destructor for the Menu Parser object */
42 WMenuParser WMenuParserCreate(const char *file_name, void *file,
43 const char *include_default_paths)
45 WMenuParser parser;
47 parser = menu_parser_create_new(file_name, file, include_default_paths);
48 menu_parser_register_preset_macros(parser);
49 return parser;
52 void WMenuParserDelete(WMenuParser parser)
54 if (parser->include_file) {
55 /* Trick: the top parser's data are not wmalloc'd, we point on the
56 * provided data so we do not wfree it; however for include files
57 * we did wmalloc them.
58 * This code should not be used as the wfree is done when we reach
59 * the end of an include file; however this may not happen when an
60 * early exit occurs (typically when 'readMenuFile' does not find
61 * its expected command). */
62 fclose(parser->include_file->file_handle);
63 wfree((char *) parser->include_file->file_name);
64 WMenuParserDelete(parser->include_file);
67 if (parser->macros)
68 menu_parser_free_macros(parser);
70 wfree(parser);
73 static WMenuParser menu_parser_create_new(const char *file_name, void *file,
74 const char *include_default_paths)
76 WMenuParser parser;
78 parser = wmalloc(sizeof(*parser));
79 parser->include_default_paths = include_default_paths;
80 parser->file_name = file_name;
81 parser->file_handle = file;
82 parser->rd = parser->line_buffer;
84 return parser;
87 /* To report helpfull messages to user */
88 const char *WMenuParserGetFilename(WMenuParser parser)
90 return parser->file_name;
93 void WMenuParserError(WMenuParser parser, const char *msg, ...)
95 char buf[MAXLINE];
96 va_list args;
97 WMenuParser parent;
99 while (parser->include_file)
100 parser = parser->include_file;
102 va_start(args, msg);
103 vsnprintf(buf, sizeof(buf), msg, args);
104 va_end(args);
105 __wmessage("WMenuParser", parser->file_name, parser->line_number,
106 WMESSAGE_TYPE_WARNING, "%s", buf);
108 for (parent = parser->parent_file; parent != NULL; parent = parent->parent_file)
109 __wmessage("WMenuParser", parser->file_name, parser->line_number,
110 WMESSAGE_TYPE_WARNING, _(" included from file \"%s\" at line %d"),
111 parent->file_name, parent->line_number);
114 /* Read one line from file and split content
115 * The function returns False when the end of file is reached */
116 Bool WMenuParserGetLine(WMenuParser top_parser, char **title, char **command,
117 char **parameter, char **shortcut)
119 WMenuParser cur_parser;
120 enum { GET_TITLE, GET_COMMAND, GET_PARAMETERS, GET_SHORTCUT } scanmode;
121 char *token, *params = NULL;
122 char lineparam[MAXLINE];
124 lineparam[0] = '\0';
125 *title = NULL;
126 *command = NULL;
127 *parameter = NULL;
128 *shortcut = NULL;
129 scanmode = GET_TITLE;
131 read_next_line_with_filechange:
132 cur_parser = top_parser;
133 while (cur_parser->include_file)
134 cur_parser = cur_parser->include_file;
136 read_next_line:
137 if (fgets(cur_parser->line_buffer, sizeof(cur_parser->line_buffer), cur_parser->file_handle) == NULL) {
138 if (cur_parser->cond.depth > 0) {
139 int i;
141 for (i = 0; i < cur_parser->cond.depth; i++)
142 WMenuParserError(cur_parser, _("missing #endif to match #%s at line %d"),
143 cur_parser->cond.stack[i].name, cur_parser->cond.stack[i].line);
146 if (cur_parser->parent_file == NULL)
147 /* Not inside an included file -> we have reached the end */
148 return False;
150 /* We have only reached the end of an included file -> go back to calling file */
151 fclose(cur_parser->file_handle);
152 wfree((char *) cur_parser->file_name);
153 cur_parser = cur_parser->parent_file;
154 wfree(cur_parser->include_file);
155 cur_parser->include_file = NULL;
156 goto read_next_line_with_filechange;
159 cur_parser->line_number++;
160 cur_parser->rd = cur_parser->line_buffer;
162 for (;;) {
163 if (!menu_parser_skip_spaces_and_comments(cur_parser)) {
164 /* We reached the end of line */
165 if (scanmode == GET_TITLE)
166 goto read_next_line; /* Empty line -> skip */
167 else
168 break; /* Finished reading current line -> return it to caller */
171 if ((scanmode == GET_TITLE) && (*cur_parser->rd == '#')) {
172 cur_parser->rd++;
173 menu_parser_get_directive(cur_parser);
174 goto read_next_line_with_filechange;
177 if (cur_parser->cond.stack[0].skip)
178 goto read_next_line;
180 /* Found a word */
181 token = menu_parser_isolate_token(cur_parser);
182 switch (scanmode) {
183 case GET_TITLE:
184 *title = token;
185 scanmode = GET_COMMAND;
186 break;
188 case GET_COMMAND:
189 if (strcmp(token, "SHORTCUT") == 0) {
190 scanmode = GET_SHORTCUT;
191 wfree(token);
192 } else {
193 *command = token;
194 scanmode = GET_PARAMETERS;
196 break;
198 case GET_SHORTCUT:
199 if (*shortcut != NULL) {
200 WMenuParserError(top_parser, _("multiple SHORTCUT definition not valid") );
201 wfree(*shortcut);
203 *shortcut = token;
204 scanmode = GET_COMMAND;
205 break;
207 case GET_PARAMETERS:
209 char *src;
211 if (params == NULL) {
212 params = lineparam;
213 } else {
214 if ((params - lineparam) < sizeof(lineparam) - 1)
215 *params++ = ' ';
218 src = token;
219 while ((params - lineparam) < sizeof(lineparam) - 1)
220 if ( (*params = *src++) == '\0')
221 break;
222 else
223 params++;
224 wfree(token);
226 break;
230 if (params != NULL) {
231 lineparam[sizeof(lineparam) - 1] = '\0';
232 *parameter = wstrdup(lineparam);
235 return True;
238 /* Return False when there's nothing left on the line,
239 otherwise increment parser's pointer to next token */
240 Bool menu_parser_skip_spaces_and_comments(WMenuParser parser)
242 for (;;) {
243 while (isspace(*parser->rd))
244 parser->rd++;
246 if (*parser->rd == '\0') {
247 return False; /* Found the end of current line */
248 } else if ((parser->rd[0] == '\\') &&
249 (parser->rd[1] == '\n') &&
250 (parser->rd[2] == '\0')) {
251 /* Means that the current line is expected to be continued on next line */
252 if (fgets(parser->line_buffer, sizeof(parser->line_buffer), parser->file_handle) == NULL) {
253 WMenuParserError(parser, _("premature end of file while expecting a new line after '\\'") );
254 return False;
256 parser->line_number++;
257 parser->rd = parser->line_buffer;
259 } else if (parser->rd[0] == '/') {
260 if (parser->rd[1] == '/') /* Single line C comment */
261 return False; /* Won't find anything more on this line */
263 if (parser->rd[1] == '*') {
264 int start_line;
266 start_line = parser->line_number;
267 parser->rd += 2;
268 for (;;) {
269 /* Search end-of-comment marker */
270 while (*parser->rd != '\0') {
271 if ((parser->rd[0] == '*') && (parser->rd[1] == '/'))
272 goto found_end_of_comment;
274 parser->rd++;
277 /* Marker not found -> load next line */
278 if (fgets(parser->line_buffer, sizeof(parser->line_buffer), parser->file_handle) == NULL) {
279 WMenuParserError(parser, _("reached end of file while searching '*/' for comment started at line %d"), start_line);
280 return False;
283 parser->line_number++;
284 parser->rd = parser->line_buffer;
287 found_end_of_comment:
288 parser->rd += 2; /* Skip closing mark */
289 continue; /* Because there may be spaces after the comment */
292 return True; /* the '/' was not a comment, treat it as user data */
293 } else {
294 return True; /* Found some data */
299 /* read a token (non-spaces suite of characters)
300 * the result is wmalloc's, so it needs to be free'd */
301 static char *menu_parser_isolate_token(WMenuParser parser)
303 char buffer_token[sizeof(parser->line_buffer)];
304 char *token;
305 int limit = MAX_NESTED_MACROS;
307 token = buffer_token;
309 restart_token_split:
311 while (*parser->rd != '\0')
312 if (isspace(*parser->rd)) {
313 break;
315 } else if ((parser->rd[0] == '/') &&
316 ((parser->rd[1] == '*') || (parser->rd[1] == '/'))) {
317 break;
319 } else if (parser->rd[0] == '\\') {
320 if ((parser->rd[1] == '\n') || (parser->rd[1] == '\0'))
321 break;
323 parser->rd++;
324 *token++ = *parser->rd++;
326 } else if (*parser->rd == '"' ) {
327 char ch;
329 /* Double-quoted string deserve special processing because macros are not expanded
330 inside. We also remove the double quotes. */
331 parser->rd++;
332 while ((*parser->rd != '\0') && (*parser->rd != '\n')) {
333 ch = *parser->rd++;
334 if (ch == '\\') {
335 if ((*parser->rd == '\0') || (*parser->rd == '\n'))
336 break;
337 *token++ = *parser->rd++;
338 } else if (ch == '"')
339 goto found_end_dquote;
340 else
341 *token++ = ch;
344 WMenuParserError(parser, _("missing closing double-quote before end-of-line") );
346 found_end_dquote:
349 } else if (*parser->rd == '\'') {
350 char ch;
352 /* Simple-quoted string deserve special processing because we keep their content
353 as-is, including the quotes and the \-escaped text */
354 *token++ = *parser->rd++;
355 while ((*parser->rd != '\0') && (*parser->rd != '\n')) {
356 ch = *parser->rd++;
357 *token++ = ch;
358 if (ch == '\'')
359 goto found_end_squote;
362 WMenuParserError(parser, _("missing closing simple-quote before end-of-line") );
364 found_end_squote:
367 } else if (isnamechr(*parser->rd)) {
368 WParserMacro *macro;
370 macro = menu_parser_find_macro(parser, parser->rd);
371 if (macro != NULL) {
372 /* The expansion is done inside the parser's buffer
373 this is needed to allow sub macro calls */
374 menu_parser_expand_macro(parser, macro);
376 /* Restart parsing to allow expansion of sub macro calls */
377 if (limit-- > 0)
378 goto restart_token_split;
380 WMenuParserError(parser, _("too many nested macro expansion, breaking loop") );
382 while (isnamechr(*parser->rd))
383 parser->rd++;
385 break;
386 } else {
387 while (isnamechr(*parser->rd))
388 *token++ = *parser->rd++;
390 } else {
391 *token++ = *parser->rd++;
394 *token++ = '\0';
395 token = wmalloc(token - buffer_token);
396 strcpy(token, buffer_token);
398 return token;
401 /* Processing of special # directives */
402 static void menu_parser_get_directive(WMenuParser parser)
404 char *command;
406 /* Isolate the command */
407 while (isspace(*parser->rd))
408 parser->rd++;
410 command = parser->rd;
411 while (*parser->rd)
412 if (isspace(*parser->rd)) {
413 *parser->rd++ = '\0';
414 break;
415 } else {
416 parser->rd++;
419 if (strcmp(command, "include") == 0) {
420 if (!menu_parser_include_file(parser))
421 return;
423 } else if (strcmp(command, "define") == 0) {
424 menu_parser_define_macro(parser);
426 } else if (strcmp(command, "ifdef") == 0) {
427 menu_parser_condition_ifmacro(parser, 1);
429 } else if (strcmp(command, "ifndef") == 0) {
430 menu_parser_condition_ifmacro(parser, 0);
432 } else if (strcmp(command, "else") == 0) {
433 menu_parser_condition_else(parser);
435 } else if (strcmp(command, "endif") == 0) {
436 menu_parser_condition_end(parser);
438 } else {
439 WMenuParserError(parser, _("unknow directive '#%s'"), command);
440 return;
443 if (menu_parser_skip_spaces_and_comments(parser))
444 WMenuParserError(parser, _("extra text after '#' command is ignored: \"%.16s...\""),
445 parser->rd);
448 /* Extract the file name, search for it in known directories
449 * and create a sub-parser to handle it.
450 * Returns False if the file could not be found */
451 static Bool menu_parser_include_file(WMenuParser parser)
453 char buffer[MAXLINE];
454 char *req_filename, *fullfilename, *p;
455 char eot;
456 FILE *fh;
458 if (!menu_parser_skip_spaces_and_comments(parser)) {
459 WMenuParserError(parser, _("no file name found for #include") );
460 return False;
463 switch (*parser->rd++) {
464 case '<':
465 eot = '>';
466 break;
467 case '"':
468 eot = '"';
469 break;
470 default:
471 WMenuParserError(parser, _("file name must be enclosed in brackets or double-quotes for #define") );
472 return False;
475 req_filename = parser->rd;
476 while (*parser->rd) {
477 if (*parser->rd == eot) {
478 *parser->rd++ = '\0';
479 goto found_end_define_fname;
480 } else {
481 parser->rd++;
485 WMenuParserError(parser, _("missing closing '%c' in filename specification"), eot);
486 return False;
488 found_end_define_fname:
489 /* If we're inside a #if sequence, we abort now, but not sooner in
490 * order to keep the syntax check */
491 if (parser->cond.stack[0].skip)
492 return False;
494 { /* Check we are not nesting too many includes */
495 WMenuParser p;
496 int count;
498 count = 0;
499 for (p = parser; p->parent_file; p = p->parent_file)
500 count++;
502 if (count > MAX_NESTED_INCLUDES) {
503 WMenuParserError(parser, _("too many nested includes") );
504 return False;
508 /* Absolute paths */
509 fullfilename = req_filename;
510 if (req_filename[0] != '/') {
511 /* Search first in the same directory as the current file */
512 p = strrchr(parser->file_name, '/');
513 if (p != NULL) {
514 int len;
516 len = p - parser->file_name + 1;
517 if (len > sizeof(buffer) - 1)
518 len = sizeof(buffer) - 1;
520 strncpy(buffer, parser->file_name, len);
521 strncpy(buffer+len, req_filename, sizeof(buffer) - len - 1);
522 buffer[sizeof(buffer) - 1] = '\0';
523 fullfilename = buffer;
526 fh = fopen(fullfilename, "rb");
528 /* Not found? Search in wmaker's known places */
529 if (fh == NULL) {
530 if (req_filename[0] != '/') {
531 const char *src;
532 int idx;
534 fullfilename = buffer;
535 src = parser->include_default_paths;
536 while (*src != '\0') {
537 idx = 0;
538 if (*src == '~') {
539 char *home = wgethomedir();
540 while (*home != '\0') {
541 if (idx < sizeof(buffer) - 2)
542 buffer[idx++] = *home;
543 home++;
545 src++;
548 while ((*src != '\0') && (*src != ':')) {
549 if (idx < sizeof(buffer) - 2)
550 buffer[idx++] = *src;
551 src++;
554 buffer[idx++] = '/';
555 for (p = req_filename; *p != '\0'; p++)
556 if (idx < sizeof(buffer) - 1)
557 buffer[idx++] = *p;
558 buffer[idx] = '\0';
560 fh = fopen(fullfilename, "rb");
561 if (fh != NULL)
562 goto found_valid_file;
564 if (*src == ':')
565 src++;
568 WMenuParserError(parser, _("could not find file \"%s\" for include"), req_filename);
569 return False;
572 /* Found the file, make it our new source */
573 found_valid_file:
574 parser->include_file = menu_parser_create_new(wstrdup(req_filename), fh, parser->include_default_paths);
575 parser->include_file->parent_file = parser;
576 return True;
579 /* Check wether a macro exists or not, and marks the parser to ignore the
580 * following data accordingly */
581 static void menu_parser_condition_ifmacro(WMenuParser parser, Bool check_exists)
583 WParserMacro *macro;
584 int idx;
585 const char *cmd_name, *macro_name;
587 cmd_name = check_exists?"ifdef":"ifndef";
588 if (!menu_parser_skip_spaces_and_comments(parser)) {
589 WMenuParserError(parser, _("missing macro name argument to #%s"), cmd_name);
590 return;
593 /* jump to end of provided name for later checks that no extra stuff is following */
594 macro_name = parser->rd;
595 while (isnamechr(*parser->rd))
596 parser->rd++;
598 /* Add this condition to the stack of conditions */
599 if (parser->cond.depth >= wlengthof(parser->cond.stack)) {
600 WMenuParserError(parser, _("too many nested #if sequences") );
601 return;
604 for (idx = parser->cond.depth - 1; idx >= 0; idx--)
605 parser->cond.stack[idx + 1] = parser->cond.stack[idx];
607 parser->cond.depth++;
609 if (parser->cond.stack[1].skip) {
610 parser->cond.stack[0].skip = True;
611 } else {
612 macro = menu_parser_find_macro(parser, macro_name);
613 parser->cond.stack[0].skip =
614 ((check_exists) && (macro == NULL)) ||
615 ((!check_exists) && (macro != NULL)) ;
618 strcpy(parser->cond.stack[0].name, cmd_name);
619 parser->cond.stack[0].line = parser->line_number;
622 /* Swap the 'data ignore' flag because a #else condition was found */
623 static void menu_parser_condition_else(WMenuParser parser)
625 if (parser->cond.depth <= 0) {
626 WMenuParserError(parser, _("found #%s but have no matching #if"), "else" );
627 return;
630 if ((parser->cond.depth > 1) && (parser->cond.stack[1].skip))
631 /* The containing #if is false, so we continue skipping anyway */
632 parser->cond.stack[0].skip = True;
633 else
634 parser->cond.stack[0].skip = !parser->cond.stack[0].skip;
637 /* Closes the current conditional, removing it from the stack */
638 static void menu_parser_condition_end(WMenuParser parser)
640 int idx;
642 if (parser->cond.depth <= 0) {
643 WMenuParserError(parser, _("found #%s but have no matching #if"), "endif" );
644 return;
647 if (--parser->cond.depth > 0)
648 for (idx = 0; idx < parser->cond.depth; idx++)
649 parser->cond.stack[idx] = parser->cond.stack[idx + 1];
650 else
651 parser->cond.stack[0].skip = False;