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 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
)
47 parser
= menu_parser_create_new(file_name
, file
, include_default_paths
);
48 menu_parser_register_preset_macros(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
);
68 menu_parser_free_macros(parser
);
73 static WMenuParser
menu_parser_create_new(const char *file_name
, void *file
,
74 const char *include_default_paths
)
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
;
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
, ...)
99 while (parser
->include_file
)
100 parser
= parser
->include_file
;
103 vsnprintf(buf
, sizeof(buf
), msg
, 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
];
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
;
137 if (fgets(cur_parser
->line_buffer
, sizeof(cur_parser
->line_buffer
), cur_parser
->file_handle
) == NULL
) {
138 if (cur_parser
->cond
.depth
> 0) {
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 */
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
;
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 */
168 break; /* Finished reading current line -> return it to caller */
171 if ((scanmode
== GET_TITLE
) && (*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
)
181 token
= menu_parser_isolate_token(cur_parser
);
185 scanmode
= GET_COMMAND
;
189 if (strcmp(token
, "SHORTCUT") == 0) {
190 scanmode
= GET_SHORTCUT
;
194 scanmode
= GET_PARAMETERS
;
199 if (*shortcut
!= NULL
) {
200 WMenuParserError(top_parser
, _("multiple SHORTCUT definition not valid") );
204 scanmode
= GET_COMMAND
;
211 if (params
== NULL
) {
214 if ((params
- lineparam
) < sizeof(lineparam
) - 1)
219 while ((params
- lineparam
) < sizeof(lineparam
) - 1)
220 if ( (*params
= *src
++) == '\0')
230 if (params
!= NULL
) {
231 lineparam
[sizeof(lineparam
) - 1] = '\0';
232 *parameter
= wstrdup(lineparam
);
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
)
243 while (isspace(*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 '\\'") );
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] == '*') {
266 start_line
= parser
->line_number
;
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
;
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
);
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 */
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
)];
305 int limit
= MAX_NESTED_MACROS
;
307 token
= buffer_token
;
311 while (*parser
->rd
!= '\0')
312 if (isspace(*parser
->rd
)) {
315 } else if ((parser
->rd
[0] == '/') &&
316 ((parser
->rd
[1] == '*') || (parser
->rd
[1] == '/'))) {
319 } else if (parser
->rd
[0] == '\\') {
320 if ((parser
->rd
[1] == '\n') || (parser
->rd
[1] == '\0'))
324 *token
++ = *parser
->rd
++;
326 } else if (*parser
->rd
== '"' ) {
329 /* Double-quoted string deserve special processing because macros are not expanded
330 inside. We also remove the double quotes. */
332 while ((*parser
->rd
!= '\0') && (*parser
->rd
!= '\n')) {
335 if ((*parser
->rd
== '\0') || (*parser
->rd
== '\n'))
337 *token
++ = *parser
->rd
++;
338 } else if (ch
== '"')
339 goto found_end_dquote
;
344 WMenuParserError(parser
, _("missing closing double-quote before end-of-line") );
349 } else if (*parser
->rd
== '\'') {
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')) {
359 goto found_end_squote
;
362 WMenuParserError(parser
, _("missing closing simple-quote before end-of-line") );
367 } else if (isnamechr(*parser
->rd
)) {
370 macro
= menu_parser_find_macro(parser
, parser
->rd
);
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 */
378 goto restart_token_split
;
380 WMenuParserError(parser
, _("too many nested macro expansions, breaking loop"));
382 while (isnamechr(*parser
->rd
))
387 while (isnamechr(*parser
->rd
))
388 *token
++ = *parser
->rd
++;
391 *token
++ = *parser
->rd
++;
395 token
= wmalloc(token
- buffer_token
);
396 strcpy(token
, buffer_token
);
401 /* Processing of special # directives */
402 static void menu_parser_get_directive(WMenuParser parser
)
406 /* Isolate the command */
407 while (isspace(*parser
->rd
))
410 command
= parser
->rd
;
412 if (isspace(*parser
->rd
)) {
413 *parser
->rd
++ = '\0';
419 if (strcmp(command
, "include") == 0) {
420 if (!menu_parser_include_file(parser
))
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
);
439 WMenuParserError(parser
, _("unknown directive '#%s'"), command
);
443 if (menu_parser_skip_spaces_and_comments(parser
))
444 WMenuParserError(parser
, _("extra text after '#' command is ignored: \"%.16s...\""),
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
;
458 if (!menu_parser_skip_spaces_and_comments(parser
)) {
459 WMenuParserError(parser
, _("no file name found for #include") );
463 switch (*parser
->rd
++) {
471 WMenuParserError(parser
, _("file name must be enclosed in brackets or double-quotes for #define") );
475 req_filename
= parser
->rd
;
476 while (*parser
->rd
) {
477 if (*parser
->rd
== eot
) {
478 *parser
->rd
++ = '\0';
479 goto found_end_define_fname
;
485 WMenuParserError(parser
, _("missing closing '%c' in filename specification"), eot
);
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
)
494 { /* Check we are not nesting too many includes */
499 for (p
= parser
; p
->parent_file
; p
= p
->parent_file
)
502 if (count
> MAX_NESTED_INCLUDES
) {
503 WMenuParserError(parser
, _("too many nested #include's"));
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
, '/');
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 */
530 if (req_filename
[0] != '/') {
534 fullfilename
= buffer
;
535 src
= parser
->include_default_paths
;
536 while (*src
!= '\0') {
539 char *home
= wgethomedir();
540 while (*home
!= '\0') {
541 if (idx
< sizeof(buffer
) - 2)
542 buffer
[idx
++] = *home
;
548 while ((*src
!= '\0') && (*src
!= ':')) {
549 if (idx
< sizeof(buffer
) - 2)
550 buffer
[idx
++] = *src
;
555 for (p
= req_filename
; *p
!= '\0'; p
++)
556 if (idx
< sizeof(buffer
) - 1)
560 fh
= fopen(fullfilename
, "rb");
562 goto found_valid_file
;
568 WMenuParserError(parser
, _("could not find file \"%s\" for #include"), req_filename
);
572 /* Found the file, make it our new source */
574 parser
->include_file
= menu_parser_create_new(wstrdup(req_filename
), fh
, parser
->include_default_paths
);
575 parser
->include_file
->parent_file
= parser
;
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
)
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
);
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
))
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") );
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
;
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 has no matching #if"), "else");
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
;
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
)
642 if (parser
->cond
.depth
<= 0) {
643 WMenuParserError(parser
, _("found #%s but has no matching #if"), "endif");
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];
651 parser
->cond
.stack
[0].skip
= False
;