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.
- Create a new .NET Framework project.
-
Open menu Tools → NuGet Package Manager and choose command Manage NuGet Packages for Solution….
- Alternatively, simply right‑click the project in the Solution Explorer and choose command Manage NuGet Packages….
-
Switch to Browse page and in the search box just type
plexdata.cfgparser
. -
From the shown list select Plexdata.CfgParser.NET and click button
[Install]
at the right. - Confirm the dialog box and that’s it.
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.
- Data parts of configuration values must not contain additional double quotes. For more information please refer to section Inner Double Quotes.
- Leading and/or trailing section and/or value comments are not supported. Comments can only be placed behind sections and/or values.
- Multi‑line comments behind sections and/or values are not supported. Only single line comments are possible. For more information see section Multi‑line Comments.
- Creating a configuration from scratch or reading an existing configuration file may cause trouble when writing it back to disk. Please see section Losing File Content below for more information.
-
Usage of class attribute
ConfigHeader
may overwrite header information in already existing configuration files. Please see section Config Header Attribute below for more information.
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.