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.

​
As I did last time, I'll need the path to the ini file and strip away comments and blank lines.
​
Next, I'll create a new XML document.
​
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.
​
This is going to add the line to the beginning of the document
​
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.
​
If a section head is detected, then I'll create a node for it and append it to the parent node.
​
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.
​
The value, will become a text value for the new node, which is then appended to the section.
​
At the end of the process, I will have an XML document with each section as a node.
060515 2012 ManagingINI3
I can navigate the XML document like any other object.
060515 2012 ManagingINI4

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.
​
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.
​
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.
​
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.
​
<#
.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:
Essential PowerShell Learning 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.

060515 2012 ManagingINI5

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.