Scott Hanselman

XSLT with Powershell

July 24, '06 Comments [5] Posted in PowerShell | XML
Sponsored By

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.

facebook twitter subscribe
About   Newsletter
Sponsored By
Hosting By
Dedicated Windows Server Hosting by SherWeb
Monday, 24 July 2006 13:07:43 UTC
Here's a filter-based equivalent -- to be invoked as: $b = ($a | transform-xslt foo.xslt)

filter transform-xslt
{
param ([string]$xsltpath =$(read-host "Please specify the path to an XSLT"))
[xml](([xml]$_).get_outerXml() | nxslt2 - $xsltpath)
}
Peter W
Monday, 24 July 2006 15:08:39 UTC
After all your posts on PowerShell, I'm interested- piping objects around is cool, but I still don't quite 'get' it. Why would I do this instead of writing a quick command line app? How about a higher-level post about when developers should (and shouldn't) consider using PowerShell?
Tuesday, 25 July 2006 16:06:00 UTC
I came up with a function-based cmdlet that works pretty well. You can use it like so:

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
Wednesday, 26 July 2006 21:04:00 UTC
It's about being the value of "inter-object" or inter-cmdlet communication using Objects. If passing strings between stdin and stdout works with you, that's cool.
Scott Hanselman
Saturday, 29 July 2006 19:55:25 UTC
For those who are new to the XML scene, this article has some good examples of XPath queries:

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.

Disclaimer: The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way.