Text and code by Ron Maupin.
I had to build my enhanced INI class that replaces the database. My Modula-2 version used a doubly-linked list (section) of doubly-linked lists (key/value pairs) for the INI. Delphi’s generics made this much easier, and I now use the TDictionary, although it could be done with TStringList. I used the Delphi ability to have an array property (IniFile[Section,Value]) to simplify assignments and set it as the default property of the INI object. I also didn’t want to get lost in the weeds of having different properties or methods for different data types, so I built some record helpers in the application for different data types that simplify assignment (IntVar := IniFile[Section,Value].ToInt or IniFile[Section,Value] := BoolVar.ToStr).
Let me start by saying that I am not a programmer, nor am I much of a writer; I am a Network Engineer at a huge company (in the Fortune 25 with over 250,000 employees at over 10,000 locations, and heavily regulated by multiple groups of the U.S. government).
Way back at the beginning of the 1980s, I had an Apple III computer that had a whopping 512K of RAM and a monster 5 MB hard drive with Apple Pascal (based on the UCSD P-System). I used to play around with that. Until I did this project, I only had a conceptual knowledge of object-oriented programming, and no knowledge of Windows programming.
I originally built a small MS-DOS pilot in Modula-2 to demonstrate to management that this could be done, hoping that they would take it and run with it, but, due to a large merger where we doubled in size, they just ignored it for several years. I eventually got Delphi 2010 (using XE5 right now) to see if I could figure it out on my own. Poking around in the help files, and using articles such as yours, I built a complete application which many of our Network Engineers use. They can enter minimal information, and, in just a few seconds, build complete configurations for all the routers, switches, etc.
One requirement at my company is that when using a database, you must have a server, use a SQL database, and have a Database Administrator. Any application or database changes must go through an onerous change process with extended timelines. My goal was to build a small, self-contained tool that did not rely on any databases. This allows the application to be agile, and any data changes can be made in a very short time by simply editing a text file. This approach avoids the whole change process mess, and it allows me to make changes or corrections in seconds or minutes when a Network Engineer needs something or discovers a syntax error in a device configuration.
I started by building an IP unit with two classes (Tv4 for IPv4, and Tv6 for IPv6) that do all the necessary IP math, and I enhanced them to return a lot of information about IP addresses (scope, class, RFC description, etc.). To test this, I built a small IP tool that really just shows the properties of the two IP objects; the fields (prefix, address, mask length, mask, inverse mask, subnet, etc.) are tied directly to the object properties, and the tool just refreshes them whenever a change to one field is made. The Network Engineers liked it, so I made it into a usable tool.
The secret sauce is the use of tokens as values. When returning a value, the INI will inspect the value to see if it contains a token. If the value contains a token, the INI will recursively look up, in itself, the value represented by the section and key in the token. My token format is <Section:Key>. Tokens may be nested so that part of the token may be a token, allowing a Section or Key used in the token to be, or contain, a value from a different value in the INI. The token has a few options that are explained below.
One of the uses to which I put this is to enable/disable application user input fields based on values in the INI. I built five controls (TIniEdit, TIniComboBox, TIniCheckBox, TIniIPv4Edit, and TIniIPv6Edit) that each have a Section and Key property, and each know how to read and write to and from the INI. For my application, I added an Init method and Valid property to the controls. In the Init method for each control has Self.Enable := IniFile[Section,Key+’ ENABLE’].ToBool representing a Boolean value in a different Key in the same section that is rooted on the Key name with ‘ ENABLE’ added to the end of the Key name. There are also control-dependent routines in the Init methods, for example, the TIniComboBox uses Self.CommaText := IniFile[Section,Key+’ LIST’], and the list field value may be modified by the use of token(s) to look up different lists depending on the value(s) of other field(s). I validate the TIniEdit fields with a Key+’ REGEX’ value that usually gets looked up from a table based on a value from another field by using a token as the value of the REGEX field.
The application starts with a Network Engineer selecting a design type from a TIniComboBox which has a static ENABLE field value of TRUE;. The other fields have ENABLE field values that may be tied to tables in the INI that ultimately return TRUE or FALSE, depending on the design type and/or other values in the INI. For example, a TIniIPv4Edit field’s ENABLE field value may have a token that points to the value of a TIniCheckBox field (the TIniCheckBox sets its value to TRUE or FALSE); After setting the value of a field, the application calls the Init method for all the fields. The fields will then enable/disable themselves based on the returned value from their respective ENABLE fields. My application takes this sort of behavior to an extreme with multiple, nested, and recursive lookups to return either TRUE or FALSE to each field as it performs its Init method. I was initially concerned about the number of lookups (often in the millions) and the time that would take, but this works very well, and, as a memory-based INI, it is quite fast.
This design allows me to completely change the operation of the application without changing the application code by simply editing the INI file with a standard text editor. In effect, the INI is a finite-state machine, and the application uses it to set its state (which tabs are visible, which fields are enabled, what the default values, list vales, etc. are).
Download the (simplified) INI source code (I removed a Build method, and supporting routines, that I use to merge the INI values with a plaintext configuration template, outputting a complete device configuration in a plaintext file, suitable for dumping onto a network device), and below is something I wrote to help other users understand the INI:
The LiveINI file implementation is an enhanced version of a typical INI file. There is no defined standard for INI files, but there are some basic, accepted practices.
A LiveINI file is a plaintext file that contains a few basic elements:
- Optional Comments begin with a semicolon (“;”) and extend to the end of the line on which the semicolon is located. Comments are ignored. Since everything on a line from the semicolon to the end of the line is ignored, comments should be carefully placed, and semicolons cannot be used in any section names, key names, or values.
- The INI file is broken into Sections. Each section begins with a section name enclosed by square brackets (“[” and “]”). Section names are case-insensitive, and leading and trailing whitespace is ignored. Any leading and trailing characters on a line containing a section are ignored. Duplicate section names merge the duplicate sections into a single section. Sections are not hierarchical and cannot be nested.
- Each section contains zero or more Key/Value Pairs. A key/value pair consists of a key name and value separated by an equal (“=”). Key names are case-insensitive. Values may be empty. Leading and trailing whitespace on both key names and values is ignored. Sections with duplicate key names use only the last value defined for the key name.
- Tokens may be used in a key/value pair as a representation of a value from a different key/value pair in the INI file. Tokens are enclosed in angle brackets (“<” and “>”) and consist of a section name and a key name separated by a colon (“:”). Within the token, leading and trailing whitespace on section and key names is ignored. Tokens may be nested, i.e., section and/or key names within a token may be, in whole or in part, tokens that represent a value from a different section/key.
When an application requests a value from the INI, it specifies the section and key names. The INI looks up the value and returns it to the application. Given an unknown section/key combination, LiveINI will return a text error as the value. Error values returned for internal application use are converted to zero for an integer value or “FALSE” for a Boolean value.
“Standard” INI values are static and only change when the application specifically changes the value, or the value is changed by manually editing the INI file. Tokens allow LiveINI values to be dynamic and change based on other values in the LiveINI. In effect, LiveINI is a finite-state machine.
When an application using LiveINI requests a value that contains one or more tokens, the tokens are evaluated, innermost token outward, replacing the token in the result to be returned with the value that the token represents. The resulting value is returned to the application as if the value was statically defined. The token evaluation DOES NOT replace the token or change the original LiveINI in any way; only the returned value is modified. The process is transparent to the application; the application reads values the same way as using a “standard” INI.
The Network Template Tool uses LiveINI to simplify application design and reduce or eliminate application changes when external factors change, e.g., adding a new device model in an existing role does not require changes to the application. Application changes are only required for changes to the user interface, e.g., new tabs or data-entry fields.
“Standard” INI File Example
In this example, either the user must supply the region and the three DNS server addresses, or the application must have the logic and knowledge to supply the three DNS server addresses for the region. This requires the user to maintain correct information, increasing the chances of a typographical error or incorrect entry. Building the logic and knowledge into the application requires an application change in the event of an address change to one or more of the DNS servers.
[SITE] REGION= DNS SERVER 1= DNS SERVER 2= DNS SERVER 3=
LiveINI File Example
In this example the user only needs to supply the region. The DNS server addresses will be dynamically determined based on the region. The user is not burdened to find which DNS server addresses to use for the region. Using a drop-down list from which the user selects the region leaves no chance for the user to enter a typographical error. In the event of an address change to one or more DNS servers, the INI file is edited with a plaintext editor to change the DNS server addresses to the new addresses, and the application uses the new DNS server addresses when the application INI file is next loaded. The application code is not modified based on the external factor, and a user, unaware of the change to the DNS server addresses, will not enter an incorrect address. Also, the entire logic of determining DNS servers may be modified or replaced without modifying the application in any way.
[SITE] REGION= DNS SERVER 1=<DNS SERVER TABLE:<SITE:REGION> 1>> DNS SERVER 2=<DNS SERVER TABLE:<SITE:REGION> 2>> DNS SERVER 3=<DNS SERVER TABLE:<SITE:REGION> 3>> [DNS SERVER TABLE] CENTRAL 1=10.1.1.1 CENTRAL 2=10.1.1.2 CENTRAL 3=10.1.1.3 EAST 1=10.1.2.1 EAST 2=10.1.2.2 EAST 3=10.1.2.3 WEST 1=10.1.3.1 WEST 2=10.1.3.2 WEST 3=10.1.3.3
Using the LiveINI example, the user supplies “East” as the site’s region, LiveINI will:
- Read the value “<DNS SERVER TABLE:<SITE:REGION> 1>>” from the “SITE” section, “DNS SERVER 1” key.
- Discover the value contains the token “<SITE:REGION>”.
- Using the “<SITE:REGION>” token, read the value “East” from the “SITE” section, “REGION” key.
- Replace the “<SITE:REGION>” token with “East”, changing the value to “<DNS SERVER TABLE:East 1>”.
- Discover the value contains the token “<DNS SERVER TABLE:East 1>”.
- Using the “<DNS SERVER TABLE:East 1>” token, read the value “10.1.2.1” from the “DNS SERVER TABLE” section, “EAST 1” key.
- Replace the “<DNS SERVER TABLE:East 1>” token with “10.1.2.1”.
- Return the value “10.1.2.1” to the application.
The process may be less (static values or non-nested tokens) or more (tokens refer to values that also contain tokens) complex. Retrieved values are not stored or cached; the process is repeated each time the application requests a value, insuring that any returned value is current relative to changes made to any other value. LiveINI executes the process millions of times in a matter of seconds while building equipment requests and device configurations. For example, each of the possible dozens, hundreds, or thousands of devices in any site uses the same DNS server addresses, and the process is repeated for each device configuration built.
LiveINI Process Flow
LiveINI Interface section
type TLiveIni = class(TObject) private type TSection = TDictionary<string, string>; TIni = TDictionary<string, TSection>; var FIni: TIni; FChanged: Boolean; function GetValue(const Section, Key: string): string; procedure SetValue(const Section, Key, Value: string); function ValueToStr(const Section, Line: string): string; public constructor Create(); procedure Load(const InFile: string); procedure Save(const OutFile: string); destructor Destroy(); override; property Changed: Boolean read FChanged; property Values[const Section, Key: string]: string read GetValue write SetValue; default; end;
When the section in a token is the section of the token’s location, a token may use an asterisk (“*”) instead of the section name. In the previous LiveINI example, the “
In certain cases, values must be returned in a particular case (upper or lower). Tokens may use a character to specify the case of the returned value. Plus (“+”) will convert the value to upper case, and minus (“-”) will convert the value to lower case. The character must precede the section name in the token. For example, if the value for the “SITE” section, “REGION” key value is “East”, using the “<+SITE:REGION>” token will return the value “EAST”, and using the “<-SITE:REGION>” token will return the value “east”.
Both token options may be used in the same token. Since the asterisk represents the section name, and the case-specification character must precede the section name, the case-specification character must precede the asterisk in a token.
I know that there are probably better ways to do certain things, but, as a novice, I did what felt comfortable to me. I’m still finding techniques that I like, but I really haven’t changed the INI code (it is still basically what I originally built for the MS-DOS demonstration) because it does work as it is, and I hesitate to break it.
Even though I wrote this thing, it took a long time before I had a good understanding of its true power and how to use it. Step by step, I moved virtually all the program logic out of the application to the INI file. The application is now just the user interface to the INI. Even with the code to build the configurations from template text files (I had to build in some conditional processing using “IF THEN ELSE” tokens that use Boolean “AND OR NOT” alongside the INI tokens in the template files) the whole INI unit is only about 350 lines. The IP unit is about five times the size, although the IPv6 part is ¾ of that, and I could reduce it if I didn’t need the code for testing the various IP ranges, but I needed to provide that so that the IP controls can have properties to require or exclude IP range sets for validation, for example: require v4PrivateUse and v4ClassA, or exclude v4Network0 and v4LocalHost .
Comments are welcome.
Beautifully written piece. I totally enjoyed reading it.
Great article! It’s really interesting to see how a simple change to an old tried-and-true programming paradigm can spice it up.