Reputation: 4080
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
}
Upvotes: 1
Views: 237
Reputation: 440047
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.
[xml]
(System.Xml.XmlDocument
) instance first: Select-Xml
has a -Content
parameter that directly accepts XML text.[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
:
[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.
Upvotes: 1
Reputation: 34421
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
Upvotes: 0
Reputation: 6718
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
Upvotes: 3
Reputation: 2368
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
}
Upvotes: 2