Why is XDocument.Descendants() returning IEnumerator in PowerShell ISE?

Question!

I'm writing a PowerShell script to manipulate some Windows Installer XML (WiX). I'm using the new XML APIs in .NET 3.5 to do this, as I find it an easier API to work with than the DOM. The following script fragment is flatly refusing to work:

[System.Reflection.Assembly]::LoadWithPartialName("System.Xml.Linq") | Out-Null

# Basic idea: Iterate through en.wxl's l10ns, get string for each l10n, search all wxs files for that value.

pushd "C:\temp\installer_l10n"
$wxlFileName = "${pwd}\en.wxl"
$wxl = [System.Xml.Linq.XDocument]::Load($wxlFileName)
$strings = $wxl.Descendants("String")
$strings
$strings | foreach {
    $_
}
popd

The script should output each <String> tag on a separate line. I'm going to get it to do something more interesting once this bug's been solved ;-)

The XML document is a standard WiX localisation file:

<?xml version="1.0" encoding="utf-8" ?>
<WixLocalization Culture="en-US" xmlns="http://schemas.microsoft.com/wix/2006/localization">
   <String Id="0">Advertising published resource</String>
   <String Id="1">Allocating registry space</String>
   ...
</WixLocalization>

$strings is not $null (I've explicitly tested it), and if I write-host $wxl, I can see that the document has been loaded. Piping $strings into Get-Member returns an error stating that "No object has been specified to get-member" and write-host $strings does nothing. I've also tried $wxl.Descendants("WixLocalization") with the same results. Things like $wxl.Root and $wxl.Nodes work as expected. Debugging with PowerShell ISE, I see that $strings has been set to IEnumerator, rather than the expected IEnumerable<XElement>. Testing the IEnumerator with a single MoveNext and then a Current indicates that "Current = ", presumably $null.

The weird thing is that the same technique worked in a previous script. The exact same code, but with different variable names and string literals. And having just tried debugging that script too (to verify the behaviour), it seems it's now also displaying the same behaviour.

By : alastairs


Answers

This problem intrigued me, so I did some searching around. After a lot of messing around in PowerShell and searching the web I found your solution.

Credit goes to Jamie Thomson for the actual code. http://dougfinke.com/blog/index.php/2007/08/07/using-xmllinq-in-powershell/

The missing piece is to handle the namespace in the XML file. Here is the code that should work for you:

[System.Reflection.Assembly]::LoadWithPartialName("System.Xml.Linq") | Out-Null
# Basic idea: Iterate through en.wxl's l10ns, get string for each l10n, search all wxs files for that value.

$wxlFileName = "${pwd}\en.wxl"
$wxl = [System.Xml.Linq.XDocument]::Load($wxlFileName)
$ns = [System.Xml.Linq.XNamespace]”http://schemas.microsoft.com/wix/2006/localization”
$strings = $wxl.Descendants($ns + "String")
foreach ($string in $strings) {
    $string
}


I know you said you prefered not to work with the DOM, but is there any reason why this doesn't work for you?

[xml]$test = gc .\test.wml                                                                                        
$test                                                                                                             
$test.WixLocalization                                                                                             
$test.WixLocalization.String

This outputs:

PS> $test.WixLocalization.String

Id #text
-- -----
0 Advertising published resource
1 Allocating registry space

Foreach'ing over that shouldn't be too difficult at that point.



You could do it in a similar way, and treat "FileMenu" as a (fake) action?



This video can help you solving your question :)
By: admin