2021 Annual Petri Reader Survey - We want to know what's important to you! 2021 Annual Petri Reader Survey - We want to know what's important to you!
PowerShell

Managing INI Files with PowerShell Part 2

In the previous article, Managing INI Files with PowerShell, I demonstrated how you might convert a legacy INI file into something more object-oriented for use in PowerShell. At the end of the article, I showed how you could persist the data to disk using Export-Clixml. The potential issue is that the resulting file can only be used within PowerShell. Here’s what my exported sample.ini looks like.

Our sample XML file. (Image Credit: Jeff Hicks)
Our sample XML file. (Image Credit: Jeff Hicks)

The node names are not very meaningful outside of a PowerShell context. I then thought, perhaps I could use the ConvertTo-XML cmdlet. But this fails with my custom object.

An error with the Convertto-XML cmdlet. (Image Credit: Jeff Hicks)
An error with the Convertto-XML cmdlet. (Image Credit: Jeff Hicks)

And further testing shows that the end result is no different. So if I want to take my INI object and export it to a traditional XML file, I will have to do it myself. Fortunately, it isn’t that difficult in PowerShell. I’m going to create a tool, that will take an INI file and create a traditional XML file just like you would use Export-CSV.

First, I’ll need a path for the finished file.

$exportPath = "C:\work\sampleini.xml"

As I did last time, I’ll need the path to the ini file and strip away comments and blank lines.

$path = ".\sample.ini"
$all = Get-Content -Path $path | Where {$_ -notmatch "^(\s+)?;|^\s*$"}

Next, I’ll create a new XML document.

$xml = New-Object System.Xml.XmlDocument

From here I could simply begin adding nodes. But I want to create a more complete document, so I’m going to take an extra step to create an XML declaration and add it to the document.

$declare = $xml.CreateXmlDeclaration("1.0","UTF-8","yes")
$xml.AppendChild($declare)

This is going to add the line to the beginning of the document

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>

Now I can begin going through each line of content. Each section heading will be separate node that will be a child of a Sections node.

$sectionNode = $xml.CreateNode("element","Sections","")
$xml.AppendChild($sectionNode) 

If a section head is detected, then I’ll create a node for it and append it to the parent node.

if ($line -match "^\[.*\]$") {
#get section name
$sectionName = $line -replace "\[|\]",""
#create XML node
$section = $xml.CreateNode("element",$sectionName,"")
#append node to document
$sectionNode.AppendChild($section)
}

When I get to setting lines, again, I can split each line. I’m going to create a node using the setting name, i.e. the text to the left of the equal sign.
elseif ($line -match "=") {
#parse data
$data = $line.split("=").trim()
#create child node
$setting = $xml.CreateNode("element",$data[0],"")

The value, will become a text value for the new node, which is then appended to the section.

$setting.InnerText = $data[1]
$section.AppendChild($setting)

At the end of the process, I will have an XML document with each section as a node.

I can navigate the XML document like any other object.

To save the XML document to a file, I will use the Save() method. I have found that this works best when I use an explicit filesystem path. Using relative paths doesn’t always save the file where I expect. So I came up with some commands to split apart the export path and resolve the parent component.

$ExportDir = (Split-Path -Path $ExportPath -Parent | Resolve-Path).Path

This will take a path like .\myini.xml and resolve the folder to C:\Scripts, assuming that is the current directory. All I’m really doing is rebuilding the path.

$ExportFile = Split-Path -Path $ExportPath –Leaf
$saveTo = Join-Path -path $ExportDir -ChildPath $ExportFile
$xml.Save($saveTo)

I could have combined several, if not all of these steps into a single expression, but it would be cumbersome and complicated to read. There’s no penalty in breaking the process down into separate steps.

The end result is a more traditional XML file, I could use anywhere.

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Sections>
  <General>
    <Action>Start</Action>
    <Directory>c:\work</Directory>
    <ID>123ABC</ID>
  </General>
  <Application>
    <Name>foo.exe</Name>
    <Version>1.0</Version>
  </Application>
  <User>
    <Name>Jeff</Name>
    <Company>Globomantics</Company>
  </User>
</Sections>

I suppose since I have a <Sections> node, each child node could be wrapped up in a <Section> node. Or I probably could have skipped insert <Sections> altogether. Or maybe I should have called it <Configuration> or <INI>. Obviously it is up to you. If you want something different, feel free to modify my function.
Function Export-IniToXML {
<#
.Synopsis
Export a traditional INI file to XML
.Description
This command will convert a traditional INI file to XML and save to a file. Blank lines and comments starting with ; will be ignored.

An ini file like this:
;This is a sample ini
[General]
Action = Start
Directory = c:\work
ID = 123ABC

 ;this is another comment
[Application]
Name = foo.exe
Version = 1.0

[User]
Name = Jeff
Company = Globomantics

Will be exported to an XML file like this:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Sections>
  <General>
    <Action>Start</Action>
    <Directory>c:\work</Directory>
    <ID>123ABC</ID>
  </General>
  <Application>
    <Name>foo.exe</Name>
    <Version>1.0</Version>
  </Application>
  <User>
    <Name>Jeff</Name>
    <Company>Globomantics</Company>
  </User>
</Sections>

IMPORTANT: Due to the nature of XML especially in regard to limitations in naming nodes, some ini settings might not be "exportable" to this format.
.Parameter Path
The filename and path to the INI file.
.Parameter ExportPath
The filename and path for the saved XML file.
.Example
PS C:\> export-initoxml c:\scripts\sample.ini c:\scripts\sample.xml

.Notes
Last Updated: June 5, 2015
Version     : 1.0

Learn more about PowerShell:
http://jdhitsolutions.com/blog/essential-powershell-resources/

  ****************************************************************
  * DO NOT USE IN A PRODUCTION ENVIRONMENT UNTIL YOU HAVE TESTED *
  * THOROUGHLY IN A LAB ENVIRONMENT. USE AT YOUR OWN RISK.  IF   *
  * YOU DO NOT UNDERSTAND WHAT THIS SCRIPT DOES OR HOW IT WORKS, *
  * DO NOT USE IT OUTSIDE OF A SECURE, TEST SETTING.             *
  ****************************************************************
#>


[cmdletbinding(SupportsShouldProcess)]
Param(
[Parameter(Position=0,Mandatory,HelpMessage="Enter the path to an INI file")]
[Alias("fullname","pspath")]
[ValidateScript({
if (Test-Path $_) {
   $True
}
else {
   Throw "Cannot validate path $_"
}
})]     
[string]$Path,

[Parameter(Position=1,Mandatory,HelpMessage = "Enter the filename and path for the XML file")]
[ValidateNotNullorEmpty()]
[ValidateScript({
$parent = Split-Path $_
if (Test-Path $parent) {
  $True
}
else {
  Throw "Cannot validate path $parent"
}
})]
[string]$ExportPath
)

Begin {
    Write-Verbose "Starting $($MyInvocation.Mycommand)"      
} #begin

Process {
    Write-Verbose "Getting content from $(Resolve-Path $path)"
    #strip out comments that start with ; and blank lines
    $all = Get-Content -Path $path | Where {$_ -notmatch "^(\s+)?;|^\s*$"}

    Write-Verbose "Creating XML document"
    $xml = New-Object System.Xml.XmlDocument

    #create an XML declaration section
    $declare = $xml.CreateXmlDeclaration("1.0","UTF-8","yes")

    $xml.AppendChild($declare) | Out-Null

    #create section node
    $sectionNode = $xml.CreateNode("element","Sections","")
    $xml.AppendChild($sectionNode) | Out-Null

    foreach ($line in $all) {

        Write-Verbose "Processing $line"

        if ($line -match "^\[.*\]$") {
            #get section name
            $sectionName = $line -replace "\[|\]",""
            #create XML node
            $section = $xml.CreateNode("element",$sectionName,"") 
            #append node to document
            $sectionNode.AppendChild($section) | Out-Null
        }
        elseif ($line -match "=") {
            #parse data
            $data = $line.split("=").trim()
            #create child node
            $setting = $xml.CreateNode("element",$data[0],"") 
            #set value as inner text
            $setting.InnerText = $data[1] 
            #append node
            $section.AppendChild($setting)  | Out-Null
        }
        else {
            #this should probably never happen
            Write-Warning "Unexpected line $line"
        }
    } #foreach

   #Save the file to a resolved path.
   $ExportDir = (Split-Path -Path $ExportPath -Parent | Resolve-Path).Path
   $ExportFile = Split-Path -Path $ExportPath -Leaf
   Write-verbose $ExportFile
   Write-Verbose $ExportDir
   $saveTo = Join-Path -path $ExportDir -ChildPath $ExportFile

   #code to support -WhatIf since the Save() method doesn't know how
   if ($PSCmdlet.ShouldProcess($Path,"Export as XML to $SaveTo")) {
    $xml.Save($saveTo)
    Write-Verbose "File saved to $SaveTo"
   } #WhatIf

} #process

End {
    Write-Verbose "Ending $($MyInvocation.Mycommand)"
} #end

} #end function

I even included support for –WhatIf in [cmdletbinding()], since I’m going to create a file. However, because I am invoking .NET methods, they don’t recognize –WhatIf, so I have to write my own code for handling the situation.
if ($PSCmdlet.ShouldProcess($Path,"Export as XML to $SaveTo")) {
$xml.Save($saveTo)
Write-Verbose "File saved to $SaveTo"
} #WhatIf

Now you have a few tools for converting or exporting INI files to something a bit more modern. And hopefully you picked up a PowerShell trick or two along the way. Got questions? Throw them in the comments.

Related Topics:

BECOME A PETRI MEMBER:

Don't have a login but want to join the conversation? Sign up for a Petri Account

Register
Comments (0)

Leave a Reply

Register for the Hybrid Identity Protection (HIP) Europe Conference!

Hybrid Identity Protection (HIP) Europe 2021 - Virtual Conference

Mobile workforces, cloud applications, and digitalization are changing every aspect of the modern enterprise. And with radical transformation come new business risks. Hybrid Identity Protection (HIP) is the premier educational forum for identity-centric practitioners. At the inaugural HIP Europe, join your local IAM experts and Microsoft MVPs to learn all the latest from the Hybrid Identity world.