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')
225 if (params
!= NULL
) {
226 lineparam
[sizeof(lineparam
) - 1] = '\0';
227 *parameter
= wstrdup(lineparam
);
233 /* Return False when there's nothing left on the line,
234 otherwise increment parser's pointer to next token */
235 Bool
menu_parser_skip_spaces_and_comments(WMenuParser parser
)
238 while (isspace(*parser
->rd
))
241 if (*parser
->rd
== '\0')
242 return False
; // Found the end of current line
244 else if ((parser
->rd
[0] == '\\') &&
245 (parser
->rd
[1] == '\n') &&
246 (parser
->rd
[2] == '\0')) {
247 // Means that the current line is expected to be continued on next line
248 if (fgets(parser
->line_buffer
, sizeof(parser
->line_buffer
), parser
->file_handle
) == NULL
) {
249 WMenuParserError(parser
, _("premature end of file while expecting a new line after '\\'") );
252 parser
->line_number
++;
253 parser
->rd
= parser
->line_buffer
;
255 } else if (parser
->rd
[0] == '/') {
256 if (parser
->rd
[1] == '/') // Single line C comment
257 return False
; // Won't find anything more on this line
258 if (parser
->rd
[1] == '*') {
261 start_line
= parser
->line_number
;
264 /* Search end-of-comment marker */
265 while (*parser
->rd
!= '\0') {
266 if (parser
->rd
[0] == '*')
267 if (parser
->rd
[1] == '/')
268 goto found_end_of_comment
;
272 /* Marker not found -> load next line */
273 if (fgets(parser
->line_buffer
, sizeof(parser
->line_buffer
), parser
->file_handle
) == NULL
) {
274 WMenuParserError(parser
, _("reached end of file while searching '*/' for comment started at line %d"), start_line
);
277 parser
->line_number
++;
278 parser
->rd
= parser
->line_buffer
;
281 found_end_of_comment
:
282 parser
->rd
+= 2; // Skip closing mark
283 continue; // Because there may be spaces after the comment
285 return True
; // the '/' was not a comment, treat it as user data
287 return True
; // Found some data
291 /* read a token (non-spaces suite of characters)
292 the result os wmalloc's, so it needs to be free'd */
293 static char *menu_parser_isolate_token(WMenuParser parser
, WParserMacro
*list_macros
)
297 int limit
= MAX_NESTED_MACROS
;
302 while (*parser
->rd
!= '\0')
303 if (isspace(*parser
->rd
))
305 else if ((parser
->rd
[0] == '/') &&
306 ((parser
->rd
[1] == '*') || (parser
->rd
[1] == '/')))
308 else if ((parser
->rd
[0] == '\\') && (parser
->rd
[1] == '\n'))
310 else if ((*parser
->rd
== '"' ) || (*parser
->rd
== '\'')) {
311 char eot
= *parser
->rd
++;
312 while ((*parser
->rd
!= '\0') && (*parser
->rd
!= '\n'))
313 if (*parser
->rd
++ == eot
)
314 goto found_end_quote
;
315 WMenuParserError(parser
, _("missing closing quote or double-quote before end-of-line") );
318 } else if (isnamechr(*parser
->rd
)) {
322 start_macro
= parser
->rd
;
323 while (isnamechr(*parser
->rd
))
326 macro
= menu_parser_find_macro(parser
, start_macro
);
330 /* Copy the chars before the macro to the beginning of the buffer to
331 leave as much room as possible for expansion */
332 expand_there
= parser
->line_buffer
;
333 if (start
!= parser
->line_buffer
)
334 while (start
< start_macro
)
335 *expand_there
++ = *start
++;
336 start
= parser
->line_buffer
;
338 /* Macro expansion will take care to keep the rest of the line after
339 the macro to the end of the line for future token extraction */
340 menu_parser_expand_macro(parser
, macro
, expand_there
,
341 sizeof(parser
->line_buffer
) - (start
- parser
->line_buffer
) );
343 /* Restart parsing to allow expansion of sub macro calls */
344 parser
->rd
= expand_there
;
346 goto restart_token_split
;
347 WMenuParserError(parser
, _("too many nested macro expansion, breaking loop") );
348 while (isnamechr(*parser
->rd
))
352 // else: the text was passed over so it will be counted in the token from 'start'
356 token
= wmalloc(parser
->rd
- start
+ 1);
357 strncpy(token
, start
, parser
->rd
- start
);
358 token
[parser
->rd
- start
] = '\0';
363 /***** Processing of special # directives *****/
364 static void menu_parser_get_directive(WMenuParser parser
)
368 /* Isolate the command */
369 while (isspace(*parser
->rd
))
371 command
= parser
->rd
;
373 if (isspace(*parser
->rd
)) {
374 *parser
->rd
++ = '\0';
378 if (strcmp(command
, "include") == 0) {
379 if (!menu_parser_include_file(parser
)) return;
381 } else if (strcmp(command
, "define") == 0) {
382 menu_parser_define_macro(parser
);
384 } else if (strcmp(command
, "ifdef") == 0) {
385 menu_parser_condition_ifmacro(parser
, 1);
387 } else if (strcmp(command
, "ifndef") == 0) {
388 menu_parser_condition_ifmacro(parser
, 0);
390 } else if (strcmp(command
, "else") == 0) {
391 menu_parser_condition_else(parser
);
393 } else if (strcmp(command
, "endif") == 0) {
394 menu_parser_condition_end(parser
);
397 WMenuParserError(parser
, _("unknow directive '#%s'"), command
);
401 if (menu_parser_skip_spaces_and_comments(parser
))
402 WMenuParserError(parser
, _("extra text after '#' command is ignored: \"%.16s...\""),
406 /* Extract the file name, search for it in known directories
407 and create a sub-parser to handle it.
408 Returns False if the file could not be found */
409 static Bool
menu_parser_include_file(WMenuParser parser
)
411 char buffer
[MAXLINE
];
412 char *req_filename
, *fullfilename
, *p
;
416 if (!menu_parser_skip_spaces_and_comments(parser
)) {
417 WMenuParserError(parser
, _("no file name found for #include") );
420 switch (*parser
->rd
++) {
421 case '<': eot
= '>'; break;
422 case '"': eot
= '"'; break;
424 WMenuParserError(parser
, _("file name must be enclosed in brackets or double-quotes for #define") );
427 req_filename
= parser
->rd
;
429 if (*parser
->rd
== eot
) {
430 *parser
->rd
++ = '\0';
431 goto found_end_define_fname
;
433 WMenuParserError(parser
, _("missing closing '%c' in filename specification"), eot
);
435 found_end_define_fname
:
437 /* If we're inside a #if sequence, we abort now, but not sooner in
438 order to keep the syntax check */
439 if (parser
->cond
.stack
[0].skip
)
442 { /* Check we are not nesting too many includes */
447 for (p
= parser
; p
->parent_file
; p
= p
->parent_file
)
449 if (count
> MAX_NESTED_INCLUDES
) {
450 WMenuParserError(parser
, _("too many nested includes") );
456 fullfilename
= req_filename
;
457 if (req_filename
[0] != '/') {
458 /* Search first in the same directory as the current file */
459 p
= strrchr(parser
->file_name
, '/');
463 len
= p
- parser
->file_name
+ 1;
464 if (len
> sizeof(buffer
) - 1) len
= sizeof(buffer
) - 1;
465 strncpy(buffer
, parser
->file_name
, len
);
466 strncpy(buffer
+len
, req_filename
, sizeof(buffer
) - len
- 1);
467 buffer
[sizeof(buffer
) - 1] = '\0';
468 fullfilename
= buffer
;
471 fh
= fopen(fullfilename
, "rb");
473 /* Not found? Search in wmaker's known places */
475 if (req_filename
[0] != '/') {
478 fullfilename
= buffer
;
479 src
= parser
->include_default_paths
;
480 while (*src
!= '\0') {
483 char *home
= wgethomedir();
484 while (*home
!= '\0')
488 while ((*src
!= '\0') && (*src
!= ':'))
491 strncpy(p
, req_filename
, sizeof(buffer
) - (p
- buffer
- 1));
492 buffer
[sizeof(buffer
) - 1] = '\0';
494 fh
= fopen(fullfilename
, "rb");
495 if (fh
!= NULL
) goto found_valid_file
;
498 WMenuParserError(parser
, _("could not find file \"%s\" for include"), req_filename
);
502 /* Found the file, make it our new source */
504 parser
->include_file
= menu_parser_create_new(wstrdup(req_filename
), fh
, parser
->include_default_paths
);
505 parser
->include_file
->parent_file
= parser
;
509 /* Check wether a macro exists or not, and marks the parser to ignore the
510 following data accordingly */
511 static void menu_parser_condition_ifmacro(WMenuParser parser
, Bool check_exists
)
515 const char *cmd_name
, *macro_name
;
517 cmd_name
= check_exists
?"ifdef":"ifndef";
518 if (!menu_parser_skip_spaces_and_comments(parser
)) {
519 WMenuParserError(parser
, _("missing macro name argument to #%s"), cmd_name
);
523 /* jump to end of provided name for later checks that no extra stuff is following */
524 macro_name
= parser
->rd
;
525 while (isnamechr(*parser
->rd
))
528 /* Add this condition to the stack of conditions */
529 if (parser
->cond
.depth
>= sizeof(parser
->cond
.stack
) / sizeof(parser
->cond
.stack
[0])) {
530 WMenuParserError(parser
, _("too many nested #if sequences") );
533 for (idx
= parser
->cond
.depth
- 1; idx
>= 0; idx
--)
534 parser
->cond
.stack
[idx
+ 1] = parser
->cond
.stack
[idx
];
535 parser
->cond
.depth
++;
537 if (parser
->cond
.stack
[1].skip
)
538 parser
->cond
.stack
[0].skip
= True
;
540 macro
= menu_parser_find_macro(parser
, macro_name
);
541 parser
->cond
.stack
[0].skip
=
542 ((check_exists
) && (macro
== NULL
)) ||
543 ((!check_exists
) && (macro
!= NULL
)) ;
545 strcpy(parser
->cond
.stack
[0].name
, cmd_name
);
546 parser
->cond
.stack
[0].line
= parser
->line_number
;
549 /* Swap the 'data ignore' flag because a #else condition was found */
550 static void menu_parser_condition_else(WMenuParser parser
)
552 if (parser
->cond
.depth
<= 0) {
553 WMenuParserError(parser
, _("found #%s but have no matching #if"), "else" );
556 if ((parser
->cond
.depth
> 1) && (parser
->cond
.stack
[1].skip
))
557 // The containing #if is false, so we continue skipping anyway
558 parser
->cond
.stack
[0].skip
= True
;
560 parser
->cond
.stack
[0].skip
= !parser
->cond
.stack
[0].skip
;
563 /* Closes the current conditional, removing it from the stack */
564 static void menu_parser_condition_end(WMenuParser parser
)
568 if (parser
->cond
.depth
<= 0) {
569 WMenuParserError(parser
, _("found #%s but have no matching #if"), "endif" );
573 if (--parser
->cond
.depth
> 0)
574 for (idx
= 0; idx
< parser
->cond
.depth
; idx
++)
575 parser
->cond
.stack
[idx
] = parser
->cond
.stack
[idx
+ 1];
577 parser
->cond
.stack
[0].skip
= False
;