Gem #141 : Con-figure it out

Let's get started...

In the Gem series on GNAT.Command_Line (Gems #138 and #139), we mentioned that there are several ways a user can control the behavior of an application. These are command-line options (as discussed in that Gem), graphical user applications (for instance using GtkAda), and configuration files. This Gem proposes various approaches for the latter.

Configuration files are user- or machine-editable files. This is a general term, though, which does not imply any specific format. Several formats are frequently used, and we discuss a few such formats in this Gem.

XML files

XML is very frequently used to store machine-editable data, but it is not exactly a human-friendly format and is extremely verbose. However, among its various pros are a rich ecosystem of tools to view and edit XML files, as well as standard APIs to parse and validate such files.

XML files can contain any type of data (including binary data, although they are not the best approach in that case), and they can be checked against grammars (also known as XML schemas).

Such files can be parsed and validated through the XML/Ada library, as discussed in Gem #21.

JSON files

JSON (JavaScript Object Notation) is a more recent format that has started to gain a strong foothold in modern applications, especially those that are web-oriented. This format is much lighter, and similar to JavaScript. Here is a quick example of such a file:

{"configuration": {
    "key1": "value1",
    "key2": 12,
    "key3": [1, 2, 3]
}

This format is easily readable and writable. One of its drawbacks, though, is the lack of syntax for comments, which are not supported. As such, the documentation needs to be put in a separate file. However, there are lots of libraries (in several programming languages) that can be used to parse these files. Most notably, all modern web browser are fully capable of receiving and then parsing these files, so they are a convenient way to exchange data (include configurations, for instance) among a web server and a web client.

In Ada, you could use the package GNATCOLL.JSON, part of the GNAT Component Collection. A future Gem will describe this package in more detail, although for now you can look at the specs.

INI files

These files were the standard for Windows applications and used the .ini extension, hence their name. This file format encompasses a vague syntax, the details of which vary from one application to another. Here is an example of such a file:

[General]
# Some documentation
key1=value1
key2=12
key3=1
key3=2

[Section2]
filename=$HOME/value2

This format contains key-value pairs, optionally organized into sections. Comments can be inserted as shown in the example above.

Such files can be parsed by the GNATCOLL.Config package, also part of the GNAT Components Collection.

The basic usage for this package is as follows.

   pragma Ada_05;
   with GNATCOLL.Config;   use GNATCOLL.Config;

   declare
      C : INI_Parser;
   begin
      Open (C, "settings.txt");
      while not C.At_End loop
         Put_Line (C.Key & " = " & C.Value & " in " & C.Section);
         C.Next;
      end loop;
   end;

The loop iterates over all key-value pairs in the file, and for each returns the value as a string, an integer, or a file. Files can be specified, in the config file, as absolute or relative to the location of the file, and GNATCOLL.Config will automatically normalize those. As a special case, the string "$HOME" is automatically substituted with the location of the user's home directory.

Since there is no grammar associated with the file, it's the application's responsibility to query the value in a valid format. In fact, in the example above, the value for "filename" can be queried as a string or a file, depending on the context, although of course it cannot be queried as an integer.

The example above uses the "object-dotted" notation (prefix form of subprogram calls) introduced by Ada 2005, but the library itself can be used with applications not programmed in Ada 2005.

While this API works as expected, the user has to store the values somewhere if he intends to use them later on. GNATCOLL.Config also provides a pool to store these for the duration of the application. This is especially convenient for preferences. In fact, this pool can be used for other configuration file formats as described above.

It is possible to parse multiple configuration files. Typically, an application would parse a system-wide settings file that contains defaults. These defaults can then be overridden by a user-specific settings file.

The pool will automatically remember where a value was read from, so that it can normalize file names relative to the given settings file.

The code would then look like:

   declare
      Pool   : Config_Pool;
      Parser : INI_Parser;
   begin
      Open (Parser, "/system/settings.txt");
      Fill (Pool, Parser);
      Open (Parser, "/user/settings.txt");
      Fill (Pool, Parser);  --  override with user-specific settings

      Put_Line ("key1 is " & Pool.Get ("key1", Section => "General"));
      Put_Line ("key3 is "
                & Pool.Get ("key3", Section => "General", Index => 1)
                & ", "

                & Pool.Get ("key3", Section => "General", Index => 2));
      Put_Line ("filename is "
                & Pool.Get_File ("filename", "General").Display_Full_Name);
   end;

However, there still remains one issue with this code. If the organization of the settings file changes (for instance, because you rename a key, or move it to a different section), you will need to find all locations in your code that referenced it, and change the name and/or section.

So GNATCOLL.Config provides one more approach which is to define the keys as global variables.

   declare
      Filename : constant Config_Key := Create ("filename", "General");
      Key1     : constant Config_Key := Create ("key1", "General");
      Key3     : constant Config_Key := Create ("key3", "General");
   begin
      Put_Line ("key1 is " & Key1.Get (Pool));
      Put_Line ("key3 is "
                & Key3.Get (Pool, 1) & ',' & Key3.Get (Pool, 2));
      Put_Line ("filename is " & Filename.Get_File.Display_Full_Name);
   end;

Now, only a single location in the code needs to be changed when the format of the configuration file changes.

By design, GNATCOLL.Config can be extended by declaring new types of parser derived from Config_Parser. These parsers could handle XML or JSON formats, and they would still be compatible with the Config_Pool described above, thus making it easy to access the configuration files throughout your application.