Remove dependency to CPP: support for #define macros
[wmaker-crm.git] / WINGs / menuparser.c
blobb39b3b17bb5bcf08f46be4ca827693ac4001431f
1 /*
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.
20 #include "wconfig.h"
22 #include <stdio.h>
23 #include <string.h>
24 #include <ctype.h>
25 #include <stdarg.h>
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);
38 /***** Constructor and Destructor for the Menu Parser object *****/
39 WMenuParser WMenuParserCreate(const char *file_name, void *file,
40 const char *include_default_paths)
42 WMenuParser parser;
44 parser = menu_parser_create_new(file_name, file, include_default_paths);
45 return parser;
48 void WMenuParserDelete(WMenuParser parser)
50 if (parser->include_file) {
51 /* Trick: the top parser's data are not wmalloc'd, we point on the
52 provided data so we do not wfree it; however for include files
53 we did wmalloc them.
54 This code should not be used as the wfree is done when we reach
55 the end of an include file; however this may not happen when an
56 early exit occurs (typically when 'readMenuFile' does not find
57 its expected command). */
58 fclose(parser->include_file->file_handle);
59 wfree((char *) parser->include_file->file_name);
60 WMenuParserDelete(parser->include_file);
63 if (parser->macros)
64 menu_parser_free_macros(parser);
66 wfree(parser);
69 static WMenuParser menu_parser_create_new(const char *file_name, void *file,
70 const char *include_default_paths)
72 WMenuParser parser;
74 parser = wmalloc(sizeof(*parser));
75 parser->include_default_paths = include_default_paths;
76 parser->file_name = file_name;
77 parser->file_handle = file;
78 parser->rd = parser->line_buffer;
80 return parser;
83 /***** To report helpfull messages to user *****/
84 const char *WMenuParserGetFilename(WMenuParser parser)
86 return parser->file_name;
89 void WMenuParserError(WMenuParser parser, const char *msg, ...)
91 char buf[MAXLINE];
92 va_list args;
93 WMenuParser parent;
95 while (parser->include_file)
96 parser = parser->include_file;
98 va_start(args, msg);
99 vsnprintf(buf, sizeof(buf), msg, args);
100 va_end(args);
101 __wmessage("WMenuParser", parser->file_name, parser->line_number, WMESSAGE_TYPE_WARNING, buf);
103 for (parent = parser->parent_file; parent != NULL; parent = parent->parent_file)
104 __wmessage("WMenuParser", parser->file_name, parser->line_number, WMESSAGE_TYPE_WARNING,
105 _(" included from file \"%s\" at line %d"),
106 parent->file_name, parent->line_number);
109 /***** Read one line from file and split content *****/
110 /* The function returns False when the end of file is reached */
111 Bool WMenuParserGetLine(WMenuParser top_parser, char **title, char **command, char **parameter, char **shortcut)
113 WMenuParser cur_parser;
114 enum { GET_TITLE, GET_COMMAND, GET_PARAMETERS, GET_SHORTCUT } scanmode;
115 char *token;
116 char lineparam[MAXLINE];
117 char *params = NULL;
119 lineparam[0] = '\0';
120 *title = NULL;
121 *command = NULL;
122 *parameter = NULL;
123 *shortcut = NULL;
124 scanmode = GET_TITLE;
126 read_next_line_with_filechange:
127 cur_parser = top_parser;
128 while (cur_parser->include_file)
129 cur_parser = cur_parser->include_file;
131 read_next_line:
132 if (fgets(cur_parser->line_buffer, sizeof(cur_parser->line_buffer), cur_parser->file_handle) == NULL) {
133 if (cur_parser->parent_file == NULL)
134 /* Not inside an included file -> we have reached the end */
135 return False;
137 /* We have only reached the end of an included file -> go back to calling file */
138 fclose(cur_parser->file_handle);
139 wfree((char *) cur_parser->file_name);
140 cur_parser = cur_parser->parent_file;
141 wfree(cur_parser->include_file);
142 cur_parser->include_file = NULL;
143 goto read_next_line_with_filechange;
145 cur_parser->line_number++;
146 cur_parser->rd = cur_parser->line_buffer;
148 for (;;) {
149 if (!menu_parser_skip_spaces_and_comments(cur_parser)) {
150 /* We reached the end of line */
151 if (scanmode == GET_TITLE)
152 goto read_next_line; // Empty line -> skip
153 else
154 break; // Finished reading current line -> return it to caller
156 if ((scanmode == GET_TITLE) && (*cur_parser->rd == '#')) {
157 cur_parser->rd++;
158 menu_parser_get_directive(cur_parser);
159 goto read_next_line_with_filechange;
162 /* Found a word */
163 token = menu_parser_isolate_token(cur_parser, top_parser->macros);
164 switch (scanmode) {
165 case GET_TITLE:
166 *title = token;
167 scanmode = GET_COMMAND;
168 break;
170 case GET_COMMAND:
171 if (strcmp(token, "SHORTCUT") == 0) {
172 scanmode = GET_SHORTCUT;
173 wfree(token);
174 } else {
175 *command = token;
176 scanmode = GET_PARAMETERS;
178 break;
180 case GET_SHORTCUT:
181 if (*shortcut != NULL) {
182 WMenuParserError(top_parser, _("multiple SHORTCUT definition not valid") );
183 wfree(*shortcut);
185 *shortcut = token;
186 scanmode = GET_COMMAND;
187 break;
189 case GET_PARAMETERS:
191 char *src;
193 if (params == NULL) {
194 params = lineparam;
195 } else {
196 if ((params - lineparam) < sizeof(lineparam)-1)
197 *params++ = ' ';
199 src = token;
200 while ((params - lineparam) < sizeof(lineparam)-1)
201 if ( (*params = *src++) == '\0')
202 break;
203 else
204 params++;
205 wfree(token);
207 break;
211 if (params != NULL) {
212 lineparam[sizeof(lineparam) - 1] = '\0';
213 *parameter = wstrdup(lineparam);
216 return True;
219 /* Return False when there's nothing left on the line,
220 otherwise increment parser's pointer to next token */
221 Bool menu_parser_skip_spaces_and_comments(WMenuParser parser)
223 for (;;) {
224 while (isspace(*parser->rd))
225 parser->rd++;
227 if (*parser->rd == '\0')
228 return False; // Found the end of current line
230 else if ((parser->rd[0] == '\\') &&
231 (parser->rd[1] == '\n') &&
232 (parser->rd[2] == '\0')) {
233 // Means that the current line is expected to be continued on next line
234 if (fgets(parser->line_buffer, sizeof(parser->line_buffer), parser->file_handle) == NULL) {
235 WMenuParserError(parser, _("premature end of file while expecting a new line after '\\'") );
236 return False;
238 parser->line_number++;
239 parser->rd = parser->line_buffer;
241 } else if (parser->rd[0] == '/') {
242 if (parser->rd[1] == '/') // Single line C comment
243 return False; // Won't find anything more on this line
244 if (parser->rd[1] == '*') {
245 int start_line;
247 start_line = parser->line_number;
248 parser->rd += 2;
249 for (;;) {
250 /* Search end-of-comment marker */
251 while (*parser->rd != '\0') {
252 if (parser->rd[0] == '*')
253 if (parser->rd[1] == '/')
254 goto found_end_of_comment;
255 parser->rd++;
258 /* Marker not found -> load next line */
259 if (fgets(parser->line_buffer, sizeof(parser->line_buffer), parser->file_handle) == NULL) {
260 WMenuParserError(parser, _("reached end of file while searching '*/' for comment started at line %d"), start_line);
261 return False;
263 parser->line_number++;
264 parser->rd = parser->line_buffer;
267 found_end_of_comment:
268 parser->rd += 2; // Skip closing mark
269 continue; // Because there may be spaces after the comment
271 return True; // the '/' was not a comment, treat it as user data
272 } else
273 return True; // Found some data
277 /* read a token (non-spaces suite of characters)
278 the result os wmalloc's, so it needs to be free'd */
279 static char *menu_parser_isolate_token(WMenuParser parser, WParserMacro *list_macros)
281 char *start;
282 char *token;
283 int limit = MAX_NESTED_MACROS;
285 start = parser->rd;
286 restart_token_split:
288 while (*parser->rd != '\0')
289 if (isspace(*parser->rd))
290 break;
291 else if ((parser->rd[0] == '/') &&
292 ((parser->rd[1] == '*') || (parser->rd[1] == '/')))
293 break;
294 else if ((parser->rd[0] == '\\') && (parser->rd[1] == '\n'))
295 break;
296 else if ((*parser->rd == '"' ) || (*parser->rd == '\'')) {
297 char eot = *parser->rd++;
298 while ((*parser->rd != '\0') && (*parser->rd != '\n'))
299 if (*parser->rd++ == eot)
300 goto found_end_quote;
301 WMenuParserError(parser, _("missing closing quote or double-quote before end-of-line") );
302 found_end_quote:
304 } else if (isnamechr(*parser->rd)) {
305 WParserMacro *macro;
306 char *start_macro;
308 start_macro = parser->rd;
309 while (isnamechr(*parser->rd))
310 parser->rd++;
312 macro = menu_parser_find_macro(parser, start_macro);
313 if (macro != NULL) {
314 char *expand_there;
316 /* Copy the chars before the macro to the beginning of the buffer to
317 leave as much room as possible for expansion */
318 expand_there = parser->line_buffer;
319 if (start != parser->line_buffer)
320 while (start < start_macro)
321 *expand_there++ = *start++;
322 start = parser->line_buffer;
324 /* Macro expansion will take care to keep the rest of the line after
325 the macro to the end of the line for future token extraction */
326 menu_parser_expand_macro(parser, macro, expand_there,
327 sizeof(parser->line_buffer) - (start - parser->line_buffer) );
329 /* Restart parsing to allow expansion of sub macro calls */
330 parser->rd = expand_there;
331 if (limit-- > 0)
332 goto restart_token_split;
333 WMenuParserError(parser, _("too many nested macro expansion, breaking loop") );
334 while (isnamechr(*parser->rd))
335 parser->rd++;
336 break;
338 // else: the text was passed over so it will be counted in the token from 'start'
339 } else
340 parser->rd++;
342 token = wmalloc(parser->rd - start + 1);
343 strncpy(token, start, parser->rd - start);
344 token[parser->rd - start] = '\0';
346 return token;
349 /***** Processing of special # directives *****/
350 static void menu_parser_get_directive(WMenuParser parser)
352 char *command;
354 /* Isolate the command */
355 while (isspace(*parser->rd))
356 parser->rd++;
357 command = parser->rd;
358 while (*parser->rd)
359 if (isspace(*parser->rd)) {
360 *parser->rd++ = '\0';
361 break;
362 } else parser->rd++;
364 if (strcmp(command, "include") == 0) {
365 if (!menu_parser_include_file(parser)) return;
367 } else if (strcmp(command, "define") == 0) {
368 menu_parser_define_macro(parser);
370 } else {
371 WMenuParserError(parser, _("unknow directive '#%s'"), command);
372 return;
375 if (menu_parser_skip_spaces_and_comments(parser))
376 WMenuParserError(parser, _("extra text after '#' command is ignored: \"%.16s...\""),
377 parser->rd);
380 /* Extract the file name, search for it in known directories
381 and create a sub-parser to handle it.
382 Returns False if the file could not be found */
383 static Bool menu_parser_include_file(WMenuParser parser)
385 char buffer[MAXLINE];
386 char *req_filename, *fullfilename, *p;
387 char eot;
388 FILE *fh;
390 if (!menu_parser_skip_spaces_and_comments(parser)) {
391 WMenuParserError(parser, _("no file name found for #include") );
392 return False;
394 switch (*parser->rd++) {
395 case '<': eot = '>'; break;
396 case '"': eot = '"'; break;
397 default:
398 WMenuParserError(parser, _("file name must be enclosed in brackets or double-quotes for #define") );
399 return False;
401 req_filename = parser->rd;
402 while (*parser->rd)
403 if (*parser->rd == eot) {
404 *parser->rd++ = '\0';
405 goto found_end_define_fname;
406 } else parser->rd++;
407 WMenuParserError(parser, _("missing closing '%c' in filename specification"), eot);
408 return False;
409 found_end_define_fname:
411 { /* Check we are not nesting too many includes */
412 WMenuParser p;
413 int count;
415 count = 0;
416 for (p = parser; p->parent_file; p = p->parent_file)
417 count++;
418 if (count > MAX_NESTED_INCLUDES) {
419 WMenuParserError(parser, _("too many nested includes") );
420 return False;
424 /* Absolute paths */
425 fullfilename = req_filename;
426 if (req_filename[0] != '/') {
427 /* Search first in the same directory as the current file */
428 p = strrchr(parser->file_name, '/');
429 if (p != NULL) {
430 int len;
432 len = p - parser->file_name + 1;
433 if (len > sizeof(buffer) - 1) len = sizeof(buffer) - 1;
434 strncpy(buffer, parser->file_name, len);
435 strncpy(buffer+len, req_filename, sizeof(buffer) - len - 1);
436 buffer[sizeof(buffer) - 1] = '\0';
437 fullfilename = buffer;
440 fh = fopen(fullfilename, "rb");
442 /* Not found? Search in wmaker's known places */
443 if (fh == NULL) {
444 if (req_filename[0] != '/') {
445 const char *src;
447 fullfilename = buffer;
448 src = parser->include_default_paths;
449 while (*src != '\0') {
450 p = buffer;
451 if (*src == '~') {
452 char *home = wgethomedir();
453 while (*home != '\0')
454 *p++ = *home++;
455 src++;
457 while ((*src != '\0') && (*src != ':'))
458 *p++ = *src++;
459 *p++ = '/';
460 strncpy(p, req_filename, sizeof(buffer) - (p - buffer - 1));
461 buffer[sizeof(buffer) - 1] = '\0';
463 fh = fopen(fullfilename, "rb");
464 if (fh != NULL) goto found_valid_file;
467 WMenuParserError(parser, _("could not find file \"%s\" for include"), req_filename);
468 return False;
471 /* Found the file, make it our new source */
472 found_valid_file:
473 parser->include_file = menu_parser_create_new(wstrdup(req_filename), fh, parser->include_default_paths);
474 parser->include_file->parent_file = parser;
475 return True;