19

I'm parsing simple (no sections) INI file in PowerShell. Here code I've came up with, is there any way to simplify it?

convertfrom-stringdata -StringData ( `
  get-content .\deploy.ini `
  | foreach-object `
    -Begin { $total = "" }  `
    { $total += "`n" + $_.ToString() } `
    -End { $total } `
).Replace("\", "\\")
Artem Tikhomirov
  • 21,497
  • 10
  • 48
  • 68
  • 3
    There was a post about this on the *Hey, Scripting Guy!* blog: Oliver Lipkau, 2011-08-20, [*Use PowerShell to Work with Any INI File*](https://blogs.technet.microsoft.com/heyscriptingguy/2011/08/20/use-powershell-to-work-with-any-ini-file/). (Archived [here](https://archive.is/17MWZ).) And an updated version of the script is in the MS script gallery: [`Get-IniContent.ps1`](https://gallery.technet.microsoft.com/scriptcenter/ea40c1ef-c856-434b-b8fb-ebd7a76e8d91). (Archived [here](https://archive.is/dik0g).) – StackzOfZtuff May 10 '16 at 10:05
  • Good tip, @StackzOfZtuff. That code is now available from the PowerShell Gallery at https://www.powershellgallery.com/packages/PsIni as module `PSIni`, installable as `Install-Module PSIni` – mklement0 Mar 22 '19 at 11:22

8 Answers8

29

After searching internet on this topic I've found a handful of solutions. All of them are hand parsing of file data so I gave up trying to make standard cmdlets to do the job. There are fancy solutions as this which support writing scenario.

There are simpler ones and as far as I need no writing support I've chose following very elegant code snippet:

Function Parse-IniFile ($file) {
  $ini = @{}

  # Create a default section if none exist in the file. Like a java prop file.
  $section = "NO_SECTION"
  $ini[$section] = @{}

  switch -regex -file $file {
    "^\[(.+)\]$" {
      $section = $matches[1].Trim()
      $ini[$section] = @{}
    }
    "^\s*([^#].+?)\s*=\s*(.*)" {
      $name,$value = $matches[1..2]
      # skip comments that start with semicolon:
      if (!($name.StartsWith(";"))) {
        $ini[$section][$name] = $value.Trim()
      }
    }
  }
  $ini
}

This one is Jacques Barathon's.

Update Thanks to Aasmund Eldhuset and @msorens for enhancements: whitespace trimming and comment support.

Update 2 Skip any name=value pairs where name starts with a semicolon ; which are comment lines. Replaced $ini [$section] = @{} with $ini[$section] = @{}.

jpmc26
  • 28,463
  • 14
  • 94
  • 146
Artem Tikhomirov
  • 21,497
  • 10
  • 48
  • 68
  • 1
    +1; works like a charm! However, I'd suggest changing the key-value regex to `(.+?)=(.+)` (non-greedy `+`) in order to handle values that contain `=`, such as SQL Server connection strings (which, incidentally, is what my INI file contained :-) ). Also, the right hand side should perhaps be `(.*)` instead of `(.+)` in order to accomodate empty values. – Aasmund Eldhuset May 16 '11 at 12:02
  • Thanks to @Aasmund-Eldhuset for the enhancement to allow embedded equal signs. I found one more enhacement needed: the script as is parses commented out lines as if they were active lines! The fix is to change the same regex to `^\s*([^#].+?)=(.*)` so that commented lines are ignored. – Michael Sorens May 18 '11 at 15:53
  • Make that _two_ enhancements: it also needs to ignore extra white space around both key and value. The regex becomes `^\s*([^#].+?)\s*=\s*(.*)` and the assignment becomes `$ini[$section][$name] = $value.trim()` – Michael Sorens May 18 '11 at 16:25
  • 1
    great, but there is a space between $ini and [$section] in line 6 that will cause an error – mishkin Oct 02 '12 at 18:25
  • @mishkin just fixed that. – Jeroen Wiert Pluimers Jul 10 '14 at 14:28
  • This wasn't working for single character names - didn't match. I changed regex for name/value to be "^\s*(.+?)\s*=\s*(.*)" which appears to work (I have not done extensive testing) – Straff Jun 30 '16 at 11:03
  • while using this function, if I try to print values on $ini object, for example : $ini["server"]["ip"] it is giving me error "Cannot index into a null array". Any pointers for it ? – Rahul Jun 29 '18 at 09:38
  • You should check whether `$ini[$section][$name]` already exists. Ini files sometimes contain arrays by repeating the same key multiple times; keys are not unique. The current code will overwrite and only contain each key once. If it does, and is of type [string],you could store in a `$tmp` variable, then convert it to a `$ini[$section][$name] = [System.Collections.ArrayList]@()`. Call `add($tmp)`, then `add($value)`. If it does, and is not of type [string], call `$ini[$section][$name].add($value)` on it. Then can be accessed using `$ini[$section][$name][3]` – aphid Apr 13 '22 at 10:22
13

Don Jones almost had it right. Try:

ConvertFrom-StringData((Get-Content .\deploy.ini) -join "`n")

-join converts the Object[] into a single string, with each item in the array separated by a newline char. ConvertFrom-StringData then parses the string into key/value pairs.

Steve Beckert
  • 139
  • 1
  • 3
5

This is really an extension to the current answer (couldn't seem to append a comment).

I messed around with this to do rudimentary handling of integers and decimals...

function Parse-IniFile ($file)
{
  $ini = @{}
  switch -regex -file $file
  {
    #Section.
    "^\[(.+)\]$"
    {
      $section = $matches[1].Trim()
      $ini[$section] = @{}
      continue
    }
    #Int.
    "^\s*([^#].+?)\s*=\s*(\d+)\s*$"
    {
      $name,$value = $matches[1..2]
      $ini[$section][$name] = [int]$value
      continue
    }
    #Decimal.
    "^\s*([^#].+?)\s*=\s*(\d+\.\d+)\s*$"
    {
      $name,$value = $matches[1..2]
      $ini[$section][$name] = [decimal]$value
      continue
    }
    #Everything else.
    "^\s*([^#].+?)\s*=\s*(.*)"
    {
      $name,$value = $matches[1..2]
      $ini[$section][$name] = $value.Trim()
    }
  }
  $ini
}
WaffleSouffle
  • 3,293
  • 2
  • 28
  • 27
3

I optimized this solution for my needs adding some things to the function and a new function for writing back the ini file:

  1. I made an ordered dictionary out of the original dictionary (hash table) to be able to preserve the file structure
  2. And to make it possible to preserve the comments and blank lines, I put them in a special key. They can be then ignored, when using data or thrown away when writing to a file as demonstrated below in the function Set-IniFile
  3. In Set-IniFile using the options -PrintNoSection and -PreserveNonData, it can be controlled if NO_SECTION should be used and if the non-data lines (not matching key=value or [section] should be preserved.

Function Get-IniFile ($file)       # Based on "https://stackoverflow.com/a/422529"
 {
    $ini = [ordered]@{}

    # Create a default section if none exist in the file. Like a java prop file.
    $section = "NO_SECTION"
    $ini[$section] = [ordered]@{}

    switch -regex -file $file 
    {    
        "^\[(.+)\]$" 
        {
            $section = $matches[1].Trim()
            $ini[$section] = [ordered]@{}
        }

        "^\s*(.+?)\s*=\s*(.*)" 
        {
            $name,$value = $matches[1..2]
            $ini[$section][$name] = $value.Trim()
        }

        default
        {
            $ini[$section]["<$("{0:d4}" -f $CommentCount++)>"] = $_
        }
    }

    $ini
}

Function Set-IniFile ($iniObject, $Path, $PrintNoSection=$false, $PreserveNonData=$true)
{                                  # Based on "http://www.out-web.net/?p=109"
    $Content = @()
    ForEach ($Category in $iniObject.Keys)
    {
        if ( ($Category -notlike 'NO_SECTION') -or $PrintNoSection )
        {
            # Put a newline before category as seperator, only if there is none 
            $seperator = if ($Content[$Content.Count - 1] -eq "") {} else { "`n" }

            $Content += $seperator + "[$Category]";
        }

        ForEach ($Key in $iniObject.$Category.Keys)
        {           
            if ( $Key.StartsWith('<') )
            {
                if ($PreserveNonData)
                    {
                        $Content += $iniObject.$Category.$Key
                    }
            }
            else
            {
                $Content += "$Key = " + $iniObject.$Category.$Key
            }
        }
    }

    $Content | Set-Content $Path -Force
}


### EXAMPLE
##
## $iniObj = Get-IniFile 'c:\myfile.ini'
##
## $iniObj.existingCategory1.exisitingKey = 'value0'
## $iniObj['newCategory'] = @{
##   'newKey1' = 'value1';
##   'newKey2' = 'value2'
##   }
## $iniObj.existingCategory1.insert(0, 'keyAtFirstPlace', 'value3')
## $iniObj.remove('existingCategory2')
##
## Set-IniFile $iniObj 'c:\myNewfile.ini' -PreserveNonData $false
##
Community
  • 1
  • 1
Mehrdad Mirreza
  • 984
  • 12
  • 20
2

One possibility is to use a .NET ini library. Nini for example.

I've translated the Simple Example from the Nini docs into PowerShell below. You need to put nini.dll into the same directory as the script.

$scriptDir = Split-Path -parent $MyInvocation.MyCommand.Definition
Add-Type -path $scriptDir\nini.dll

$source = New-Object Nini.Config.IniConfigSource("e:\scratch\MyApp.ini")

$fileName = $source.Configs["Logging"].Get("File Name")
$columns = $source.Configs["Logging"].GetInt("MessageColumns")
$fileSize = $source.Configs["Logging"].GetLong("MaxFileSize")
dan-gph
  • 16,301
  • 12
  • 61
  • 79
  • For what it's worth, I'm using [INI File Parser](https://github.com/rickyah/ini-parser) these days. It works well. – dan-gph May 17 '16 at 09:14
1

I'm not exactly sure what your source data looks like, or what your goal is. What exactly are you parsing for? Can you post a sample of the file? As-is, it looks like you're just concatenating carriage returns to the existing lines of the file and replacing \ with \.

Nor certain why you're using $_.ToString() since $_ is already a string object output by Get-Content.

Is the goal just to take a file containing a bunch of name=value pairs, and convert that to a hashtable? That's what ConvertFrom-StringData does, although that cmdlet is only available in the preview of PowerShell v2.

If your file looks like...

key1=value1
key2=value2
key3=value3

Then all you should need is

ConvertFrom-StringData (Get-Content .\deploy.ini)

I'm not sure I understand why you're tacking on extra carriage returns. There's also no need to use the -Begin and -End parameters, at least not as far as I can see from what you've posted.

Mark Wragg
  • 22,105
  • 7
  • 39
  • 68
Don Jones
  • 9,367
  • 8
  • 39
  • 49
  • Your example does not work because of 'unified' get-content cmdlet output. It outputs System.Object[]: ConvertFrom-StringData : Cannot convert 'System.Object[]' to the type 'System.String' required by parameter 'StringData' That's why I iterate wrought it and reassemble to string. – Artem Tikhomirov Jan 07 '09 at 16:25
  • That's not something I could have determined without looking at your source data, unfortunately. As I tried to indicate, I was trying to help answer your question but I need more information to do so accurately. I thought I made that clear - sorry. – Don Jones Jan 07 '09 at 19:05
  • No need to give him -1 for him being helpful. And he is stating he does not know for sure what you want. – Lars Truijens Jan 07 '09 at 21:28
  • An "ini file without sections" could be anything. I have INI files which use key:value, others which use key=value, and others which use an entirely different format. And as I sit here testing it, Get-Content is returning String objects when I give it a text file. If you don't want help, don't ask. – Don Jones Jan 07 '09 at 22:47
0

Here my solution to parse INI file with Powershell :

config.ini:

[readme]
; This is a sample configuration file
; Comments start with ';', as in php.ini

[asset]
; Allows you to select assets based on custom fields
environnement_field = "Environnement"
environnement_values[] = "production"
decommissioned_field = "Deco"
decommissioned_values[] = "oui"
decommissioned_values[] = "yes"
decommissioned_values[] = "vrai"
decommissioned_values[] = "true"

[form]
second_section[one] = "1 associated"
second_section[two] = "2 associated"
second_section[] = "1 unassociated"
second_section[] = "2 unassociated"
second_section[] = 1
second_section[] = 1.3
second_section[] = 2.2

[second_section]
path = "/usr/local/bin"
URL = "http://www.example.com/~username"
second_section[one] = "1 associated"
second_section[two] = "2 associated"
second_section[three] = 3
second_section[four] = 4.4
second_section[] = "1 unassociated"
second_section[] = "2 unassociated"
second_section[] = 1
second_section[] = 2.2

[third_section]
phpversion[] = "5.0"
phpversion[] = "5.1"
phpversion[] = "5.2"
phpversion[] = "5.3"

my_script.ps1 :

$PSScriptRoot

function Parse-IniFile ($filePath)
{
    $ini = [ordered]@{}
    $count = @{}
    switch -regex -file $filePath
    {
        #Section.
        "^\[(.+)\]$"
        {
            $section = $matches[1].Trim()
            $ini[$section] = [ordered]@{}
            $count[$section] = @{}
            $CommentCount = 0
            continue
        }
        # Comment
        "^(;.*)$"
        {
            $value = $matches[1]
            $CommentCount = $CommentCount + 1
            $name = "Comment" + $CommentCount
            if ($section -eq $null) {
                $section = "header"
                $ini[$section] = [ordered]@{}
            }
            $ini[$section][$name] = $value
            continue
        }

        #Array Int.
        "^\s*([^#][\w\d_-]+?)\[]\s*=\s*(\d+)\s*$"
        {
            $name,$value = $matches[1..2]
            if (!$ini[$section][$name]) {
                $ini[$section][$name] = [ordered]@{}
            }
            if (!$count[$section][$name]) {
                $count[$section][$name] = 0
            }
            $ini[$section][$name].Add($count[$section][$name], [int]$value)
            $count[$section][$name] += 1
            continue
        }
        #Array Decimal
        "^\s*([^#][\w\d_-]+?)\[]\s*=\s*(\d+\.\d+)\s*$"
        {
            $name,$value = $matches[1..2]
            if (!$ini[$section][$name]) {
                $ini[$section][$name] = [ordered]@{}
            }
            if (!$count[$section][$name]) {
                $count[$section][$name] = 0
            }
            $ini[$section][$name].Add($count[$section][$name], [decimal]$value)
            $count[$section][$name] += 1
            continue
        }
        #Array Everything else
        "^\s*([^#][\w\d_-]+?)\[]\s*=\s*(.*)"
        {
            $name,$value = $matches[1..2]
            if (!$ini[$section][$name]) {
                $ini[$section][$name] = [ordered]@{}
            }
            if (!$count[$section][$name]) {
                $count[$section][$name] = 0
            }
            $ini[$section][$name].Add($count[$section][$name], $value.Trim())
            $count[$section][$name] += 1
            continue
        }

        #Array associated Int.
        "^\s*([^#][\w\d_-]+?)\[([\w\d_-]+?)]\s*=\s*(\d+)\s*$"
        {
            $name, $association, $value = $matches[1..3]
            if (!$ini[$section][$name]) {
                $ini[$section][$name] = [ordered]@{}
            }
            $ini[$section][$name].Add($association, [int]$value)
            continue
        }
        #Array associated Decimal
        "^\s*([^#][\w\d_-]+?)\[([\w\d_-]+?)]\s*=\s*(\d+\.\d+)\s*$"
        {
            $name, $association, $value = $matches[1..3]
            if (!$ini[$section][$name]) {
                $ini[$section][$name] = [ordered]@{}
            }
            $ini[$section][$name].Add($association, [decimal]$value)
            continue
        }
        #Array associated Everything else
        "^\s*([^#][\w\d_-]+?)\[([\w\d_-]+?)]\s*=\s*(.*)"
        {
            $name, $association, $value = $matches[1..3]
            if (!$ini[$section][$name]) {
                $ini[$section][$name] = [ordered]@{}
            }
            $ini[$section][$name].Add($association, $value.Trim())
            continue
        }

        #Int.
        "^\s*([^#][\w\d_-]+?)\s*=\s*(\d+)\s*$"
        {
            $name,$value = $matches[1..2]
            $ini[$section][$name] = [int]$value
            continue
        }
        #Decimal.
        "^\s*([^#][\w\d_-]+?)\s*=\s*(\d+\.\d+)\s*$"
        {
            $name,$value = $matches[1..2]
            $ini[$section][$name] = [decimal]$value
            continue
        }
        #Everything else.
        "^\s*([^#][\w\d_-]+?)\s*=\s*(.*)"
        {
            $name,$value = $matches[1..2]
            $ini[$section][$name] = $value.Trim()
            continue
        }
    }

    return $ini
}

function Set-IniFile ($ini, $filePath)
{
    $output = @()
    foreach($section in $ini.Keys)
    {
        # Put a newline before category as seperator, only if there is null 
        $seperator = if ($output[$output.Count - 1] -eq $null) { } else { "`n" }
        $output += $seperator + "[$section]";

        foreach($key in $ini.$section.Keys)
        {
            if ( $key.StartsWith('Comment') )
            {
                $output += $ini.$section.$key
            }
            elseif ($ini.$section.$key -is [System.Collections.Specialized.OrderedDictionary]) {
                foreach($subkey in $ini.$section.$key.Keys) {
                    if ($subkey -is [int]) {
                        $output += "$key[] = " + $ini.$section.$key.$subkey
                    } else {
                        $output += "$key[$subkey] = " + $ini.$section.$key.$subkey
                    }
                }
            }
            else
            {
                $output += "$key = " + $ini.$section.$key
            }
        }
    }

    $output | Set-Content $filePath -Force
}

$ini = Parse-IniFile -filePath ".\config.ini"
Set-IniFile -ini $ini -filePath ".\config_copy.ini"

Write-Host "=first_section"
$ini["first_section"]
Write-Host "=second_section"
$ini["second_section"]
Write-Host "=second_section.second_section"
$ini["second_section"]["second_section"]
Write-Host "=third_section"
$ini["third_section"]
Write-Host "=third_section.phpversion"
$ini["third_section"]["phpversion"]

The output :

PS C:\Users\itesant> .\my_script.ps1

Name                           Value                                                                                      
----                           -----                                                                                      
=readme
Comment1                       ; This is a sample configuration file                                                      
Comment2                       ; Comments start with ';', as in php.ini                                                   
=asset
Comment1                       ; Allows you to select assets based on custom fields                                       
environnement_field            "Environnement"                                                                            
environnement_values           {0}                                                                                        
decommissioned_field           "Deco"                                                                                     
decommissioned_values          {0, 1, 2, 3}                                                                               
=form
second_section                 {one, two, 0, 1...}                                                                        
=form.second_section
one                            "1 associated"                                                                             
two                            "2 associated"                                                                             
three                          3                                                                                          
four                           4,4                                                                                        
0                              "1 unassociated"                                                                           
1                              "2 unassociated"                                                                           
2                              1                                                                                          
3                              2,2                                                                                        
=second_section
path                           "/usr/local/bin"                                                                           
URL                            "http://www.example.com/~username"                                                         
second_section                 {one, two, three, four...}                                                                 
=second_section.second_section
one                            "1 associated"                                                                             
two                            "2 associated"                                                                             
three                          3                                                                                          
four                           4,4                                                                                        
0                              "1 unassociated"                                                                           
1                              "2 unassociated"                                                                           
2                              1                                                                                          
3                              2,2                                                                                        
=third_section
phpversion                     {0, 1, 2, 3}                                                                               
=third_section.phpversion
0                              "5.0"                                                                                      
1                              "5.1"                                                                                      
2                              "5.2"                                                                                      
3                              "5.3"    

You can see some regex here :

Section: https://regex101.com/r/maYLKE/3

Comment: https://regex101.com/r/IE5FJH/2

Array Integer: https://regex101.com/r/AuuOi3/2

Array Decimal: https://regex101.com/r/Erjjym/2

Array Everything else: https://regex101.com/r/guC1Yd/2

Array associated Integer: https://regex101.com/r/Us56SL/2

Array associated Decimal: https://regex101.com/r/MrCZ9n/2

Array associated Everything else: https://regex101.com/r/TbYcyf/4

0

nini looks like a library ... not sure for powershell

powershell crash

first step

[void][system.reflection.assembly]::loadfrom("nini.dll") (refer add-type now in ps2 )

after you can use it

$iniwr = new-object nini.config.iniconfigsource("...\ODBCINST.INI") 

$iniwr.Configs et boom 
Steve Czetty
  • 6,147
  • 9
  • 39
  • 48
raoul
  • 1