59f2f4d4e3c45aedd7ead8c6e5b7002c0d13372f
[dbuscron] / dbuscron / parser.py
1 # encoding: utf-8
2 from __future__ import with_statement
3 import os
4 import re
5 from dbuscron.bus import DbusBus
6
7 def unescape_():
8     h = '[0-9A-Fa-f]'
9     r = re.compile(r'\\x('+h+r'{2})|\\u('+h+'{4})')
10
11     def unescape(value):
12         if not (value and
13                 (r'\x' in value or r'\u' in value)):
14             return value
15
16         return r.sub(
17             lambda m: chr(int(m.group(1), 16))
18                 if m.group(1) is not None else
19                     unichr(int(m.group(2), 16))
20                         .encode('utf-8'),\
21             value)
22     return unescape
23 unescape = unescape_()
24
25 def product(*args):
26     if args:
27         head, tail = args[0], args[1:]
28         for h in head:
29             for t in product(*tail):
30                 yield (h,) + t
31
32     else:
33         yield ()
34
35 class CrontabParserError(SyntaxError):
36     def __init__(self, message, lineno, expected=None):
37         if expected:
38             if isinstance(expected, (tuple, list)):
39                 exp = ' (expected %s or %s)' % (', '.join(expected[:-1]), expected[-1])
40         else:
41             exp = ''
42
43         msg = '%s%s at line %d' % (message, exp, lineno)
44
45         SyntaxError.__init__(self, msg)
46
47 def CrontabParser(*filenames):
48     for filename in filenames:
49         if not os.path.exists(filename):
50             continue
51
52         if os.path.isfile(filename):
53             parser_class = FileParser
54
55         elif os.path.isdir(filename):
56             parser_class = DirectoryParser
57
58         else:
59             raise SystemError("I can parse only directories or simple files.")
60
61         return parser_class(filename)
62
63     raise SystemError("Can't find any config file or directory.")
64
65 class FileParser(object):
66     __fields_sep = re.compile(r'\s+')
67     __envvar_sep = re.compile(r'\s*=\s*')
68     __fields_chk = {
69             'bus_'         : None,
70             'type_'        : ('signal', 'method_call', 'method_return', 'error'),
71             'sender_'      : None,
72             'interface_'   : re.compile(r'^[a-zA-Z][a-zA-Z0-9_.]+$'),
73             'path_'        : re.compile(r'^/[a-zA-Z0-9_/]+$'),
74             'member_'      : re.compile(r'^[a-zA-Z][a-zA-Z0-9_]+$'),
75             'destination_' : None,
76             'args_'        : None,
77             }
78     __fields = [
79             'bus_',
80             'type_',
81             'sender_',
82             'interface_',
83             'path_',
84             'member_',
85             'destination_',
86             'args_',
87             ]
88
89     def __init__(self, fname):
90         self.__bus = DbusBus()
91         self.__filename = fname
92         self.__environ = dict()
93
94     @property
95     def environ(self):
96         return self.__environ
97
98     def _iterate_file(self, filename):
99         # bus type sender interface path member destination args command
100         lineno = 0
101         with open(filename) as f:
102             for line in f:
103                 lineno += 1
104                 line = line.strip()
105
106                 if not line or line.startswith('#'):
107                     continue
108
109                 parts = self.__fields_sep.split(line, 8)
110                 if len(parts) < 9:
111                     parts = self.__envvar_sep.split(line, 1)
112                     if len(parts) == 2:
113                         self.__environ[parts[0]] = parts[1]
114                         continue
115
116                     raise CrontabParserError('Unexpected number of records', lineno)
117
118                 rule = [('s', 'S'), self.__fields_chk['type_'], (None,), (None,), (None,), (None,), (None,), (None,)]
119
120                 for p in range(0, 8):
121                     if parts[p] != '*':
122                         rule[p] = parts[p].split(',')
123
124                 command = parts[8]
125
126                 for r in product(*rule):
127                     r = list(r)
128                     if r[0] == 'S':
129                         r[0] = self.__bus.system
130                     elif r[0] == 's':
131                         r[0] = self.__bus.session
132                     else:
133                         raise CrontabParserError('Unexpected bus value', lineno, expected=('S', 's', '*'))
134
135                     if r[7]:
136                         r[7] = map(unescape, r[7].split(';'))
137
138                     ruled = dict()
139                     for i, f in enumerate(self.__fields):
140                         if r[i] is not None and self.__fields_chk[f]:
141                             if isinstance(self.__fields_chk[f], tuple):
142                                 if r[i] not in self.__fields_chk[f]:
143                                     raise CrontabParserError('Unexpected %s value' % (f.strip('_')), lineno,
144                                             expected=self.__fields_chk[f])
145                             else:
146                                 if not self.__fields_chk[f].match(r[i]):
147                                     raise CrontabParserError('Incorrect %s value' % (f.strip('_')), lineno)
148                         ruled[f] = r[i]
149
150                     yield ruled, command
151
152     def __iter__(self):
153         return self._iterate_file(self.__filename)
154
155 class DirectoryParser(CrontabParser):
156
157     def __init__(self, dirname, recursive=False):
158         self.__dirname = dirname
159         self.__recursive = recursive
160         super(DirectoryParser, self).__init__(None)
161
162     def _dirwalker_plain(self):
163         for i in os.listdir(self.__dirname):
164             if os.path.isfile(i):
165                 yield i
166
167     def _dirwalker_recursive(self):
168         for r, d, f in os.walk(self.__dirname):
169             for i in f:
170                 yield i
171
172     def __iter__(self):
173
174         if self.__recursive:
175             dirwalker = self._dirwalker_recursive
176         else:
177             dirwalker = self._dirwalker_plain
178
179         for fname in dirwalker():
180             fullname = os.path.join(self.__dirname, fname)
181             self.__filename = fullname
182             for item in self._iterate_file(fullname):
183                 yield item
184
185 def OptionsParser(args=None, help=u'', **opts):
186
187     from optparse import OptionParser
188     import dbuscron
189     parser = OptionParser(usage=help, version="%prog " + dbuscron.__version__)
190     for opt, desc in opts.iteritems():
191         names = desc.pop('names')
192         desc['dest'] = opt
193         parser.add_option(*names, **desc)
194
195     return parser.parse_args(args)[0]
196