If there is one thing that no two Operating Systems can seem to agree on, it’s how best to store configuration information. Linux programs generally store their configuration in .config files, which have a, ironically enough, “proprietary” text format (that is, program A’s config file will be unlikely to follow the same conventions as program B’s, and so forth). For Windows, there are a lot of options to choose from. the earliest model was a INI, or initialization file; an INI file was laid out something like this:
[sectionname]
valuename=value
valuename2=value2
[nextsection]
morevalues=moreitems
evenmorevalues=evenmoredata
Basically, it consisted of a set of “Sections” (the text in square brackets) each of which contained values (the name/value pairs within).
WriteProfileString, ReadProfileString, WritePrivateProfileString and ReadPrivateProfileString
The two “private” variants of this Kernel32 function almost always appear in programming-oriented discussions about INI files, but the former two have been lost in time.
the “INI” file structure was essentially “standardized” as far as windows applications were concerned with Windows 3.0. This was more a de facto, rather then a de juere standard, because the win.ini, system.ini, and various other Windows system based “ini” files used that form. Windows 3.0 offered the WriteProfileString and ReadProfileString functions. These functions are still present, even in the windows 7 Version of kernel32.dll:
C:\Windows\System32>dumpbin /exports kernel32.dll | find "ProfileString"
579 241 0001E6A7 GetPrivateProfileStringA
580 242 0003018B GetPrivateProfileStringW
606 25C 00032B13 GetProfileStringA
607 25D 00031E72 GetProfileStringW
1323 52A 00033FB8 WritePrivateProfileStringA
1324 52B 000335CA WritePrivateProfileStringW
1330 531 0008A982 WriteProfileStringA
1331 532 00033669 WriteProfileStringW
the non- “private” versions of these functions were pretty similar; there were also variations that allowed for writing/reading directly from integers as well as entire structures. Windows 3.0 only had the “non” private versions- and they all dealt exclusively with win.ini.
So, using WriteProfileString in kernel.dll, you could save your applications configuration data to and read it from a section in win.ini. This would have worked fine, with the minor inconvenience of a large win.ini file, but there was a caveat- the functions could not work with an INI file larger then 64K.
So, this technical limitation combined with the aftertaste of having a win.ini file that even comes near to approaching 64K guided the implementation of the “private” versions of these functions. These took, in addition to the values that the older functions did, a parameter specifying a filename. So now, applications could read and write values and sections to their own “private profile” INI files (thus the name). They still had the 64K limit, but this is hardly ever approached.
Fast forward a few years, and we have the Windows Registry. This is where Microsoft encourages us to save our application specific data. And I won’t argue- it works great. There is of course a minor caveat- it’s not as “user editable” as an INI file. if you ask somebody “change such and such value in program.ini” to solve a problem, they will usually have no problem, but ask them to change values in the registry and your asking for trouble, first- they could change the wrong value (after all, it’s pretty much a hierarchal version of the old win.ini, but without a size limitation), second, it’s not as “discoverable”. you can open an INI file, change it, make backups, etc. You can do this with a registry key but it’s not as simple and intuitive. backups involve exporting .reg files.
Now, with .NET, we are being encouraged to save our data into XML files. Are we not now back where we started? we started with text based INI files, moved to a monolithic binary hierarchal database, and we are now back to a text based format. The only real difference between INI and XML is the fact that XML is inherently heirarchal, so it’s easier to make code that works with either XML or the registry without problems. INI is limited to a fixed structure where there are two layers- the sections, and the values.
In either case, sometimes for a simple application there is no need to get involved in either the registry or XML; or, maybe you just like the simplicity and user-editability of an INI file. This is why I use INI files for most of my applications.
Given the fact that we have the API functions to work with INI files, you might think that my class-based solution may use them. This is not the case. you see, first, they are deprecated- second, they are limited to 64K, and lastly, and perhaps most importantly, they can sometimes not even read INI files at all- thanks to the fact that their functionality will often look in the registry for data. (this establishes it quite loudly as a compatibility function, not something to depend on for modern applications).
Instead, I opted to create a INI file parser. Thankfully, because of the simple structure it’s not hard to create something like this.
First, we need to think of an appropriate object model. we have sections, values, and the INI file itself. the base-level representation should be an abstract class that sections, and values can derive from:
1 2 3 4 5 |
public abstract class INIItem { public abstract override String ToString(); } |
you may be thinking, “why make an abstract class?” well, consider for a moment, comments, in the INI file. by convention, an INI file can have comments inside it, indicated by a semicolon as the first non-space character on the line. We could simply discard them entirely, but ideally, we would preserve the comments between loading the INI file and saving it. Here, we can create a derived class representing a comment:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public class INIComment : INIItem { public String Comment {get;set;} public INIComment(String pComment) { Comment = pComment; } public override string ToString() { return Comment; } } |
the Section itself can contain a list of INIItem objects, which can represent either INIComment or an INIDataItem, which is shown here:
1 2 3 4 5 6 7 8 9 10 11 |
public class INIDataItem : INIItem { public String Name { get; set; } public String Value { get; set; } public override string ToString() { return Name + "=" + Value; } } |
At this point, I have decided that it would make sense to Load the INI file entirely into memory. INI files are usually small in size, and considering the convention with XML files is to keep the XMLDocument in memory it’s not that new of an approach.
Now, we need a INISection class; this will be used to represent each Section in an INI file; It needs a Name, a list of Values, and a “eolComment” (end of line comment) for when there is a comment on the same line.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
public class INISection { public List<INIItem> INIItems; public String Name { get; set; } public String eolComment { get; set; } public INISection(String pName, string peolComment, List<INIItem> Values) { Name = pName; INIItems = Values; if (peolComment == null) peolComment = ""; eolComment = peolComment; } public INIDataItem this[String index] { get { INIDataItem returnthis = getValues().FirstOrDefault((w) => w.Name.Equals(index, StringComparison.OrdinalIgnoreCase)); if (returnthis == null) { //returnthis = new INISection(index, null, new List<INIItem>()); returnthis = new INIDataItem(index, ""); INIItems.Add(returnthis); } return returnthis; } set { //remove any existing value with the given name... INIItem itemfound = getValues().FirstOrDefault(w => w.Name.Equals(index, StringComparison.OrdinalIgnoreCase)); if (itemfound != null) INIItems.Remove(itemfound); INIItems.Add(value); } } public IEnumerable<inidataitem> getValues() { foreach (INIItem loopitem in INIItems) { INIDataItem casted = loopitem as INIDataItem; if (casted != null) yield return casted; } } public override string ToString() { return "[" + Name + "] (" + getValues().Count().ToString() + " Values, " + (INIItems.Count() - getValues().Count()).ToString() + " Comments."; } } |
Now this can appear a bit more daaunting then it really is. The bulk of the code is in the indexer, which allows you to modify an INI file like this:
1 2 3 |
INIFile myini= new INIFile(INIFilename); myini["section"]["value"].Value="a new value"; Configurationsettions.UseLongformat=myini["formatting"]["uselongformat"].Value; |
It’s actually a lot shorter then it would have been had I not used lambda expressions:
1 2 |
INIDataItem returnthis = getValues().FirstOrDefault((w) => w.Name.Equals(index, StringComparison.OrdinalIgnoreCase)); |
first, this is operating on the IEnumerable being given back from getValues(); getValues is what is known as an iterator method, a simplified way to think of it is that the return value is a set of all the values that were “yield returned” from that function. In this case, getValues() returns all the items that can be cast to an INIDataItem. This ensures that the lambda expression used with FirstOrDefault() has access to the Name field to perform the appropriate comparison.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 |
public class INIFile { public List<INISection> Sections { get; set; } public INIFile() { Sections = new List<INISection>(); } public INIFile(String filename) { LoadINI(filename); } //Indexer... public INISection this[String index] { get { INISection returnthis = Sections.FirstOrDefault((w) => w.Name.Equals(index, StringComparison.OrdinalIgnoreCase)); if (returnthis == null) { returnthis = new INISection(index, "", new List<INIItem>()); Sections.Add(returnthis); } return returnthis; } set { //remove any existing value with the given name... INISection itemfound = Sections.FirstOrDefault(w => w.Name.Equals(index, StringComparison.OrdinalIgnoreCase)); if (itemfound != null) Sections.Remove(itemfound); Sections.Add(value); } } private static INIDataItem ParseINIValue(String valueline) { int equalspos = valueline.IndexOf('='); String valuename = "", valuedata = ""; if (equalspos == -1) return null; valuename = valueline.Substring(0, equalspos).Trim(); valuedata = valueline.Substring(equalspos + 1).Trim(); return new INIDataItem(valuename, valuedata); } public void LoadINI(String Filename) { using (var newreader = new StreamReader(File.OpenRead(Filename), true)) { LoadINI(newreader); newreader.Close(); } } public void LoadINI(String Filename, Encoding pEncoding) { using (var newreader = new StreamReader(File.OpenRead(Filename), pEncoding)) { LoadINI(newreader); newreader.Close(); } } public void LoadINI(StreamReader fromstream) { String currentline = null; Sections = new List<INISection>(); INISection globalsection = new INISection("cINIFilecsGlobals", "", new List<INIItem>()); INISection currentSection = globalsection; //while there is still text to read. while ((currentline = fromstream.ReadLine()) != null) { //trim the read in line... currentline = currentline.Trim(); //if it starts with a square bracket, it's a section. if (currentline.StartsWith("[")) { //parse out the section name... String newsectionname = currentline.Substring(1, currentline.IndexOf(']') - 1); String eolComment = ""; if (currentline.IndexOf(';') > -1) eolComment = currentline.Substring(currentline.IndexOf(';')); currentSection = new INISection(newsectionname, eolComment, new List<INIItem>()); Sections.Add(currentSection); } else if (currentline.StartsWith(";")) { //add a new Comment INIItem to the current section. INIItem newitem = new INIComment(currentline); currentSection.INIItems.Add(newitem); } else { INIDataItem createitem = ParseINIValue(currentline); if (createitem != null) currentSection.INIItems.Add(createitem); } } if (globalsection.INIItems.Count() > 0) Sections.Add(globalsection); } public void SaveINI(String filename) { using (StreamWriter swriter = new StreamWriter(File.OpenWrite(filename), Encoding.ASCII)) { SaveINI(swriter); swriter.Close(); } } public void SaveINI(String filename, Encoding pEncoding) { using (StreamWriter swriter = new StreamWriter(File.OpenWrite(filename), pEncoding)) { SaveINI(swriter); swriter.Close(); } } public void SaveINI(StreamWriter tostream) { //save to the given stream. foreach (INISection loopsection in Sections) { //don't write out "[global]" for the global section, if present. if (!loopsection.Name.Equals("cINIFilecsGlobals", StringComparison.OrdinalIgnoreCase)) { tostream.Write("[" + loopsection.Name + "]"); if (loopsection.eolComment.Length > 0) tostream.WriteLine(" " + loopsection.eolComment); else tostream.WriteLine(); } foreach (INIItem itemloop in loopsection.INIItems) { tostream.WriteLine(itemloop.ToString()); } } } } |
And that’s the INIFile. the INIFile itself has a Indexer similiar to what the INISection class has, but it deals with the list of Sections. relatively straightforward, for the most part. LoadINI and SaveINI are both overloaded with a few different parameters, from passing in a StreamReader/Writer to simply giving a filename. For reading in an INI file into a class structure we simply read every line (the while ((currentline = fromstream.ReadLine()) != null) loop ) and take an appropriate action based on what we find- if it starts with a square bracket, it’s treated as a section. a new section is added with that name, and the local variable “currsection” is changed to point to it. if it starts with a semicolon it is treated as a comment (and an appropriate INIComment object is added to the INISection pointed at by currsection, which by default is an imaginary “global” section). Otherwise if there is an equals sign in the line, it is parsed (using the ParseINIValue() function) into a appropriate INIValue object, and that object is added to the current section.
writing it back into a stream is pretty much the opposite; loop through all the sections, and for every section, if the name is not the globally defined intrinsic item, then write out the appropriate section header (the name in square brackets), as well as any end of line comment defined for that section. then loop through all the values and write out the ToString() from it as a single line. (remember, the INIComment will return a comment line (starting with 😉 and the INIDataItem object will return a properly constructed Name=Value string.
The Full Source can be downloaded here:
Have something to say about this post? Comment!