Reading and Modifying INI Files with Scripts

The initialization or INI file format has been around a long time.  In early versions of Windows (especially Windows 3.x) INI files were used to store most Windows and application configuration information.  Even though the Windows Registry, XML files, and other formats have taken their place in many instances, INI files can still be useful.  For example, MDT uses INI files (CustomSettings.ini and Bootstrap.ini) to store rules processing instructions.  If you are not familiar with the INI file format, this Wikipedia article is a good introduction: https://en.wikipedia.org/wiki/INI_file.

INI files are useful for storing information that is not too hierarchical.  They are also easy to read and edit with a text editor.  I still use them quiet a bit to store things like script configuration information.  Therefore, I often need to read and modify entries in INI files from my scripts.  In this post I will talk about some of the options for doing this with several Windows scripting languages.

VBScript

Since VBScript is the lingua franca of MDT scripting, I will start there.  The supported methods for programmatically interacting with INI files on the Windows platform are the “Profile” APIs like GetPrivateProfileString and WritePrivateProfileString.  Unfortunately, the Windows Script Host cannot directly access Windows APIs.  So most methods for accessing INI files in VBScript usually involve reading the text of the file and using code to parse the text.

Fortunately, if you are writing MDT scripts this work has been done for you.  The Utility class in ZTIUtility.vbs has the following INI file functions:

  • oUtility.ReadIni(file, section, item): Allows the specified item to be read from an .ini file.
  • oUtility.WriteIni(file, section, item, value): Allows the specified item to be written to an .ini file.
  • oUtility.Sections(file): Reads the sections of an .ini file and stores them in an object for reference.
  • oUtility.SectionContents(file, section): Reads the contents of the specified .ini file and stores them in an object.

However, if you are not creating MDT scripts or would like greater options for handling INI files in an MDT script I have created a VBScript class (IniDocument) with a large number of method and properties.

Methods:

  • Load: Load the INI file into memory.
  • Save: Save the INI file back to disk.
  • SaveAs: Save the INI file back to disk using another file name.
  • ReadValue: Read the Value for the first occurrence of a Key in an INI file section.
  • ReadKeyNames: Read all Key names in INI file Section.
  • ReadSectionNames: Read all Section Names in an INI file.
  • WriteEntry: Write a Key & Value entry to an INI file section.
  • ReadValueAll: Read all Values for a given Key name in an INI file (i.e. if there are duplicate Keys in a section).
  • ReadKeyValueLines: Read all raw Key=Value lines in INI file Section.
  • ReadKeyValuePairs: Read all Key/Value pairs in INI file Section into a 2D Array.
  • ReadInfLines: Read all INF-type lines (not Key=Value) in INI/INF file Section.
  • ReadComments: Read all comment lines and trailing comments in INI/INF file Section.
  • ReadAllLines: Read all raw lines in INI/INF file Section.
  • AddEntry: Add a Key & Value entry to an INI section (does not validate if Key exists and will not change existing values with the same Key name).
  • AddTextLine: Write a text line to the INI/INF file section.
  • DeleteKey: Delete the Key for the first occurrence of a Key in an INI file section.
  • DeleteKeyAll: Delete all occurrences of a Key in an INI file section (i.e. if there are duplicate Keys in a section).
  • DeleteSection: Delete an INI file section.
  • ExpandINFStringVariables: Expands String variables (entries found in the [Strings] section of an INF file).

Properties:

  • FileLoaded: True if an INI file has been loaded into memory with the Load method (read only).
  • FileSaved: False if changes need to be save to disk (read only).
  • FilePath: Path to INI file (read only).
  • IsUnicode: Load method will set this based on the encoding of the file.  May be changed so that Save and SaveAs will use the new encoding when saving.
  • SpaceEqualsSign: Place spaces on either side of the equals sign, e.g. Key = Value.
  • QuoteKeys: Place double quotes around the Key name, e.g. "Key"=Value.
  • QuoteValues: Place double quotes around the Value name, e.g. Key="Value".
  • QuoteTextLines: Place double quotes around text (INF) lines, e.g. "c:\sysprep\sysprep.exe -clean".
  • LeadingCharacter: Place leading characters (spaces, tabs, or blanks) in front of the Key or text line (i.e. "indent" the line).
  • QtyLeadingCharacter: Number of leading characters (spaces, tabs, or blanks) in front of the Key or text line.
  • StringsSection: Name of localized "Strings" section in an INF file.
  • CommentString: Character or string of charaters that go at the beginning of a string or line to designate a comment.

I have included this class in a VBScript, IniDocument.vbs, in the attached ZIP file.  This VBScript can be used as in a script element in an MDT script if desired:

<script language="VBScript" src="IniDocument.vbs"/>

You can also copy the class into your own VBScripts.  I have also included another VBScript based on this class, IniCommand.vbs, that is a command line utility for reading and modifying INI files.  You can look at the Main Script section for examples of how to use the class in your own scripts.

CMD Scripts

Trying to read and modify INI files in command shell scripts in more challenging due to the limited ability to do complicated text parsing.  Therefore, you usually have to rely on an external utility or script.  One such utility is iniman.exe from the Windows Server 2003 Resource Kit utilities.  However, this is a .NET console application and cannot be used in WinPE.

What I use for reading and modifying INI files in my CMD scripts is the previously mentioned IniCommand.vbs script.  This script supports most of the methods and properties of the IniDocument class.  Run the following command for the full usage:

cscript //nologo "<path>\IniCommand.vbs" /?

Here are a few example of using IniCommand.vbs to read values and set environment variables.   To read a FileSystem key in the Unattended section of Unattend.txt and set the value in a FileSystem environment variable (with all files in the same folder), use the following commands (the last line may wrap due to screen width):

set SCRIPTDIR=%~dp0
set SCRIPTDIR=%SCRIPTDIR:~0,-1%
for /F "tokens=*" %%i in ('cscript //nologo "%SCRIPTDIR%\IniCommand.vbs" /file:"%SCRIPTDIR%\Unattend.txt" /section:Unattended /key:FileSystem /cmd:ReadValue') do set FileSystem=%%i>nul

To set all the values in the Unattended section as environment variables, use the following commands (the last line may wrap due to screen width):

set SCRIPTDIR=%~dp0
set SCRIPTDIR=%SCRIPTDIR:~0,-1%
for /F "tokens=1,2* delims== " %%i in ('cscript //nologo "%SCRIPTDIR%\IniCommand.vbs" /file:"%SCRIPTDIR%\unattend.txt" /section:Unattended /cmd:ReadKeyValuePairs') do set %%i=%%j>nul

Task such as setting/changing values, deleting keys, deleting sections, etc. are fairly easy to do with IniCommand.vbs.  The usage text explains all the options.

Window PowerShell

I was inspired to write this post because I recently had a need to modify some INI files in a PowerShell script.  I was initially going to simply use IniCommand.vbs.  However, I decided to see what could be done using native PowerShell code.

Several colleagues had functions similar to the code found here.  This is my slightly improved version:

function Convert-IniFile ($file)
{
$REGEX_INI_COMMENT_STRING = ";"
$REGEX_INI_SECTION_HEADER = "^\s*(?!$($REGEX_INI_COMMENT_STRING))\s*\[\s*(.*[^\s*])\s*]\s*$"
$REGEX_INI_KEY_VALUE_LINE = "^\s*(?!$($REGEX_INI_COMMENT_STRING))\s*([^=]*)\s*=\s*(.*)\s*$"

    $ini = @{}
switch -regex -file $file {
"$($REGEX_INI_SECTION_HEADER)" {
$section = $matches[1]
$ini[$section] = @{}
}
"$($REGEX_INI_KEY_VALUE_LINE)" {
$name,$value = $matches[1..2]
if ($name -ne $null -and $section -ne $null)
{
$ini[$section][$name] = $value
}
}
}
$ini
}

This converts the file to a hashtable with keys representing the section names and values that are hashtables containing the key/value content of each section.  This is actually very good for reading INI file keys/values.  You can directly query a key value using syntax like this:

$iniFile = Convert-IniFile "Unattend.txt"
$FileSystem = $iniFile["Unattended"]["FileSystem"]

You can also use the following function I created to convert the key/value entries in a section to script scope variables (or all the sections in the INI file by passing a blank string as $section):

function Convert-IniToVariables ($iniHashTable,$section)
{
if ($section -ne "")
{
foreach ($key in $iniHashTable[$section].Keys)
{
$iniKeyValue = "$($iniHashTable[$section][$key])"
$iniKeyValueLine = "`$script:$($key) = `'$($iniKeyValue)`'"
invoke-expression $iniKeyValueLine
}
}
else
{
foreach ($section in $iniHashTable.Keys)
{
foreach ($key in $iniHashTable[$section].Keys)
{
$iniKeyValue = "$($iniHashTable[$section][$key])"
$iniKeyValueLine = "`$script:$($key) = `'$($iniKeyValue)`'"
invoke-expression $iniKeyValueLine
}
}
}
}

Unfortunately, using this type of code to modify and then save the file back to disk is not as satisfactory.  For example, using the Save-IniFile function I created (shown below) writes the file sections and key=value lines back to disk in the hashtable order, not the original file order.  So while the file is still valid, it is not easy to find entries where you originally placed them and may break applications that depend on the file order.

function Save-IniFile ($iniHashTable, $file)
{
    if (Test-Path $file) {remove-item $file}

    foreach ($section in $iniHashTable.Keys)
{
Add-Content -path $file "[$($section)]"
foreach ($key in $iniHashTable[$section].Keys)
{
Add-Content -path $file "$($key)=$($iniHashTable[$section][$key])"
}
Add-Content -path $file ""
}
}

Since I needed to make modification to INI file and did not want reordering of the file contents to occur, I decided to look for additional techniques.  In my June 7, 2009 post I described how I used Windows APIs in C# code compiled in PowerShell to change the time zone.  The same can be done with the “Profile” APIs I described above.  Lee Holmes of the Windows PowerShell team gave samples of how this can be done in this post for PowerShell v1 and this post for PowerShell v2.  I had originally planned to create a .NET class that encapsulated all the Profile APIs that I could with PowerShell.  Luckily, someone already did this work for us.

There is a free .NET class available called IniReader.  This class includes nearly all the profile APIs and can be used with PowerShell very easily.  The only file needed from the IniReader download to use this with PowerShell is IniReader.cs.  The only Profile API missing from this class is GetPrivateProfileSection.  If you want to add support for this API, add the following API declaration and methods to the IniReader class in IniReader.cs:

/// <summary>
/// The GetPrivateProfileSection function retrieves the keys and values for the specified section of an initialization file.
/// </summary>
/// <param name="lpAppName">Pointer to a null-terminated string specifying the name of the section containing the data. This section name is typically the name of the calling application.</param>
/// <param name="lpszReturnBuffer">Pointer to a buffer that receives the key name and value pairs associated with the named section. The buffer is filled with one or more null-terminated strings; the last string is followed by a second null character.</param>
/// <param name="nSize">Specifies the size, in TCHARs, of the buffer pointed to by the lpReturnedString parameter.</param>
/// <param name="lpFileName">Pointer to a null-terminated string containing the name of the initialization file. If this parameter does not contain a full path for the file, the function searches the Windows directory for the file. If the file does not exist and lpFileName does not contain a full path, the function creates the file in the Windows directory. The function does not create a file if lpFileName contains the full path and file name of a file that does not exist.</param>
/// <returns>If the function succeeds, the return value is nonzero.<br>If the function fails, the return value is zero.</br></returns>
[DllImport("KERNEL32.DLL", EntryPoint="GetPrivateProfileSectionA", CharSet=CharSet.Ansi)]
private static extern int GetPrivateProfileSection (string lpAppName, byte[] lpszReturnBuffer, int nSize, string lpFileName);

/// <summary>Retrieves retrieves the keys and values for the specified section of an INI file.</summary>
/// <returns>Returns an ArrayList with the key=value entries.</returns>
public ArrayList GetSection(string section) {
try {
byte[] buffer = new byte[MAX_ENTRY];
GetPrivateProfileSection(section, buffer, MAX_ENTRY, Filename);
string [] parts = Encoding.ASCII.GetString(buffer).Trim('\0').Split('\0');
return new ArrayList(parts);
} catch {}
return null;
}
/// <summary>Retrieves retrieves the keys for the specified section of an INI file.</summary>
/// <returns>Returns an ArrayList with the key entries.</returns>
public ArrayList GetSectionKeys(string section) {
try {
byte[] buffer = new byte[MAX_ENTRY];
GetPrivateProfileSection(section, buffer, MAX_ENTRY, Filename);
string [] parts = Encoding.ASCII.GetString(buffer).Trim('\0').Split('\0');
ArrayList list = new ArrayList();
foreach (string line in parts)
{
string [] lineparts = line.Split('=');
list.Add(lineparts[0]);
}
return list;
} catch {}
return null;
}

As an example of the usage of this class with PowerShell, I took the C# sample in the download (TestApp.cs) and translated it into a PowerShell v2 script (TestScript.ps1 in my attachment).  IniReader.cs must be in the same folder as this script.

$invocation = (Get-Variable MyInvocation -Scope 0).Value
$scriptPath = Split-Path $Invocation.MyCommand.Path

$iniReaderCSharp = Join-Path $scriptPath "IniReader.cs"
Add-Type -path $iniReaderCSharp

$iniFilePath = Join-Path $scriptPath "Test.ini"
$ini = New-Object Org.Mentalis.Files.IniReader($iniFilePath)
$ini.Write("Section1", "KeyString", "MyString")
$ini.Write("Section1", "KeyInt", 5)
$ini.Write("Section2", "KeyBool", $true)
[Byte[]] $z = 0, 123, 255
$ini.Write("Section2", "KeyBytes", $z)
$ini.Write("Section3", "KeyLong", [long] 123456789101112)
$ini.Section = "Section1"
Write-Host "String: " + $ini.ReadString("KeyString")
Write-Host "Int: " + $ini.ReadInteger("KeyInt", 0).ToString()
Write-Host "Bool: " + $ini.ReadBoolean("Section2", "KeyBool", $false).ToString()
Write-Host "Long: " + $ini.ReadLong("Section3", "KeyLong", 0).ToString()
Write-Host "Byte 1 in byte array: " + $ini.ReadByteArray("Section2", "KeyBytes")[1].ToString()
$ini.DeleteKey("Section2", "KeyBytes")
$ini.DeleteSection("Section3")
$sections = $ini.GetSectionNames()
foreach ($section in $sections)
{
Write-Host $section
}

# Code below requires GetPrivateProfileSection API
# declaration and GetSection and GetSectionKeys
# methods added to IniReader.cs.
$section1 = $ini.GetSection("Section1")
foreach ($entry in $section1)
{
Write-Host $entry
}
$section2 = $ini.GetSectionKeys("Section2")
foreach ($key in $section2)
{
Write-Host $key
}

Modifications made using this class will preserve the file order correctly.  I have also included a PowerShell v1 version of this script (TestScript_PSv1.ps1) in the attachment.  This version requires that CompileCSharpLib.ps1 (included in the attachment) be present in the same folder as well.

 

Disclaimer: The information on this site is provided "AS IS" with no warranties, confers no rights, and is not supported by the authors or Microsoft Corporation. Use of included script samples are subject to the terms specified in the Terms of Use .

This post was contributed by Michael Murgolo, a Senior Consultant with Microsoft Services - U.S. East Region

IniFileScripts.zip