INI Files Meet Regex and Linq in C# to Avoid the WayBack Machine of Kernal32.Dll

BetweenStonesWhat if you are stuck having to deal with older technology such as INI files while using the latest and greatest C# and .Net there is available? This article discusses an alternate way to read INI files and extract the data from those dusty tomes while  easily accessing the resulting data from dictionaries. Once the data resides in the dictionaries we can easily extract the data using the power of the indexer on section name followed by key name within the section. Such as IniFile[“TargetSection”][“TargetKey”] which will return a string of the value of that key in the ini file for that section.

Note all the code is one easy code section at the bottom of the article so don’t feel you have to copy each sections code.

Overview

If you are reading this, chances are you know what INI files are and don’t need a refresher. You may have looked into using the Win32 Kern32.dll method GetPrivateProfileSection to achieve your goals. Ack!  “Set the Wayback machine Sherman!” Thanks but no thanks.

Here is how to do this operation using Regular Expressions (Kinda a way back machine but very useful) and Linq to Object to get the values into a dictionary format so we can write this line of code to access the data within the INI file:

string myValue = IniFile[“SectionName”][“KeyName”];

The Pattern

Let me explain the Regex Pattern. If you are not so inclined to understand the semantics of it skip to the next section.

string pattern = @"
^                           # Beginning of the line
((?:\[)                     # Section Start
 (?<Section>[^\]]*)         # Actual Section text into Section Group
 (?:\])                     # Section End then EOL/EOB
 (?:[\r\n]{0,}|\Z))         # Match but don't capture the CRLF or EOB
 (                          # Begin capture groups (Key Value Pairs)
   (?!\[)                    # Stop capture groups if a [ is found; new section
   (?<Key>[^=]*?)            # Any text before the =, matched few as possible
   (?:=)                     # Get the = now
   (?<Value>[^\r\n]*)        # Get everything that is not an Line Changes
   (?:[\r\n]{0,4})           # MBDC \r\n
  )+                        # End Capture groups";

Our goal is to use Named Match groups. Each match will have its section name in the named group called  “Section”  and all of the data, which is the key and value pairs will be named “Key” and “Value” respectively.  The trick to the above pattern is found in line eight. That stops the match when a new section is hit using the Match Invalidator (?!). Otherwise our key/values would bleed into the next section if not stopped.

The Data

Here is the data for your perusal.

string data = @"[WindowSettings]
Window X Pos=0
Window Y Pos=0
Window Maximized=false
Window Name=Jabberwocky

[Logging]
Directory=C:\Rosetta Stone\Logs
";

We are interested in “Window Name” and “Directory”.

The Linq

Ok, if you thought the regex pattern was complicated, the Linq to Objects has some tricks up its sleeve as well. Primarily since our pattern matches create a single match per section with the accompany key and value data in two separate named match capture collections, that presents a problem. We need to join the the capture collections together, but there is no direct way to do that for the join in Linq because that link is only an indirect by the collections index number.

How do we get the two collections to be joined?

Here is the code:

Dictionary<string, Dictionary<string, string>> InIFile
= ( from Match m in Regex.Matches( data, pattern, RegexOptions.IgnorePatternWhitespace | RegexOptions.Multiline )
 select new
 {
  Section = m.Groups["Section"].Value,

  kvps = ( from cpKey in m.Groups["Key"].Captures.Cast<Capture>().Select( ( a, i ) => new { a.Value, i } )
     join cpValue in m.Groups["Value"].Captures.Cast<Capture>().Select( ( b, i ) => new { b.Value, i } ) on cpKey.i equals cpValue.i
     select new KeyValuePair<string, string>( cpKey.Value, cpValue.Value ) ).ToDictionary( kvp => kvp.Key, kvp => kvp.Value )

  } ).ToDictionary( itm => itm.Section, itm => itm.kvps );

Explanation:

  • Line 1: Our end goal object is a Dictionary where the key is the Section name and the value is a sub-dictionary with all the keys and values found in that section.
  • Line 2: The regex needs IPW because we have commented the pattern. It needs multiline because we are spanning multiple lines and need ^ to match each individual line and not just the beginning.
  • Line 5: This is the easiest item, simply access the named capture group “Section” for the section name.
  • Line 7 (.Captures) : Each one of the keys and values are in the specialized capture collection property off of the match.
  • Line 7 (.Cast<Capture>) : Since capture is specialized list and not a true generic list, such as List<string> we are going to Cast it(Cast<(Of <(TResult>) it (to IEnumerable<(Of <(T>)>),so we can access the standard query operators, i.e. the extension methods which are available to IEnumerable<T>. Short answer, so we can call .Select.
  • Line 7 (.Select): Because each list does not have a direct way to associate the data, we are going to create a new object that has a property which will have that index number, along with the target data value. That will allow us join it to the other list.
  • Line 7 (Lambda) : The lambda has two parameters, the first is our actual regex Capture object represented by a. The i is the index value which we need for the join. We then call new and create a new entity with two properties, the first is actual value of the Key found of the Capture class property “Value” and the second is i the index value.
  • Line 8 (Join) : We are going to join the data together using the direct properties of our new entity, but first we need to recreate the magic found in Line 7 for our Values capture collection. It is the same logic as the previous line so I will not delve into its explanation in detail.
  • Line 8 (on cpKey.i equals cpValue.i) : This is our association for the join on the new entities and yay, where index value i equals the other index value i allows us to do that. This is the keystone of all we are doing.
  • Line 9 (new KeyValuePair) : Ok we are now creating each individual linq projection item of the data as a KeyValuePair object. This could be removed for a new entity, but I choose to use the KeyValuePair class.
  • Line 9 (ToDictionary) : We want to easily access these key value pairs in the future, so we are going to place the Key into a Key of a dictionary and the dictionary key’s value from the actual Value.
  • Line 11 (ToDictionary) : Here is where we take the projection of the previous lines of code and create the end goal dictionary where the key name is the section and the value is the sub dictionary created in Line 9.

Whew…what is the result?

Console.WriteLine( InIFile["WindowSettings"]["Window Name"] ); // Jabberwocky
Console.WriteLine( InIFile["Logging"]["Directory"] );          // C:\Rosetta Stone\Logs

Summary

Thanks to the power of regular expressions and Linq we don’t have to use the old methods to extract and process the data. We can easily access the information using the newer structures. Hope this helps and that you may have learned something new from something old.

Code All in One Place

Here is all the code so you don’t have to copy it from each section above. Don’t forget to include the using System.Text.RegularExpressions to do it all.

string data = @"[WindowSettings]
Window X Pos=0
Window Y Pos=0
Window Maximized=false
Window Name=Jabberwocky

[Logging]
Directory=C:\Rosetta Stone\Logs
";
string pattern = @"
^                           # Beginning of the line
((?:\[)                     # Section Start
     (?<Section>[^\]]*)     # Actual Section text into Section Group
 (?:\])                     # Section End then EOL/EOB
 (?:[\r\n]{0,}|\Z))         # Match but don't capture the CRLF or EOB
 (                          # Begin capture groups (Key Value Pairs)
  (?!\[)                    # Stop capture groups if a [ is found; new section
  (?<Key>[^=]*?)            # Any text before the =, matched few as possible
  (?:=)                     # Get the = now
  (?<Value>[^\r\n]*)        # Get everything that is not an Line Changes
  (?:[\r\n]{0,4})           # MBDC \r\n
  )+                        # End Capture groups";

Dictionary<string, Dictionary<string, string>> InIFile
= ( from Match m in Regex.Matches( data, pattern, RegexOptions.IgnorePatternWhitespace | RegexOptions.Multiline )
    select new
    {
        Section = m.Groups["Section"].Value,

        kvps = ( from cpKey in m.Groups["Key"].Captures.Cast<Capture>().Select( ( a, i ) => new { a.Value, i } )
                 join cpValue in m.Groups["Value"].Captures.Cast<Capture>().Select( ( b, i ) => new { b.Value, i } ) on cpKey.i equals cpValue.i
                 select new KeyValuePair<string, string>( cpKey.Value, cpValue.Value ) ).ToDictionary( kvp => kvp.Key, kvp => kvp.Value )

    } ).ToDictionary( itm => itm.Section, itm => itm.kvps );

Console.WriteLine( InIFile["WindowSettings"]["Window Name"] ); // Jabberwocky
Console.WriteLine( InIFile["Logging"]["Directory"] );          // C:\Rosetta Stone\Logs
Share

8 Comments

  1. Veovis says

    it seems to work for key-value pair entry. But what about this kind of entry :

    [Product.Add.Reg]
    HKLM,”SYSTEM\CurrentControlSet\Services\RpcSs”,”ServiceSidType”,0x00010001,0x1
    HKLM,”SYSTEM\CurrentControlSet\Services\RpcSs”,”Type”,0x00010001,0x10
    HKLM,”SYSTEM\CurrentControlSet\Services\stisvc”,”ServiceSidType”,0x00010001,0x1
    HKLM,”SYSTEM\CurrentControlSet\Services\stisvc”,”Type”,0x00010001,0x10

    Reply
    • omegaman says

      @Veovis: Is the whole ini file setup like that? Or just that section? So to answer your question, yes that will fail. To make it work the regex pattern, in the key/value section, and possibly the usage of key value pairs in the code logic would have to be adapted to that pattern.

      Reply
  2. Veovis says

    Yep, I take it from a windows update (extract update-KB123456.exe with /x:FOLDER switch).
    Okay, I’ll develop a quick wrapper with the old apis :).
    I think this can be adapted to my situation, but I’m afraid this problem too difficult for my project.
    However, your solution is nice to see :).

    Thanks.

    Reply
  3. […] a generic collection, and it doesn’t therefore support IEnumerable<T>. The solution (which I found here) […]

    Reply
  4. punker76 says

    Hi,

    Empty Sections need following pattern:


    )* # End Capture groups”;

    not

    )+ # End Capture groups”;

    best regards

    Reply
  5. Wynand de Wit says

    I presume using System.Text.RegularExpressions is not the only include?

    Do you need to include something for Dictionary?

    Reply
  6. Chris says

    Thanks for this awesome example of avoiding use of the kernal32.
    However, I’m seeing quite a few oversights.
    1. INI comments aren’t ignored. ( ;…. )
    2. Ignoring whitespace where users tend to put it where they shouldn’t.
    3. Dictionary’s are case sensative. The “WindowSettings” key may actually be “windowssettings” or “Windowsettings”. In reality, we need to ignore the case, and just store the key and the property name in in Uppercase.
    4. Multiple Key names of the same name. One of the most cited complaints I’ve seen of Microsoft, is the lack of API’s to deal with this issue.
    I’ve waded my way thorugh the regular expression to deal with some of these, but the linq is a bit beyond me.

    Here’s what I’ve come up with.

    private static Dictionary<string, Dictionary<string, List>> GetINIFile(string filename)
    {
        string data = System.IO.File.ReadAllText(filename);
    
        string pattern = @"
    ^ # Beginning of the line
    (?:\s*(?:\[) # Section Start
    (?[^\]]*) # Actual Section text into Section Group
    (?:\]) # Section End then EOL/EOB
    (?:[\r\n]{0,}|\Z)) # Match but don’t capture the CRLF or EOB
    (?: # Begin capture groups
    (?!\[) # Stop capture groups if a [ is found; new section
    (?:(?:\s*;[^\r\n]*) # Ignore lines beginning with Comment character ; (ignoring whitespace)
    (?:[\r\n]?)) # End of the Comment line
    |(?: # Begin capture groups (Key Value Pairs)
    (?:\s*) # Ignore whitespace around KeyName
    (?[^=\r\n]*?) # Any text before the =, matched few as possible
    (?:\s*) # Ignore whitespace around KeyName
    (?:=) # Get the = now
    (?[^\r\n]*) # Get everything that is not an Line Changes
    (?:[\r\n]*) # MBDC \r\n
    ))* # End Capture groups";
    
        MatchCollection matches = Regex.Matches(data, pattern, RegexOptions.IgnorePatternWhitespace | RegexOptions.Multiline);
    
        Dictionary<string, Dictionary<string, List>> iniFile = new Dictionary<string, Dictionary<string, List>>();
        foreach (Match match in matches)
        {
            Dictionary<string, List> kvps = new Dictionary<string, List>();
            for (int i = 0; i < match.Groups["Key"].Captures.Count; i++)
            {
                string key = match.Groups["Key"].Captures[i].Value.ToUpper();
                if (!kvps.ContainsKey(key))
                    kvps.Add(key, new List());
                kvps[key].Add(match.Groups["Value"].Captures[i].Value);
            }
            iniFile.Add(match.Groups["Section"].Value.ToUpper(), kvps);
        }
    
        return iniFile;
    }
    
    Reply
  7. OmegaMan says

    Excellent suggestions Chris! I need to relook into this article and update it with the suggestions provided by yourself and others!

    Reply

Leave a comment

(required)
(required) (will not be published)

This site uses Akismet to reduce spam. Learn how your comment data is processed.