Powershell & XML - nested nodes & xsi:type= blah blah blah

2k Views Asked by At

I run a dedicated server for an alpha/early access game called Space Engineers, occasionally they patch and break things and i am trying to create a powershell script to trouble shoot problems that may occur. I am ok with powershell, never messed with XML.

The first step is disabling all objects in the game so that they cause less calculations on the server. like so ->

$power = Get-Content 'F:\DedicatedServer\DataDir\SE Survival 2\Saves\VPS RC 1\SANDBOX_0_0_0_.sbs' -raw
$power = $power -replace "<Enabled>true</Enabled>", "<Enabled>false</Enabled>"
$power | Out-File 'F:\DedicatedServer\DataDir\SE Survival 2\Saves\VPS RC 1\SANDBOX_0_0_0_.sbs' -Encoding ascii

The second and third steps will be to bring all medical rooms and power sources (not shown) online. This is where i get into trouble.

[xml]$myXML = Get-Content 'F:\DedicatedServer\DataDir\SE Survival 2\Saves\VPS RC 1\SANDBOX_0_0_0_.sbs'

$ns = New-Object System.Xml.XmlNamespaceManager($myXML.NameTable)
$ns.AddNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance")

$node = $myXML.SelectNodes('//SectorObjects/MyObjectBuilder_EntityBase/CubeBlocks/MyObjectBuilder_CubeBlock‌​[@xsi:type="MyObjectBuilder_MedicalRoom"]/Enabled', $ns) | % {    
    #set all med bays to be enabled.

    $switch= Select-XML -XML $myXML -XPath $node
    $switch.Node.InnerText = $switch.Node.InnerText.Replace("false", "true")

    $myXML.Save('F:\DedicatedServer\DataDir\SE Survival 2\Saves\VPS RC 1\SANDBOX_0_0_0_.sbs')
}

Here is an excerpt of the XML file with non relevant data removed -

<?xml version="1.0"?>
<MyObjectBuilder_Sector xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <Position>
  </Position>
  <SectorEvents> 
  </SectorEvents>
  <AppVersion>1044014</AppVersion>
  <SectorObjects>
    <MyObjectBuilder_EntityBase xsi:type="MyObjectBuilder_CubeGrid">
      <EntityId>72230476941025901</EntityId>
      <PersistentFlags>CastShadows InScene</PersistentFlags>
      <PositionAndOrientation>
      </PositionAndOrientation>
      <GridSizeEnum>Large</GridSizeEnum>
      <CubeBlocks>
        <MyObjectBuilder_CubeBlock xsi:type="MyObjectBuilder_CubeBlock">          
        </MyObjectBuilder_CubeBlock>
        <MyObjectBuilder_CubeBlock xsi:type="MyObjectBuilder_MedicalRoom">
          <SubtypeName>LargeMedicalRoom</SubtypeName>
          <EntityId>72107097601717796</EntityId>
          <Min x="4" y="1" z="-1" />
          <BlockOrientation Forward="Forward" Up="Up" />
          <ColorMaskHSV x="0" y="0.15" z="0.25" />
          <Owner>144233151425053409</Owner>
          <ShareMode>Faction</ShareMode>
          <CustomName>Lurch Enterprises</CustomName>
          <ShowOnHUD>false</ShowOnHUD>
          <Enabled>false</Enabled>
          <SteamUserId>0</SteamUserId>
        </MyObjectBuilder_CubeBlock>

the XML file is not updating with the desired changes. Which are change every instance of

<MyObjectBuilder_CubeBlock xsi:type="MyObjectBuilder_MedicalRoom">
      <Enabled>false</Enabled>

to true.

The current errors are:

Select-Xml : Cannot validate argument on parameter 'XPath'. The argument is null or empty. Provide an argument that is not null or empty, and then try the command again.
At line:16 char:44
+     $switch= Select-XML -XML $myXML -XPath $node
+                                            ~~~~~
    + CategoryInfo          : InvalidData: (:) [Select-Xml], ParameterBindingValidationException
    + FullyQualifiedErrorId : ParameterArgumentValidationError,Microsoft.PowerShell.Commands.SelectXmlCommand

You cannot call a method on a null-valued expression.
At line:17 char:5
+     $switch.Node.InnerText = $switch.Node.InnerText.Replace("false", "true")
+     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (:) [], RuntimeException
    + FullyQualifiedErrorId : InvokeMethodOnNull

and answered! here is the final product that does what i wanted.

$filePath = '<your save file path here>\SANDBOX_0_0_0_.sbs'

#troubleshooting script
#switch EVERYTHING off
$power = Get-Content $filePath -raw
$power = $power -replace "<Enabled>true</Enabled>", "<Enabled>false</Enabled>"
$power | Out-File $filePath -Encoding ascii

#turn on medbays, reactors, batteries, solar panels ONLY
[xml]$myXML = Get-Content $filePath
$ns = New-Object System.Xml.XmlNamespaceManager($myXML.NameTable)
$ns.AddNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance")
$nodes = $myXML.SelectNodes("//SectorObjects/MyObjectBuilder_EntityBase/CubeBlocks/MyObjectBuilder_CubeBlock[@xsi:type='MyObjectBuilder_MedicalRoom']/Enabled|//SectorObjects/MyObjectBuilder_EntityBase/CubeBlocks/MyObjectBuilder_CubeBlock[@xsi:type='MyObjectBuilder_Reactor']/Enabled|//SectorObjects/MyObjectBuilder_EntityBase/CubeBlocks/MyObjectBuilder_CubeBlock[@xsi:type='MyObjectBuilder_BatteryBlock']/Enabled|//SectorObjects/MyObjectBuilder_EntityBase/CubeBlocks/MyObjectBuilder_CubeBlock[@xsi:type='MyObjectBuilder_SolarPanel']/Enabled", $ns)
ForEach($node in $nodes)
{
$node.InnerText = "true"
}
$myXML.Save($filePath)
2

There are 2 best solutions below

13
On BEST ANSWER

To be able to use xsi prefix in your XPath you need to register prefix-to-namespace URI mapping to XmlNamespaceManager, then pass the XmlNamespaceManager to SelectNodes() method :

.....
$ns = New-Object System.Xml.XmlNamespaceManager($myXML.NameTable)
$ns.AddNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance")
$node = $myXML.SelectNodes("your xpath that contains xsi prefix here", $ns)
.....

UPDATE :

Turned out there was hidden character before [ in your XPath that's why you got "invalid token" error. I'm not familiar with PowerShell specific syntax, but this worked fine for me :

[xml]$myXML = Get-Content "F:\DedicatedServer\DataDir\SE Survival 2\Saves\VPS RC 1\SANDBOX_0_0_0_.sbs"

$ns = New-Object System.Xml.XmlNamespaceManager($myXML.NameTable)
$ns.AddNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance")
$nodes = $myXML.SelectNodes("//SectorObjects/MyObjectBuilder_EntityBase/CubeBlocks/MyObjectBuilder_CubeBlock[@xsi:type='MyObjectBuilder_MedicalRoom']/Enabled", $ns)
ForEach($node in $nodes)
{
    $node.InnerText = "true"
}
$myXML.Save("F:\DedicatedServer\DataDir\SE Survival 2\Saves\VPS RC 1\SANDBOX_0_0_0_.sbs")
0
On

If you are planning to do more XML modification you might find it more useful to use native XML technologies. So although it doesn't solve the problem like you tried to I'd like to offer you a solution using BaseX (full disclosure: I am a member of the project team). Personally, I find it much simpler instead of using scripting solutions. After downloading you can run BaseX using:

basex -u -i input.xml query.xq

This assumes your input is in the input.xml file. The -u flag states that updates to the file should be propagated back to the file. The query is stored in query.xq aand has the following content:

//SectorObjects/MyObjectBuilder_EntityBase/CubeBlocks/MyObjectBuilder_CubeBlock[@xsi:type="MyObjectBuilder_MedicalRoom"]/Enabled[. = "false"] ! (replace value of node . with 'true')

This simple states to replace each Enabled element with text equal to false to be replaced with true. It uses a simple XQuery Update expression. The ! notation is just a handy shortening, the following query does the same, but might be slightly more readable:

for $e in //SectorObjects/MyObjectBuilder_EntityBase/CubeBlocks/MyObjectBuilder_CubeBlock[@xsi:type="MyObjectBuilder_MedicalRoom"]/Enabled[. = "false"]
return replace value of node . with 'true'

To me, these two lines (one for executing the program, one for the XQuery) do look like much simpler.