Last Modified: 05/30/2001
This package helps programmers write software that is more easily configurable. From the end user's perspective, command line arguments and dialog boxes may be suitable for some tasks, but neither option is satisfactory for most scientific programming. Command line arguments are great for quickly changing a few parameters for each run of a calculation, but they break down when there are a lot of options involved. Dialog boxes look great, but unless they provide an easy way to switch between vastly different configurations and allow the user to express the configuration concisely, they get in the way of work. My personal preference as a user and programmer is file-based configurations. This is the only way I've been able to get the versatility and expressiveness of a command line with the intuitive structure of dialog boxes.
This package provides the user with an easy to use formalism for program configuration and provides the programmer a very simple set of routines for retrieving the user's options. The user fills out a file, the program reads its configuration from that file. One of these days, I may build a GUI for editing these files, but so far, editing them in a text editor has been convenient enough.
Suppose you are working on a program to find faces in images. There are a lot of options to be set: the name of the input file, the name of a directory containing training examples, various thresholds, iteration counts and tolerances, window sizes, kernel sizes, etc. Suppose you're polishing up and trying to figure out which set of parameters works best. You need to try out different configurations, save old sets of configurations in case you find yourself in a dead end, reuses piece of old configurations, and generally not waste time. Here's a snippet of configuration from my face finder.
section Library {
section Testing {
jebfile = skin.jeb
cachefile = skin.cache
skindump = skin.pgm
focal_length = 50.0
starting_depth = 3.0
dotest = f # don't run a test unless
# explicitly overridden in
# the test's section.
gmixture = f
config = f
section Linalg {
dotest = f
ls_solver_1 = f
ls_solver_2 = f
semi_ortho_matrix_fname = semi_ortho.pgm
n_non_ortho = 2
}
section Construction {
dotest = t;
coeffs = {-170.802 -480.473 -68.5568 336.854 -52.1304 -180.587 420.313 -36.9678 158.834 420.233 -10.6145 149.071 3.03145 -5.2396
6 -77.2672 -195.354 -30.7757 -112.373 -106.244 216.804 -296.789 -91.2857 86.0481 189.719 -76.1543 -7.98662 270.463 24.9486 -262.505}
outfname = constructed.pgm
}
... }
configuration Brixton {
Library:Testing:jebfile = brixton.jeb
Library:Testing:cachefile = brixton.cache
Library:Testing:colorfaceinput = face50.ppm
Library:Testing:Eigenfaces:Prefilter:skin_content_thresh = 0.000001
}
|
Table 1: Part of a configuration file.
The configuration file consists of sections and configurations. A section contains a set of key-value pairs, each key corresponding to some option. Sections can be nested for the sake of modularity and have inheritance semantics which allows the user to write complex configurations without being verbose. In Table 1, the "Library" section contains sections "Linalg" and "Construction", for example. "Linalg" inherits all the options of "Library" and overrides the ones it defines explicitly. Configurations are subroutines that can be called from within a section or from the program. Configurations are used to assign different values to a section. For example, when called, the "Brixton" configuration modifies the variable "Library:Testing:jebfile" from "skin.jeb" to "brixton.jeb".
The user defines a section using the section keyword, followed by the name of the section. The body of the section is enclosed in braces. The body can contain key-value pairs, sub sections, and directives such as inherit and call. In this example, keys in section "Foo" are assigned values:
section Foo {
Tolerance = 1e-4
InputFile = foo.data
UseColor = t
DownsamplingPyramid = {1 2 4 32}
TransformationMatrix = {1 0 0 1 3 4}
}
|
Table 2: Assigning values to keys.
The supported built-in types are integers, floats, strings, bools, and arrays of these. The last section explains how a program can recover these value in the native format from the package. Strings with spaces can be specified by using double quotes. C escape sequences and multiline strings are also supported.
section Foo {
opt1 = oldvalue
opt3 = given
section Foo-Derived {
opt1 = 0.1
opt2 = {4 2}
}
}
|
Table 3: Nesting and inheritance.
The fully qualified name of "Foo-Derived" is "Foo::Foo-Derived". Nesting allows configuration options to be organized hierarchically. But they provide much more significant functionality: in the above example, "Foo:Foo-Derived:opt1" takes on the value "newvalue" and "Foo:opt1" is "oldvalue", as expected, but you can also query "Foo:Foo-Derived:opt3" and get "given". Hence, nested sections inherit options from their enclosing section. In addition, you can add another parent to your class using the inherit keyword to inherit from multiple sections:
section UsefulValues {
MachineType = Linsux-i386
Verbose = f
}
section Foo {
opt1 = oldvalue
opt3 = given
section Foo-Derived {
inherit UsefValues
opt1 = 0.1
opt2 = {4 2}
}
}
|
Table 4: The inherit keyword.
The inherit keyword makes the keys defined in "UsefulValues" available to "Foo::Foo-Derived". This happens because "UsefulValues" becomes a parent section of "Foo-Derived" in the same way "Foo" is a parent class of "Foo-Derived".
A configuration sets the value of individual assignments inside a section. Because it introduces non-local changes, I discourage its use, but I've found it useful on occasion. Here's an example of a configuratin:
configuration ChangeFoo {
Foo:Foo-Derive:opt3 = 53
Foo:opt1 = blah
} |
Table 5: A configuration
Lookup<T>() where T is one of bool, int, float, or string is identical to plain lookup(), except it converts the resulting string to the desired type and returns this value instead.
List_lookup<T>() returns a vector<T>. This is the function to use for reading a list from the configuration file. If you want to read "Foo:Foo-derived:opt1" in Table 3, you would use list_lookup<int>("Foo:Foo-derived:opt1") to retrived a vector of int's. Note that vector<> is the C++ std::vector.
CallSubroutine() takes the name of a configuration and executes its body.
Error handling is done through execptions. If there is a syntax error, an exception of type std::runtime_error is thrown. The what() method of this object explains the error as usual.
This package is available as a tar file. You can also view the files individually:
config.cc This file contains a test stub. Look for main().
configuration.y and configuration.l The lex and yacc files.
ntpm.conf A sample config file.
Type gmake. Run ./conf.