Попытка импортировать дочерние элементы 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='&Public' value='Public' help='Folder containing public functions that can be used by the user.'/>
<choice label='&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
Однако это означает, что вы можете использовать эту нотацию только для доступа к элементам, чей путь в иерархии вы уже знаете - запросы не поддерживаются (хотя с поддержкой XPathSelect-Xml
командлет выходит).Это отображение игнорирует квалификаторы пространства имен (префиксы), поэтому вы должны использовать простое имя элемента без какого-либо префикса пространства имен; например, если входной документ имеет
plaster:parameters
элемент, вы должны относиться к нему как простоparameters
,Как бы удобна ни была точечная нотация, в ней есть подводные камни, наиболее заметным из которых является то, что квази-листовые элементы - те, которые вообще не имеют дочерних узлов, или только неэлементные дочерние узлы, такие как текстовый узел - возвращаются как строки, а не элементы, что затрудняет их изменение.
Вкратце: соответствие между XML DOM и объектной моделью PowerShell не является и не может быть точным.
.GetElementsByTagName()
метод:Возвращает коллекцию всех элементов с указанным именем тега во всем документе на всех уровнях иерархии (даже при вызове из внутреннего узла).
Как таковой, он не допускает сложного выбора целевых элементов, и документация рекомендует использовать.SelectSingleNode()
/.SelectNodes()
вместо.Хотя вы можете передать URI пространства имен в качестве второго аргумента, это не обязательно; если вы этого не сделаете, вы должны указать имя элемента (тега) буквально в точности так, как оно встречается в документе, включая его спецификатор пространства имен, если таковой имеется.