support \xXX escape characters in args field
[dbuscron] / dbuscron / parser.py
1 # encoding: utf-8
2 from __future__ import with_statement
3 import re
4 from dbuscron.bus import DbusBus
5
6 def unescape(value):
7     if not value or r'\x' not in value:
8         return value
9
10     r = re.compile(r'\\x([0-9A-Fa-f]{2})')
11     return r.sub(lambda m: chr(int(m.group(1), 16)), value)
12
13 def product(*args):
14     if args:
15         head, tail = args[0], args[1:]
16         for h in head:
17             for t in product(*tail):
18                 yield (h,) + t
19
20     else:
21         yield ()
22
23 class CrontabParserError(SyntaxError):
24     def __init__(self, message, lineno, expected=None):
25         if expected:
26             if isinstance(expected, (tuple, list)):
27                 exp = ' (expected %s or %s)' % (', '.join(expected[:-1]), expected[-1])
28         else:
29             exp = ''
30
31         msg = '%s%s at line %d' % (message, exp, lineno)
32
33         SyntaxError.__init__(self, msg)
34
35 class CrontabParser(object):
36     __fields_sep = re.compile(r'\s+')
37     __envvar_sep = re.compile(r'\s*=\s*')
38     __fields_chk = {
39             'bus_'         : None,
40             'type_'        : ('signal','method_call','method_return','error'),
41             'sender_'      : None,
42             'interface_'   : re.compile(r'^[a-zA-Z][a-zA-Z0-9_.]+$'),
43             'path_'        : re.compile(r'^/[a-zA-Z0-9_/]+$'),
44             'member_'      : re.compile(r'^[a-zA-Z][a-zA-Z0-9_]+$'),
45             'destination_' : None,
46             'args_'        : None,
47             }
48     __fields = [
49             'bus_',
50             'type_',
51             'sender_',
52             'interface_',
53             'path_',
54             'member_',
55             'destination_',
56             'args_',
57             ]
58
59     def __init__(self, fname):
60         self.__bus = DbusBus()
61         self.__filename = fname
62         self.__environ = dict()
63
64     @property
65     def environ(self):
66         return self.__environ
67
68     def __iter__(self):
69         # bus type sender interface path member destination args command
70         lineno = 0
71         with open(self.__filename) as f:
72             for line in f:
73                 lineno += 1
74                 line = line.strip()
75
76                 if not line or line.startswith('#'):
77                     continue
78
79                 parts = self.__fields_sep.split(line, 8)
80                 if len(parts) < 9:
81                     parts = self.__envvar_sep.split(line, 1)
82                     if len(parts) == 2:
83                         self.__environ[parts[0]] = parts[1]
84                         continue
85
86                     raise CrontabParserError('Unexpected number of records', lineno)
87
88                 rule = [('s','S'), self.__fields_chk['type_'], (None,), (None,), (None,), (None,), (None,), (None,)]
89
90                 for p in range(0, 8):
91                     if parts[p] != '*':
92                         rule[p] = parts[p].split(',')
93
94                 command = parts[8]
95  
96                 for r in product(*rule):
97                     r = list(r)
98                     if r[0] == 'S':
99                         r[0] = self.__bus.system
100                     elif r[0] == 's':
101                         r[0] = self.__bus.session
102                     else:
103                         raise CrontabParserError('Unexpected bus value', lineno, expected=('S', 's', '*'))
104
105                     if r[7]:
106                         r[7] = map(unescape, r[7].split(';'))
107
108                     ruled = dict()
109                     for i, f in enumerate(self.__fields):
110                         if self.__fields_chk[f]:
111                             if isinstance(self.__fields_chk[f], tuple):
112                                 if r[i] not in self.__fields_chk[f]:
113                                     raise CrontabParserError('Unexpected %s value' % (f.strip('_')), lineno,
114                                             expected=self.__fields_chk[f])
115                             else:
116                                 if not self.__fields_chk[f].match(r[i]):
117                                     raise CrontabParserError('Incorrect %s value' % (f.strip('_')), lineno)
118                         ruled[f] = r[i]
119
120                     yield ruled, command
121
122 def OptionsParser(args=None, help=u'', **opts):
123
124     from optparse import OptionParser
125     import dbuscron
126     parser = OptionParser(usage=help, version="%prog "+dbuscron.__version__)
127     for opt, desc in opts.iteritems():
128         names = desc.pop('names')
129         desc['dest'] = opt
130         parser.add_option(*names, **desc)
131
132     return parser.parse_args(args)[0]
133