尝试将XML子级从一个文件导入到另一个文件

时间:2018-07-17 00:28:42

标签: xml powershell xpath xml-namespaces

我查看了this个帖子,发现它几乎完全是我需要做的。但是,鉴于本文中的建议,我无法产生预期的输出。基本上,我试图从包含以下内容的XML(</parameter>)文件中导入$ManifestFile元素:

<?xml version="1.0" encoding="utf-8"?>
<plasterManifest
  schemaVersion="1.1"
  templateType="Project" xmlns="http://www.microsoft.com/schemas/PowerShell/Plaster/v1">
  <metadata>
    <name>PlasterTestProject</name>
    <id>4c08dedb-7da7-4193-a2c0-eb665fe2b5e1</id>
    <version>0.0.1</version>
    <title>Testing creating custom Plaster Template for CI/CD</title>
    <description>Testing out creating a module project with Plaster for complete CI/CD files.</description>
    <author>Catherine Meyer</author>
    <tags></tags>
  </metadata>
  <parameters>
        <parameter name='AuthorName' type="user-fullname" prompt="Module author's name" />
        <parameter name='ModuleName' type="text" prompt="Name of your module" />
        <parameter name='ModuleDescription' type="text" prompt="Brief description on this module" />
        <parameter name='ModuleVersion' type="text" prompt="Initial module version" default='0.0.1' />
        <parameter name='GitLabUserName' type="text" prompt="Enter the GitLab Username to be used" default="${PLASTER_PARAM_FullName}"/>
        <parameter name="GitLubRepo" type="text" prompt="GitiLab repo name for this module" default="${PLASTER_PARAM_ModuleName}"/>
        <parameter name='ModuleFolders' type = 'multichoice' prompt='Please select folders to include' default='0,1'>
            <choice label='&amp;Public' value='Public' help='Folder containing public functions that can be used by the user.'/>
            <choice label='&amp;Private' value='Private' help='Folder containing internal functions that are not exposed to users'/>
        </parameter>
    </parameters>
</plasterManifest>

我正在尝试导入的文档($NewManifestFile)如下:

<?xml version="1.0" encoding="utf-8"?>
<plasterManifest schemaVersion="1.1" templateType="Project" xmlns="http://www.microsoft.com/schemas/PowerShell/Plaster/v1">
  <metadata>
     <name>test3</name>
     <id>8c028f40-cdc6-40dc-8442-f5256a8c0ed9</id>
     <version>0.0.1</version>
     <title>test3</title>
     <description>SDSKL</description>
     <author>NAME</author>
    <tags> </tags>
  </metadata>
  <parameters>
  </parameters>
  <content>
  </content>
</plasterManifest>

我编写的代码如下:

$ManifestFile = [xml](Get-Content ".\PlasterManifest.xml")
$NewManifestFile = [xml](Get-Content $PlasterMetadata.Path)
$NewManifestFile.plasterManifest.metadata.name

$Parameters = $ManifestFile.SelectSingleNode("//plasterManifest/parameters/parameter")
$Parameters
$NewParameters = $NewManifestFile.SelectSingleNode("//plasterManifest/parameters")
#Importing the parameters and content
foreach ($parameter in $Parameters) {
   $NewParamElem = $ManifestFile.ImportNode($parameter, $true)
   $NewParameters.AppendChild($NewParamElem)
}
[void]$NewManifestFile.save($PlasterMetadata.Path)

现在,它不会出错,但也完全不会导入。似乎某些元素未在某处正确分配。我尝试了很多替代方法,这似乎是唯一接近我想要的方法。有什么建议么?

2 个答案:

答案 0 :(得分:3)

您当前的方法存在几个问题:

  • 您不是将元素从源文档中导入到目标文档中,即使将其插入到目标文档的DOM中也是前提条件。

  • 您正在使用.SelectSingleNode()选择源文档节点,尽管-我想-您打算使用.SelectNodes()选择 all {{ 1}}元素。

  • 您缺少文档的命名空间管理,这是通过<parameter> / .SelectSingleNode()成功进行XPath查询的前提。

    • 但是,考虑到名称空间管理很复杂,下面的解决方案采用了解决方法。如果您确实想处理名称空间-这是正确的方法-请参阅Ansgar Wiechers' helpful answer

这是一个固定的带注释的解决方案:

.SelectNodes()

可选的背景信息:

  • $ManifestFile = [xml](Get-Content -Raw ./PlasterManifest.xml) $NewManifestFile = [xml](Get-Content -Raw $PlasterMetadata.Path) # Get the <parameters> element in the *source* doc. # Note that PowerShell's dot notation-based access to the DOM does # NOT require namespace management. $ParametersRoot = $ManifestFile.plasterManifest.parameters # Get the parent of the <parameter> elements in the *destination* doc. # Note: Ideally we'd also use dot notation in order to avoid the need for namespace # management, but since the target <parameters> element is *empty*, # PowerShell represents it as a *string* rather than as an XML element. # Instead, we use .GetElementsByTagName() to locate the element and rely # on the knowledge that there is only *one* in the whole document. $NewParametersRoot = $NewManifestFile.GetElementsByTagName('parameters')[0] # Import the source element's subtree into the destination document, so it can # be inserted into the DOM later. $ImportedParametersRoot = $NewManifestFile.ImportNode($ParametersRoot, $True) # For simplicity, replace the entire <parameters> element, which # obviates the need for a loop. # Note the need to call .ReplaceChild() on the .documentElement property, # not on the document object itself. $null = $NewManifestFile.documentelement.ReplaceChild($ImportedParametersRoot, $NewParametersRoot) # Save the modified destination document. $NewManifestFile.Save($PlasterMetadata.Path) / .SelectSingleNode() 因为它们接受XPath queries,是最灵活,功能最强大的元素定位方法(节点),但是如果输入文档声明了名称空间(例如您的情况下的.SelectNodes()),它们确实需要显式的名称空间处理

    • 注意:如果给定的输入文档声明了名称空间,而您却忽略了按如下所述处理它们,则xmlns="http://www.microsoft.com/schemas/PowerShell/Plaster/v1" / .SelectSingleNode()仅针对所有查询返回.SelectNodes()(如果元素不合格)。使用名称(例如$null)和失败以及使用名称空间限定的(使用名称空间前缀的名称)(例如parameters)。

    • 命名空间处理涉及以下步骤(请注意,给定文档可能具有多个命名空间声明,但为简单起见,这些指令仅假设一个):

      • 实例化名称空间管理器,并将其与输入文档[名称表]关联。

      • 将名称空间的URI与符号标识符相关联。如果输入文档中的名称空间声明是用于 default 名称空间的-plaster:parameters-您不能将其用作符号标识符(名称xmlns是保留的),并且必须简单地< em>选择一个。

      • 然后,当您调用xmlns / .SelectSingleNode()时,必须使用该符号标识符作为查询字符串中的元素名称前缀;例如,如果您(自选)符号标识符为.SelectNodes(),并且您正在文档中的任意位置寻找元素plaster,则可以使用查询字符串parameters

        < / li>
      • Ansgar Wiechers' helpful answer演示了所有这些内容。

  • 相比之下, PowerShell的点符号始终是与命名空间无关的,而'//plaster:pararameters'方法可以,因此它们不需要显式的名称空间处理。

    • 注意事项:虽然这降低了复杂性,但只有在知道正确地处理命名空间并不需要正确处理名称空间的情况下,才应使用它。

    • PowerShell的点符号:

      • PowerShell方便地将XML文档的DOM(输入文档中节点的层次结构)映射到具有属性的嵌套对象中,从而允许您使用常规的点符号钻取到文档中;例如,XPath查询.GetElementByTagNames()的等效项为'/root/elem'
        但是,这意味着您只能使用此表示法来访问其在层次结构中已经知道的路径的元素-不支持查询(尽管启用了XPath的Select-Xml cmdlet除外)。

      • 此映射忽略名称空间限定符(前缀),因此,您必须使用纯元素名称,且不带任何名称空间前缀;例如,如果输入文档中包含$xmlDoc.root.elem元素,则必须仅将其称为plaster:parameters

      • 像点符号一样方便,它带有陷阱,其中最值得注意的是准叶元素-要么没有子元素全部或仅 non-element 个子节点(例如文本节点)的节点将作为 strings 而不是 elements 返回,因此很难修改它们。
        简而言之:XML DOM和PowerShell对象模型之间的映射不是(也不能是)精确的。

    • .GetElementsByTagName() method

      • 在整个层次结构的所有级别(即使从内部调用)也返回具有指定标签名称的 all 元素的集合,在整个文档中为 节点)。
        因此,它不允许对目标元素进行复杂的选择,因此文档建议改为使用parameters / .SelectSingleNode()

      • 虽然您可以传递名称空间URI作为第二个参数,但这不是必需的。如果不这样做,则必须按字面意义指定元素(标签)名称,与在文档中出现的名称完全相同,包括其名称空间限定符(如果存在)。

答案 1 :(得分:2)

正如mklement0所指出的那样,您的XML文档具有名称空间,因此在选择带有XPath表达式的节点时需要一个名称空间管理器。使用点访问来选择节点可以带您进行名称空间管理,但是由于点访问并不总是可以人们期望的那样工作,因此我仍然建议坚持使用SelectNodes()并使用适当的名称空间管理器。

$uri = 'http://www.microsoft.com/schemas/PowerShell/Plaster/v1'

[xml]$ManifestFile = Get-Content 'C:\path\to\old.xml'
$nm1 = New-Object Xml.XmlNamespaceManager $ManifestFile.NameTable
$nm1.AddNamespace('ns1', $uri)

[xml]$NewManifestFile = Get-Content 'C:\path\to\new.xml'
$nm2 = New-Object Xml.XmlNamespaceManager $NewManifestFile.NameTable
$nm2.AddNamespace('ns2', $uri)

$ManifestFile.SelectNodes('//ns1:parameter', $nm1) | ForEach-Object {
    $newnode = $NewManifestFile.ImportNode($_, $true)
    $parent  = $NewManifestFile.SelectSingleNode('//ns2:parameters', $nm2)
    $parent.AppendChild($newnode) | Out-Null
}

$NewManifestFile.Save('C:\path\to\new.xml')