tl;博士
正如您所怀疑的,名称冲突阻止了.Item
对感兴趣的 XML 元素上的属性的访问;解决显式枚举父元素的问题:
$xml.PatchScan.Machine.Product | % { $_.Item | select BulletinId, PatchName, Status }
%
ForEach-Object
是cmdlet的内置别名;见底部的解释。
作为替代方案,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
Select-Xml
将匹配的 XML 节点包装在外部对象中,因此需要访问该.Node
属性。
PowerShell 对 XML DOM 的改编(点表示法):
PowerShell修饰包含在实例中的对象层次结构[System.Xml.XmlDocument]
(例如,使用 cast 创建[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 文档的信息,请参阅此答案。
点表示法的缺点是可能存在名称冲突,如果偶然的输入 XML 元素名称恰好与固有 [System.Xml.XmlElement]
属性名称(对于单元素属性)或固有[Array]
属性名称(对于数组值属性;[System.Object[]]
源自[Array]
)。
如果发生名称冲突:如果正在访问的属性包含:
[System.Xml.XmlElement]
有关此差异的讨论以及如何在发生碰撞时访问内在属性,请参见最后一节。
解决方法是使用cmdlet 使用数组值属性的显式枚举,如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 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 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]看起来System.Xml.XmlElement
,当 PowerShell在幕后调整底层类型时,它并没有公开它的属性,而是通过get_*
访问器方法,它仍然允许访问,就好像它们是属性一样,但是添加了 PowerShell 的附带-但是-善意的财产优先。如果您对此有更多了解,请告诉我们。