PowerShell: formatting data from XML into another shape

That is not at all what I meant, but I’m tired, so let’s just do it. The source xml from Microsoft here.

Take a look at that

XMLFirstClass01

With that, we can do something like this:

$xml = [xml] (Get-Content .\PurchaseOrder.xml)

$xml.Root.Customers.Customer

, which gives you this:

XMLFirstClass02

That could then be formatted into something like this:

$xml = [xml] (Get-Content $source\PurchaseOrder.xml)

$customers = $xml.Root.Customers.Customer

$customers | % { “`{ $($_.CustomerID), $($_.CompanyName), $($_.Contacttitle) `}”}

(note that XML done like this in PowerShell is case-insensitive)

XMLFirstClass03

 

 

 

Advertisements

PowerShell and XML: SpecFlow bindings

In work this week, I needed to programmatically edit an app.config to include SpecFlow binding references. I thought that I could just turn to my other posts on XML editing, and that would cover it. Wrong. Those posts did not include how to add an element, then sibling elements within that element, with those sibling elements taking an attribute. As pictures, I wanted to get from this

SpecFlowXml01

to this:

SpecFlowXml02

So going back to our starting picture..

SpecFlowXml01

, we can select the <specFlow> node in PowerShell, and find it has nothing (do note that XML object notation is case-sensitive):

$xml = [xml] (Get-Content “C:\temp\app.config”)
$xml.SelectNodes(‘//configuration/specflow’)

SpecFlowXml03

If you then execute this, then this is what you see in the PowerShell ISE…

SpecFlowXml04 SpecFlowXml05

And this is the output I wanted, which PowerShell has done. The PowerShell for this is not as clean as the dot notation you can use when elements, attributes, values already exist, but it ain’t too bad:

SpecFlowXml06

There is however a Yeah-But in this: if you run that PowerShell a second time, you get this:

SpecFlowXml07

Rather than make things too noisy, in my use-case I know that the StepAssemblies and its children are either wholly there and well-formed, or fully absent. So testing for the existence of the stepAssemblies node is enough to decide whether to update the XML (code):

SpecFlowXml09

This will always give us back this, assuming our starting point above:

SpecFlowXml06

PowerShell: XML editing continued

This shows that in editing XML in PowerShell, if you assume that there is a single instance of an element, and there is more, then you get a very misleading error.

Simple piece of XML:

PSXml01

A few lines to update the firstName value:

PSXml02

All fine:

PSXml03

Now add in another record to the original file:

PSXml04

Run the same command again:

PSXml02

This time…

PSXml05

Actually as the contrast is poor, this is the error:

The property ‘firstName’ cannot be found on this object. Verify that the property exists and can be set.
At line:3 char:1
+ $xml.students.student.firstName = “Harry”
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (:) [], RuntimeException
+ FullyQualifiedErrorId : PropertyAssignmentException

The claim that ‘firstName’ might not exist is clearly wrong, but it is our/my own fault for being sloppy.

If we are now more precise about the record we are looking for:

PSXml06

, we get this…

PSXml07

We can do the update in 1 line…

PSXml08

, and now the file is updated OK:

PSXml09

Pastebin here here

Adding new elements with attributes

Using the state just now of the file as our starting point…

PSXml09

, this will only create a new record if that id (103) does not already exist…

PSXml10

pastebin

, giving this on the next execution, and all executions after that (i.e. the Rebecca rows):

PSXml11

pastebin

PowerShell: XML editing

Given an app.config or web.config, I want to update the value of an attribute, without directly editing the config file. MSDN has an example we can use to test it out.

psxml01

Let’s set the debug and mode values to true and Forms respectively. This person has a good article all about it.

This is the saved content we can work with:

<?xml version="1.0"?>
<!--
    Note: As an alternative to hand editing this file you can use the
    Web Site Administration Tool to configure settings for your application. Use
    the Web site->Asp.Net Configuration option in Visual Studio.
    A full list of settings and comments can be found in
    machine.config.comments usually located in
    \Windows\Microsoft.Net\Framework\v2.x\Config
-->
<configuration xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0">
    <appSettings/>
    <connectionStrings/>
    <system.web>
        <!--
            Set compilation debug="true" to insert debugging
            symbols into the compiled page. Because this
            affects performance, set this value to true only
            during development.
        -->
        <compilation debug="false"/>
        <!--
            The <authentication> section enables configuration
            of the security authentication mode used by
            ASP.NET to identify an incoming user.
        -->
        <authentication mode="Windows"/>
        <!--
            The <customErrors> section enables configuration
            of what to do if/when an unhandled error occurs
            during the execution of a request. Specifically,
            it enables developers to configure html error pages
            to be displayed in place of a error stack trace.

        <customErrors mode="RemoteOnly" defaultRedirect="GenericErrorPage.htm">
            <error statusCode="403" redirect="NoAccess.htm"/>
            <error statusCode="404" redirect="FileNotFound.htm"/>
        </customErrors>
        -->
    </system.web>
</configuration>
Here is the PowerShell that does it all:
$webConfig = "C:\data\sandbox\ASP\web.config"
$xml = [xml] (get-content $webConfig)
$xml.configuration.'system.web'.compilation.debug = "true"
$xml.configuration.'system.web'.authentication.mode = "Forms"
$xml.Save($webConfig);

psxml02

That’s the PowerShell object way – you could also use XPath.

A more complex example involves more than 1 element instance (i.e. of the same name), giving the parser a challenge working out how to uniquely identify the required attribute to be updated:

webconfig01

In this second example,let’s update a specific attribute within sectionGroup . Note that there are 2 of these sectionGroups. Let us say we only want the sectionGroup where the value of the attribute [name] is [system.net]. And let’s say that within that, we only want the [section] where name=”webRequestModules”. And then having found that, we want to update the corresponding [type] attribute to “test this”.

Probably the most compelling thing I can show you is the ISE entry to achieve this. There are a lot of variables going on, but you have the advantage of Intellisense, as you will see when you try it for yourself:

webconfig02

… which achieves the desired result, without any manual editing of the config file:

webconfig03

The aim of updating the post for a more complex scenario was to show that a not-very-technical support person could understand how to do this kind of update. However that is perhaps not a great idea: better perhaps to prepare a library of known entries that a support person could execute from a batch or powershell command line. I had just played with something that reasoned a bit more at runtime, but to avoid too much error handling, if we take the above example, we could just have something hardcoded like: Set-XMLConfigFile(“AuthenticationModules”, “Just a test 2”) where Set-XMLConfigFile would call into an crude validator that checks for a white list set of values that the “system” knows about, and updates those.