Home > Back-end >  Using Powershell to add to XML file
Using Powershell to add to XML file

Time:10-23

First of all, my coding/scripting skills are 30 years old... So total newbie here. But being forced to deal with some XML. Everything I have done so far I have found on here, but its not quite 100% yet.

(The below is made up, of course, I hope there arent any typos...)

Here is the situation. I have multiple files:

  • library1.xml
  • library2.xml
  • etc

Each file has inventory of each library and looks like this:

library1.xml

<books>
  <book>
    <title>Tale of Two Cities</title>
    <author>Charles Dickens</author>
  </book>
  <book>
    <title>Lord of the Flies</title>
    <author>William Golding</author>
  </book>
</books>

library2.xml

<books>
  <book>
    <title>The Red Badge of Courage</title>
    <author>Stephen Crane</author>
  </book>
  <book>
    <title>The Grapes of Wrath</title>
    <author>John Steinbeck</author>
  </book>
</books>

I have already found and tweaked scripts that will merge these into 1 file and keep the XML structure correct.

But before I merge them, I want it to add which library it was found at. Library 1, 2, etc. I found a script that does that by parsing the filename.

$files = ls *.xml

foreach ($file in $files) {
    [xml]$contents = gc $file.fullname
    $xmlelement_file = $contents.CreateElement('library')
    $xmlelement_file.InnerText = $file.basename
    $contents.books.book.AppendChild($xmlelement_file)
    $contents.Save($file.fullname)
}

The output I am looking to get, after processing the library1.xml file, is:

<books>
  <book>
    <title>Tale of Two Cities</title>
    <author>Charles Dickens</author>
    <library>library1</library>
  </book>
  <book>
    <title>Lord of the Flies</title>
    <author>William Golding</author>
    <library>library1</library>
  </book>
</books>

And then something similar for the other files.

However, when I run this, the resulting files add library1 and library2 to the appropriate files, but ONLY to the last book in each file:

<books>
  <book>
    <title>Tale of Two Cities</title>
    <author>Charles Dickens</author>
  </book>
  <book>
    <title>Lord of the Flies</title>
    <author>William Golding</author>
    <library>library1</library>
  </book>
</books>

I am thinking that I need another loop to run through each book, but cant figure it out.

Any help would be appreciated!

(I would actually prefer to do all of this in Bash vs. Powershell, as I am acquiring the XML files via API calls on a Linux system in the first place. But the only hits I found via Google were in Powershell (which I had never used before today), so I went in that direction...)

CodePudding user response:

If you want to add nodes to the nodes in each file, containing the BaseName of the file itself, just do

$xml = [System.Xml.XmlDocument]::new()

Get-ChildItem -Path 'D:\Test' -Filter '*.xml' -File | ForEach-Object {
    # load the xml file. This way, you are ensured to get the file encoding correct
    $xml.Load($_.FullName)
    foreach ($book in $xml.books.book) {
        $libnode = $xml.CreateElement('library')
        $libnode.InnerText = $_.BaseName
        [void]$book.AppendChild($libnode)
    }
    $xml.Save($_.FullName)
}

Using your example files you will end up with

library1.xml

<books>
  <book>
    <title>Tale of Two Cities</title>
    <author>Charles Dickens</author>
    <library>library1</library>
  </book>
  <book>
    <title>Lord of the Flies</title>
    <author>William Golding</author>
    <library>library1</library>
  </book>
</books>

library2.xml

<books>
  <book>
    <title>The Red Badge of Courage</title>
    <author>Stephen Crane</author>
    <library>library2</library>
  </book>
  <book>
    <title>The Grapes of Wrath</title>
    <author>John Steinbeck</author>
    <library>library2</library>
  </book>
</books>

CodePudding user response:

This is a good question to ask, and there are many ways to accomplish it.

What you're currently doing is the "C#"ish way of doing things: Using the [XmlElement] class to create elements by hand. This will work but ends up being more code that intended under many circumstances.

Another we can approach this is by modifying the XML we find.

Get-ChildItem -Filter *.xml | # Get all XML files (you may want to filter this)
    Select-Xml -XPath .     | # Get the root node of each file
    ForEach-Object {
        # Store the node in xPathMatch
        $xPathMatch = $_
        # and get an escaped version of the filename.
        $fileName   = [Security.SecurityElement]::Escape("$(
            $xPathMatch.Path | Split-Path -Leaf)"
        )
        # walk over each book in .books
        foreach ($bookNode in $xPathMatch.Node.books.book) {
            # If there was already a library element,
            if ($bookNode.library) 
            {
                # update it's contents.
                $bookNode.library = "$fileName"
            } else 
            {
                # If there was not, create one.
                $bookNode.innerXml  = "<library>$($fileName)</library>"
            }                
        }
        # At this point all of the nodes have been changed,
        # so the last thing for us to do is .Save
        $xPathMatch.Node.Save($xPathMatch.Path)
    }
  • Related