【问题标题】:PowerShell get first line of node and associated line numberPowerShell 获取节点的第一行和关联的行号
【发布时间】:2019-12-08 22:02:36
【问题描述】:

给定 XML,例如

<?xml version="1.0"?>
<Products>
    <Product_Group id="Robot">
        <Product id="RSAPRO2017">
            <DefaultShortcut>Autodesk Robot Structural Analysis Professional 2017.lnk</DefaultShortcut>
            <ProgramFolder>C:\Program Files\Autodesk\Autodesk Robot Structural Analysis Professional 2017</ProgramFolder>
            <UserAppDataRoaming>C:\Users\$(userName)\AppData\Roaming\Autodesk\Autodesk Robot Structural Analysis Professional 2017</UserAppDataRoaming>
        </Product>
    </Product_Group>
</Products>

还有一个 PowerShell 变量 $node 使用 SelectSingleNode 来选择产品,有没有办法 1:只获取节点的第一行,所以&lt;Product id="RSAPRO2017"&gt;,和 2:获取 XML 中的行号,因此在此示例中,行号为 4。

我可以用Write-Host "&lt;$($node.name)&gt;" 显示#1 的近似值,但这并不能提供完整的行。

这里的目标是验证一些复杂的 XML 并提供一个错误日志,其中包含允许人们快速轻松地修改他们的 XML 的详细信息。因此,例如,由于正确的节点名称是 Product 我可能有一个错误日志

Invalid node (Productt) at line 4
    <Productt id="RSAPRO2017">

我可以得到整个节点的 OuterXML,但是有些节点有 40-50 行,所以没有帮助。我可以使用 RegEx 提取第一行以获取第一个 对之间的所有内容,但我想知道是否有内置的 PowerShell/XPath 方法,而不是自己滚动?而且,获取行号似乎......一般来说是不确定的。

【问题讨论】:

  • 在您的&lt;Productt id="RSAPRO2017"&gt; 示例中,结束标记是&lt;/Product&gt; 还是&lt;/Productt&gt;?也就是说,您是要报告 无效 的 XML(例如,根据实际模式或您硬编码的模式)还是 格式不正确的 XML ?我不确定后者是否可以使用 .NET BCL 阅读器(或者,如果有的话,您不需要检测错误,因为阅读器会抛出异常)。此外,System.Xml.Linqexplicit interface implementation 可能是一个替代方案。

标签: xml powershell


【解决方案1】:

正如@BACON 评论的那样,如果 xml 格式错误,PowerShell 中的 xml 阅读器将引发异常。 PowerShell 中的[xml] 类型实现显示了这一点:

[xml]$malformed = @'
<?xml version="1.0"?>
<Products>
    <Product_Group id="Robot">
        <Productt id="RSAPRO2017">
            <DefaultShortcut>Autodesk Robot Structural Analysis Professional 2017.lnk</DefaultShortcut>
            <ProgramFolder>C:\Program Files\Autodesk\Autodesk Robot Structural Analysis Professional 2017</ProgramFolder>
            <UserAppDataRoaming>C:\Users\$(userName)\AppData\Roaming\Autodesk\Autodesk Robot Structural Analysis Professional 2017</UserAppDataRoaming>
        </Product>
    </Product_Group>
</Products>
'@

产生的输出包括:

错误:“第 4 行位置 10 上的 'Productt' 开始标记不匹配 “产品”的结束标签。

如果您想更聪明地处理错误信息,请将整个内容包装在 try {} catch {} 中,并尝试从自动 $Error 数组中提取数据,其中最近的错误保存在位置为零 ($Error[0])。

try {} 尝试做某事,catch {} 仅在尝试失败时运行。您也可以在catch 之后添加finally {} 来做某事,不管错误/成功。

try {
[xml]@'
<?xml version="1.0"?>
<Products>
    <Product_Group id="Robot">
        <Productt id="RSAPRO2017">
            <DefaultShortcut>Autodesk Robot Structural Analysis Professional 2017.lnk</DefaultShortcut>
            <ProgramFolder>C:\Program Files\Autodesk\Autodesk Robot Structural Analysis Professional 2017</ProgramFolder>
            <UserAppDataRoaming>C:\Users\$(userName)\AppData\Roaming\Autodesk\Autodesk Robot Structural Analysis Professional 2017</UserAppDataRoaming>
        </Product>
    </Product_Group>
</Products>

'@}
catch {
$Error[0].Exception.Message
}

【讨论】:

  • 所有,澄清一下,我的意思是测试格式良好的 XML,无效信息对我的命名约定是任意的。因此,在我的(不完美)示例中,无效的“Productt”节点将在开始标签和结束标签中引用,以制作格式良好的 XML,但出于我的目的,XML 无效,因为“Product_Group”节点的唯一有效子节点是一个“产品”节点。例如,如果有人复制粘贴根节点来启动他们的产品节点,“产品”也将是无效的。
【解决方案2】:

虽然这看起来确实像是针对架构文件进行验证的工作(在这种情况下,请参阅 How do I use PowerShell to Validate XML files against an XSD?),但如果您想要执行硬编码验证,您可以使用 XDocument class 来获取行信息。

无论您获得XDocument 实例([System.Xml.Linq.XDocument]::Load()[System.Xml.Linq.XDocument]::Parse()),您都需要传递[System.Xml.Linq.LoadOptions] value,指定您希望跟踪线路信息。行信息通过 IXmlLineInfo interface 提供,它由 XObject class 及其后代明确实现,但在 PowerShell 中您可以自动访问其成员。

鉴于SO59240313.xml...

<?xml version="1.0"?>
<Products>
    <Product_Group id="Robot">
        <Product id="RSAPRO2017">
            <DefaultShortcut>Autodesk Robot Structural Analysis Professional 2017.lnk</DefaultShortcut>
            <ProgramFolder>C:\Program Files\Autodesk\Autodesk Robot Structural Analysis Professional 2017</ProgramFolder>
            <UserAppDataRoaming>C:\Users\$(userName)\AppData\Roaming\Autodesk\Autodesk Robot Structural Analysis Professional 2017</UserAppDataRoaming>
        </Product>
        <Productt id="SomeId">
            <DefaultShortcut>Some shortcut</DefaultShortcut>
            <ProgramFolder>Some program path</ProgramFolder>
            <UserAppDataRoaming>Some roaming path</UserAppDataRoaming>
        </Productt>
    </Product_Group>
</Products>

...这段代码...

function ReportUnexpectedElement([System.Xml.Linq.XElement] $element)
{
    Write-Warning "Unexpected element ""$($element.Name)"" found at position $($element.LinePosition) of line $($element.LineNumber)."
}

$document = [System.Xml.Linq.XDocument]::Load(
    "$PWD\SO59240313.xml",
    [System.Xml.Linq.LoadOptions]::SetLineInfo
)

if ($document.Root.Name -ne 'Products')
{
    ReportUnexpectedElement $document.Root
}
else
{
    $productGroupElement = $document.Root.Element('Product_Group')
    if ($null -eq $productGroupElement)
    {
        Write-Warning 'Product_Group element not found.'
    }
    else
    {
        foreach ($productGroupChildElement in $productGroupElement.Elements())
        {
            if ($productGroupChildElement.Name -ne 'Product')
            {
                ReportUnexpectedElement $productGroupChildElement
            }
        }
    }
}

...将打印此输出...

警告:在第 9 行的位置 4 发现意外元素“Productt”。

我没有看到一种易于访问的方法来检索元素的完整开始标记,但您可以通过像这样修改 ReportUnexpectedElement 函数来自己重建它...

function ReportUnexpectedElement([System.Xml.Linq.XElement] $element)
{
    $elementBuilder = New-Object -TypeName 'System.Text.StringBuilder'

    # Most StringBuilder methods return a reference to that same instance,
    # so remember to suppress them from the function output
    $elementBuilder.Append('<').Append($element.Name) `
        | Out-Null
    foreach ($attribute in $element.Attributes())
    {
        $elementBuilder.AppendFormat(' {0}="{1}"', $attribute.Name, $attribute.Value) `
            | Out-Null
    }
    $elementBuilder.Append('>') `
        | Out-Null

    Write-Warning "Unexpected element ""$($elementBuilder.ToString())"" found at position $($element.LinePosition) of line $($element.LineNumber)."
}

...给定相同的 XML 文件,将打印此输出...

警告:在第 9 行的第 4 位发现意外元素“”。

【讨论】:

  • Hmmmmm,我使用 XPath 已经很久了,但现在看来我最好重构 Linq,而我正在重构其他所有内容,这会很容易。 @BACON,您对性能影响有感觉吗?迁移到 Linq 会比 XPath 快还是慢? XPath 现在需要几秒钟来加载一些较大的 XML 文件集,这有点令人烦恼,因为它发生在我最了解性能的启动时,但所有事情都认为它是次要的。
  • 另外,关于 XSD,我已经研究过了,但我可能以不太理想的方式使用 XML,这似乎使 XSD 更难使用。例如,我还在XML中定义了“任务”,&一个任务,一个副本,可以有一对一&lt;Source&gt;&lt;Destination&gt;的情况,或者一对多,或者多对一。并且可能是多对多,只要源节点和目标节点相等并且它们是交替的。用代码验证这一点很麻烦,但有可能,但用 XSD 验证似乎是不可能的,或者需要对 XSD 有深入的理解和手工编码。比我还深。
  • 我不能说出两者之间的任何性能差异,但它是asked before。有extension methods 允许将XPath 与XNode 实例一起使用,因此您可以使用[System.Xml.XPath.Extensions]::XPathSelectElements($document, '//Product') 检索所有&lt;Product /&gt; 元素,尽管也可以使用简单的$document.Descendants('Product') 完成这样一个简单的查询。但是,请注意该课程备注部分的第一段。
  • 有趣。我看了一下实现 LINQ 需要什么,看起来……很乱。对我来说可能很难单独维护,因为它与周围的所有其他代码是如此不同。所有这些都只是为了行号,尤其是当我的最终目标是拥有一个 GUI 并且无论如何都不再需要将客户放入 XML 时,这似乎不是一个理想的方法。但看起来 LINQ 不仅仅是另一种处理 XML 的方式,所以我可能应该把它作为一个学习过程来玩,即使我现在不重构。
  • XML 只是 LINQ 的一小部分。您将看到支持扩展方法及其查询语法的语言(C#/VB.NET)的最大好处;这些都不适用于 PowerShell,尽管前者可以通过它们各自的类 directly 调用。对于一些常见的 LINQ 方法(Where()Select() 等),已经存在类似的 PowerShell cmdlet。至于这里问的是什么,据我所知XDocument - 不是XmlDocument 也不是Select-Xml - 是解析带有行信息的XML 的唯一内置方法。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-06-15
  • 2011-09-07
  • 2011-06-22
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多