Summary: Guest Blogger Oliver Lipkau shares two Windows PowerShell functions to simplify reading and writing to INI files.
Microsoft Scripting Guy Ed Wilson here. Today, we have another guest blog post, this one written by Oliver Lipkau. Oliver has written a guest blog post before, and he was a judge for the 2011 Scripting Games. In addition to this, Oliver has the Microsoft Community Contributor award. With that as background, take it away, Oliver.
When I first started working with Windows PowerShell, I was amazed by all the built-in cmdlets and what you can do with them. After a bit of playing around and writing my first scripts, I noticed Windows PowerShell has many cmdlets to read and write different types of files, such as CSV, XML, HTML and plain text. But not INI files. This is weird, I thought. A lot of programs and tools use an INI file to save their settings. Even I like to save some settings from my GUI Windows PowerShell scripts (last position, last size, and so on) into INI files. Why won’t Windows PowerShell read them?
Well, there is no point in whining about it. Let’s fix this!
First, we need to take a look at what an INI file looks like. My Windows 7 computer has a system.ini file that looks like the following:
; for 16-bit app support
[386Enh]
woafont=dosapp.fon
EGA80WOA.FON=EGA80WOA.FON
EGA40WOA.FON=EGA40WOA.FON
CGA80WOA.FON=CGA80WOA.FON
CGA40WOA.FON=CGA40WOA.FON
[drivers]
wave=mmdrv.dll
timer=timer.drv
If we take a look at the keys, we notice they look a lot like the hash tables that Ed has blogged about. So in this case, we can translate it into a hash table by doing the following:
$386Enh = @{“EGA80WOA.FON”=”EGA80WOA.FON”;”EGA40WOA.FON”=”EGA40WOA.FON”;”CGA80WOA.FON”=”CGA80WOA.FON”;”CGA40WOA.FON”=”CGA40WOA.FON”}
$drivers = @{“wave”=”mmdrv.dll”;”timer”=”timer.drv”}
But what about the other sections? They can form a hash table as well, as shown here:
$system = @{“386Enh”=$386Enh;”drivers”=$drivers}
Each section has a unique name, which turns into the key of the level 1 hash table, and the value of these are also hash tables. The output from the above hash tables appears in the following figure.
Fine, but how do we do that in a script that will work for any INI file? Easy: “switch -regex” (get-help switch). To figure out if a line is a comment, section, or key, we will use a regular expression with the switch statement.
switch -regex –file pathToFile
The regex pattern ^[(.+)]$”, “^(;.*)$”,”(.+?)\s*=(.*) looks weird if not scary, but these are the regex strings that will do the magic. The first will only match lines that are sections; the second pattern is for comments; and the third is for keys. So let’s finally start scripting. The complete Get-iniContent function is shown here:
function Get-IniContent ($filePath)
{
$ini = @{}
switch -regex -file $FilePath
{
“^\[(.+)\]” # Section
{
$section = $matches[1]
$ini[$section] = @{}
$CommentCount = 0
}
“^(;.*)$” # Comment
{
$value = $matches[1]
$CommentCount = $CommentCount + 1
$name = “Comment” + $CommentCount
$ini[$section][$name] = $value
}
“(.+?)\s*=(.*)” # Key
{
$name,$value = $matches[1..2]
$ini[$section][$name] = $value
}
}
return $ini
}
This function works fine, but if you want it to look nicer, I have uploaded an advanced function to the Scripting Guys Script Repository.
“Awesome. Now what?” Now you have access to all the INI data. For example:
$iniContent = Get-IniContent “c:\temp\file.ini”
$iniContent[“386Enh”]
$value = $iniContent[“386Enh”][“EGA80WOA.FON”]
$iniContent[“386Enh”].Keys | %{$iniContent[“386Enh”][$_]}
And whatever else you can think of.
What if I want to change the file, or write a new INI file? Aha! Out-IniFile to the rescue. Well, not yet, because we haven’t written it yet. But stay tuned, because we will do that now. If we get an object such as the $iniContent above as input, it’s easy to write it to an INI file. First we need to walk through all keys of the level 1 hash:
foreach ($i in $InputObject.keys)
Now a quick check to see if the value of the current key is a hash (badly written INI files may not have sections):
if (!($($InputObject[$i].GetType().Name) -eq “Hashtable”))
If not, we write the key name to the file as section and index into the value:
Add-Content -Path $outFile -Value “[$i]”
Foreach ($j in $($InputObject[$i].keys | Sort-Object))
And to finish, just write the current key to the file:
Add-Content -Path $outFile -Value “$j=$($InputObject[$i][$j])”
Now, put all these parts put together into a function and we have Out-IniFile. The complete Out-IniFile function is shown here.
function Out-IniFile($InputObject, $FilePath)
{
$outFile = New-Item -ItemType file -Path $Filepath
foreach ($i in $InputObject.keys)
{
if (!($($InputObject[$i].GetType().Name) -eq “Hashtable”))
{
#No Sections
Add-Content -Path $outFile -Value “$i=$($InputObject[$i])”
} else {
#Sections
Add-Content -Path $outFile -Value “[$i]”
Foreach ($j in ($InputObject[$i].keys | Sort-Object))
{
if ($j -match “^Comment[\d]+”) {
Add-Content -Path $outFile -Value “$($InputObject[$i][$j])”
} else {
Add-Content -Path $outFile -Value “$j=$($InputObject[$i][$j])”
}
}
Add-Content -Path $outFile -Value “”
}
}
}
This function is also available as an advanced function that has some extra parameters and checks. I have uploaded it to the Scripting Guys Script Repository. Just a reminder that you can load these functions in your profile so that you can always use them.
Oliver, this was a great guest post. And we now have two very nice, useful functions. Thank you for sharing your time and your expertise with us.
I invite you to follow me on Twitter and Facebook. If you have any questions, send email to me at scripter@microsoft.com, or post your questions on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.
Ed Wilson, Microsoft Scripting Guy
Why not using the official INI-handler from the OS? like this?:
cls
$ErrorActionPreference = "continue"
# create a demo-ini-file:
$filename = "$env:tmp\test.ini"
$file = [System.IO.StreamWriter] $filename
$file.WriteLine("[category_1]")
$file.WriteLine("value_a = 123")
$file.WriteLine("[category_2]")
$file.WriteLine("value_b = abc ;some comment")
$file.WriteLine(";this is a comment-line at the end of the file.")
$file.close()
# initialize ini-reader:
$signature = @'
[DllImport("kernel32.dll")]
public static extern uint GetPrivateProfileString(
string lpAppName,
string lpKeyName,
string lpDefault,
...
This was useful after 8 years.. I removed Sort-Object and made the section level ($ini) an ordered dictionary, so that the items’ order will not change.