Попытка импортировать дочерние элементы XML из одного файла в другой

Я заглянул в этот пост и обнаружил, что это почти то, что мне нужно сделать. Тем не менее, я не могу произвести ожидаемый результат, учитывая предложение в этом посте. В основном я пытаюсь импортировать </parameter> элементы из XML ($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 ответа

Решение

Как указывал 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')

Есть несколько проблем с вашим текущим подходом:

  • Вы не импортируете элементы из исходного документа в целевой документ, даже если это является необходимым условием для вставки его в DOM конечного документа.

  • Вы используете .SelectSingleNode() выбрать узлы исходного документа, хотя - я полагаю - вы хотели использовать .SelectNodes() выбрать все <parameter> элементы.

  • Вам не хватает управления пространством имен для документов, что является необходимым условием для успешных запросов XPath через .SelectSingleNode() / .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() / .SelectNodes() Поскольку они принимают запросы XPath, являются наиболее гибкими и мощными методами для нахождения элементов (узлов), представляющих интерес, в документе XML, но они требуют явной обработки пространства имен, если входной документ объявляет пространства имен (такие как xmlns="http://www.microsoft.com/schemas/PowerShell/Plaster/v1" в твоем случае):

    • Примечание. Если заданный входной документ объявляет пространство имен, и вы не обращаетесь с ними, как описано ниже, .SelectSingleNode() / .SelectNodes() просто вернись $null для всех запросов, если используются неквалифицированные имена элементов (например, parameters) и завершается ошибкой с определенными для пространства имен (с префиксом пространства имен) (например, plaster:parameters).

    • Обработка пространства имен включает в себя следующие этапы (обратите внимание, что данный документ может иметь несколько объявлений пространства имен, но для простоты инструкции предполагают только одно):

      • Создайте экземпляр менеджера пространства имен и свяжите его с входным документом [таблица имен].

      • Свяжите URI пространства имен с символическим идентификатором. Если объявление пространства имен во входном документе предназначено для пространства имен по умолчанию - xmlns - вы не можете использовать это как свой символический идентификатор (имя xmlns зарезервирован) и должен просто выбрать один.

      • Затем, когда вы звоните .SelectSingleNode() / .SelectNodes() Вы должны использовать этот символьный идентификатор в качестве префикса имени элемента в строках запроса; например, если ваш (самостоятельно выбранный) символический идентификатор plaster и вы ищете элемент parameters в любом месте документа вы бы использовали строку запроса '//plaster:pararameters'

      • Полезный ответ Ansgar Wiechers демонстрирует все это.

  • Напротив, точечная нотация PowerShell всегда независима от пространства имен и .GetElementByTagNames() Метод может быть, поэтому они не требуют явной обработки пространства имен.

    • Предостережение: хотя это уменьшает сложность, вы должны использовать его, только если знаете, что правильная обработка пространства имен не является необходимостью для правильной обработки входного документа.

    • Точечная запись PowerShell:

      • PowerShell удобно отображает DOM XML-документа - иерархию узлов во входном документе - на вложенный объект со свойствами, что позволяет детализировать документ с помощью регулярных точечных обозначений; например, эквивалент запроса XPath '/root/elem' было бы $xmlDoc.root.elem
        Однако это означает, что вы можете использовать эту нотацию только для доступа к элементам, чей путь в иерархии вы уже знаете - запросы не поддерживаются (хотя с поддержкой XPath Select-Xml командлет выходит).

      • Это отображение игнорирует квалификаторы пространства имен (префиксы), поэтому вы должны использовать простое имя элемента без какого-либо префикса пространства имен; например, если входной документ имеет plaster:parameters элемент, вы должны относиться к нему как просто parameters,

      • Как бы удобна ни была точечная нотация, в ней есть подводные камни, наиболее заметным из которых является то, что квази-листовые элементы - те, которые вообще не имеют дочерних узлов, или только неэлементные дочерние узлы, такие как текстовый узел - возвращаются как строки, а не элементы, что затрудняет их изменение.
        Вкратце: соответствие между XML DOM и объектной моделью PowerShell не является и не может быть точным.

    • .GetElementsByTagName() метод:

      • Возвращает коллекцию всех элементов с указанным именем тега во всем документе на всех уровнях иерархии (даже при вызове из внутреннего узла).
        Как таковой, он не допускает сложного выбора целевых элементов, и документация рекомендует использовать .SelectSingleNode() / .SelectNodes() вместо.

      • Хотя вы можете передать URI пространства имен в качестве второго аргумента, это не обязательно; если вы этого не сделаете, вы должны указать имя элемента (тега) буквально в точности так, как оно встречается в документе, включая его спецификатор пространства имен, если таковой имеется.

Другие вопросы по тегам