简体   繁体   中英

Powershell: Read / Edit named nodes

I would like to read and edit node content from a given XML file and write the changed content back.

Content of the given XML file:

<?xml version="1.0" encoding="UTF-8"?>
<SimBase.Document Type="ScenarioFile" version="4,5" id="Test">
  <Descr>AceXML Document</Descr>
  <Filename>Test.fxml</Filename>
  <Flight.Sections>
    ...
    <Section Name="DateTimeSeason">
      <Property Name="Season" Value="Summer" />
      <Property Name="Year" Value="2020" />
      <Property Name="Day" Value="224" />
      <Property Name="Hours" Value="12" />
      <Property Name="Minutes" Value="2" />
      <Property Name="Seconds" Value="17" />
    </Section>
    ...
  </Flight.Sections>
</SimBase.Document>

Reading and writing back a new copy of the XML file works fine but I am at total loss how the read / edit the content of the node named 'DateTimeSeason' resp. its nodes since I do not know XPath.

Content of the script that I have written:

Write-Host "Edit XML file"

# Load local functions
. .\LocalFunctions.ps1

# Working environment
$flightFileDir     = 'E:\Temp\PowerShell\P3D\'
$flightFileNameIn  = 'MauleLSZH.fxml'
$flightFileNameOut = 'MauleLSZHNew.fxml'
$flightFileIn      =  $flightFileDir + $flightFileNameIn
$flightFileOut     =  $flightFileDir + $flightFileNameOut

# Get correct date and time for flight file
$dateTimeArr = SetupDateTime
#$dateTimeArr           # output content of resulting array

# Set up new XML object and read file content
$xmlFlight  = New-Object System.XML.XMLDocument
$xmlFlight.Load($flightFileIn)

# Edit content of node named 'DateTimeSeason'
$data = $xmlFlight.'SimBase.Document'.'Flight.Sections'.Section[@name -eq 'DateTimeSeason'].Property[@name = 'Year']
$data                   # output for test purposes

# Output modified flight file
$xmlFlight.Save($flightFileOut)

Write-Host "New XML file created"

Thanks a lot for any help or hints Hannes

You were close; here's a corrected version that adds a new "Property" and edits the "Value" of an existing one.

# Set up new XML object and read file content
$xmlFlight  = New-Object System.XML.XMLDocument
$xmlFlight.Load($flightFileIn)

# Create new "Property" and append to the "DateTimeSeason" node
[System.Xml.XmlElement]$newElement = $xmlFlight.CreateElement('Property')
$newElement.SetAttribute('Name','NewName')
$newElement.SetAttribute('Value','NewValue')
$data = $xmlFlight.'SimBase.Document'.'Flight.Sections'.SelectSingleNode("//Section[@Name='DateTimeSeason']")
[void]$data.AppendChild($newElement)

# Edit "Year" Property on the "DateTimeSeason" node
$xmlFlight.'SimBase.Document'.'Flight.Sections'.SelectSingleNode("//Section[@Name='DateTimeSeason']").SelectSingleNode("//Property[@Name = 'Year']").Value = '2021'
# Could also use: $data.SelectSingleNode("//Property[@Name = 'Year']").Value = '2021'

$data.Property # output for test purposes

# Output modified flight file
$xmlFlight.Save($flightFileOut)

Write-Host "New XML file created"

Note: XPaths are case-sensitive and "@name" would not have worked here; "@Name" must be used.

To offer a more concise, PowerShell-idiomatic solution , using the Select-Xml cmdlet:

# XPath query to locate the element of interest.
$xpathQuery = '/SimBase.Document/Flight.Sections/Section[@Name="DateTimeSeason"]/Property[@Name="Year"]'

# Get the element of interest (<Property Name="Year" Value="2020" />)...
$elem = (Select-Xml $xpathQuery $flightFileIn).Node

# ... update its 'Value' attribute ...
$elem.Value = 2021

# ... and save the modified document to the output file.
$elem.OwnerDocument.Save($flightFileOut)
  • Using Select-Xml allows you to extract information directly from an XML file using an Xpath query, without having to manually parse the file contents into an XML DOM via the [xml] type ( System.Xml.XmlDocument ) first.

  • Select-Xml returns Microsoft.PowerShell.Commands.SelectXmlInfo instances that wrap the XML node(s) (System.Xml.XmlNode ) returned by the XPath query, which can be accessed via the .Node property.

    • As an aside: It would be convenient to have an option to make Select-Xml return the XML node(s) directly , which doesn't exist as of PowerShell 7.1 but is the subject of GitHub suggestion #13669 .
  • To gain access to the enclosing XML document ( [xml] ) instance, use the .OwnerDocument property of the nodes, which is what allows you to call the .Save() method on the latter.

    • Note: Be sure to specify a full output path, because .NET's working directory usually differs from PowerShell's.

As for what you tried :

$data = $xmlFlight.'SimBase.Document'.'Flight.Sections'.Section[@name -eq 'DateTimeSeason'].Property[@name = 'Year']

You mistakenly mixed PowerShell's property-based access (eg .'SimBase.Document'.'Flight.Sections' ) with XPath syntax (eg, [@name -eq 'DateTimeSeason'] ).

While PowerShell's property-based adaptation of the XML DOM is very convenient, it has no built-in query capabilities, so you need to use PowerShell's usual filtering functionality, notably the .Where() array method (using the Where-Object cmdlet is also an option, but is slower):

# Returns element <Property Name="Year" Value="2020" />
$data = $xmlFlight.'SimBase.Document'.'Flight.Sections'.Section.
  Where({ $_.Name -eq 'DateTimeSeason' }).Property.
  Where({ $_.Name -eq 'Year' })

# Sample modification of the 'Value' attribute.
$data.Value = '2021'

Alternatively, you can mix property-based access and XPath queries, but only by passing the XPath queries to calls to the .SelectSingleNode() or SelectNodes() method, as shown in Brian Reynolds' answer .

Note : As Brian points out, XPath and XML in general are case- sensitive - unlike PowerShell's property-based access.

However, for conceptual clarity I recommend using either property-based access with PowerShell filtering or a .SelectSingleNode() / SelectNodes() method on the document (root) :

$data = $xmlFlight.SelectSingleNode('/SimBase.Document/Flight.Sections/Section[@Name="DateTimeSeason"]/Property[@Name="Year"]')

Or, if you're willing to assume that <Property> elements occur only as children of <Section> elements, for instance, you can take a shortcut via // :

$data = $xmlFlight.SelectSingleNode("//Section/Property[@Name = 'Year']")

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM