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
, WParserMacro
*list_macros
);
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
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
, WMESSAGE_TYPE_WARNING
, "%s", buf
);
107 for (parent
= parser
->parent_file
; parent
!= NULL
; parent
= parent
->parent_file
)
108 __wmessage("WMenuParser", parser
->file_name
, parser
->line_number
, WMESSAGE_TYPE_WARNING
,
109 _(" included from file \"%s\" at line %d"),
110 parent
->file_name
, parent
->line_number
);
113 /***** Read one line from file and split content *****/
114 /* The function returns False when the end of file is reached */
115 Bool
WMenuParserGetLine(WMenuParser top_parser
, char **title
, char **command
, char **parameter
, char **shortcut
)
117 WMenuParser cur_parser
;
118 enum { GET_TITLE
, GET_COMMAND
, GET_PARAMETERS
, GET_SHORTCUT
} scanmode
;
120 char lineparam
[MAXLINE
];
128 scanmode
= GET_TITLE
;
130 read_next_line_with_filechange
:
131 cur_parser
= top_parser
;
132 while (cur_parser
->include_file
)
133 cur_parser
= cur_parser
->include_file
;
136 if (fgets(cur_parser
->line_buffer
, sizeof(cur_parser
->line_buffer
), cur_parser
->file_handle
) == NULL
) {
137 if (cur_parser
->cond
.depth
> 0) {
140 for (i
= 0; i
< cur_parser
->cond
.depth
; i
++)
141 WMenuParserError(cur_parser
, _("missing #endif to match #%s at line %d"),
142 cur_parser
->cond
.stack
[i
].name
, cur_parser
->cond
.stack
[i
].line
);
145 if (cur_parser
->parent_file
== NULL
)
146 /* Not inside an included file -> we have reached the end */
149 /* We have only reached the end of an included file -> go back to calling file */
150 fclose(cur_parser
->file_handle
);
151 wfree((char *) cur_parser
->file_name
);
152 cur_parser
= cur_parser
->parent_file
;
153 wfree(cur_parser
->include_file
);
154 cur_parser
->include_file
= NULL
;
155 goto read_next_line_with_filechange
;
157 cur_parser
->line_number
++;
158 cur_parser
->rd
= cur_parser
->line_buffer
;
161 if (!menu_parser_skip_spaces_and_comments(cur_parser
)) {
162 /* We reached the end of line */
163 if (scanmode
== GET_TITLE
)
164 goto read_next_line
; // Empty line -> skip
166 break; // Finished reading current line -> return it to caller
168 if ((scanmode
== GET_TITLE
) && (*cur_parser
->rd
== '#')) {
170 menu_parser_get_directive(cur_parser
);
171 goto read_next_line_with_filechange
;
173 if (cur_parser
->cond
.stack
[0].skip
)
177 token
= menu_parser_isolate_token(cur_parser
, top_parser
->macros
);
181 scanmode
= GET_COMMAND
;
185 if (strcmp(token
, "SHORTCUT") == 0) {
186 scanmode
= GET_SHORTCUT
;
190 scanmode
= GET_PARAMETERS
;
195 if (*shortcut
!= NULL
) {
196 WMenuParserError(top_parser
, _("multiple SHORTCUT definition not valid") );
200 scanmode
= GET_COMMAND
;
207 if (params
== NULL
) {
210 if ((params
- lineparam
) < sizeof(lineparam
)-1)
214 while ((params
- lineparam
) < sizeof(lineparam
)-1)
215 if ( (*params
= *src
++) == '\0')
226 char eot
, *src
, *dst
;
230 if ((eot
== '"') || (eot
== '\'')) {
234 if ((dst
> *title
) && (dst
[-1] == eot
))
240 if (params
!= NULL
) {
241 lineparam
[sizeof(lineparam
) - 1] = '\0';
242 *parameter
= wstrdup(lineparam
);
248 /* Return False when there's nothing left on the line,
249 otherwise increment parser's pointer to next token */
250 Bool
menu_parser_skip_spaces_and_comments(WMenuParser parser
)
253 while (isspace(*parser
->rd
))
256 if (*parser
->rd
== '\0')
257 return False
; // Found the end of current line
259 else if ((parser
->rd
[0] == '\\') &&
260 (parser
->rd
[1] == '\n') &&
261 (parser
->rd
[2] == '\0')) {
262 // Means that the current line is expected to be continued on next line
263 if (fgets(parser
->line_buffer
, sizeof(parser
->line_buffer
), parser
->file_handle
) == NULL
) {
264 WMenuParserError(parser
, _("premature end of file while expecting a new line after '\\'") );
267 parser
->line_number
++;
268 parser
->rd
= parser
->line_buffer
;
270 } else if (parser
->rd
[0] == '/') {
271 if (parser
->rd
[1] == '/') // Single line C comment
272 return False
; // Won't find anything more on this line
273 if (parser
->rd
[1] == '*') {
276 start_line
= parser
->line_number
;
279 /* Search end-of-comment marker */
280 while (*parser
->rd
!= '\0') {
281 if (parser
->rd
[0] == '*')
282 if (parser
->rd
[1] == '/')
283 goto found_end_of_comment
;
287 /* Marker not found -> load next line */
288 if (fgets(parser
->line_buffer
, sizeof(parser
->line_buffer
), parser
->file_handle
) == NULL
) {
289 WMenuParserError(parser
, _("reached end of file while searching '*/' for comment started at line %d"), start_line
);
292 parser
->line_number
++;
293 parser
->rd
= parser
->line_buffer
;
296 found_end_of_comment
:
297 parser
->rd
+= 2; // Skip closing mark
298 continue; // Because there may be spaces after the comment
300 return True
; // the '/' was not a comment, treat it as user data
302 return True
; // Found some data
306 /* read a token (non-spaces suite of characters)
307 the result os wmalloc's, so it needs to be free'd */
308 static char *menu_parser_isolate_token(WMenuParser parser
, WParserMacro
*list_macros
)
312 int limit
= MAX_NESTED_MACROS
;
317 while (*parser
->rd
!= '\0')
318 if (isspace(*parser
->rd
))
320 else if ((parser
->rd
[0] == '/') &&
321 ((parser
->rd
[1] == '*') || (parser
->rd
[1] == '/')))
323 else if ((parser
->rd
[0] == '\\') && (parser
->rd
[1] == '\n'))
325 else if ((*parser
->rd
== '"' ) || (*parser
->rd
== '\'')) {
326 char eot
= *parser
->rd
++;
327 while ((*parser
->rd
!= '\0') && (*parser
->rd
!= '\n'))
328 if (*parser
->rd
++ == eot
)
329 goto found_end_quote
;
330 WMenuParserError(parser
, _("missing closing quote or double-quote before end-of-line") );
333 } else if (isnamechr(*parser
->rd
)) {
337 start_macro
= parser
->rd
;
338 while (isnamechr(*parser
->rd
))
341 macro
= menu_parser_find_macro(parser
, start_macro
);
345 /* Copy the chars before the macro to the beginning of the buffer to
346 leave as much room as possible for expansion */
347 expand_there
= parser
->line_buffer
;
348 if (start
!= parser
->line_buffer
)
349 while (start
< start_macro
)
350 *expand_there
++ = *start
++;
351 start
= parser
->line_buffer
;
353 /* Macro expansion will take care to keep the rest of the line after
354 the macro to the end of the line for future token extraction */
355 menu_parser_expand_macro(parser
, macro
, expand_there
,
356 sizeof(parser
->line_buffer
) - (start
- parser
->line_buffer
) );
358 /* Restart parsing to allow expansion of sub macro calls */
359 parser
->rd
= expand_there
;
361 goto restart_token_split
;
362 WMenuParserError(parser
, _("too many nested macro expansion, breaking loop") );
363 while (isnamechr(*parser
->rd
))
367 // else: the text was passed over so it will be counted in the token from 'start'
371 token
= wmalloc(parser
->rd
- start
+ 1);
372 strncpy(token
, start
, parser
->rd
- start
);
373 token
[parser
->rd
- start
] = '\0';
378 /***** Processing of special # directives *****/
379 static void menu_parser_get_directive(WMenuParser parser
)
383 /* Isolate the command */
384 while (isspace(*parser
->rd
))
386 command
= parser
->rd
;
388 if (isspace(*parser
->rd
)) {
389 *parser
->rd
++ = '\0';
393 if (strcmp(command
, "include") == 0) {
394 if (!menu_parser_include_file(parser
)) return;
396 } else if (strcmp(command
, "define") == 0) {
397 menu_parser_define_macro(parser
);
399 } else if (strcmp(command
, "ifdef") == 0) {
400 menu_parser_condition_ifmacro(parser
, 1);
402 } else if (strcmp(command
, "ifndef") == 0) {
403 menu_parser_condition_ifmacro(parser
, 0);
405 } else if (strcmp(command
, "else") == 0) {
406 menu_parser_condition_else(parser
);
408 } else if (strcmp(command
, "endif") == 0) {
409 menu_parser_condition_end(parser
);
412 WMenuParserError(parser
, _("unknow directive '#%s'"), command
);
416 if (menu_parser_skip_spaces_and_comments(parser
))
417 WMenuParserError(parser
, _("extra text after '#' command is ignored: \"%.16s...\""),
421 /* Extract the file name, search for it in known directories
422 and create a sub-parser to handle it.
423 Returns False if the file could not be found */
424 static Bool
menu_parser_include_file(WMenuParser parser
)
426 char buffer
[MAXLINE
];
427 char *req_filename
, *fullfilename
, *p
;
431 if (!menu_parser_skip_spaces_and_comments(parser
)) {
432 WMenuParserError(parser
, _("no file name found for #include") );
435 switch (*parser
->rd
++) {
436 case '<': eot
= '>'; break;
437 case '"': eot
= '"'; break;
439 WMenuParserError(parser
, _("file name must be enclosed in brackets or double-quotes for #define") );
442 req_filename
= parser
->rd
;
444 if (*parser
->rd
== eot
) {
445 *parser
->rd
++ = '\0';
446 goto found_end_define_fname
;
448 WMenuParserError(parser
, _("missing closing '%c' in filename specification"), eot
);
450 found_end_define_fname
:
452 /* If we're inside a #if sequence, we abort now, but not sooner in
453 order to keep the syntax check */
454 if (parser
->cond
.stack
[0].skip
)
457 { /* Check we are not nesting too many includes */
462 for (p
= parser
; p
->parent_file
; p
= p
->parent_file
)
464 if (count
> MAX_NESTED_INCLUDES
) {
465 WMenuParserError(parser
, _("too many nested includes") );
471 fullfilename
= req_filename
;
472 if (req_filename
[0] != '/') {
473 /* Search first in the same directory as the current file */
474 p
= strrchr(parser
->file_name
, '/');
478 len
= p
- parser
->file_name
+ 1;
479 if (len
> sizeof(buffer
) - 1) len
= sizeof(buffer
) - 1;
480 strncpy(buffer
, parser
->file_name
, len
);
481 strncpy(buffer
+len
, req_filename
, sizeof(buffer
) - len
- 1);
482 buffer
[sizeof(buffer
) - 1] = '\0';
483 fullfilename
= buffer
;
486 fh
= fopen(fullfilename
, "rb");
488 /* Not found? Search in wmaker's known places */
490 if (req_filename
[0] != '/') {
494 fullfilename
= buffer
;
495 src
= parser
->include_default_paths
;
496 while (*src
!= '\0') {
499 char *home
= wgethomedir();
500 while (*home
!= '\0') {
501 if (idx
< sizeof(buffer
) - 2)
502 buffer
[idx
++] = *home
;
507 while ((*src
!= '\0') && (*src
!= ':')) {
508 if (idx
< sizeof(buffer
) - 2)
509 buffer
[idx
++] = *src
;
513 for (p
= req_filename
; *p
!= '\0'; p
++)
514 if (idx
< sizeof(buffer
) - 1)
518 fh
= fopen(fullfilename
, "rb");
519 if (fh
!= NULL
) goto found_valid_file
;
525 WMenuParserError(parser
, _("could not find file \"%s\" for include"), req_filename
);
529 /* Found the file, make it our new source */
531 parser
->include_file
= menu_parser_create_new(wstrdup(req_filename
), fh
, parser
->include_default_paths
);
532 parser
->include_file
->parent_file
= parser
;
536 /* Check wether a macro exists or not, and marks the parser to ignore the
537 following data accordingly */
538 static void menu_parser_condition_ifmacro(WMenuParser parser
, Bool check_exists
)
542 const char *cmd_name
, *macro_name
;
544 cmd_name
= check_exists
?"ifdef":"ifndef";
545 if (!menu_parser_skip_spaces_and_comments(parser
)) {
546 WMenuParserError(parser
, _("missing macro name argument to #%s"), cmd_name
);
550 /* jump to end of provided name for later checks that no extra stuff is following */
551 macro_name
= parser
->rd
;
552 while (isnamechr(*parser
->rd
))
555 /* Add this condition to the stack of conditions */
556 if (parser
->cond
.depth
>= sizeof(parser
->cond
.stack
) / sizeof(parser
->cond
.stack
[0])) {
557 WMenuParserError(parser
, _("too many nested #if sequences") );
560 for (idx
= parser
->cond
.depth
- 1; idx
>= 0; idx
--)
561 parser
->cond
.stack
[idx
+ 1] = parser
->cond
.stack
[idx
];
562 parser
->cond
.depth
++;
564 if (parser
->cond
.stack
[1].skip
)
565 parser
->cond
.stack
[0].skip
= True
;
567 macro
= menu_parser_find_macro(parser
, macro_name
);
568 parser
->cond
.stack
[0].skip
=
569 ((check_exists
) && (macro
== NULL
)) ||
570 ((!check_exists
) && (macro
!= NULL
)) ;
572 strcpy(parser
->cond
.stack
[0].name
, cmd_name
);
573 parser
->cond
.stack
[0].line
= parser
->line_number
;
576 /* Swap the 'data ignore' flag because a #else condition was found */
577 static void menu_parser_condition_else(WMenuParser parser
)
579 if (parser
->cond
.depth
<= 0) {
580 WMenuParserError(parser
, _("found #%s but have no matching #if"), "else" );
583 if ((parser
->cond
.depth
> 1) && (parser
->cond
.stack
[1].skip
))
584 // The containing #if is false, so we continue skipping anyway
585 parser
->cond
.stack
[0].skip
= True
;
587 parser
->cond
.stack
[0].skip
= !parser
->cond
.stack
[0].skip
;
590 /* Closes the current conditional, removing it from the stack */
591 static void menu_parser_condition_end(WMenuParser parser
)
595 if (parser
->cond
.depth
<= 0) {
596 WMenuParserError(parser
, _("found #%s but have no matching #if"), "endif" );
600 if (--parser
->cond
.depth
> 0)
601 for (idx
= 0; idx
< parser
->cond
.depth
; idx
++)
602 parser
->cond
.stack
[idx
] = parser
->cond
.stack
[idx
+ 1];
604 parser
->cond
.stack
[0].skip
= False
;