Use PowerShell to Work with Any INI File

Use PowerShell to Work with Any INI File

  • Comments 13
  • Likes

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.

Image of output of hash tables

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

 

 

Your comment has been posted.   Close
Thank you, your comment requires moderation so it may take a while to appear.   Close
Leave a Comment
  • Excellent and very useful.  I never though of using a hash for an ini section.

  • @JRV -- I agree. I thought it was a really clever implentation. This is a cool idea -- I appreciate Oliver writing this.

  • Oliver, this is great, I'm going to use it. Congrats man! Paulo Marques [MSFT]

  • Hi Oliver,

    Superb!

    A really good use of the hash data structure: the ini sections look and feel like a hash.

    Nothing to add but one little thing: we could finish the switch statement with a "default" branch to report unrecognized ini lines ...

    Klaus.

  • Thanks for the suggestion Klaus.

    I will add it to the advanced function, but with a switch to ignore the "default" case.

    I kinda like the idea, that lines that don't match ini format are ignored.

  • Line that are incorrect are ignored by the API so your script follows the existing behavior.

    Bad lines allow us to comment or comment out lines with the semi-colon ad with other ilegal characters at teh beginning of a line.

    ; comment

    = comment

    rem comment

    Where rem=comment would be a legal pair.

    other characters that used to work on WIn98 are ], !, ), %, null, carriage return.

  • Great script. Small problem though. If you have comma delimited values in a key, it balks you with a 'Cannot index into a null array' error.

  • i've a little Problem - when reading a comment like

    ; gruppe=blabla

    Get-IniContent makes 2 lines from the last comment in section

    ; gruppe                       A45GS-mplus,A45GS-mplus-L

    Comment4                       ; gruppe=A45GS-mplus,A45GS-mplus-L

    I'm not that experienced to find the reason

    Thanks, Thomas

  • sorry, I crossposted my little question to Oliver directly now

    Thomas

  • Read strings from an INI file with no errors using custom 'type'

    $code=@'

       using System;

       using System.Collections.Generic;

       using System.Text;

       using System.Runtime.InteropServices;

       public class ProfileAPI{

           [DllImport("kernel32.dll")]

           public static extern uint GetPrivateProfileString(

               string SectionName,

               string KeyName,

               string Default,

               StringBuilder ReturnedString,

               uint Size,

               string FileName);

    }

    '@

    add-type $code

    $sb=New-Object System.Text.StringBuilder(256)

    [profileapi]::GetPrivateProfileString('section2','test3','dummy',$sb,$sb.Capacity,"$pwd\test.ini")

    Write-Host ('Returned value is {0}.' -f $sb.ToString()) -ForegroundColor green

    Test.ini lookes like this:

    ########

    [section1]

    test1=Ini file value #1

    ; comments

    test2=value2

    ! what!

    [section2]

    test3=a value stored after some comment lines

    #######

    You can place all manner of odd items in the file and they will be ignored if not in "name=value " format.

    We can wrap the remainder of the API calls the same way.  This one is the most asked for.

  • Here is a better copy:

    gallery.technet.microsoft.com/.../Edit-old-fashioned-INI-f8fbc067

  • for a simple search use this:
    get-inivalue inifile sectionname key
    it wil return a value

    function get-inivalue ($filepath, $section, $key)
    {
    $ini = @{}
    $result = $false
    $sectionfound = $false
    switch -regex -file $filepath
    {
    "^\[(.+)\]" # section
    {
    if ($sectionfound)
    {
    break
    }
    else
    {
    if ($matches[1] -eq $section)
    {
    $sectionfound = $true
    }
    }
    }
    "(.+?)\s*=(.*)" # key
    {
    if ($sectionfound)
    {
    if ($matches[1] -eq $key)
    {
    return $matches[2]
    }
    }
    }
    }
    }

  • GREAT!!!! Thank you!!!