root/misc/configparse.d

Revision 126, 17.1 kB (checked in by KirkMcDonald, 1 year ago)

* Fixed default arguments in optparse.
* Added comboparse and combotest. (Will add docs later.)

Line 
1 /*
2 Copyright (c) 2007 Kirk McDonald
3
4 Permission is hereby granted, free of charge, to any person obtaining a copy of
5 this software and associated documentation files (the "Software"), to deal in
6 the Software without restriction, including without limitation the rights to
7 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
8 of the Software, and to permit persons to whom the Software is furnished to do
9 so, subject to the following conditions:
10
11 The above copyright notice and this permission notice shall be included in all
12 copies or substantial portions of the Software.
13
14 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 SOFTWARE.
21 */
22 /++
23 Configuration file parsing, in the style of Python's ConfigParser.
24
25 Config files are divided into sections, which contain options. Section headers
26 are in the form "[section name]". Options may be in either the form "key=value"
27 or "key: value". A given line may only contain a single option or section
28 header. Whitespace is stripped from the beginnings and ends of keys and values.
29
30 Options existing before any section headers are said to be in the global
31 section. This is treated as a section named "General". It is entirely possible
32 to use config files only containing options that are in the global section, and
33 to write code which doesn't even seem to know about the ability to divide
34 options into sections.
35
36 Lines beginning with '#' or ';' are treated as comments and ignored.
37 +/
38 module configparse;
39
40 version (Tango) {
41     // Although I've placed these Tango imports here, Tango support is
42     // incomplete.
43     import tango.io.FileSystem : FileSystem;
44     import tango.io.FilePath : FilePath;
45     import tango.io.FileProxy : FileProxy;
46     import tango.text.Util : locatePrior, split;
47     import tango.stdc.stdlib : getenv, strlen;
48     version (Windows) {
49         const char[] SystemPathSeperator = ";";
50     } else {
51         const char[] SystemPathSeperator = ":";
52     }
53 } else {
54     import std.stream : Stream, BufferedFile, FileMode;
55     import std.string : find, strip, toString, tolower, split;
56     import std.file : exists;
57     import std.conv : toInt, toReal, ConvError;
58     import std.path : getBaseName, getDirName, join, pathsep;
59     import std.c.stdlib : getenv;
60     import std.c.string : strlen;
61 }
62
63 /// Thrown when configparse is unable to parse a file.
64 class ConfigParseError : Exception {
65     this(char[] msg) { super(msg); }
66 }
67 /// Thrown when configparse is unable to coerce a value to a requested type.
68 class ConfigConvertError : Exception {
69     this(char[] msg) { super(msg); }
70 }
71 /// Thrown when user adds already-existing section to parser.
72 class DuplicateSectionError : Exception {
73     this(char[] msg) { super(msg); }
74 }
75 /// Thrown when a section is not found.
76 class SectionNotFoundError : Exception {
77     this(char[] msg) { super(msg); }
78 }
79 /// Thrown when an option is not found.
80 class OptionNotFoundError : Exception {
81     this(char[] msg) { super(msg); }
82 }
83
84 /// The name of the global section. This is currently "General".
85 const char[] GLOBAL_SECTION = "General";
86
87 /*
88 A brief note about arg0:
89
90 The first element of the args array differs based on operating system and which
91 library (Phobos or Tango) is in use. In most cases, arg0 is the precise string
92 used to start the program. In Phobos on Windows, arg0 is always the complete
93 path to the EXE.
94 */
95
96 // Returns the name of the executable, minus directories.
97 version (Tango) {
98     char[] get_program_name(char[] path) {
99         version(Windows) {
100             char delimiter = '\\';
101         } else {
102             char delimiter = '/';
103         }
104         uint idx = locatePrior(path, delimiter);
105         if (idx == path.length) return path;
106         return path[idx+1 .. $];
107     }
108 } else {
109     char[] get_program_name(char[] path) {
110         version(Windows) {
111             // (Unicode note: ".exe" only contains 4 code units, so this slice
112             // should Just Work.) (Although it remains to be seen how robust
113             // this code actually is.)
114             assert(path[$-4 .. $] == ".exe");
115             path = path[0 .. $-4];
116         }
117         return getBaseName(path);
118     }
119 }
120 char[] env(char* var) {
121     char* s = getenv(var);
122     return s[0 .. strlen(s)];
123 }
124 /++
125 Returns the path of a config file (filename) in the same directory as the
126 executable (arg0).
127 +/
128 char[] same_dir(char[] arg0, char[] filename) {
129     version (Tango) {
130         scope binary = new FilePath(arg0);
131         scope file = new FilePath(filename);
132         char[] dir = binary.getPath();
133         scope bin_name = new FilePath(binary.getFullName());
134         if (dir == "") {
135             char[][] paths = split(env("PATH"), SystemPathSeparator);
136             foreach (path; paths) {
137                 if ((new FileProxy(bin_name.join(path))).isExisting())
138                     return file.join(path).toUtf8();
139             }
140             return "";
141         } else {
142             return file.join(dir);
143         }
144     } else {
145         version (Windows) {
146             return join(getDirName(arg0), filename);
147         } else {
148             char[] dir = getDirName(arg0);
149             if (dir == "") {
150                 char[][] paths = split(env("PATH"), pathsep);
151                 foreach (path; paths) {
152                     if (exists(join(path, getBaseName(arg0))))
153                         return join(path, filename);
154                 }
155                 return "";
156             } else {
157                 return join(dir, filename);
158             }
159         }
160     }
161 }
162 /++
163 Returns a nice "default" list of config files. arg0 should be the first element
164 of the args array passed to main(). The optional name parameter should be the _name of the config file,
165 minus any extension. It defaults to the executable's _name, minus any directory
166 or extension.
167
168 On Windows, the returned filenames are:
169 $(UL
170     $(LI name.ini alongside the .exe)
171     $(LI %HOMEDRIVE%%HOMEPATH%\name.ini)
172     $(LI .\name.ini)
173 )
174
175 On all other platforms (e.g. Linux), the returned filenames are:
176 $(UL
177     $(LI /etc/name.conf)
178     $(LI name.ini alongside the binary)
179     $(LI ~/.namerc)
180     $(LI ./name.conf)
181 )
182 +/
183 char[][] config_files(char[] arg0, char[] name=null) {
184     if (name is null) name = get_program_name(arg0);
185     version (Windows) {
186         name ~= ".ini";
187         return [
188             same_dir(arg0, name),
189             env("HOMEDRIVE")~env("HOMEPATH")~"\\"~name,
190             name
191         ];
192     } else {
193         return [
194             "/etc/"~name~".conf",
195             same_dir(arg0, name~".ini"),
196             "~/."~name~"rc",
197             name~".conf"
198         ];
199     }
200 }
201
202 /++
203 The ConfigParser class represents a config file.
204 +/
205 class ConfigParser {
206     char[][char[]][char[]] config;
207     this() {
208         config[GLOBAL_SECTION] = null;
209     }
210
211     /++
212     Returns a list of the _sections in the parser, including the global
213     section. ("General" by default.)
214     +/
215     char[][] sections() {
216         return this.config.keys;
217     }
218     /++
219     Adds a section named name to the parser.
220     
221     Throws: DuplicateSectionError if the section already exists.
222     +/
223     void add_section(char[] name) {
224         auto section = name in config;
225         if (section is null) {
226             this.config[name] = null;
227         } else {
228             throw new DuplicateSectionError("Section '"~name~"' already exists.");
229         }
230     }
231     /// Requests if a section exists in the parser.
232     bool has_section(char[] name) {
233         return (name in config) !is null;
234     }
235     private void no_section(char[] section) {
236         throw new SectionNotFoundError("Configuration section '"~section~"' not found.");
237     }
238     private void no_option(char[] section, char[] option) {
239         throw new OptionNotFoundError("Configuration option '"~option~"' in section '"~section~"' not found.");
240     }
241     /++
242     Returns a list of _options in the given section.
243
244     Throws: SectionNotFoundError
245     +/
246     char[][] options(char[] section=GLOBAL_SECTION) {
247         auto s = section in config;
248         if (s is null) no_section(section);
249         return s.keys;
250     }
251     /++
252     Returns a list of _values in the given section.
253
254     Throws: SectionNotFoundError
255     +/
256     char[][] values(char[] section=GLOBAL_SECTION) {
257         auto s = section in config;
258         if (s is null) no_section(section);
259         return s.values;
260     }
261     /++
262     Returns whether the given option exists in the given section. This method
263     (and all of the others in this form) will set option to section, and
264     section to the global _section, when only one argument is provided. (In
265     other words, providing only one argument will check for options in the
266     global _section.) This will silently return false if the section does not
267     exist.
268     +/
269     bool has_option(char[] section, char[] option=null) {
270         if (option is null) {
271             option = section;
272             section = GLOBAL_SECTION;
273         }
274         auto s = section in config;
275         if (s is null) return false;
276         auto o = option in (*s);
277         if (o is null) return false;
278         else return true;
279     }
280     /++
281     Reads a series of files into the parser, in the order provided. Files that
282     don't exist will be silently skipped. Options specified in later files will
283     override those specified in earlier files.
284
285     Returns: A list of files _read in.
286     Throws: ConfigParseError if a file contains errors.
287     +/
288     char[][] read(char[][] filenames...) {
289         char[][] files_read;
290         foreach (filename; filenames) {
291             if (!exists(filename)) continue;
292             files_read ~= filename;
293             Stream s = new BufferedFile(filename);
294             this.read(s, filename);
295             delete s;
296         }
297         return files_read;
298     }
299     /++
300     Reads in any Stream as a config _file.
301
302     Params:
303         filename = An optional argument for clarifying error output.
304     Throws: ConfigParseError if the stream contains errors.
305     +/
306     void read(Stream file, char[] filename="") {
307         if (filename.length != 0) filename ~= " ";
308         char[] section = GLOBAL_SECTION;
309         char[] key, value;
310         auto s = section in config;
311         if (s is null) config[section] = null;
312         foreach (ulong line_no, char[] line; file) {
313             line = strip(line);
314             if (line.length == 0 || line[0] == '#' || line[0] == ';') continue;
315             else if (line[0] == '[' && line[$-1] == ']') {
316                 section = line[1 .. $-1].dup;
317                 s = section in config;
318                 if (s is null) {
319                     config[section] = null;
320                     s = section in config;
321                 }
322             } else {
323                 int assign = find(line, '=');
324                 if (assign == -1) assign = find(line, ':');
325                 if (assign == -1) throw new ConfigParseError("Error in config file "~filename~"on line "~.toString(line_no)~".");
326                 key = line[0 .. assign];
327                 value = line[assign+1 .. $];
328                 key = strip(key).dup;
329                 value = strip(value).dup;
330                 (*s)[key] = value;
331             }
332         }
333     }
334     /++
335     Retrieves the value of an option in the given section. If only one argument
336     is supplied, it is treated as an _option in the global _section.
337
338     Throws:
339         SectionNotFoundError if the section doesn't exist.
340         OptionNotFoundError if the option doesn't exist.
341     +/
342     char[] opIndex(char[] section, char[] option=null) {
343         if (option is null) {
344             option = section;
345             section = GLOBAL_SECTION;
346         }
347         auto s = section in config;
348         if (s is null) no_section(section);
349         auto o = option in (*s);
350         if (o is null) no_option(section, option);
351         return *o;
352     }
353     /// An alias of opIndex.
354     alias opIndex get;
355     /++
356     Retrieves the value of an option in the given section in a manner identical
357     to opIndex, and attempts to convert it to an integer.
358
359     Throws: ConfigConvertError if the value cannot be converted.
360     +/
361     int getint(char[] section, char[] option=null) {
362         char[] o = this[section, option];
363         try {
364             return toInt(o);
365         } catch(ConvError e) {
366             throw new ConfigConvertError("Could not convert '"~o~"' to an integer in option "~section~"."~option~".");
367         }
368     }
369     /++
370     Retrieves the value of an option in the given section in a manner identical
371     to opIndex, and attempts to convert it to a real.
372
373     Throws: ConfigConvertError if the value cannot be converted.
374     +/
375     real getreal(char[] section, char[] option=null) {
376         char[] o = this[section, option];
377         try {
378             return toReal(o);
379         } catch(ConvError e) {
380             throw new ConfigConvertError("Could not convert '"~o~"' to a real in option "~section~"."~option~".");
381         }
382     }
383     /++
384     Retrieves the value of an option in the given section in a manner identical
385     to opIndex, and attempts to convert it to a boolean. The value is first
386     converted to lower-case. The accepted values for true are "yes," "_true,"
387     "on," and "1." The accepted values for false are "no," "_false," "off," and
388     "0."
389
390     Throws: ConfigConvertError if the value cannot be converted.
391     +/
392     bool getbool(char[] section, char[] option=null) {
393         char[] o = tolower(this[section, option]);
394         switch (o) {
395             case "yes", "true", "on", "1":
396                 return true;
397             case "no", "false", "off", "0":
398                 return false;
399             default:
400                 throw new ConfigConvertError("Could not convert '"~o~"' to a boolean in option "~section~"."~option~".");
401         }
402     }
403     /++
404     Stores value to option in section. If option is not supplied, section is
405     treated like an _option in the global _section.
406
407     Throws: SectionNotFoundError if section does not exist.
408     +/
409     void opIndexAssign(char[] value, char[] section, char[] option=null) {
410         if (option is null) {
411             option = section;
412             section = GLOBAL_SECTION;
413         }
414         auto s = section in config;
415         if (s is null) no_section(section);
416         (*s)[option] = value;
417     }
418     /++
419     Calls opIndexAssign after converting value to a string.
420     +/
421     void opIndexAssign(int value, char[] section, char[] option=null) {
422         this.opIndexAssign(.toString(value), section, option);
423     }
424     /++
425     Calls opIndexAssign after converting value to a string.
426     +/
427     void opIndexAssign(real value, char[] section, char[] option=null) {
428         this.opIndexAssign(.toString(value), section, option);
429     }
430     /++
431     Calls opIndexAssign after converting value to a string.
432     +/
433     void opIndexAssign(bool value, char[] section, char[] option=null) {
434         this.opIndexAssign(.toString(value), section, option);
435     }
436     /++
437     Removes the specified option in section. Silently does nothing if option
438     does not exist.
439
440     Throws: SectionNotFoundError if the section does not exist.
441     +/
442     void remove_option(char[] section, char[] option=null) {
443         if (option is null) {
444             option = section;
445             section = GLOBAL_SECTION;
446         }
447         auto s = section in config;
448         if (s is null) no_section(section);
449         (*s).remove(option);
450     }
451     /++
452     Removes section. Silently does nothing if section does not exist.
453     +/
454     void remove_section(char[] section) {
455         this.config.remove(section);
456     }
457     /++
458     Writes the config _file to file. Note that ConfigParser does not preserve
459     the ordering of options or sections, although it will always _write the
460     global section first.
461     +/
462     void write(Stream file) {
463         auto global = config[GLOBAL_SECTION];
464         bool printed_globals = false;
465         foreach (k, v; global) {
466             file.writeLine(k ~ " = " ~ v);
467             printed_globals = true;
468         }
469         foreach (name, section; this.config) {
470             if (name == GLOBAL_SECTION) continue;
471             if (printed_globals) file.writeLine("");
472             printed_globals = true;
473             file.writeLine("["~name~"]");
474             foreach (k, v; section) {
475                 file.writeLine(k ~ " = " ~ v);
476             }
477         }
478         file.flush();
479     }
480     /++
481     Writes the config file to the file specified by filename. If the file
482     already exists, it is erased and written over. If it does not exist, it is
483     created.
484     +/
485     void write(char[] filename) {
486         Stream s = new BufferedFile(filename, FileMode.OutNew);
487         this.write(s);
488         s.close();
489         delete s;
490     }
491 }
Note: See TracBrowser for help on using the browser.