Plexdata CFG Parser

Overview

The Plexdata CFG Parser represents a library allowing reading and writing of old‑fashioned CFG and INI files into structured configuration items. It is also possible to configure the behavior of how files are read or written.

Furthermore, a set of structured configuration items can be converted into user‑defined classes. This makes it possible to directly read a configuration file into a class structure. Of course, writing a configuration file from a class structure is possible as well.

Different styles of configuration files are also supported. Already predefined are the Windows and Linux styles, but configuring user‑defined styles is supported as well.

Licensing

The software has been published under the terms of

MIT License

Copyright © 2019 plexdata.de

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Installation

The binary files of the Plexdata CFG Parser are provided as NuGet package and can be obtained from https://www.nuget.org/packages/Plexdata.CfgParser.NET. How to install this NuGet package manually is explained there.

Using the Plexdata CFG Parser together with Visual Studio.

Additionally, all releases can be downloaded from GitHub. Please visit page Plexdata CFG Parser to find all available versions.

Introduction

In the age of having configuration file formats like JSON or XML a configuration file format like INI seems to be pretty old‑fashioned. And indeed it is. But from time to time it is useful to support a standard INI configuration file. And this library is intended for exactly these special use‑cases.

As first let’s have a glance at what exactly is meant when talking about INI files.

In fact, a standard INI file is nothing else but a collection of key‑value‑pairs. Additionally, these key‑value‑pairs can be grouped into sections. See example below for some details.

[general]
enable-show-pages: yes
enable-show-styles: no
default-language: english

[network]
server-address: 192.168.5.42
server-port: 45054

On the other hand, each of the systems uses a different format for such types of configuration files. For example, Windows INI files usually use the equal sign (=) to separate a label from its value. And vice versa, on Linux systems a colon (:) is used instead. This makes it almost impossible to load configuration files of both systems with the same software.

Reading

Reading data from an external CFG file is pretty easy. The only thing to do is to call the Read() method from class ConfigReader. As result of this an instance of class ConfigContent is returned. See below for an example.

String filename = @"C:\config.ini";
ConfigContent content = ConfigReader.Read(filename);

Did no exception occur then an access to each configuration section as well as to each value of a section is easily possible by using the array operator. Assuming the read configuration file contains a section named general and there in a value named version. Accessing this particular value can be done as shown in example right here below.

ConfigSection section = content["general"];
if (section != null)
{
    ConfigValue value = section["version"];
    if (value != null)
    {
        Version version = ValueConverter.Convert(value.Value, typeof(Version)) as Version;
        Trace.WriteLine($"The verion is {version}");
    }
}

Sometimes it might be useful to find errors and/or misplaced configuration details. For this purpose it is possible the provide an additional list of type ConfigWarning. This list is filled up during file parsing and contains items in case of issues could be determined. How to determine configuration warnings is shown in example below.

String filename = @"C:\config.ini";
List<ConfigWarning> warnings = new List<ConfigWarning>();
ConfigContent content = ConfigReader.Read(filename, warnings);
foreach (ConfigWarning warning in warnings)
{
    Trace.WriteLine(warning);
}

Another aspect to be considered when processing a configuration file is the fact that users can create and/or modify a configuration file. And for sure, users are not perfect and they do mistakes. Such a mistake could be that lines are included which do not represent a value type.

Being able to find this kind of values, class ConfigContent provides a property named Others. How to use property Others is demonstrated below.

String filename = @"C:\config.ini";
ConfigContent content = ConfigReader.Read(filename);
ConfigOthers others = content.Others;
if (others.IsValid)
{
    for (Int32 index = 0; index < others.Count; index++)
    {
        ConfigOther other = others[index];
        Trace.WriteLine(other.Value);
    }
}

Writing

In contrast to reading a configuration file, writing it requires a bit more effort. And how to accomplish this task is part of this section.

First of all, an instance of class ConfigContent must be created and configured as well. See following example that shows how to prepare a configuration before it can be written.

ConfigContent content = new ConfigContent();
ConfigSection section = new ConfigSection("general", "The general section contains global values.");
ConfigValue value = new ConfigValue("enable-show-pages", "yes");
section.Append(value);
value = new ConfigValue("enable-show-styles", "no");
section.Append(value);
value = new ConfigValue("default-language", "english", "Using english, german and french is possible.");
section.Append(value);
content.Append(section);
section = new ConfigSection("network", "The network section contains network values.");
value = new ConfigValue("server-address", "192.168.5.42", "Using the host name is also possible.");
section.Append(value);
value = new ConfigValue("server-port", "45054");
section.Append(value);
content.Append(section);

Another way to initialize the configuration content is to use the array operators. How this can be done is shown here below.

ConfigContent content = new ConfigContent();
content["general"] = new ConfigSection();
content["general"].Comment = new ConfigComment("The general section contains global values.");
content["general"]["enable-show-pages"] = new ConfigValue();
content["general"]["enable-show-pages"].Value = "yes";
content["general"]["enable-show-styles"] = new ConfigValue();
content["general"]["enable-show-styles"].Value = "no";
content["general"]["default-language"] = new ConfigValue();
content["general"]["default-language"].Value = "english";
content["general"]["default-language"].Comment = new ConfigComment("Using english, german and french is possible.");
content["network"] = new ConfigSection();
content["network"].Comment = new ConfigComment("The network section contains network values.");
content["network"]["server-address"] = new ConfigValue();
content["network"]["server-address"].Value = "192.168.5.42";
content["network"]["server-address"].Comment = new ConfigComment("Using the host name is also possible.");
content["network"]["server-port"] = new ConfigValue();
content["network"]["server-port"].Value = "45054";

After a configuration has been initialized successfully, it can be written into a file. How to write such a configuration file is done as follows.

String filename = @"C:\config.ini";
ConfigWriter.Write(content, filename);

With the above configuration, the written result would like as shown below.

[general] # The general section contains global values.
enable-show-pages = yes
enable-show-styles = no
default-language = english # Using english, german and french is possible.

[network] # The network section contains network values.
server-address = 192.168.5.42 # Using the host name is also possible.
server-port = 45054

On the other hand, the Plexdata CFG Parser also supports a configuration file header. How to use this feature is explained right here.

As done for the content, the header of a configuration file must be configured as well. Here below please find the needed steps.

ConfigComment comment = new ConfigComment("header comment line 1");
content.Header.Append(comment);
comment = new ConfigComment("header comment line 2");
content.Header.Append(comment);

After writing the above configuration, the written result would like as shown below.

# header comment line 1
# header comment line 2

[section-1] 
...

Related to the file header, two more details should be discussed. The first detail is t he usage of placeholders and the second detail is the usage of the default header.

In conjunction with the configuration file header, it will be possible to include current file name and/or current file date. How to use these placeholders is demonstrated below.

ConfigComment comment = new ConfigComment($"File name: \"{ConfigDefines.FileNamePlaceholder}\"");
content.Header.Append(comment);
comment = new ConfigComment($"File date: \"{ConfigDefines.FileDatePlaceholder}\"");
content.Header.Append(comment);

After writing the above configuration, the written result would like as shown below.

# File name: "config.ini"
# File date: "2019-10-29 17:42:23"

[section-1] 
...

The feature of having a default header is born because of the need of providing users with the rules of how to use a configuration file. For this purpose class ConfigSettings provides a static method named CreateDefaultHeader(). How to use this feature is shown below.

ConfigContent content = new ConfigContent();
content.Header = ConfigSettings.CreateDefaultHeader("Auto-generated configuration file.", true);

After writing the above configuration, the written result would like as shown below.

# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Auto-generated configuration file.
# File name: config.ini
# File date: 2019-10-29 17:42:23
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Header rules:
# - Each header line must start with a comment marker.
# - Each of the header lines must be a pure comment line.
# - Each header comment line must be in front on any other content.
# Comment Rules:
# - Comments can be tagged by character '#' or by character ';'.
# - Comments can be placed in a single line but only as header type.
# - Comments can be placed at the end of line of each section.
# - Comments can be placed at the end of line of each value-data-pair.
# Section Rules:
# - Sections are enclosed in '[' and ']'.
# - Section names should not include white spaces.
# Value Rules:
# - Values can have an empty data part.
# - Value names should not include white spaces.
# - Values without a section are treated as 'others'.
# - Values are built as pair of 'name:data' or of 'name=data'.
# - Value data that use '#', ';', '[', ']', ':' or '=' must be enclosed by '"'.
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

[section-1] 
...

On the other hand, someone may want to use a header, but not this huge header from above. For this purpose class ConfigSettings provides another static method named CreateStandardHeader(). How to use this method is shown below.

ConfigContent content = new ConfigContent();
content.Header = ConfigSettings.CreateStandardHeader("Do not change this file!", true);

After writing the above configuration, the written result would like as shown below.

# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Do not change this file!
# File name: config.ini
# File date: 2019-10-29 17:42:23
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Parsing

Another feature of the Plexdata CFG Parser is the possibility to put the content of a read configuration file into a user‑defined class structure. How to use this functionality is part of this section.

Standard Parsing

But before starting with an example some details should be discussed. The advantage of this feature is the guarantee of type‑safeness. This is indeed pretty important because of otherwise each value must be converted manually. But on the other hand, the disadvantage is that an additional implementation is required.

As first it is necessary to implement its own class structure. The following code snippet shows an example of such a class structure. Further is shows how to configure these classes to be able to use the parsing feature.

public class GeneralSettings
{
    [ConfigValue("enable-show-pages", Default = "yes")]
    public Boolean EnableShowPages { get; set; }
    [ConfigValue("enable-show-styles", Default = "no")]
    public Boolean EnableShowStyles { get; set; }
    [ConfigValue("default-language", Comment = "Using english, german and french is possible.")]
    public String DefaultLanguage { get; set; }
    [ConfigIgnore]
    public DateTime Timestamp { get; set; }
}

public class NetworkSettings
{
    [ConfigValue("server-address", Comment = "Usage of IPv4 or IPv6 is possible.")]
    public IPAddress ServerAddress { get; set; }
    [ConfigValue("server-port")]
    public UInt16 ServerPort { get; set; }
    [ConfigIgnore]
    public DateTime Timestamp { get; set; }
}

[ConfigHeader(IsExtended = false, Title = "Do not change this file!", Placeholders = true)]
public class ProgramSettings
{
    [ConfigSection("general", Comment = "The general section contains global values.")]
    public GeneralSettings GeneralSettings { get; set; }
    [ConfigSection("network", Comment = "The network section contains network values.")]
    public NetworkSettings NetworkSettings { get; set; }
}

With these setting classes in background, reading and parsing a configuration file can be done like shown below.

String filename = @"C:\network.ini";
ConfigContent content = ConfigReader.Read(filename);
ProgramSettings settings = ConfigParser<ProgramSettings>.Parse(content);

Custom Parsing

In addition to the above, it is possible to setup a configuration class structure that can process custom types. For this purpose the Plexdata CFG Parser provides an interface called ICustomParser<TType>. This interface must be implemented by users. Thereafter, this user‑defined interface implementation can be used simply by tagging the related property with attribute CustomParser. Here are the steps required to accomplish this task.

First of all, a suitable custom type is needed. It is possible to implement anything wanted. The code snippet below shows an example of such a custom type.

public class CustomType
{
    public Int32 Value1 { get; set; }
    public Int32 Value2 { get; set; }
    public Int32 Value3 { get; set; }
    public Int32 Value4 { get; set; }
}

As next, the above mentioned interface ICustomParser<TType> should be implemented. How to do this is demonstrated in code snippet below.

public class CustomTypeParser : ICustomParser<CustomType>
{
    // Called when reading a configuration takes place.
    public CustomType Parse(String label, String value, Object fallback, CultureInfo culture)
    {
        String[] items = value.Split(',');

        if (items.Length != 4)
        {
            throw new FormatException("Custom type must contain four items.");
        }

        return new CustomType
        {
            Value1 = Convert.ToInt32(items[0].Trim()),
            Value2 = Convert.ToInt32(items[1].Trim()),
            Value3 = Convert.ToInt32(items[2].Trim()),
            Value4 = Convert.ToInt32(items[3].Trim())
        };
    }

    // Called when writing a configuration takes place.
    public String Parse(String label, CustomType value, Object fallback, CultureInfo culture)
    {
        return $"{value.Value1},{value.Value2},{value.Value3},{value.Value4}";
    }
}

Now it would be useful to implement all classes representing the whole configuration content. This is pretty much alike as shown in section above. The only exception is the usage of attribute CustomParser for all custom types. Here below a very simple example implementation of that class structure.

public class CustomSection
{
    [ConfigValue]
    [CustomParser(typeof(CustomTypeParser))]
    public CustomType CustomValue { get; set; }
}

public class CustomConfig
{
    [ConfigSection]
    public CustomSection Section1 { get; set; }
}

The example above includes one highlighted line of code. This code line shows that attribute CustomParser gets a parameter representing the type of a user‑defined implementation of interface ICustomParser<TType>. That’s it.

With all these information in mind, reading and parsing a fitting configuration file would look like shown below.

String filename = @"C:\custom-type.ini";
ConfigContent content = ConfigReader.Read(filename);
CustomConfig settings = ConfigParser<CustomConfig>.Parse(content);

Setup

Yet another feature of the Plexdata CFG Parser is the possibility to use a special configuration. Unfortunately, only three types are supported at the moment. These are the types for Unix‑styled configuration files, for Windows‑styled configuration files as well as a mixture of Unix‑style and Windows‑style.

Choosing a different configuration setting is pretty easy. The only thing to do is to apply a new setting instance to the Settings property of class ConfigSettings. Please see below for an example.

ConfigSettings.Settings = new ConfigSettingsUnix();

With the configuration used in section Writing a written configuration file would look like shown below.

[general] # The general section contains global values.
enable-show-pages: True
enable-show-styles: False
default-language: english # Using english, german and french is possible.

[network] # The network section contains network values.
server-address: 192.168.5.42 # Usage of IPv4 or IPv6 is possible.
server-port: 45054

In this context, two things should be noted. As first the configuration settings must be changed before creating a new configuration content. Secondly, the configuration settings are only relevant during writing a configuration file.

More information about using the configuration settings as well as about all other details can be found inside the complete API documentation. This API documentation is available as CHM file and can be downloaded from the releases page on GitHub.

Data Types

The integrated Value Converter supports String, Version, IPAddress, Char, Char?, Boolean, Boolean?, SByte, SByte?, Byte, Byte?, Int16, Int16?, UInt16, UInt16?, Int32, Int32?, UInt32, UInt32?, Int64, Int64?, UInt64, UInt64?, DateTime, DateTime?, Decimal, Decimal?, Double, Double?, Single, Single?, Guid, Guid? as well as Enum types.

It is also possible to convert any other type by implementing interface ICustomParser<TType> accordingly.

Limitation

There are some limitations when using the Plexdata CFG Parser. Clarifying them is task of this section.

Known Issues

Inner Double Quotes

Inner double quotes like shown below are not supported at the moment.
value-label="Inner double quote characters (") are not supported"
Escaping them like "inner \"value" does unfortunately not work.

Multi‑line Comments

Multi‑line comments like shown below won’t work.

label-1=value-1 # comment line 1
                # comment line 2
                # comment line 3
label-2=value-2

Only single line comments, as shown as follows, are supported.

[section-1] # section comment...
label-1=value-1 # value 1 comment...
label-2=value-2 # value 2 comment...

Please keep in mind, single line comments are exclusively reserved for the header of a configuration file.

Losing File Content

The complete content of an already existing file is disposed as soon as a configuration content is written back to this file. This means, it is impossible to selectively update only the data that have been changed.

Config Header Attribute

As mentioned above, the usage of class attribute ConfigHeader may overwrite header information in already existing configuration files. This problem could come to light if for example an already existing is configuration file was read and has been parsed into a user-defined model class and this class is saved into the same configuration file afterwards. In such a case and with enabled header generation the previously used file header will be replaced by the automatically created header.