1

Powershell newbie here. I need to write a script that will read the basename of multiple XML files and then use that value as data to populate a new element which will also be created by the running of the script. So each XML will be updated with one new element which contains the filename of that XML.

I don't seem to be grasping the proper syntax for looping through the files. The best results I've managed will write the element tags and the data within, but it writes everything to every file. So if I have three XML files in the target directory, each file will be updated with three instances of the new element (one element containing its own filename, and the other two containing the filenames of the other two XML files).

I have tried a few different approaches; treating the elements as strings seems to be the easiest for a beginner like myself, but I've tried inserting the fields as XML elements as well (to no avail). Here is the relevant snippet of the XML, before being updated (each of these elements is a child of the root element of the XML, with no descendants of their own):

<CustomerNumber>1234</CustomerNumber>
<OrderDate>2018-04-19</OrderDate>

After processing, the updated file will be as follows:

<CustomerNumber>1234</CustomerNumber>
<OrderNumber>A5678</OrderNumber>
<OrderDate>2018-04-19</OrderDate>

So I'll be inserting the basename of the file ("A5678" from "A5678.xml") as the order number. I've run through many iterations of the code, but here is what I most recently attempted:

$AllFiles = Get-ChildItem "C:\Powershell\Projects\Orders\*.xml";
$FileNames = New-Object System.Collections.ArrayList;
$OrderNum = @'
<OrderNumber>$Name</OrderNumber>`r`n<OrderDate>
'@
ForEach($File in $AllFiles) 
{
    $FileNames.Add($File.Name.SubString(0,5));  
}
$UniqueNames = $FileNames | get-unique;
ForEach($Name in $UniqueNames)
{
    Get-ChildItem $AllFiles | ForEach {
    (Get-Content $_ | ForEach {$_ -replace '<OrderDate>', $ExecutionContext.InvokeCommand.ExpandString($OrderNum)}) | Set-Content $_
}}

As you can see, this attempt treats the elements as strings. I am using the opening tag of the immediately-following element as a point of reference for the replacement, because that is a field that will always be present. The preceding element, although required, may not always immediately precede. (An optional element, CustomerName, will sometimes precede the OrderDate.)

I'm not too sure about the use of "$ExecutionContext.InvokeCommand.ExpandString" - I just learned it from another user here, and though it worked in my last script, I don't know that this is also the proper application (I thought I had grasped the underlying concept, but that appears to not be the case). I readily confess to only the most superficial understanding of these concepts, and though I'm now constantly researching and learning about Powershell, I've only just scratched the surface.

Thanks for any assistance you can provide.

M Jenks
  • 85
  • 4

1 Answers1

0

I'm going to make a bunch of assumptions when answering this...

  1. All the document share the same schema
  2. Elements aren't schema qualified
  3. There is a root element in each document

Here is an example that should help you craft a script.

In a directory called "pussy_monster", I've created two files: A123.xml and A456.xml. Each file has the following content:

<thing>
    <CustomerNumber>1234</CustomerNumber>
    <OrderDate>2018-04-19</OrderDate>
</thing>

I fire up a Posh session. I navigate to "pussy_monster", and I run the following:

$files = ls *.xml

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

File name added.

enter image description here

Lemme run through the XML piece of that script. I loop through the XML files in the folder. For each document, I parse it:

    [xml]$contents = gc $file.fullname

I create the new 'OrderNumber' element:

    $xmlelement_file = $contents.CreateElement('OrderNumber')

I add the file name to that new element.

    $xmlelement_file.InnerText = $file.basename

I add the element to the root level element.

    $contents.thing.AppendChild($xmlelement_file)

I save the document. In this case, I overwrite it. If you have permissions assigned directly to that file, they-be-gone.

    $contents.Save($file.fullname)

That's the secret sauce. Good luck!

ADDING TO ORIGINAL ANSWER

Requester asks how to insert an XML element before another element in the same context. You would use the InsertBefore() method:

[xml]$doc = gc .\A123.xml
$new = $doc.CreateElement('test')
$old = $doc.thing.SelectSingleNode('CustomerNumber')
$doc.thing.InsertBefore($new, $old)
$doc.Save("$(pwd)\A123.xml")

This creates a new element "test", and it inserts it before the "CustomerNumber" element.

enter image description here

Adam
  • 3,891
  • 3
  • 19
  • 42
  • Thanks for your time and trouble! The documents do share the same schema, yes, but no, the elements aren't unqualified. Yes, there is a root element. Your code works to append the new element, but I need to insert if before "OrderDate", and I can't seem to make that work? I've tried using InsertBefore, but it just won't put it in the correct place in the file. I'm using the correct XPath, so I don't know what the deal is. That's why I tried to get the data into the file as a string. – M Jenks Apr 19 '18 at 19:40
  • More reading is leading me to believe this is due to the namespace? Needs to be defined in the script, yes? – M Jenks Apr 19 '18 at 19:49
  • You'd use the ```InsertBefore```method: [xml]$doc = gc .\A123.xml; $new = $doc.CreateElement('test'); $old = $doc.thing.SelectSingleNode('CustomerNumber'); $doc.thing.InsertBefore($new, $old); $doc.Save("$(pwd)\pus.xml"); – Adam Apr 19 '18 at 20:06
  • I've been trying InsertBefore, but it still just appends the element to the end of the file instead of placing it before the OrderDate. I'll work on it more tomorrow. Thanks again! – M Jenks Apr 19 '18 at 20:19
  • Updated answer to include snippet on adding an element before another element. – Adam Apr 19 '18 at 20:35