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')
232 char eot
, *src
, *dst
;
236 if ((eot
== '"') || (eot
== '\'')) {
242 if ((dst
> *title
) && (dst
[-1] == eot
))
249 if (params
!= NULL
) {
250 lineparam
[sizeof(lineparam
) - 1] = '\0';
251 *parameter
= wstrdup(lineparam
);
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
)
262 while (isspace(*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 '\\'") );
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] == '*') {
285 start_line
= parser
->line_number
;
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
;
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
);
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 */
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
)
323 int limit
= MAX_NESTED_MACROS
;
328 while (*parser
->rd
!= '\0')
329 if (isspace(*parser
->rd
)) {
332 } else if ((parser
->rd
[0] == '/') &&
333 ((parser
->rd
[1] == '*') || (parser
->rd
[1] == '/'))) {
336 } else if ((parser
->rd
[0] == '\\') && (parser
->rd
[1] == '\n')) {
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") );
350 } else if (isnamechr(*parser
->rd
)) {
354 start_macro
= parser
->rd
;
355 while (isnamechr(*parser
->rd
))
358 macro
= menu_parser_find_macro(parser
, start_macro
);
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
;
379 goto restart_token_split
;
381 WMenuParserError(parser
, _("too many nested macro expansion, breaking loop") );
383 while (isnamechr(*parser
->rd
))
388 /* else: the text was passed over so it will be counted in the token from 'start' */
393 token
= wmalloc(parser
->rd
- start
+ 1);
394 strncpy(token
, start
, parser
->rd
- start
);
395 token
[parser
->rd
- start
] = '\0';
400 /* Processing of special # directives */
401 static void menu_parser_get_directive(WMenuParser parser
)
405 /* Isolate the command */
406 while (isspace(*parser
->rd
))
409 command
= parser
->rd
;
411 if (isspace(*parser
->rd
)) {
412 *parser
->rd
++ = '\0';
418 if (strcmp(command
, "include") == 0) {
419 if (!menu_parser_include_file(parser
))
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
);
438 WMenuParserError(parser
, _("unknow directive '#%s'"), command
);
442 if (menu_parser_skip_spaces_and_comments(parser
))
443 WMenuParserError(parser
, _("extra text after '#' command is ignored: \"%.16s...\""),
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
;
457 if (!menu_parser_skip_spaces_and_comments(parser
)) {
458 WMenuParserError(parser
, _("no file name found for #include") );
462 switch (*parser
->rd
++) {
470 WMenuParserError(parser
, _("file name must be enclosed in brackets or double-quotes for #define") );
474 req_filename
= parser
->rd
;
475 while (*parser
->rd
) {
476 if (*parser
->rd
== eot
) {
477 *parser
->rd
++ = '\0';
478 goto found_end_define_fname
;
484 WMenuParserError(parser
, _("missing closing '%c' in filename specification"), eot
);
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
)
493 { /* Check we are not nesting too many includes */
498 for (p
= parser
; p
->parent_file
; p
= p
->parent_file
)
501 if (count
> MAX_NESTED_INCLUDES
) {
502 WMenuParserError(parser
, _("too many nested includes") );
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
, '/');
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 */
529 if (req_filename
[0] != '/') {
533 fullfilename
= buffer
;
534 src
= parser
->include_default_paths
;
535 while (*src
!= '\0') {
538 char *home
= wgethomedir();
539 while (*home
!= '\0') {
540 if (idx
< sizeof(buffer
) - 2)
541 buffer
[idx
++] = *home
;
547 while ((*src
!= '\0') && (*src
!= ':')) {
548 if (idx
< sizeof(buffer
) - 2)
549 buffer
[idx
++] = *src
;
554 for (p
= req_filename
; *p
!= '\0'; p
++)
555 if (idx
< sizeof(buffer
) - 1)
559 fh
= fopen(fullfilename
, "rb");
561 goto found_valid_file
;
567 WMenuParserError(parser
, _("could not find file \"%s\" for include"), req_filename
);
571 /* Found the file, make it our new source */
573 parser
->include_file
= menu_parser_create_new(wstrdup(req_filename
), fh
, parser
->include_default_paths
);
574 parser
->include_file
->parent_file
= parser
;
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
)
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
);
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
))
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") );
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
;
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" );
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
;
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
)
641 if (parser
->cond
.depth
<= 0) {
642 WMenuParserError(parser
, _("found #%s but have no matching #if"), "endif" );
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];
650 parser
->cond
.stack
[0].skip
= False
;