Using PowerShell to Generate XML Documents

Using PowerShell to Generate XML Documents

  • Comments 8

<Note - the scripts and datafiles used in this blog entry are attached as a zip file> 

A number of scenarios require the creation of XML documents. This blog shows you how you can use PowerShell scripts to easily create XML Documents.

Here-Strings are one of my favorite features of PowerShell. A Here-String starts on a line with an @" and ends on a line with a "@ at the beginning. In between those lines, you can put anything you want. It can have carriage returns, double quotes, single quotes, backslashes – whatever you want and it will be included in the string. There are 2 flavors of Here-Strings, single quoted and double-quoted here strings. The difference is that a double quoted here string supports variable expansion and subexpression execution. More on that later.

Below is an example of a Here-String that is an xml document. The things to note are that this is in a PS1 file (the ramifications of that will come later) and that the Here-String preserves double quotes (used in the XML attributes).

PS> cat recipe1.ps1
@"
<?xml version="1.0" encoding="UTF-8" ?>
<?dsd href="recipes.dsd" mce_href="recipes.dsd"?>
<collection xmlns="http://recipes.org" xmlns:xsi="http://www.w3.org/2001/XMLS
chema-instance" xsi:schemaLocation="http://recipes.org recipes.xsd">
<description>Some recipes used in the XML tutorial.</description>
<recipe>
<title>Beef Parmesan with Garlic Angel Hair Pasta</title>
<ingredient name="beef cube steak" amount="1.5" unit="pound" />
<ingredient name="onion, sliced into thin rings" amount="1" />
<ingredient name="spaghetti sauce" amount="1" unit="jar" />
<ingredient name="shredded mozzarella cheese" amount="0.5" unit="cup" />
<ingredient name="angel hair pasta" amount="12" unit="ounce" />
<nutrition calories="1167" fat="23" carbohydrates="45" protein="32" />
</recipe>
</collection>
"@
PS>
PS> # Now we'll run it and notice that we get the XML document
PS> .\recipe1.ps1
<?xml version="1.0" encoding="UTF-8" ?>
<?dsd href="recipes.dsd" mce_href="recipes.dsd"?>
<collection xmlns="http://recipes.org" xmlns:xsi="http://www.w3.org/2001/XMLS
chema-instance" xsi:schemaLocation="http://recipes.org recipes.xsd">
<description>Some recipes used in the XML tutorial.</description>
<recipe>
<title>Beef Parmesan with Garlic Angel Hair Pasta</title>
<ingredient name="beef cube steak" amount="1.5" unit="pound" />
<ingredient name="onion, sliced into thin rings" amount="1" />
<ingredient name="spaghetti sauce" amount="1" unit="jar" />
<ingredient name="shredded mozzarella cheese" amount="0.5" unit="cup" />
<ingredient name="angel hair pasta" amount="12" unit="ounce" />
<nutrition calories="1167" fat="23" carbohydrates="45" protein="32" />
</recipe>
</collection>
PS>
PS> # Let's prove that it is well formed XML by casting it to XML and working with it
PS> $r=[xml](.\recipe1.ps1)
PS> $r

xml dsd collection
--- --- ----------
version="1.0" encoding=... href="recipes.dsd" mce_href="recipes.dsd" collection


PS> $r.Collection


xmlns : http://recipes.org
xsi : http://www.w3.org/2001/XMLSchema-instance
schemaLocation : http://recipes.org recipes.xsd
description : Some recipes used in the XML tutorial.
recipe : recipe



PS> $r.Collection.Recipe

title ingredient nutrition
----- ---------- ---------
Beef Parmesan with Garl... {beef cube steak, onion... nutrition


PS> $r.Collection.Recipe.Ingredient |ft -auto

name amount unit
---- ------ ----
beef cube steak 1.5 pound
onion, sliced into thin rings 1
green bell pepper, sliced in rings 1
Italian seasoned bread crumbs 1 cup
grated Parmesan cheese 0.5 cup
olive oil 2 tablespoon
spaghetti sauce 1 jar
shredded mozzarella cheese 0.5 cup
angel hair pasta 12 ounce
minced garlic 2 teaspoon

So far, so good but now let's party. Let's leverage the fact that double-quoted here-string support subexpression execution. What this means is that anything that has a $ in front of it is treated like a variable and anything of the form $() is treated as a subexpression and executed. In either case, the element is replaced with its results. NOTE: Variables expansion does not support properties if you want properties you need to put that in a subexpression (e.g. $x.y will not work [$x will be expanded and then ".y" will be added to the end] you need to use $($x.y) instead). So now let's make a copy of this file in recipe2.ps1 and make it personal by changing the description to:

<description>$($ENV:USERNAME)'s favorite recipes.</description>

Now when we access it:

PS> $x=[xml](.\recipe2.ps1)
PS> $x.collection


xmlns : http://recipes.org
xsi : http://www.w3.org/2001/XMLSchema-instance
schemaLocation : http://recipes.org recipes.xsd
description : jsnover's favorite recipes.
recipe : recipe

So far we've just talked about how you can take XML and "convey" it through PowerShell but now let's shift and talk about "generation". Imagine that the data you need comes from somewhere else and you need to get it into XML. This is where the subexpression expansion really helps you. Let's take our ingredients and put them into an CSV file and then modify our XML with a subexpression to import them:

PS> cat ingredients.csv
name,amount,unit
"beef cube steak",1.5,pound
"onion, sliced into thin rings",1,
"green bell pepper, sliced in rings",1,
"Italian seasoned bread crumbs",1,cup
"grated Parmesan cheese",0.5,cup
"olive oil",2,tablespoon
"spaghetti sauce",1,jar
"shredded mozzarella cheese",0.5,cup
"angel hair pasta",12,ounce
"minced garlic",2,teaspoon
PS>
PS> cat recipe3.ps1
@"
<?xml version="1.0" encoding="UTF-8" ?>
<?dsd href="recipes.dsd" mce_href="recipes.dsd"?>
<collection xmlns="http://recipes.org" xmlns:xsi="http://www.w3.org/2001/XMLS
chema-instance" xsi:schemaLocation="http://recipes.org recipes.xsd">
<description>Some recipes used in the XML tutorial.</description>
<recipe>
<title>Beef Parmesan with Garlic Angel Hair Pasta</title>
$(
Import-Csv (Read-Host -Prompt "FileName for Ingredients") | foreach {
'<ingredient name="{0}" amount="{1}" unit="{2}" />' -f $_.Name,
$_.Amount,
$_.Unit
}
)
<nutrition calories="1167" fat="23" carbohydrates="45" protein="32" />
</recipe>
</collection>
"@
PS>
PS>
PS> .\recipe3.ps1
FileName for Ingredients: ingredients.csv
<?xml version="1.0" encoding="UTF-8" ?>
<?dsd href="recipes.dsd" mce_href="recipes.dsd"?>
<collection xmlns="http://recipes.org" xmlns:xsi="http://www.w3.org/2001/XMLS
chema-instance" xsi:schemaLocation="http://recipes.org recipes.xsd">
<description>Some recipes used in the XML tutorial.</description>
<recipe>
<title>Beef Parmesan with Garlic Angel Hair Pasta</title>
<ingredient name="beef cube steak" amount="1.5" unit="pound" /> <ingredient nam
e="onion, sliced into thin rings" amount="1" unit="" /> <ingredient name="green
bell pepper, sliced in rings" amount="1" unit="" /> <ingredient name="Italian
seasoned bread crumbs" amount="1" unit="cup" /> <ingredient name="grated Parmes
an cheese" amount="0.5" unit="cup" /> <ingredient name="olive oil" amount="2" u
nit="tablespoon" /> <ingredient name="spaghetti sauce" amount="1" unit="jar" />
<ingredient name="shredded mozzarella cheese" amount="0.5" unit="cup" /> <ingr
edient name="angel hair pasta" amount="12" unit="ounce" /> <ingredient name="mi
nced garlic" amount="2" unit="teaspoon" />
<nutrition calories="1167" fat="23" carbohydrates="45" protein="32" />
</recipe>
</collection>
PS>
PS> $r=[xml](.\recipe2.ps1)
PS> $r.Collection.Recipe.Ingredient |ft -auto

name amount unit
---- ------ ----
beef cube steak 1.5 pound
onion, sliced into thin rings 1
spaghetti sauce 1 jar
shredded mozzarella cheese 0.5 cup
angel hair pasta 12 ounce

From there, it just gets better! Here is a little script which you can pipeline objects to and it will create an XML document.

function New-Xml
{
param($RootTag="ROOT",$ItemTag="ITEM", $ChildItems="*", $Attributes=$Null)

Begin {
$xml = "<$RootTag>`n"
}


Process {
$xml += " <$ItemTag"
if ($Attributes)
{
foreach ($attr in $_ | Get-Member -type *Property $attributes)
{ $name = $attr.Name
$xml += " $Name=`"$($_.$Name)`""
}
}
$xml += ">`n"
foreach ($child in $_ | Get-Member -Type *Property $childItems)
{
$Name = $child.Name
$xml += " <$Name>$($_.$Name)</$Name>`n"
}
$xml += " </$ItemTag>`n"
}

End {
$xml += "</$RootTag>`n"
$xml
}
}

And here it is in action:

PS> . .\new-xml.ps1
PS> gps a*

Handles NPM(K) PM(K) WS(K) VM(M) CPU(s) Id ProcessName
------- ------ ----- ----- ----- ------ -- -----------
270 6 1332 3964 33 0.08 2588 alg
34 2 436 1960 16 9.95 2800 ApntEx
107 4 1864 7276 37 151.95 220 Apoint


PS> gps a* |New-XML -RootTag PROCESSES -ItemTag PROCESS -Attribute=id,ProcessName -ChildItems WS,Handles
<PROCESSES>
<PROCESS ProcessName="alg">
<WS>4059136</WS>
<Handles>270</Handles>
</PROCESS>
<PROCESS ProcessName="ApntEx">
<WS>2007040</WS>
<Handles>34</Handles>
</PROCESS>
<PROCESS ProcessName="Apoint">
<WS>7450624</WS>
<Handles>107</Handles>
</PROCESS>
</PROCESSES>


PS> gps a* |New-XML -RootTag Root -ItemTag Item -ChildItems Name,ID,Handles
<Root>
<Item>
<Name>alg</Name>
<Id>2588</Id>
<Handles>270</Handles>
</Item>
<Item>
<Name>ApntEx</Name>
<Id>2800</Id>
<Handles>34</Handles>
</Item>
<Item>
<Name>Apoint</Name>
<Id>220</Id>
<Handles>107</Handles>
</Item>
</Root>

And of course, don't forget to leverage the PowerShell to pick the exact set of elements you want in your XML:

PS> gps |where {$_.handles -ge 1000}|sort handles |New-XML Big Process Handles Id,ProcessName
<Big>
<Process Id="3516" ProcessName="explorer">
<Handles>1058</Handles>
</Process>
<Process Id="1900" ProcessName="searchindexer">
<Handles>1596</Handles>
</Process>
<Process Id="1776" ProcessName="svchost">
<Handles>2836</Handles>
</Process>
</Big>

I think if you play with it a bit, you'll find that PowerShell is a very powerful way to easily create XML documents.

Enjoy!

Jeffrey Snover [MSFT]
Windows Management Partner Architect
Visit the Windows PowerShell Team blog at: http://blogs.msdn.com/PowerShell
Visit the Windows PowerShell ScriptCenter at: http://www.microsoft.com/technet/scriptcenter/hubs/msh.mspx

Attachment: Fun with XML.zip
Leave a Comment
  • Please add 5 and 2 and type the answer here:
  • Post
  • Very cool! And even though this has been the first time I've gone through a PowerShell tutorial the elegant nature of it all is starting to make a lot more sense.

  • One suggestion - you could make it more robust by adding and using the following one-line function in the obvious places (it works safely for both attribute and element values)

    function Convert-XmlString([string] text)

    {

    # xml-escape any characters which might be interpreted as XML markup - http://www.w3.org/TR/2006/REC-xml-20060816/#syntax

    return $text.replace("&", "&amp;").replace("'", "&apos;").replace('"', "&quot;").replace("<", "&lt;").replace(">", "&gt;")

    }

  • Great point - thanks!

    Jeffrey Snover [MSFT]

    Windows Management Partner Architect

    Visit the Windows PowerShell Team blog at:    http://blogs.msdn.com/PowerShell

    Visit the Windows PowerShell ScriptCenter at:  http://www.microsoft.com/technet/scriptcenter/hubs/msh.mspx

  • I think there's a small error in the first example you give for new-xml.ps1:

    gps a* |New-XML -RootTag PROCESSES -ItemTag PROCESS -Attribute=id,ProcessName -ChildItems WS,Handles

    should be

    gps a* |New-XML -RootTag PROCESSES -ItemTag PROCESS -Attribute id,ProcessName -ChildItems WS,Handles

    (remove the '=' after -Attribute).

    Strangely the script still runs, but it just ignores the 'id'.  Actually I guess it takes '=id' as the first element of an array of attribute names but of course there is no such attribute name.

  • I recently needed to generate XML from PowerShell and was disappointed to see the PowerShell blog use

  • A while back, Jeffrey posted an article on how to use string expansion and XML casts to build XML documents

  • Check out Dmitry Sotnikov's blog Out-vCard: Exporting Outlook Address Book . It lets you do things like:

  • Check out Dmitry Sotnikov&#39;s blog Out-vCard: Exporting Outlook Address Book . It lets you do things

Page 1 of 1 (8 items)