XSLT with Powershell
UPDATE: I've put a snapshot of this code at https://github.com/shanselman/nxslt2 as Oleg's properties have disappeared from the net.
I was talking to Chris at Portland Codecamp 2.0 yesterday and he was wondering if there were any XML cmdlets for XSLT in Powershell. There sure should be.
I wrote a few scripts with XsltCommand and System.Xml.Xsl stuff, but then thought it'd be easier and cleaner to use the rocking cool (and far more flexible) NXSLT2, by the very smart Oleg Tkachenko. One could right a bunch of cool XML cmdlets and get them shiny, wonderful and all integrated in a few days. Or, one could channel MacGyver and slap something together. Of course, you could just call nxslt from the command-line in the regular way:
nxslt2 file.xml file.xslt
But since one is always working with XML in powershell in variables, why not a little hack to make it easier:
PS>$a = [xml](get-content foo.xml)
PS>$b = transform-xslt $a foo.xslt
And $b now contains the transformed document as an instances of a [xml] type (System.Xml.XmlDocument).
Other things that could be done to make this easier/cleaner/more-PowerShelly would be to have it allow pipeline input, but I find the syntax confusing (within scripts) to make a parameter bind to pipeline input.
Thought (assuming this doesn't already exist): It'd be cool if one could say something line [param pipeline] in a function to indicate which parameter was the default for pipelining.This is was 'filters' are for...the let functions get at the current pipeline via $_.
So, go download NXSLT2 and put it in somewhere in your PATH. Then dot-source this function in your Microsoft.Powershell_profile.ps1:
. transform-xslt.ps1
...where transform-xslt is
UPDATE: Here's a better version with some changes suggested by Peter Wong. Note that it's a filter not a function so it supports pipelining using the $_ variable:
filter transform-xslt
{
param ( [string]$xsltpath =$(read-host "Please specify the path to an XSLT") )
$PRIVATE:tempString = $_
if ($PRIVATE:tempString -is [System.String])
{
$PRIVATE:tempString = [xml]$PRIVATE:tempString
}
if ($_ -is [xml])
{
$PRIVATE:tempString = ([xml]$_).get_outerXml()
}
[xml]($PRIVATE:tempString | nxslt2 - $xsltpath)
}
...and DO improve it (as it's crap now), and post your improvements here!
UPDATE#2: Keith Hill has a great article on making functions/filters that behave like truly integrated cmdlets. The difference between a Cmdlets (typically written in .NET and compiled) and functions is the level of integration with the intrinsic cmdlets. If you can't tell the difference between your new behavior/function/cmdlet and the built-in ones, you've succeeded. Here is his version that supports piped in objects as well as arrays of input files.
function Transform-Xml {
param([string]$stylesheetPath=$(throw '$stylesheetPath is required'),
[string[]]$xmlPath)
begin {
function applyStylesheetToXml([xml]$xml) {
$result = $xml.get_OuterXml() | nxslt2.exe - $stylesheetPath
[string]::join([environment]::newline, $result)
}
function applyStylesheetToXmlFile($sourcePath) {
$rpath = resolve-path $sourcePath
$result = nxslt2.exe $rpath $stylesheetPath
[string]::join([environment]::newline, $result)
}
}
process {
if ($_) {
if ($_ -is [xml]) {
applyStylesheetToXml $_
}
elseif ($_ -is [IO.FileInfo]) {
applyStylesheetToXmlFile $_.FullName
}
elseif ($_ -is [string]) {
if (test-path -type Leaf $_) {
applyStylesheetToXmlFile $_
}
else {
applyStylesheetToXml $_
}
}
else {
throw "Pipeline input type must be one of: [xml], [string] or [IO.FileInfo]"
}
}
}
end {
if ($xmlPath) {
foreach ($path in $xmlPath) {
applyStylesheetToXmlFile $path
}
}
}
}
Here's a simple example of usage:
PS[1] C:\Documents and Settings\shanselm\Desktop\xslt
> get-content foo.xml
<company>
<name>XYZ Inc.</name>
<address1>One Abc Way</address1>
<address2>Some avenue</address2>
<city>Tech city</city>
<coun\try>Neverland</country>
</company>PS[2] C:\Documents and Settings\shanselm\Desktop\xslt
> Get-Content foo.xslt
<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="1.0">
<xsl:output method="xml" />
<xsl:template match="company">
<poo>
<c><xsl:value-of select="/company/name"/></c>
<a1><xsl:value-of select="/company/address1"/></a1>
<a2><xsl:value-of select="/company/address2"/></a2>
<c1><xsl:value-of select="/company/city"/></c1>
<c2><xsl:value-of select="/company/country"/></c2>
</poo>
</xsl:template>
</xsl:stylesheet>
PS[3] C:\Documents and Settings\shanselm\Desktop\xslt
> $a = (get-content foo.xml) -as [xml]
PS[4] C:\Documents and Settings\shanselm\Desktop\xslt
> $b = transform-xslt $a foo.xslt
PS[5] C:\Documents and Settings\shanselm\Desktop\xslt
> $b
xml poo
--- ---
poo
PS[6] C:\Documents and Settings\shanselm\Desktop\xslt
> $b.poo
c : XYZ Inc.
a1 : One Abc Way
a2 : Some avenue
c1 : Tech city
c2 : Neverland
By the way, an unrelated-to-Powershell-but-related-to-XML note, do check out DonXML's XPathMania, a nicely integrated "why wasn't it built-in" Visual Studio add-in (screenshot).
About Scott
Scott Hanselman is a former professor, former Chief Architect in finance, now speaker, consultant, father, diabetic, and Microsoft employee. He is a failed stand-up comic, a cornrower, and a book author.
About Newsletter
Transform-Xml foo.xsl bar.xml
Transform-Xml foo.xsl bar.xml, baz.xml
gci *.xml | Transform-Xml foo.xsl
gci *.xml | Transform-Xml foo.xsl bar.xml
[xml](gc bar.xml) | Transform-Xml foo.xsl
"...some xml..." | Transform-Xml foo.xsl
Check it out here:
http://keithhill.spaces.msn.com/blog/cns!5A8D2641E0963A97!595.entry
http://www.codeproject.com/soap/myXPath.asp
It has helped me out quite a bit. I like DonXML's XPathMania, but I was a total newbie to the XMLPath stuff. The above link helped a lot. It is written by michael zhao, and has several good XPath sample queries.
Comments are closed.
filter transform-xslt
{
param ([string]$xsltpath =$(read-host "Please specify the path to an XSLT"))
[xml](([xml]$_).get_outerXml() | nxslt2 - $xsltpath)
}