]> icculus.org git repositories - dana/openbox.git/blob - openbox/action_parser.c
parse filters and actionlists inside an action list correctly
[dana/openbox.git] / openbox / action_parser.c
1 /* -*- indent-tabs-mode: nil; tab-width: 4; c-basic-offset: 4; -*-
2
3    action_parser.c for the Openbox window manager
4    Copyright (c) 2011        Dana Jansens
5
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.
10
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.
15
16    See the COPYING file for a copy of the GNU General Public License.
17 */
18
19 #include "action_parser.h"
20 #include "action.h"
21 #include "action_filter.h"
22 #include "action_list.h"
23 #include "action_value.h"
24 #include "gettext.h"
25
26 #ifdef HAVE_STRING_H
27 #  include <string.h>
28 #endif
29
30 struct _ObActionList;
31 struct _ObActionListTest;
32 struct _ObActionValue;
33
34 #define SKIP " \t"
35 #define IDENTIFIER_FIRST G_CSET_a_2_z G_CSET_A_2_Z G_CSET_DIGITS "-_"
36 #define IDENTIFIER_NTH IDENTIFIER_FIRST G_CSET_LATINS G_CSET_LATINC
37 #define ESCAPE_SEQS "\"()"
38
39 struct _ObActionList* parse_list(ObActionParser *p,
40                                  GTokenType end,
41                                  gboolean *e);
42 struct _ObActionList* parse_action(ObActionParser *p, gboolean *e);
43 struct _ObActionList* parse_filter(ObActionParser *p, gboolean *e);
44 struct _ObActionListTest* parse_filter_test(ObActionParser *p, gboolean *e);
45 struct _ObActionValue* parse_value(ObActionParser *p,
46                                    gboolean allow_actions,
47                                    gboolean *e);
48 gchar* parse_string(ObActionParser *p, guchar end, gboolean *e);
49
50 struct _ObActionParser
51 {
52     gint ref;
53     GScanner *scan;
54 };
55
56 ObActionParser* action_parser_new(void)
57 {
58     ObActionParser *p;
59     GScannerConfig config = {
60         .cset_skip_characters = SKIP,
61         .cset_identifier_first =  IDENTIFIER_FIRST,
62         .cset_identifier_nth = IDENTIFIER_NTH,
63         .cpair_comment_single = "#\n",
64         .case_sensitive = FALSE,
65         .skip_comment_multi = TRUE,
66         .skip_comment_single = TRUE,
67         .scan_comment_multi = FALSE,
68         .scan_identifier = TRUE,
69         .scan_identifier_1char = TRUE,
70         .scan_identifier_NULL = FALSE,
71         .scan_symbols = TRUE,
72         .scan_binary = FALSE,
73         .scan_octal = FALSE,
74         .scan_float = TRUE,
75         .scan_hex = FALSE,
76         .scan_hex_dollar = FALSE,
77         .scan_string_sq = FALSE,
78         .scan_string_dq = FALSE,
79         .numbers_2_int = TRUE,
80         .int_2_float = FALSE,
81         .identifier_2_string = FALSE,
82         .char_2_token = TRUE,
83         .symbol_2_token = FALSE,
84         .scope_0_fallback = FALSE,
85         .store_int64 = FALSE
86     };
87
88     p = g_slice_new(ObActionParser);
89     p->ref = 1;
90     p->scan = g_scanner_new(&config);
91     return p;
92 }
93
94 void action_parser_ref(ObActionParser *p)
95 {
96     ++p->ref;
97 }
98
99 void action_parser_unref(ObActionParser *p)
100 {
101     if (p && --p->ref < 1) {
102         g_scanner_destroy(p->scan);
103         g_slice_free(ObActionParser, p);
104     }
105 }
106
107 ObActionList* action_parser_read_string(ObActionParser *p, const gchar *text)
108 {
109     gboolean e;
110
111     g_scanner_input_text(p->scan, text, strlen(text));
112     p->scan->input_name = "(console)";
113
114     e = FALSE;
115     return parse_list(p, G_TOKEN_EOF, &e);
116 }
117
118 ObActionList* actions_parser_read_file(ObActionParser *p,
119                                        const gchar *file,
120                                        GError **error)
121 {
122     GIOChannel *ch;
123     gboolean e;
124
125     ch = g_io_channel_new_file(file, "r", error);
126     if (!ch) return NULL;
127
128     g_scanner_input_file(p->scan, g_io_channel_unix_get_fd(ch));
129     p->scan->input_name = file;
130
131     e = FALSE;
132     return parse_list(p, G_TOKEN_EOF, &e);
133 }
134
135 /*************  Parser functions.  The list is the entry point.  ************
136 BNF for the language:
137
138 TEST       := KEY=VALUE | KEY
139 ACTION     := [FILTER] ACTION ELSE END | ACTIONNAME ACTIONOPTS | {ACTIONLIST}
140 ELSE       := nil | \| ACTION
141 END        := \n | ; | EOF
142 ACTIONLIST := ACTION ACTIONLIST | ACTION
143 FILTER     := FILTERORS
144 FILTERORS  := FILTERANDS \| FILTERORS | FILTERANDS
145 FILTERANDS := TEST, FILTERANDS | TEST
146 ACTIONOPTS := ACTIONOPT ACTIONOPTS | ACTIONOPT
147 ACTIONOPT  := ATTRIBUTE:WORD | ATTRIBUTE:STRING | ATTRIBUTE:{ACTIONLIST}
148 WORD   := string of text without any spaces
149 STRING := "TEXT" | (TEXT) |
150   where TEXT is a string, any occurance of the closing quote character must
151   be escaped by an backslash.
152   \\ \( \) and \" are all valid escaped characters.
153 **************                                                   ************/
154
155 gpointer parse_error(ObActionParser *p, GTokenType exp, const gchar *message,
156                      gboolean *e)
157 {
158     g_scanner_unexp_token(p->scan, exp, NULL, NULL, NULL, message, TRUE);
159     *e = TRUE;
160     return NULL;
161 }
162
163 ObActionList* parse_list(ObActionParser *p, GTokenType end, gboolean *e)
164 {
165     ObActionList *first, *last;
166     GTokenType t;
167
168     first = last = NULL;
169
170     t = g_scanner_peek_next_token(p->scan);
171     while (t != end && t != G_TOKEN_EOF) {
172         if (t == '\n')
173             g_scanner_get_next_token(p->scan); /* skip empty lines */
174         else if (t == ';') {
175             g_scanner_get_next_token(p->scan); /* separator */
176         }
177         else if (t == G_TOKEN_IDENTIFIER || t == '[' || t == '{') {
178             ObActionList *next;
179
180             /* parse the next action and stick it on the end of the list */
181             next = parse_action(p, e);
182             if (last) last->next = next;
183             if (!first) first = next;
184             last = next;
185         }
186         else {
187             g_scanner_get_next_token(p->scan);
188             parse_error(p, (end ? end : G_TOKEN_NONE),
189                         _("Expected an action or end of action list"), e);
190         }
191
192         if (*e) break; /* don't parse any more after an error */
193
194         t = g_scanner_peek_next_token(p->scan);
195     }
196
197     /* eat the ending character */
198     g_scanner_get_next_token(p->scan);
199
200     return first;
201 }
202
203 ObActionList* parse_action(ObActionParser *p, gboolean *e)
204 {
205     GTokenType t;
206     ObActionList *al;
207     gchar *name;
208     GHashTable *config;
209
210     t = g_scanner_get_next_token(p->scan);
211
212     if (t == '[') return parse_filter(p, e);
213     if (t == '{') return parse_list(p, '}', e);
214
215     /* check for a name */
216     if (t != G_TOKEN_IDENTIFIER)
217         return parse_error(p, G_TOKEN_NONE, _("Expected an action name"), e);
218
219     /* read the action's name */
220     name = g_strdup(p->scan->value.v_string);
221     config = g_hash_table_new_full(g_str_hash, g_str_equal, g_free,
222                                    (GDestroyNotify)action_value_unref);
223
224     /* read the action's options key:value pairs */
225     t = g_scanner_peek_next_token(p->scan);
226     while (t == G_TOKEN_IDENTIFIER || t == '\\') {
227         if (t == '\\') {
228             g_scanner_get_next_token(p->scan); /* eat it */
229             t = g_scanner_get_next_token(p->scan); /* check for '\n' */
230             if (t != '\n')
231                 parse_error(p, G_TOKEN_NONE, _("Expected newline"), e);
232         }
233         else {
234             gchar *key;
235             ObActionValue *value;
236
237             g_scanner_get_next_token(p->scan); /* eat the key */
238             t = g_scanner_peek_next_token(p->scan); /* check for ':' */
239             if (t != ':') {
240                 g_scanner_get_next_token(p->scan);
241                 parse_error(p, ':', NULL, e);
242                 break; /* don't read any more options */
243             }
244
245             /* save the key */
246             key = g_strdup(p->scan->value.v_string);
247             g_scanner_get_next_token(p->scan); /* eat the ':' */
248
249             /* read the value */
250             value = parse_value(p, TRUE, e);
251
252             /* check if we read a value (regardless of errors), and save
253                the key:value pair if we did. */
254             if (value)
255                 g_hash_table_replace(config, key, value);
256             else
257                 g_free(key); /* didn't read any value */
258         }
259
260         if (*e) break; /* don't parse any more if there was an error */
261
262         t = g_scanner_peek_next_token(p->scan);
263     }
264
265     al = g_slice_new(ObActionList);
266     al->ref = 1;
267     al->isfilterset = FALSE;
268     al->u.action = action_new(name, config);
269     al->next = NULL;
270     g_free(name);
271     g_hash_table_unref(config);
272     return al;
273 }
274
275 ObActionList* parse_filter(ObActionParser *p, gboolean *e)
276 {
277     GTokenType t;
278     ObActionList *al, *thendo, *elsedo;
279     ObActionListTest *test;
280
281     /* read the filter tests */
282     test = parse_filter_test(p, e);
283     if (*e) {
284         action_list_test_destroy(test);
285         return NULL;
286     }
287
288     /* read the action for the filter */
289     thendo = parse_action(p, e);
290     elsedo = NULL;
291
292     if (!*e) {
293         /* check for else case */
294         t = g_scanner_peek_next_token(p->scan);
295         if (t == '|') {
296             g_scanner_get_next_token(p->scan); /* eat it */
297
298             /* read the else action for the filter */
299             elsedo = parse_action(p, e);
300         }
301     }
302
303     al = g_slice_new(ObActionList);
304     al->ref = 1;
305     al->isfilterset = TRUE;
306     al->u.f.test = test;
307     al->u.f.thendo = thendo;
308     al->u.f.elsedo = elsedo;
309     al->next = NULL;
310     return al;
311 }
312
313 ObActionListTest* parse_filter_test(ObActionParser *p, gboolean *e)
314 {
315     GTokenType t;
316     gchar *key;
317     ObActionValue *value;
318     gboolean and;
319     ObActionFilter *filter;
320     ObActionListTest *next;
321
322     t = g_scanner_get_next_token(p->scan);
323     if (t == ']') /* empty */
324         return NULL;
325
326     else if (t != G_TOKEN_IDENTIFIER)
327         return parse_error(p, G_TOKEN_NONE,
328                            _("Expected a filter test lvalue"), e);
329
330     /* got a key */
331     key = g_strdup(p->scan->value.v_string);
332     value = NULL;
333     and = FALSE;
334     next = NULL;
335
336     /* check if it has a value also */
337     t = g_scanner_peek_next_token(p->scan);
338     if (t == '=') {
339         g_scanner_get_next_token(p->scan); /* eat the = */
340         value = parse_value(p, FALSE, e);
341     }
342
343     /* don't allow any errors (but value can be null if not present) */
344     if (*e) {
345         g_free(key);
346         action_value_unref(value);
347         return NULL;
348     }
349
350     filter = action_filter_new(key, value);
351     if (!filter) {
352         gchar *m;
353         m = g_strdup_printf(_("Unable to create filter: %s"), key);
354         parse_error(p, G_TOKEN_NONE, m, e);
355         g_free(m);
356     }
357
358     g_free(key);
359     action_value_unref(value);
360     if (!filter)
361         return NULL;
362
363     /* check if there is another test and how we're connected */
364     t = g_scanner_get_next_token(p->scan);
365     if (t == ',') { /* and */
366         and = TRUE;
367         next = parse_filter_test(p, e);
368     }
369     else if (t == '|') { /* or */
370         and = FALSE;
371         next = parse_filter_test(p, e);
372     }
373     else if (t == ']') /* end of the filter */
374         ;
375     else
376         parse_error(p, ']', NULL, e);
377
378     /* don't allow any errors */
379     if (*e) {
380         action_filter_unref(filter);
381         action_list_test_destroy(next);
382         return NULL;
383     }
384     else {
385         ObActionListTest *test;
386
387         test = g_slice_new(ObActionListTest);
388         test->filter = filter;
389         test->and = and;
390         test->next = next;
391         return test;
392     }
393 }
394
395 ObActionValue* parse_value(ObActionParser *p,
396                            gboolean allow_actions,
397                            gboolean *e)
398 {
399     GTokenType t;
400     ObActionValue *v;
401
402     v = NULL;
403     t = g_scanner_get_next_token(p->scan);
404     if (t == G_TOKEN_IDENTIFIER) {
405         v = action_value_new_string(p->scan->value.v_string);
406     }
407     else if (t == '"')
408         v = action_value_new_string(parse_string(p, '"', e));
409     else if (t == '(')
410         v = action_value_new_string(parse_string(p, ')', e));
411     else if (t == '{' && allow_actions) {
412         ObActionList *l = parse_list(p, '}', e);
413         if (l) v = action_value_new_action_list(l);
414     }
415     else
416         parse_error(p, G_TOKEN_NONE, _("Expected an option value"), e);
417     return v;
418 }
419
420
421 gchar* parse_string(ObActionParser *p, guchar end, gboolean *e)
422 {
423     GString *buf;
424     GTokenType t;
425     const gchar *error_message = NULL;
426
427     /* inside a string we want everything to be parsed as text (identifiers) */
428     p->scan->config->cset_skip_characters = "";
429     p->scan->config->cset_identifier_first = IDENTIFIER_NTH " ";
430     p->scan->config->cset_identifier_nth = IDENTIFIER_NTH " ";
431
432     buf = g_string_new("");
433
434     t = g_scanner_get_next_token(p->scan);
435     while (t != end) {
436         if (t == G_TOKEN_IDENTIFIER)
437             g_string_append(buf, p->scan->value.v_string);
438         else if (t == G_TOKEN_EOF) {
439             error_message = _("Missing end of quoted string");
440             goto parse_string_error;
441         }
442         else if (t == '\\') { /* escape sequence */
443             t = g_scanner_get_next_token(p->scan);
444             if (!strchr(ESCAPE_SEQS, t)) {
445                 error_message = _("Unknown escape sequence");
446                 goto parse_string_error;
447             }
448             g_string_append_c(buf, t);
449         }
450         else /* other single character */
451             g_string_append_c(buf, t);
452
453         t = g_scanner_get_next_token(p->scan);
454     }
455
456     /* reset to their default values */
457     p->scan->config->cset_skip_characters = SKIP;
458     p->scan->config->cset_identifier_first = IDENTIFIER_FIRST;
459     p->scan->config->cset_identifier_nth = IDENTIFIER_NTH;
460
461     {
462         gchar *v = buf->str;
463         g_string_free(buf, FALSE);
464         return v;
465     }
466
467 parse_string_error:
468     g_scanner_unexp_token(p->scan, G_TOKEN_NONE, NULL, NULL, NULL,
469                           error_message, TRUE);
470     g_string_free(buf, TRUE);
471     *e = TRUE;
472     return NULL;
473 }