1 """Config file parsing
2
3 All of the scripts in this project use the optparse package to handle
4 command line options.
5
6 This module provides a very simple mechanism for taking config files
7 as input instead of specifying options on the command line. This is
8 convenient, for example, for running experiments repeatedly where a
9 whole load of options need to be given to set parameters.
10
11 The options in the file are stored in the following format::
12 optname=value
13
14 - The files may contain comments beginning with a '#'.
15 - To specify arguments, just put the argument on a line of its own.
16 - To specify flags, put the flag name on a line of its own preceded by a +.
17
18 You can only use long option names currently. This is best practice
19 anyway, as it makes the file more readable.
20
21 The config options are simply transformed into a string of
22 command-line-like options and added to the actual command-line options.
23
24 Don't forget to put a comment in the file so you know what script it's
25 for!
26
27 Additionally, lines beginning with '%%' are treated as directives.
28 - C{%% INCLUDE filename}: includes another config file.
29 - C{%% ARG i value}: treats C{value} as the ith argument. If you
30 specify any arguments in this way, you should specify them all like
31 this. Allows the arguments not to be given in order.
32 - C{%% DEF name value}: defines or defines the value of the variable
33 C{name}. This value may subsequently be used with a %{name}
34 substitution.
35 - C{%% ABSTRACT}: declares the whole file to be abstract, i.e. it cannot
36 be used directly, but only as an include in another file. You should
37 put this in any file that relies on including files to supply required
38 options/arguments.
39 - C{%% REQUIRE option}: requires the user to specify the named option on
40 the command line when using this config file.
41
42 You may use certain substitutions in the options. %{X} will be replaced
43 by a value if one can be found. The following sources are consulted (in
44 this order):
45 - a variable X defined with a DEF directive;
46 - a constant X from the settings file.
47 One purpose of this is to allow you to specify paths relative to the
48 project root, etc, rather than where the script is run.
49
50 A linebreak preceded by a \ will be ignored. Whitespace at the start
51 of the subsequent line will be ignored (but not whitespace before the
52 \).
53
54 """
55 """
56 ============================== License ========================================
57 Copyright (C) 2008, 2010-12 University of Edinburgh, Mark Granroth-Wilding
58
59 This file is part of The Jazz Parser.
60
61 The Jazz Parser is free software: you can redistribute it and/or modify
62 it under the terms of the GNU General Public License as published by
63 the Free Software Foundation, either version 3 of the License, or
64 (at your option) any later version.
65
66 The Jazz Parser is distributed in the hope that it will be useful,
67 but WITHOUT ANY WARRANTY; without even the implied warranty of
68 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
69 GNU General Public License for more details.
70
71 You should have received a copy of the GNU General Public License
72 along with The Jazz Parser. If not, see <http://www.gnu.org/licenses/>.
73
74 ============================ End license ======================================
75
76 """
77 __author__ = "Mark Granroth-Wilding <mark.granroth-wilding@ed.ac.uk>"
78
79 import sys
82 """
83 A really simple interface to options stored in config files.
84 Can also process a string as if read from the contents of a file.
85
86 """
87 - def __init__(self, filename, string=False):
88 self.options = []
89 self.flags = []
90 self.arguments = []
91 self.required_options = []
92
93 if string:
94
95 self.filename = None
96 self.lines = filename.split("\n")
97 else:
98
99 self.filename = filename
100 with open(self.filename, 'r') as cfile:
101 self.lines = cfile.readlines()
102 self.parse_lines()
103
104 @staticmethod
107
109 """
110 Parses the lines stored in self.lines. Called by initialization.
111 You can call this straight off if you've instantiated with a string.
112
113 """
114 from jazzparser import settings
115 import os
116
117 conf_lines = self.lines
118
119 numbered_args = {}
120 defined_variables = {}
121
122 def _do_substitutions(value, full_line):
123
124
125 while "%{" in value:
126 opener = value.index("%{")
127 closer = value.find("}", opener)
128 if closer == -1:
129
130 raise ConfigFileReadError, "no matching close brace (}) found in %s" % full_line
131 const_name = value[opener+2:closer]
132
133 if const_name in defined_variables:
134
135 sub_value = defined_variables[const_name]
136 elif hasattr(settings, const_name):
137
138 sub_value = getattr(settings, const_name)
139 else:
140 raise ConfigFileReadError, "no setting or variable "\
141 "'%s' found to make substitution in: %s" % \
142 (const_name, full_line.strip())
143
144 value = value.replace("%{"+const_name+"}", sub_value)
145 return value
146
147
148 def _preprocess_lines(lines, included=False):
149 _proc_lines = []
150
151
152 lines = [l.rstrip("\n") for l in lines]
153
154 joined_lines = []
155 to_join = []
156 for line in lines:
157 if line.endswith("\\"):
158 to_join.append(line[:-1].lstrip())
159 else:
160 if len(to_join) > 0:
161 to_join.append(line.lstrip())
162 joined_lines.append("".join(to_join))
163 to_join = []
164 else:
165 joined_lines.append(line)
166
167 for line in joined_lines:
168 use_line = True
169
170 if "#" in line:
171 line = line[:line.index("#")]
172 line = line.strip()
173
174
175 if line.startswith("%%"):
176
177 use_line = False
178
179 line = _do_substitutions(line[2:], line)
180
181 directive = line.split()[0].strip().lower()
182 args = line.split()[1:]
183
184 if directive == "include":
185 if len(args) != 1:
186 raise ConfigFileReadError, "INCLUDE directive "\
187 "requires a filename argument"
188
189 if self.filename is None:
190
191 filename = args[0]
192 else:
193 filename = os.path.join(
194 os.path.dirname(self.filename),
195 args[0])
196 filename = os.path.abspath(filename)
197 try:
198 file = open(filename, 'r')
199 except IOError:
200 raise ConfigFileReadError, "could not open "\
201 "included config file %s" % filename
202
203 try:
204
205 file_lines = _preprocess_lines(file.readlines(), included=True)
206 _proc_lines.extend(file_lines)
207 finally:
208 file.close()
209 elif directive == "arg":
210 if len(args) != 2:
211 raise ConfigFileReadError, "ARG directive "\
212 "requires an argument number and a value"
213 arg_num = int(args[0])
214 numbered_args[arg_num] = args[1]
215 elif directive == "def":
216 if len(args) != 2:
217 raise ConfigFileReadError, "DEF directive "\
218 "requires a variable name and a value"
219 defined_variables[args[0]] = args[1]
220 elif directive == "abstract":
221 if not included:
222
223
224
225 raise ConfigFileReadError, "encountered ABSTRACT "\
226 "directive in a non-included file. You should "\
227 "not use this file directly, but as an include "\
228 "in another config file."
229 elif directive == "require":
230 if len(args) != 1:
231 raise ConfigFileReadError, "REQUIRE directive "\
232 "needs an option name"
233 self.required_options.append(args[0])
234
235 else:
236 raise ConfigFileReadError, "unknown directive: %s" % directive
237
238 if use_line and len(line) > 0:
239 _proc_lines.append(line)
240 return _proc_lines
241
242 proc_lines = _preprocess_lines(conf_lines)
243 if len(numbered_args) > 0:
244
245 num_args = max(numbered_args.keys())
246 for i in range(num_args+1):
247 if i not in numbered_args:
248 raise ConfigFileReadError, "missing argument: "\
249 "arg number %s was given but %s was not" % \
250 (num_args, i)
251
252 self.arguments = [numbered_args[i] for i in range(num_args+1)]
253
254 for line in proc_lines:
255 if line.startswith("+"):
256
257 self.flags.append(line[1:])
258 elif "=" in line:
259
260 opt, __, val = line.partition("=")
261 self.options.append((opt.strip(), _do_substitutions(val.strip(), line)))
262 else:
263
264 if len(numbered_args) > 0:
265 raise ConfigFileReadError, "cannot mix numbered args "\
266 "and non-numbered args: %s" % line
267 self.arguments.append(_do_substitutions(line.strip(), line))
268
270 """
271 Get a list of strings containing all the config options in a form ready
272 to be passed to optparse as if they were command-line options.
273
274 """
275 return sum([["--%s" % opt, "%s" % val] for (opt,val) in self.options], []) \
276 + ["--%s" % flag for flag in self.flags] \
277 + self.arguments
278
280 """
281 An alternative to calling parser.parse_args() which adds a --config
282 option to the parser's options and uses it to read in a config
283 file if it's given.
284
285 The args will potentially get parsed twice: once to get the config
286 file and then again to incorporate options from the file.
287
288 @return: (options, arguments) tuple, as given by parser.parse_args().
289
290 """
291 import sys
292
293 parser.add_option("--%s" % option_name, dest="%s" % option_name, action="store", help="read options in from a config file.")
294
295 options,arguments = parser.parse_args()
296
297 conf_file = getattr(options, option_name)
298 if conf_file is not None:
299
300 conf = ConfigFile(conf_file)
301
302 for required in conf.required_options:
303 if not hasattr(options, required):
304 raise ConfigFileReadError, "config file requires non-existent "\
305 "command line option '%s'" % required
306 elif getattr(options, required) is None:
307 print "Error: config file requires that you "\
308 "give the option '%s' on the command line" % required
309 sys.exit(1)
310
311 conf_strings = conf.get_strings()
312 if len(conf_strings) > 0:
313
314 options, file_arguments = parser.parse_args(args=conf_strings)
315
316
317 options, cl_arguments = parser.parse_args(values=options)
318
319 arguments = file_arguments + cl_arguments
320 return options,arguments
321
324