简短概述:
如您所怀疑的,名称冲突阻碍了对感兴趣的XML元素上的.Item
属性的访问;使用父级元素的显式枚举来解决问题:
$xml.PatchScan.Machine.Product |
% { $_.Item | select BulletinId, PatchName, Status }
%
是内置别名,代表 ForEach-Object
命令;请参见底部的说明部分。
作为一种替代方案,Ansgar Wiecher的有用答案提供了一个简洁的XPath解决方案,既高效又允许复杂查询。
另外:PowerShell v3+附带了Select-Xml
cmdlet,它以文件路径作为参数,允许单管道解决方案:
(Select-Xml -LiteralPath X:\folder\my.xml '//Product/Item[@Class="Patch"]').Node |
Select-Object BulletinId, PatchName, Status
注意:
PowerShell对XML DOM(点表示法)的适应:
PowerShell“装饰”了包含在[System.Xml.XmlDocument]
实例中的对象层次结构(例如使用转换[xml]
创建):
在每个级别上,使用命名为输入文档特定元素和属性的属性[1];例如:
([xml] '<foo><bar>baz</bar></foo>').foo.bar # -> 'baz'
([xml] '<foo><bar id="1" /></foo>').foo.bar.id # -> '1'
将给定层次结构级别上的多个相同名称的元素隐式转换为数组(具体而言,类型为[object[]]
);例如:
([xml] '<foo><C>one</C><C>two</C></foo>').foo.C[1] # -> 'two'
正如示例(以及您在问题中的代码)所示,这允许通过方便的点符号访问。
注意:如果您使用点符号来定位至少具有一个属性和/或子元素的元素,则返回元素本身(XmlElement
实例);否则,它是元素的文本内容;有关通过点符号更新XML文档的信息,请参见this answer。
点符号的缺点是可能会出现名称冲突,如果偶然输入的XML元素名称恰好与内在的[System.Xml.XmlElement]
属性名称(对于单个元素属性)或内在的[Array]
属性名称(对于数组值属性;[System.Object[]]
派生自[Array]
)相同。
在名称冲突的情况下: 如果被访问的属性包含:
参见最后一节,了解此差异的讨论以及在发生冲突时如何访问内在的 [System.Xml.XmlElement]
属性。
解决方法是使用显式枚举数组值属性,使用上面演示过的 ForEach-Object
命令,下面是完整示例:
[xml] $xml = @'
<PatchScan>
<Machine>
<Product>
<Name>Windows 10 Pro (x64)</Name>
<Item Class="Patch">
<BulletinId>MSAF-054</BulletinId>
<PatchName>windows10.0-kb3189031-x64.msu</PatchName>
<Status>Installed</Status>
</Item>
<Item Class="Patch">
<BulletinId>MSAF-055</BulletinId>
<PatchName>windows10.0-kb3189032-x64.msu</PatchName>
<Status>Not Installed</Status>
</Item>
</Product>
<Product>
<Name>Windows 7 Pro (x86)</Name>
<Item Class="Patch">
<BulletinId>MSAF-154</BulletinId>
<PatchName>windows7-kb3189031-x86.msu</PatchName>
<Status>Partly Installed</Status>
</Item>
<Item Class="Patch">
<BulletinId>MSAF-155</BulletinId>
<PatchName>windows7-kb3189032-x86.msu</PatchName>
<Status>Uninstalled</Status>
</Item>
</Product>
</Machine>
</PatchScan>
'@
# Enumerate the array-valued .Product property explicitly, so that
# the .Item property can successfully be accessed on each XmlElement instance.
$xml.PatchScan.Machine.Product |
ForEach-Object { $_.Item | Select-Object BulletinID, PatchName, Status }
以上产生:
Class BulletinId PatchName Status
----- ---------- --------- ------
Patch MSAF-054 windows10.0-kb3189031-x64.msu Installed
Patch MSAF-055 windows10.0-kb3189032-x64.msu Not Installed
Patch MSAF-154 windows7-kb3189031-x86.msu Partly Installed
Patch MSAF-155 windows7-kb3189032-x86.msu Uninstalled
深入了解:当什么属性被遮蔽时:
注意:在这里,遮蔽的意思是,在名称冲突的情况下,“获胜”的属性 - 其值被报告的属性 - 有效地隐藏了另一个属性,从而将其“放在阴影中”。
在使用数组的点符号表示法时,一个名为成员访问枚举的特性就会发挥作用,它适用于PowerShell v3+中的任何集合;换句话说:这种行为不仅仅适用于[xml]
类型。
简而言之:隐式地访问集合上的属性将访问集合中每个成员(集合中的项)的属性,并将结果值作为数组([System.Object[]]
)返回;例如:
# Using member-access enumeration, collect the value of the .prop property from
# the array's individual *members*.
> ([pscustomobject] @{ prop = 10 }, [pscustomobject] @{ prop = 20 }).prop
10
20
然而,如果集合类型本身具有该名称的属性,则集合自己的属性优先;例如:
# !! Since arrays themselves have a property named .Count,
# !! member-access enumeration does NOT occur here.
> ([pscustomobject] @{ count = 10 }, [pscustomobject] @{ count = 20 }).Count
2 # !! The *array's* count property was accessed, returning the count of elements
在使用点符号与[xml]
(PowerShell修饰的System.Xml.XmlDocument
和System.Xml.XmlElement
实例)时,PowerShell添加的附加属性会遮盖类型固有属性:[2]
虽然这种行为很容易理解,但结果取决于具体的输入,这也可能是危险的:
例如,在以下示例中,附加的name
子元素会遮盖元素本身上同名的固有属性:
> ([xml] '<xml><child>foo</child></xml>').xml.Name
xml # OK: The element's *own* name
> ([xml] '<xml><name>foo</name></xml>').xml.Name
foo # !! .name was interpreted as the incidental *child* element
如果您确实需要访问内置类型的属性,请使用.get_<property-name>()
:
> ([xml] '<xml><name>foo</name></xml>').xml.get_Name()
xml # OK - intrinsic property value to use of .get_*()
[1] 如果给定元素具有相同名称的属性和元素,则PowerShell报告两者,作为数组[object[]]
的元素。
[2] 表面上看起来,当PowerShell在幕后适应基础System.Xml.XmlElement
类型时,它不会直接公开其属性,而是通过get_*
访问器方法进行访问,这仍然允许访问,就好像它们是属性,但由于PowerShell添加的附带但合法的属性优先级更高。如果您对此了解更多,请让我们知道。
Item
与[Array]
类型的属性名称发生了冲突。 - mklement0