First, let's define a sample XML document named "books.xml" in our current directory:
<?xml version="1.0" encoding="UTF-8"?> <books> <book> <title>Of Mice And Men</title> <author>John Steinbeck</author> <pageCount>187</pageCount> <publishers> <publisher> <isbn>978-88-58702-15-4</isbn> <name>Pascal Covici</name> <year>1937</year> <binding>Hardcover</binding> <first>true</first> </publisher> <publisher> <isbn>978-05-82461-46-8</isbn> <name>Longman</name> <year>2009</year> <binding>Hardcover</binding> </publisher> </publishers> <characters> <character name="Lennie Small" /> <character name="Curley's Wife" /> <character name="George Milton" /> <character name="Curley" /> </characters> <film>True</film> </book> <book> <title>The Hunt for Red October</title> <author>Tom Clancy</author> <pageCount>387</pageCount> <publishers> <publisher> <isbn>978-08-70212-85-7</isbn> <name>Naval Institute Press</name> <year>1984</year> <binding>Hardcover</binding> <first>true</first> </publisher> <publisher> <isbn>978-04-25083-83-3</isbn> <name>Berkley</name> <year>1986</year> <binding>Paperback</binding> </publisher> <publisher> <isbn>978-08-08587-35-4</isbn> <name>Penguin Putnam</name> <year>2010</year> <binding>Paperback</binding> </publisher> </publishers> <characters> <character name="Marko Alexadrovich Ramius" /> <character name="Jack Ryan" /> <character name="Admiral Greer" /> <character name="Bart Mancuso" /> <character name="Vasily Borodin" /> </characters> <film>True</film> </book> </books>
What we want to do is add a few new books to this document, let's say Patriot Games by Tom Clancy (yes, I'm a fan of Clancy's works ^__^) and a Sci-Fi favourite: The Hitchhiker's Guide to the Galaxy by Douglas Adams mainly because Zaphod Beeblebrox is just fun to read.
Somehow we've acquired the data for the new books and saved them as a list of PSCustomObjects:
$newBooks = @( [PSCustomObject] @{ "Title" = "Patriot Games"; "Author" = "Tom Clancy"; "PageCount" = 540; "Publishers" = @( [PSCustomObject] @{ "ISBN" = "978-0-39-913241-4"; "Year" = "1987"; "First" = $True; "Name" = "Putnam"; "Binding" = "Hardcover"; } ); "Characters" = @( "Jack Ryan", "Prince of Wales", "Princess of Wales", "Robby Jackson", "Cathy Ryan", "Sean Patrick Miller" ); "film" = $True; }, [PSCustomObject] @{ "Title" = "The Hitchhiker's Guide to the Galaxy"; "Author" = "Douglas Adams"; "PageCount" = 216; "Publishers" = @( [PSCustomObject] @{ "ISBN" = "978-0-33-025864-7"; "Year" = "1979"; "First" = $True; "Name" = "Pan Books"; "Binding" = "Hardcover"; } ); "Characters" = @( "Arthur Dent", "Marvin", "Zaphod Beeblebrox", "Ford Prefect", "Trillian", "Slartibartfast", "Dirk Gently" ); "film" = $True; } );
Now we need to define a few skeleton XML structures for our new data to go into. Basically, you want to create a skeleton/template for each list of data. In our example, that means we need a template for the book, characters, and publishers. We can also use this to define a few default values, such as the value for the film
tag.
$t_book = [xml] @' <book> <title /> <author /> <pageCount /> <publishers /> <characters /> <film>False</film> </book> '@; $t_publisher = [xml] @' <publisher> <isbn/> <name/> <year/> <binding/> <first>false</first> </publisher> '@; $t_character = [xml] @' <character name="" /> '@;
We're done with set-up.
Now that we're all set-up with our sample data, let's add the custom objects to the XML Document Object.
# Read the xml document $xml = [xml] Get-Content .\books.xml; # Let's show a list of titles to see what we've got currently: $xml.books.book | Select Title, Author, @{N="ISBN";E={If ( $_.Publishers.Publisher.Count ) { $_.Publishers.publisher[0].ISBN} Else { $_.Publishers.publisher.isbn}}};; # Outputs: # title author ISBN # ----- ------ ---- # Of Mice And Men John Steinbeck 978-88-58702-15-4 # The Hunt for Red October Tom Clancy 978-08-70212-85-7 # Let's show our new books as well: $newBooks | Select Title, Author, @{N="ISBN";E={$_.Publishers[0].ISBN}}; # Outputs: # Title Author ISBN # ----- ------ ---- # Patriot Games Tom Clancy 978-0-39-913241-4 # The Hitchhiker's Guide to the Galaxy Douglas Adams 978-0-33-025864-7 # Now to merge the two: ForEach ( $book in $newBooks ) { $root = $xml.SelectSingleNode("/books"); # Add the template for a book as a new node to the root element [void]$root.AppendChild($xml.ImportNode($t_book.book, $true)); # Select the new child element $newElement = $root.SelectSingleNode("book[last()]"); # Update the parameters of that new element to match our current new book data $newElement.title = [String]$book.Title; $newElement.author = [String]$book.Author; $newElement.pageCount = [String]$book.PageCount; $newElement.film = [String]$book.Film; # Iterate through the properties that are Children of our new Element: ForEach ( $publisher in $book.Publishers ) { # Create the new child publisher element # Note the use of "SelectSingleNode" here, this allows the use of the "AppendChild" method as it returns # a XmlElement type object instead of the $Null data that is currently stored in that leaf of the # XML document tree [void]$newElement.SelectSingleNode("publishers").AppendChild($xml.ImportNode($t_publisher.publisher, $true)); # Update the attribute and text values of our new XML Element to match our new data $newPublisherElement = $newElement.SelectSingleNode("publishers/publisher[last()]"); $newPublisherElement.year = [String]$publisher.Year; $newPublisherElement.name = [String]$publisher.Name; $newPublisherElement.binding = [String]$publisher.Binding; $newPublisherElement.isbn = [String]$publisher.ISBN; If ( $publisher.first ) { $newPublisherElement.first = "True"; } } ForEach ( $character in $book.Characters ) { # Select the characters xml element $charactersElement = $newElement.SelectSingleNode("characters"); # Add a new character child element [void]$charactersElement.AppendChild($xml.ImportNode($t_character.character, $true)); # Select the new characters/character element $characterElement = $charactersElement.SelectSingleNode("character[last()]"); # Update the attribute and text values to match our new data $characterElement.name = [String]$character; } } # Check out the new XML: $xml.books.book | Select Title, Author, @{N="ISBN";E={If ( $_.Publishers.Publisher.Count ) { $_.Publishers.publisher[0].ISBN} Else { $_.Publishers.publisher.isbn}}}; # Outputs: # title author ISBN # ----- ------ ---- # Of Mice And Men John Steinbeck 978-88-58702-15-4 # The Hunt for Red October Tom Clancy 978-08-70212-85-7 # Patriot Games Tom Clancy 978-0-39-913241-4 # The Hitchhiker's Guide to the Galaxy Douglas Adams 978-0-33-025864-7
We can now write our XML to disk, or screen, or web, or wherever!
While this may not be the procedure for everyone I found it to help avoid a whole bunch of [void]$xml.SelectSingleNode("/complicated/xpath/goes[here]").AppendChild($xml.CreateElement("newElementName")
followed by $xml.SelectSingleNode("/complicated/xpath/goes/here/newElementName") = $textValue
I think the method detailed in the example is cleaner and easier to parse for normal humans.
It may be possible to change the template to include elements with children instead of breaking out each section as a separate template. You just have to take care to clone the previous element when you loop through the list.