1

I cannot select the FirstLogonCommands node. Why does $firstLogonCommands always come back as null and how can the FirstLogonCommands section be properly selected?

# Load the XML content
$xml = @"
<unattend xmlns="urn:schemas-microsoft-com:unattend" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">
  <settings pass="offlineServicing">
  </settings>
  <settings pass="windowsPE">
    <component name="Microsoft-Windows-International-Core-WinPE" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
      <SetupUILanguage>
        <UILanguage>en-US</UILanguage>
      </SetupUILanguage>
      <InputLocale>0809:00000809</InputLocale>
      <SystemLocale>en-GB</SystemLocale>
      <UILanguage>en-US</UILanguage>
      <UserLocale>en-GB</UserLocale>
    </component>
  </settings>
  <settings pass="oobeSystem">
    <component name="Microsoft-Windows-International-Core" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
      <InputLocale>0809:00000809</InputLocale>
    </component>
    <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
      <FirstLogonCommands>
        <!-- Content here -->
      </FirstLogonCommands>
    </component>
  </settings>
</unattend>
"@

# Create an XML document object
$xmlDocument = New-Object System.Xml.XmlDocument
$xmlDocument.LoadXml($xml)

# Select the FirstLogonCommands node
$firstLogonCommands = $xmlDocument.DocumentElement.SelectSingleNode('//settings[@pass="oobeSystem"]/component[@name="Microsoft-Windows-Shell-Setup"]/FirstLogonCommands')

# Check if the FirstLogonCommands node exists
if ($firstLogonCommands -eq $null) {
    Write-Output "FirstLogonCommands section not found under 'Microsoft-Windows-Shell-Setup' component in the 'oobeSystem' settings pass."
} else {
    Write-Output "FirstLogonCommands section found:"
    Write-Output $firstLogonCommands.InnerXml
}
8
  • 3
    The xml has a namespace. This question is a duplicate of How do I access an element with xpath with a namespace in powershell? Commented Mar 4, 2024 at 6:27
  • I tried to use the code you referenced, and I'm still left with completely null output. What you provided is probably obvious to those with better knowledge of xml,, but it is not an answer in itself. $ns = @{dns="urn:schemas-microsoft-com:unattend"}; $items = Select-Xml -Xml $xml -XPath '//dns:item' -Namespace $ns. Could you give me a few extra pointers please? I've provided a complete xml layoutt in my question, but still I have null output, so I don't see how the other question answers this (maybe it does, but I don't see how to implement it here). Commented Mar 4, 2024 at 6:55
  • I've also tried in this way $items = Select-Xml -Xml $xml -XPath '//dns:settings/component/FirstLogonCommands' -Namespace $ns and some other variations. Still null. I don't see how the other question answers this, except in a vague way. I'm trying to extrapolate from that answer how to get to a solution, but all I have is null output. Commented Mar 4, 2024 at 7:14
  • 3
    You just need to include the dns for each item in the path Select-Xml -Xml $xmlDocument -XPath '//dns:settings[@pass="oobeSystem"]/dns:component[@name="Microsoft-Windows-Shell-Setup"]/dns:FirstLogonCommands' -Namespace $ns Commented Mar 4, 2024 at 8:29
  • 3
    That's great, it works, and thankyou 👍, but this was far from obvious to me on how to proceed here. I think this is a valid question that could benefit others in a similar situation. Commented Mar 4, 2024 at 8:41

4 Answers 4

3

You just need to include the dns for each item in the path:

$ns = @{dns="urn:schemas-microsoft-com:unattend"}
Select-Xml -Xml $xmlDocument -XPath '//dns:settings[@pass="oobeSystem"]/dns:component[@name="Microsoft-Windows-Shell-Setup"]/dns:FirstLogonCommands' -Namespace $ns
Sign up to request clarification or add additional context in comments.

Comments

2

You can drill down where you want this way.

$DesiredSetting = $xmlDocument.unattend.settings | Where-Object {$_.pass -eq 'oobeSystem'}
$DesiredComponent = $DesiredSetting.component | Where-Object {$_.name -eq 'Microsoft-Windows-Shell-Setup'}
$firstLogonCommands = $DesiredComponent.FirstLogonCommands

But make sure you place the $null on the left side of -eq, otherwise it will not work correctly.

if ($null -eq $firstLogonCommands.'#comment') {
   Write-Output "FirstLogonCommands section not found under 'Microsoft-Windows-Shell-Setup' component in the 'oobeSystem' settings pass."
} else {
   Write-Output "FirstLogonCommands section found:"
   Write-Output $firstLogonCommands.InnerXml
}

Comments

1

To juxtapose and complement the existing answers:

  • derloopkat's helpful answer is the direct solution to your problem: It corrects your Select-Xml call to make it namespace-aware, via a hashtable of namespace-prefix-to-URI mapping(s) passed to -Namespace, and shows that each element name in your XPath query must be namespace-qualified.

    • As an aside:
      • You don't need to load your XML text into an [xml] (System.Xml.XmlDocument) instance first: Select-Xml has a -Content parameter that directly accepts XML text.
      • In cases where you do need an [xml] instance based on in-memory XML text, you can just cast the text (string) directly to [xml], e.g. [xml] '<foo>bar</foo>'
  • Darin's helpful answer shows a namespace-agnostic OO alternative that relies on PowerShell's adaptation of the [xml] DOM:

    • In essence, PowerShell allows you to treat any parsed [xml] document as an object graph that you can drill into using regular dot notation, because PowerShell surfaces XML (child) elements and XML attributes as namespace-less properties on each object (XML node) in the graph.

    • E.g., $xmlDocument.unattend.settings enumerates all <settings> child elements of the <unattend> root element; in other words: it is the equivalent of XPath /unattend/settings, without having to worry about namespaces.

  • jdweng's answer uses an alternative XML-parsing API, System.Linq.Xml.XDocument:

    • While this API is newer than [xml] (System.Xml.XmlDocument) and in many ways more convenient than the latter, it is important to note:
      • Both APIs are and will likely continue to be supported.

      • In the context of PowerShell, given the aforementioned convenient XML DOM adaptation based on [xml], and the fact that Select-Xml is also [xml]-based, it is usually simpler to stick with [xml]:

        • Case in point: jdweng's answer in essence manually recreates (in flattened form) the DOM adaptation of the input XML document that you get for free when you use [xml] (and, as an aside, does so in an inefficient fashion, due to repeated Add-Member calls and the unnecessary use of an [ArrayList] instance to manually create an output list).

        • With respect to XPath support, specifically, the two APIs offer very similar API surfaces, so there's also no advantage to using [System.Xml.Linq.XDocument] - see below.

        • However, when it comes to structural modifications of XML documents, the System.Linq.Xml.XDocument-based API is preferable even in PowerShell; see this answer for an example.


For the sake of completeness: Here's the equivalent [System.Xml.Linq.XDocument] XPath solution:

# Simplified input XML
# 'THE TEXT OF INTEREST.' is what should be extracted.
$xml = @"
<unattend xmlns="urn:schemas-microsoft-com:unattend" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">
  <settings pass="offlineServicing"></settings>
  <settings pass="windowsPE"></settings>
  <settings pass="oobeSystem">
    <component name="Microsoft-Windows-International-Core" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
      <InputLocale>0809:00000809</InputLocale>
    </component>
    <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
      <FirstLogonCommands>
        THE TEXT OF INTEREST.
      </FirstLogonCommands>
    </component>
  </settings>
</unattend>
"@

# Parse the XML text.
$doc = [System.Xml.Linq.XDocument]::Parse($xml)

# Create an XML namespace manager that declares the input XML's
# default namespace URI and associates it with a self-chosen prefix, 'dns'
# NOTE:
#   * The analogous thing happens behind the scenes for [xml], when you pass
#     a hashtable with prefix-to-URI mappings to Select-Xml -Namespace,
#     as in derloopkat's answer.
$nsm = [System.Xml.XmlNamespaceManager]::new([System.Xml.NameTable]::new())
$nsm.AddNamespace('dns', 'urn:schemas-microsoft-com:unattend')

# Now base your XPath query on the self-chosen namespace prefix, making
# sure that it is used with each element name in the path.
[System.Xml.XPath.Extensions]::XPathSelectElement(
  $doc, 
  '//dns:settings[@pass="oobeSystem"]/dns:component[@name="Microsoft-Windows-Shell-Setup"]/dns:FirstLogonCommands', 
  $nsm
).Value.Trim()

The above prints THE TEXT OF INTEREST., proving that the text content of the element of interest was successfully extracted.

6 Comments

I disagree. Most people who use the Net Library strongly recommend using Xml Linq over XML.
@jdweng, this answer explains why using [xml] is easier from PowerShell, because of the integration built into its language and cmdlets. This is separate from the question which API is preferable overall (as the answer notes, in general the System.Xml.Linq API is in many ways more convenient).
Easier doesn't necessarily mean better. XML Linq has much better methods than XML. XML is definitely not better. The legacy libraries requires more lines of code to perform same as Xml Linq.
@jdweng, no one is making the argument that System.Xml is better. The argument that is being made is: Due to its integration into PowerShell, its use there is usually easier, more concise, and therefore generally better. Nothing stops you from using System.Xml.Linq in PowerShell (a testament to its flexibility), but in most cases it'll be more work, as evidenced by your own answer (as noted, it is a cumbersome recreation of what PowerShell's System.Xml DOM adaptation gives you for free), and readers should be made aware of that.
I keep on telling you that concise is not good. It was called Power Programming in the 1960's and most large Programming Companies do not allow it because it is hard to maintain. Code must be easily readable.
|
0

Try following code which uses XML Linq

using assembly System.Xml.Linq 

# Load the XML content
$xml = @"
<unattend xmlns="urn:schemas-microsoft-com:unattend" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">
  <settings pass="offlineServicing">
  </settings>
  <settings pass="windowsPE">
    <component name="Microsoft-Windows-International-Core-WinPE" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
      <SetupUILanguage>
        <UILanguage>en-US</UILanguage>
      </SetupUILanguage>
      <InputLocale>0809:00000809</InputLocale>
      <SystemLocale>en-GB</SystemLocale>
      <UILanguage>en-US</UILanguage>
      <UserLocale>en-GB</UserLocale>
    </component>
  </settings>
  <settings pass="oobeSystem">
    <component name="Microsoft-Windows-International-Core" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
      <InputLocale>0809:00000809</InputLocale>
    </component>
    <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
      <FirstLogonCommands>
        <!-- Content here -->
      </FirstLogonCommands>
    </component>
  </settings>
</unattend>
"@
$doc = [System.Xml.Linq.XDocument]:: Parse($xml)
$ns = $doc.Root.GetDefaultNamespace()

$table = [System.Collections.ArrayList]::new()
foreach($setting in $doc.Descendants($ns + 'settings'))
{
   $newRow = [pscustomobject]@{}
   $newRow | Add-Member -NotePropertyName pass -NotePropertyValue $setting.Attribute('pass').Value
   
   $component = $setting.Element($ns + 'component')

   if($component -ne $null)
   {
      foreach($attribute in $component.Attributes())
      {
         $newRow | Add-Member -NotePropertyName $attribute.Name.LocalName -NotePropertyValue $attribute.Value
      }
      foreach($element in $component.Elements())
      {
         if(-not $element.HasElements)
         {
            $newRow | Add-Member -NotePropertyName $element.Name.LocalName -NotePropertyValue $element.Value
         }
      }

   }

   $table.Add($newRow) | out-null
}
$table | Format-List

Here is the results

pass : offlineServicing

pass                  : windowsPE
name                  : Microsoft-Windows-International-Core-WinPE
processorArchitecture : amd64
publicKeyToken        : 31bf3856ad364e35
language              : neutral
versionScope          : nonSxS
InputLocale           : 0809:00000809
SystemLocale          : en-GB
UILanguage            : en-US
UserLocale            : en-GB

pass                  : oobeSystem
name                  : Microsoft-Windows-International-Core
processorArchitecture : amd64
publicKeyToken        : 31bf3856ad364e35
language              : neutral
versionScope          : nonSxS
InputLocale           : 0809:00000809

6 Comments

Offering a technologically unrelated solution without addressing the problem stated in the question and without explaining why a different technology may be called for - if at all - is unhelpful. In the case at hand, the OP's problem had a straightforward solution that required only a small tweak to their existing code, as shown in derloopkat's answer.
@mklement0 : My answer is superior and using the newer XML Linq Net Library instead of the legacy XML Net Library.
@derloopkat's is definitely the straight path to the answer that I needed, but I do like seeing this Linq alternative that may be useful to anyone that comes to this question to see both approaches (and I'll almost certainly make use of this Linq also so I appreciate both answers).
I posted my results. My solution gives a flat results which is easier to work with than other solutions. I could of just done the Xml Load method and go same simple results as derloopkat's, but I went must further a parse the entire file.
@YorSubs: The Linq alternative is great in C#, but [xml] is usually the better choice in PowerShell, for the reasons now spelled out in my answer. Case in point: this answer manually recreates (in flattened form) the DOM adaptation of the input XML document that you get for free when you use [xml] (see Darin's answer). By contrast, using a proper XPath solution - which is what your question calls for - is quite similar between Linq and [xml], so there's also no advantage there (see my answer for how to use XPath queries with Linq).
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.