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.
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
)
48 parser
= menu_parser_create_new(file_name
, file
, include_default_paths
);
49 menu_parser_register_preset_macros(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
);
69 menu_parser_free_macros(parser
);
74 static WMenuParser
menu_parser_create_new(const char *file_name
, void *file
,
75 const char *include_default_paths
)
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
;
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
, ...)
100 while (parser
->include_file
)
101 parser
= parser
->include_file
;
104 vsnprintf(buf
, sizeof(buf
), msg
, 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
];
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
;
138 if (fgets(cur_parser
->line_buffer
, sizeof(cur_parser
->line_buffer
), cur_parser
->file_handle
) == NULL
) {
139 if (cur_parser
->cond
.depth
> 0) {
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 */
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
;
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 */
169 break; /* Finished reading current line -> return it to caller */
172 if ((scanmode
== GET_TITLE
) && (*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
)
182 token
= menu_parser_isolate_token(cur_parser
, top_parser
->macros
);
186 scanmode
= GET_COMMAND
;
190 if (strcmp(token
, "SHORTCUT") == 0) {
191 scanmode
= GET_SHORTCUT
;
195 scanmode
= GET_PARAMETERS
;
200 if (*shortcut
!= NULL
) {
201 WMenuParserError(top_parser
, _("multiple SHORTCUT definition not valid") );
205 scanmode
= GET_COMMAND
;
212 if (params
== NULL
) {
215 if ((params
- lineparam
) < sizeof(lineparam
) - 1)
220 while ((params
- lineparam
) < sizeof(lineparam
) - 1)
221 if ( (*params
= *src
++) == '\0')
231 if (params
!= NULL
) {
232 lineparam
[sizeof(lineparam
) - 1] = '\0';
233 *parameter
= wstrdup(lineparam
);
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
)
244 while (isspace(*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 '\\'") );
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] == '*') {
267 start_line
= parser
->line_number
;
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
;
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
);
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 */
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
)];
306 int limit
= MAX_NESTED_MACROS
;
308 token
= buffer_token
;
312 while (*parser
->rd
!= '\0')
313 if (isspace(*parser
->rd
)) {
316 } else if ((parser
->rd
[0] == '/') &&
317 ((parser
->rd
[1] == '*') || (parser
->rd
[1] == '/'))) {
320 } else if (parser
->rd
[0] == '\\') {
321 if ((parser
->rd
[1] == '\n') || (parser
->rd
[1] == '\0'))
325 *token
++ = *parser
->rd
++;
327 } else if (*parser
->rd
== '"' ) {
330 /* Double-quoted string deserve special processing because macros are not expanded
331 inside. We also remove the double quotes. */
333 while ((*parser
->rd
!= '\0') && (*parser
->rd
!= '\n')) {
336 if ((*parser
->rd
== '\0') || (*parser
->rd
== '\n'))
338 *token
++ = *parser
->rd
++;
339 } else if (ch
== '"')
340 goto found_end_dquote
;
345 WMenuParserError(parser
, _("missing closing double-quote before end-of-line") );
350 } else if (*parser
->rd
== '\'') {
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')) {
360 goto found_end_squote
;
363 WMenuParserError(parser
, _("missing closing simple-quote before end-of-line") );
368 } else if (isnamechr(*parser
->rd
)) {
371 macro
= menu_parser_find_macro(parser
, parser
->rd
);
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 */
379 goto restart_token_split
;
381 WMenuParserError(parser
, _("too many nested macro expansion, breaking loop") );
383 while (isnamechr(*parser
->rd
))
388 while (isnamechr(*parser
->rd
))
389 *token
++ = *parser
->rd
++;
392 *token
++ = *parser
->rd
++;
396 token
= wmalloc(token
- buffer_token
);
397 strcpy(token
, buffer_token
);
402 /* Processing of special # directives */
403 static void menu_parser_get_directive(WMenuParser parser
)
407 /* Isolate the command */
408 while (isspace(*parser
->rd
))
411 command
= parser
->rd
;
413 if (isspace(*parser
->rd
)) {
414 *parser
->rd
++ = '\0';
420 if (strcmp(command
, "include") == 0) {
421 if (!menu_parser_include_file(parser
))
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
);
440 WMenuParserError(parser
, _("unknow directive '#%s'"), command
);
444 if (menu_parser_skip_spaces_and_comments(parser
))
445 WMenuParserError(parser
, _("extra text after '#' command is ignored: \"%.16s...\""),
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
;
459 if (!menu_parser_skip_spaces_and_comments(parser
)) {
460 WMenuParserError(parser
, _("no file name found for #include") );
464 switch (*parser
->rd
++) {
472 WMenuParserError(parser
, _("file name must be enclosed in brackets or double-quotes for #define") );
476 req_filename
= parser
->rd
;
477 while (*parser
->rd
) {
478 if (*parser
->rd
== eot
) {
479 *parser
->rd
++ = '\0';
480 goto found_end_define_fname
;
486 WMenuParserError(parser
, _("missing closing '%c' in filename specification"), eot
);
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
)
495 { /* Check we are not nesting too many includes */
500 for (p
= parser
; p
->parent_file
; p
= p
->parent_file
)
503 if (count
> MAX_NESTED_INCLUDES
) {
504 WMenuParserError(parser
, _("too many nested includes") );
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
, '/');
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 */
531 if (req_filename
[0] != '/') {
535 fullfilename
= buffer
;
536 src
= parser
->include_default_paths
;
537 while (*src
!= '\0') {
540 char *home
= wgethomedir();
541 while (*home
!= '\0') {
542 if (idx
< sizeof(buffer
) - 2)
543 buffer
[idx
++] = *home
;
549 while ((*src
!= '\0') && (*src
!= ':')) {
550 if (idx
< sizeof(buffer
) - 2)
551 buffer
[idx
++] = *src
;
556 for (p
= req_filename
; *p
!= '\0'; p
++)
557 if (idx
< sizeof(buffer
) - 1)
561 fh
= fopen(fullfilename
, "rb");
563 goto found_valid_file
;
569 WMenuParserError(parser
, _("could not find file \"%s\" for include"), req_filename
);
573 /* Found the file, make it our new source */
575 parser
->include_file
= menu_parser_create_new(wstrdup(req_filename
), fh
, parser
->include_default_paths
);
576 parser
->include_file
->parent_file
= parser
;
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
)
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
);
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
))
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") );
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
;
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" );
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
;
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
)
643 if (parser
->cond
.depth
<= 0) {
644 WMenuParserError(parser
, _("found #%s but have no matching #if"), "endif" );
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];
652 parser
->cond
.stack
[0].skip
= False
;