Use PowerShell to Work with Any INI File


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

 

 

Comments (19)

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

  2. jrv says:

    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.

  3. mredwilson says:

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

  4. Anonymous says:

    Thought I would leave this comment as I hit the same problem as Thomas Schwerdtfeger

    The problem is in the Key section. It was a simple fix (once I remember enough of my reg expressions)
    Change this line
    "(.+?)s*=s*(.*)" # Key
    To
    "^(?!;)(.+?)s*=s*(.*)" # Key

    Basically I’m just adding a negative forward look assertion.

    Other than this great script; saved me a lot of time.

  5. jrv says:

    Excellent and very useful.  I never though of using a hash for an ini section.

  6. jrv says:

    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,"$pwdtest.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.

  7. Klaus Schulte says:

    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.

  8. 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.

  9. JP585 says:

    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.

  10. thomas schwerdtfeger says:

    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

  11. Thomas Schwerdtfeger says:

    sorry, I crossposted my little question to Oliver directly now

    Thomas

  12. Bert Nieuwenampsen says:

    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]
    }
    }
    }
    }
    }

  13. Enrico says:

    GREAT!!!! Thank you!!!

  14. Francesco says:

    Hi,

    My name Francesco and sorry for my English, I have a problem with function Get-IniContent.

    I have read a ini file with a special character ex:

    [Località]
    Bari =134
    Napoli =136

    result the Get-IniContent is
    Localit� {Bari, Napoli}

    does anyone know how to fix

    Ghanks

  15. Ryan Cichewicz says:

    This seems to work well with one exception, how can i Rename a key, using the example INI above:
    [386Enh]
    woafont=dosapp.fon
    EGA80WOA.FON=EGA80WOA.FON

    lets say I want woafont=dosapp.fon to now be renamed to SOMENEWKEY=dosapp.fon
    Or i want to delete that key entirely?

    If i can do these things it would also allow me to comment and un-comment lines

  16. kallesh says:

    Excellent :).. thanks a lot for the post
    A little bit changes to this code helped me a lot

    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
    if($section -eq [string]::IsNullOrEmpty)
    {
    $ini[$section][$name] = $value
    }
    }
    "(.+?)s*=(.*)" # Key
    {
    $name,$value = $matches[1..2]
    $ini[$section][$name] = $value
    }
    }
    return $ini
    }

  17. freeman says:

    Hi,
    i also have a problem with special chars in the ini file.
    My ini content:
    [SectionTest]
    Value=Tést

    The output is: Value=T�st

    How i can fix this?

    Thank you.

  18. freeman says:

    I have found a solution:

    function Get-IniContent ($filePath)
    {
    $ini = @{}
    switch -regex (Get-Content -Path $FilePath)
    {
    ….

Skip to main content