views:

698

answers:

2

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"&gt;
   <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.

+1  A: 

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.

James Pogran
Thanks for the response. As this is a problem at work, I'll try it out when I'm back in the office on Monday.
alastairs
This did work, although I accepted the other answer as it used the LINQ code I already had in place.
alastairs
+3  A: 

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
}
JasonMArcher
Thanks! I must have read that blog post 100 times whilst Googling around this, and missed Jamie Thomson's comments each time! Seems to be a vagary of LINQ to XML in PowerShell, as this doesn't happen in C#.
alastairs