Calendar

<<  September 2017  >>
MonTueWedThuFriSatSun
28293031123
45678910
11121314151617
18192021222324
2526272829301
2345678

View posts in large calendar

RecentComments

None

 
 
     
 
Displaying the output of Get-DirAsXml in an explorer with Powershell.  Although Powershell is a command line interface it can use forms to display output and get input. For an explorer the first thing we need is a treeview so we can use the System.Windows.Forms.TreeView control. Add it to a System.Windows.Forms.Form then recursively wander down the xml tree adding TreeNodes to it for all the nodes in the xml. Here is some code that will do that. .highlightbackground{ width: 100%; /*height: 100%;*/ overflow: auto; font-family: consolas, "Courier New", courier, mono; font-size: 12px; background-color: #eeeeee; color: #000000; padding: 2px 2px 2px 2px; white-space: nowrap; } .linenumberlist{border-left: solid 1px green;padding: 0px;} .linenumber{color: green;padding-left: 5px;} .foreground{color: #000000;} .command:color: #0000ff;} .commandparam{color: #000080;} .commandarg{color: #8a2be2;} .number{color: #800080;} .string{color: #8b0000;} .variable{color: #ff4500;} .type{color: #008080;} .operator{color: #a9a9a9;} .keyword{color: #00008b;} .comment{color: #006400;} .statementsep{color: #c86400;} .linecont{color: #c86400;} function Form-DisplayDir{    param ([xml]$XML)    function DisplayDir_wander($el, $tnode){        foreach($e in $el.get_ChildNodes()){ #|sort-object name){            $tn = new-object System.Windows.Forms.TreeNode            $tn.Text = $e.name            [Void]$tnode.Nodes.add( $tn )            if ($e.get_Name() -eq "folder"){                DisplayDir_wander ($e) ($tn)            }        }    }    $FORM = new-object Windows.Forms.Form        $FORM.Size = new-object System.Drawing.Size(300,310)        $FORM.text = "Form-DisplayDir"        $RESULT = ""    $TREEVIEW = new-object windows.forms.TreeView     $TREEVIEW.size = new-object System.Drawing.Size(295,269)       $TREEVIEW.Anchor = "top, left, bottom, right"    $TREEVIEW.add_AfterSelect({        $RESULT = $TREEVIEW.SelectedNode.FullPath    })            $r = $XML.selectSingleNode("/*")    $tn = new-object System.Windows.Forms.TreeNode     $tn.Text = $r.get_Name()                    DisplayDir_wander ($r) ($tn)            [void]$TREEVIEW.Nodes.Add($tn)    $FORM.Controls.Add($TREEVIEW)    [void]$FORM.showdialog()    $RESULT} You can use it like this PS> . .\Form-DisplayDir.ps1 # dot source the scriptPS> Form-DisplayDir (Get-DirAsXml test) It might be displayed like this.   PS> Form-DisplayDir (Get-DirAsXml test -props @{Length=""}) Will display this form with the file sizes in bytes shown.   The next thing to do is add an event handler to the TreeView. I like to use the AfterSelect event rather than the Click event because it doesn't fire when you expand or collapse nodes. $TREEVIEW.add_AfterSelect({    $RESULT = $TREEVIEW.SelectedNode.FullPath}) This bit of code can then be used as a simple folder/file selector. You could add the event to a Button and just stick $RESULT into the pipeline after the form closes. PS> Form-DisplayDir (Get-DirAsXml test) root\test\test2\test.txtor PS> Form-DisplayDir (Get-DirAsXml test) | SomeScriptThatNeedsAFilePath.ps1 Here is the code Form-DisplayDir.zip (621.00 bytes) The next thing to do is add a System.Windows.Forms.ListView control to display the files.         $LISTVIEW = new-object windows.forms.ListView    $LISTVIEW.Location = new-object System.Drawing.Size(300, 0)    $LISTVIEW.Size = new-object System.Drawing.Size(295,269)    $LISTVIEW.Anchor = "top, left, bottom, right"    $LISTVIEW.View = [System.Windows.Forms.View]::Details    $LISTVIEW.AllowColumnReorder = $true    [void]$LISTVIEW.Columns.Add("Name", -2, [windows.forms.HorizontalAlignment]::left)    foreach($att in $XML.SelectSingleNode("//file"            ).SelectNodes("@*[not(name()='Name')]")){        [void]$LISTVIEW.Columns.Add($att.get_Name(), -2, [windows.forms.HorizontalAlignment]::Right)    }    $TREEVIEW.add_AfterSelect({        $LISTVIEW.Items.Clear()        $xmlnode = $TREEVIEW.SelectedNode.Tag        foreach($child in $xmlnode.get_ChildNodes()){            $item = new-object windows.forms.ListViewItem($child.Name)            foreach($column in ($LISTVIEW.Columns|where{$_.Text -ne "Name"})){                if ($child.($column.Text) -ne $null){                    $item.SubItems.Add($child.($column.Text))                }            }            $LISTVIEW.Items.Add($item)        }    })    $FORM.Controls.Add($LISTVIEW) The TreeView AfterSelect event has been changed to populate the ListView. To make it look a bit more like Explorer you probably want to add a couple more columns to the xml PS> Form-DisplayDirExp (Get-DirAsXml test -props @{Length="";LastWriteTime=""}) It might be displayed like this   Here is the code Form-DisplayDirExp.zip (953.00 bytes) It is getting there but it is a bit confusing without icons, especially for the folders. There are a few different ways of doing this. You could create a System.Windows.Forms.ImageList object and add images to that then assign it to the $TREEVIEW.ImageList property but it gets quite labour intensive to make a lot of little icons. You could have just 2 icons, one for folders and one for files. So add some code like this $ImageList = New-Object System.Windows.Forms.ImageList $imageList.Images.Add([System.Drawing.Bitmap]::FromFile("folder.bmp"))$imageList.Images.Add([System.Drawing.Bitmap]::FromFile("file.bmp"))$TREEVIEW.ImageList = $ImageList $LISTVIEW.SmallImageList = $ImageList  It might be displayed like this   The best way to add icons is to use the SystemImageList or Shell Icon Cache. It is used everywhere and if you are using TreeView and ListView controls then one would think there would be a simple way of saying Hey! TreeView."UseTheEffingSystemImageList" But it is not that simple. You have to get down to the nitty gritty with System.Runtime.InteropServices SHGetFileInfo and SendMessage. So I wrapped all that 'c++ twitter' up in a file that you can just 'dot source' and then use the 2 methods (SetSystemImageListHandle, GetSystemImageListIndex) it contains. int SetSystemImageListHandle(Control, int Size) sets the ImageList for the TreeView/ListView control to the SystemImageList and the Size to large or small (0, 1). int GetSystemImageListIndex(string Filename, boolean Dir, int Size) gets the image index for the filename. Dir is a boolean indicating if it is a Folder and Size is large or small (0, 1). ImageListHandle.zip (2.19 kb) Here are some code snippets. . .\ImageListHandle.ps1$SHGFI_SMALLICON = 1; $SHGFI_LARGEICON = 0$LISTVIEW = new-object System.Windows.Forms.ListView$FORM.Controls.Add($LISTVIEW)[void][cjb.Shell32]::SetSystemImageListHandle($LISTVIEW, $SHGFI_SMALLICON)$TREEVIEW = new-object System.Windows.Forms.TreeView$FORM.Controls.Add($TREEVIEW)[void][cjb.Shell32]::SetSystemImageListHandle($TREEVIEW, $SHGFI_SMALLICON)$tn = new-object System.Windows.Forms.TreeNode$tn.ImageIndex = $tn.SelectedImageIndex = [cjb.Shell32]::GetSystemImageListIndex($e.Name, ($e.get_Name() -eq "folder"), $SHGFI_SMALLICON)$idx = [cjb.Shell32]::GetSystemImageListIndex($n.Name, ($n.get_Name() -eq "folder"), $SHGFI_SMALLICON)$item = new-object windows.forms.ListViewItem($n.Name,$idx) For this to work properly the control must be added to the form before SetSystemImageListHandle is called. Here is the version that uses the SystemImageList Form-DisplayDirExpIcons.zip (1.09 kb) It might be displayed like this A useful thing to set on the ListView control is AutoResizeColumns. This can have one of two values, HeaderSize or ColumnContent and resizes the columns depending on the width of the text. $LISTVIEW.AutoResizeColumns([Windows.Forms.ColumnHeaderAutoResizeStyle]::HeaderSize) Another is AllowColumnReorder. This allows you to move the columns around as you like $LISTVIEW.AllowColumnReorder = $true With all of the attributes that you can have in the xml it might be handy to specify which columns you want displayed in the ListView. This can be done by passing an array of attribute names that will be displayed as columns. Here Get-DirAsXml adds 9 attributes (Name + 2props + 6extendedProps) PS> Form-DisplayDirExp (Get-DirAsXml test -props @{Length="";LastWriteTime=""} -ExtendedProps @{Title=""; Subject=""; Author=""; Category=""; Keywords=""; Comments=""}) -columns @("Name", "Length", "LastWriteTime") So instead of displaying all 9 file properties it will only display Name, Length and LastWriteTime in that order. But supposing you DO want to see the other file properties? You could add a third pane to the Explorer     $LISTVIEW2 = new-object windows.forms.ListView    $FORM.Controls.Add($LISTVIEW2)    $LISTVIEW2.Location = new-object System.Drawing.Size(600, 0)    $LISTVIEW2.Size = new-object System.Drawing.Size(295,269)    $LISTVIEW2.Anchor = "top, bottom, right"    $LISTVIEW2.View = [System.Windows.Forms.View]::Details    [void]$LISTVIEW2.Columns.Add("Name", -2, [windows.forms.HorizontalAlignment]::Left)    [void]$LISTVIEW2.Columns.Add("Value", -2, [windows.forms.HorizontalAlignment]::Right)    $LISTVIEW.add_ItemActivate({        $LISTVIEW2.Items.Clear()        $xmlnode = $LISTVIEW.SelectedItems[0].Tag        foreach($att in $xmlnode.Attributes){            $item = new-object windows.forms.ListViewItem($att.get_Name())            $item.SubItems.Add($att.Value)            $LISTVIEW2.Items.Add($item)        }    })   It might be displayed like this  Here is the code Form-DisplayDirExpIcons3Pane.zip (1.27 kb) You could set the ToolTipText on the ListViewItems. First set ShowItemToolTips to true. This must be done before you set the SystemImageList handle if you are using icons. $LISTVIEW.ShowItemToolTips = $true Then build up your own ToolTipText or if you specified -ExtendedProps @{InfoTip=""} on Get-DirAsXml then you can use it as you populate the ListView control $item.ToolTipText = $child.InfoTip This might be displayed like this (with cursor) Adding a context menu to the ListView is quite easy.     $LISTVIEW.ContextMenu = $cm = new-object Windows.Forms.ContextMenu    $mi = $cm.MenuItems.Add("Do Thing One")    $mi.add_Click({        $XMLNODE = $LISTVIEWNODE.Tag        Write-Host "Thing One invoked on $LISTVIEWNODE."    })    $mi = $cm.MenuItems.Add("Do Thing Two")    $mi.add_Click({        $XMLNODE = $LISTVIEWNODE.Tag        Write-Host "Thing Two invoked on $LISTVIEWNODE."    })    $LISTVIEW.add_MouseDown({        if ($_.Button -eq [Windows.Forms.MouseButtons]::Right){            $LISTVIEWNODE = $LISTVIEW.GetItemAt($_.X, $_.Y)        }else{            $LISTVIEWNODE = $null        }    }) This might be displayed like this On the Host console you will see Here is the code Form-DisplayDirExpMenu.zip (1.29 kb) With all those ExtendedProperties you can get from Get-DirAsXml a search functionality is mighty useful. Here is some code to add an XPath search to the explorer. Any valid XPath can be used such as //file[contains(@Author, 'Chr')]  or //file[contains(@BitRate, '44')]. A find duplicate file names: //file[@Name = preceding::file/@Name]. See this previous entry for how to add a Checksum attribute then find duplicate files //file[@Checksum = preceding::file/@Checksum].         $SEARCHBOX = new-object windows.forms.TextBox    $SEARCHBOX.Location = new-object System.Drawing.Point(85, 275)    $SEARCHBOX.Size = new-object System.Drawing.Size(490,20)    $SEARCHBOX.Anchor = "left, bottom, right"    $FORM.Controls.Add($SEARCHBOX)    $BUTTON = new-object windows.forms.button    $BUTTON.Location = new-object System.Drawing.Point(10, 275)    $BUTTON.Anchor = "left, bottom"    $BUTTON.Text = "Search"    $BUTTON.add_Click({        $LISTVIEW.Items.Clear()        $nodes = $XML.SelectNodes($SEARCHBOX.Text)        foreach($child in $nodes){            $idx = [cjb.Shell32]::GetSystemImageListIndex($child.Name, ($child.get_Name() -eq "folder"), $SHGFI_SMALLICON)            $item = new-object windows.forms.ListViewItem($child.Name, $idx)            $item.Tag = $child            $item.ToolTipText = $child.InfoTip            foreach($column in ($LISTVIEW.Columns|where{$_.Text -ne "Name"})){                if ($child.($column.Text) -ne $null){                    $item.SubItems.Add($child.($column.Text))                }else{                    $item.SubItems.Add("")                }            }            $LISTVIEW.Items.Add($item)        }        $LISTVIEW.AutoResizeColumns([Windows.Forms.ColumnHeaderAutoResizeStyle]::ColumnContent)    })    $FORM.Controls.Add($BUTTON)    It might be displayed like this (with cursor) Here is the code Form-DisplayDirExpSearch.zip (1.26 kb) Another thing you might want to do is drag'n'drop. I will go back to the 3 pane solution but with a ListView in the middle and a TreeView on either side. The drag'n'drop rule is simple. Things can be dragged only from the ListView to the right side TreeView.         $LISTVIEW.add_ItemDrag({        param($sender, $e)        $nodes = @()        foreach($lvi in $sender.SelectedItems){            $nodes += $lvi.Tag        }        $sender.DoDragDrop($nodes, [System.Windows.Forms.DragDropEffects]::Link)    })        $TREEVIEW2.add_DragDrop({        param($sender, $e)        $point = new-object System.Drawing.Point -ArgumentList @($e.X, $e.Y)        $targetPoint = $sender.PointToClient($point)        $dropnode = $sender.GetNodeAt($targetPoint)        if ($dropnode.Tag.get_Name() -eq "folder"){            $nodes = $e.Data.GetData("System.Object[]")            foreach($n in $nodes){                $newnode = $XML2.ImportNode($n, $true)                $dropnode.Tag.AppendChild($newnode)            }            $dropnode.Nodes.Clear()            DisplayDir_wander ($dropnode.Tag) ($dropnode) ($true)        }    }) It might be displayed like this (without cursor)   So imagine that the left is a list of all files in a tree, on the right are groups and topics in a knowledgebase. It might be a movie or mp3 library with moods or genres. It needs human interaction to decide how the resulting xml is layed out and what it contains. Here is a possibility, it takes the usual as input plus another xml that it creates on the fly. The output when the form is closed is sent to the T-Dax2Wpl translet to create a Windows Media Player playlist. Or it might be an inventory list for a software release and all the resulting files are copied to an output media. PS> Form-DisplayDirExp (Get-DirAsXml test -props @{Length="";LastWriteTime=""} -ExtendedProps @{Title="";Subject=""; Author="";Category="";Keywords=""; Comments=""}) -columns @("Name", "Length", "LastWriteTime") -xml2 ([xml]'<root><folder Name="Knowledge"><folder Name="Topic" /><folder Name="Group" /></folder></root>') | T-Dax2Wpl Here is the code Form-DisplayDirExpDragDrop.zip (1.72 kb)
One would think this should be easy. Just put Powershell into the Language of an msxsl:script element. But it doesn't work :-D Pity.  I rooted around and the language attribute can be any of the CodeDomProviders available on the system. PS> [System.CodeDom.Compiler.CodeDomProvider]::GetAllCompilerInfo()CodeDomProviderType IsCodeDomProviderTypeValid------------------- --------------------------Microsoft.CSharp.CSharpCodeProvider TrueMicrosoft.VisualBasic.VBCodeProvider TrueMicrosoft.JScript.JScriptCodeProvider TrueMicrosoft.VisualC.CppCodeProvider True According to thisthere won't be one for Powershell soon. Never mind, I still want to use Powershell from Xslt. So I tried this. function T-AddChecksum{    param ($inxml)    begin{       . pslib:\xml\invoke-transform.ps1       [xml]$xslt = @'<?xml version="1.0" encoding="utf-8"?><xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:msxsl="urn:schemas-microsoft-com:xslt" xmlns:cjb="cjb">  <xsl:template match="node()|@*">    <xsl:copy>      <xsl:apply-templates select="@*|node()"/>    </xsl:copy>  </xsl:template>  <xsl:template match="*[local-name()='file']">    <xsl:variable name="fname"><xsl:call-template name="get-path" /></xsl:variable>    <xsl:copy>      <xsl:apply-templates select="@*|node()"/>      <xsl:attribute name="Checksum"><xsl:value-of select="cjb:GetChecksum(string($fname))" /></xsl:attribute>    </xsl:copy>  </xsl:template>  <xsl:template name="get-path">    <xsl:for-each select="ancestor-or-self::*[not(@Root)]">      <xsl:value-of select="@Base" /><xsl:text>\</xsl:text><xsl:value-of select="@Name" />    </xsl:for-each>  </xsl:template></xsl:stylesheet>'@$func = @'    function GetChecksum([string]$file){        $stream = [System.IO.File]::OpenRead($file)        $sha256 = new-object System.Security.Cryptography.SHA256Managed        $checksum = $sha256.ComputeHash($stream)        [System.BitConverter]::ToString($checksum).Replace("-", [system.String]::Empty)    }'@       $pso = new-object psobject       $pso = add-member -inputobject $pso -membertype scriptmethod -name GetChecksum -value {$func} -PassThru    }    process{        if ($_ -is [xml]){            [xml](invoke-transform -inxml $_ -inxsl $xslt -extensionobjects @{"cjb"=$pso})        }    }    end{        if ($inxml -is [xml]){            [xml](invoke-transform -inxml $inxml -inxsl $xslt -extensionobjects @{"cjb"=$pso})        }    }} But that doesn't work. It is so long since I wrote it I can't remember if it worked, worked under version 1.0, worked in the CTP, seemed like a good idea that should work but didn't or it didn't work at all. Anyway it still looks like a good idea to me. This is the object created with C# in the previous example. PS > $cs|gmTypeName: ChecksumName MemberType Definition ---- ---------- ---------- Equals Method bool Equals(System.Object obj) GetChecksum Method string GetChecksum(string file)GetHashCode Method int GetHashCode() GetType Method type GetType() ToString Method string ToString() This is the Powershell object created with PSObject PS D:\powershell\temp> $pso|gmTypeName: System.Management.Automation.PSCustomObjectName MemberType Definition ---- ---------- ---------- Equals Method bool Equals(System.Object obj)GetHashCode Method int GetHashCode() GetType Method type GetType() ToString Method string ToString() GetChecksum ScriptMethod System.Object GetChecksum(); So it is not a valid Xslt extension object? Right, so we can call C# from Xslt via an extension object and we can call Powershell from C# so all we have to do is write a C# wrapper around the Powershell script and we can call Powershell from Xslt. function T-AddChecksum{    param ($inxml)    begin{        . pslib:\xml\invoke-transform.ps1        [xml]$xslt = @'<?xml version="1.0" encoding="utf-8"?><xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:msxsl="urn:schemas-microsoft-com:xslt" xmlns:cjb="cjb">  <xsl:template match="node()|@*">    <xsl:copy>      <xsl:apply-templates select="@*|node()"/>    </xsl:copy>  </xsl:template>  <xsl:template match="*[local-name()='file']">    <xsl:variable name="fname"><xsl:call-template name="get-path" /></xsl:variable>    <xsl:copy>      <xsl:apply-templates select="@*|node()"/>      <xsl:attribute name="Checksum"><xsl:value-of select="cjb:GetChecksum(string($fname))" /></xsl:attribute>    </xsl:copy>  </xsl:template>  <xsl:template name="get-path">    <xsl:for-each select="ancestor-or-self::*[not(@Root)]">      <xsl:value-of select="@Parent" /><xsl:text>\</xsl:text><xsl:value-of select="@Name" />    </xsl:for-each>  </xsl:template></xsl:stylesheet>'@        $code = @'using System;using System.Management.Automation;public partial class PSCaller{    ScriptBlock _script;    public PSCaller(ScriptBlock script) { Script = script; }    public ScriptBlock Script    {        get { return _script; }        set { _script = value; }    }    public String GetChecksum(String file)    {        try {            return (string)Script.Invoke(file)[0].BaseObject;        } catch(Exception ex) {            throw new InvalidOperationException("Script failed***!", ex);        }    }}'@       Add-Type -TypeDefinition $code       $psco = new-object PSCaller({            $file = $args[0]            $stream = [System.IO.File]::OpenRead($file)            $sha256 = new-object System.Security.Cryptography.SHA256Managed            $checksum = $sha256.ComputeHash($stream)            [System.BitConverter]::ToString($checksum).Replace("-", [system.String]::Empty)       })    }    process{        [xml](invoke-transform -inxml $_ -inxsl $xslt -extensionobjects @{"cjb"=$psco})    }    end{        [xml](invoke-transform -inxml $inxml -inxsl $xslt -extensionobjects @{"cjb"=$psco})    }} This works but it is a bit clunky. The name and signature 'String GetChecksum(String file)' is in the C# script and is seperate from the actual code that is in the Powershell script. A right mashup :-) What we need is a way of describing the signature and Powershell script in one place. I started looking at DSLs. Here is a simple class/method DSL $dsl = {    psclass public PSCaller{        method String GetChecksum([String]){            $file = $args[0]            $stream = [System.IO.File]::OpenRead($file)            $sha256 = new-object System.Security.Cryptography.SHA256Managed            $checksum = $sha256.ComputeHash($stream)            [System.BitConverter]::ToString($checksum).Replace("-", [system.String]::Empty)        }    }} I was using 'class' as the class keyword but it is now a Powershell reserved word so I am using 'psclass'. It can handle multiple methods with multiple arguments and the arguments can be any type i.e. [System.Xml.XmlElement]. This example code defines 1 method with the signature 'String GetChecksum([String])' followed by the script itself. It produces the following C# using System; using System.Collections; using System.Management.Automation; using System.Xml; public class PSCaller { ScriptBlock script0; public PSCaller(ScriptBlock Script0) { script0 = Script0; } public String GetChecksum(string p0) { ScriptBlock Script = script0; try { return (String)Script.Invoke(p0)[0].BaseObject; } catch (Exception ex) { throw new InvalidOperationException("Script failed***!", ex); } } } and an array of ScriptBlocks. It then compiles the code and passes the array of ScriptBlocks to the new object constructor. [string]$code = &$dslAdd-Type -TypeDefinition $code -ReferencedAssemblies System.xml$NewObject = new-object PSCaller($scriptArray) This is what the object looks like. PS > $newobject|gmTypeName: PSCaller2Name MemberType Definition ---- ---------- ---------- Equals Method bool Equals(System.Object obj) GetChecksum Method string GetChecksum(string p0) GetDifferentChecksum Method string GetDifferentChecksum(string p0)GetHashCode Method int GetHashCode() GetType Method type GetType() ToString Method string ToString() So this is what a Translet might look like function T-AddChecksum{    param ($inxml)    begin{       . pslib:\xml\invoke-transform.ps1       . pslib:\xml\Create-ObjectFromDSL.ps1        [xml]$xslt = @'<?xml version="1.0" encoding="utf-8"?><xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:msxsl="urn:schemas-microsoft-com:xslt" xmlns:cjb="cjb">  <xsl:template match="node()|@*">    <xsl:copy>      <xsl:apply-templates select="@*|node()"/>    </xsl:copy>  </xsl:template>  <xsl:template match="*[local-name()='file']">    <xsl:variable name="fname"><xsl:call-template name="get-path" /></xsl:variable>    <xsl:copy>      <xsl:apply-templates select="@*|node()"/>      <xsl:attribute name="Checksum"><xsl:value-of select="cjb:GetChecksum(string($fname))" /></xsl:attribute>    </xsl:copy>  </xsl:template>  <xsl:template name="get-path">    <xsl:for-each select="ancestor-or-self::*[not(@Root)]">      <xsl:value-of select="@Parent" /><xsl:text>\</xsl:text><xsl:value-of select="@Name" />    </xsl:for-each>  </xsl:template></xsl:stylesheet>'@                $dsl = {            psclass public PSCaller{                method String GetChecksum([String]){                    $file = $args[0]                    $stream = [System.IO.File]::OpenRead($file)                    $sha256 = new-object System.Security.Cryptography.SHA256Managed                    $checksum = $sha256.ComputeHash($stream)                    [System.BitConverter]::ToString($checksum).Replace("-", [system.String]::Empty)                }                method String GetDifferentChecksum([String]){                    $file = $args[0] #different                    $stream = [System.IO.File]::OpenRead($file)                    $sha256 = new-object System.Security.Cryptography.SHA256Managed                    $checksum = $sha256.ComputeHash($stream)                    [System.BitConverter]::ToString($checksum).Replace("-", [system.String]::Empty)                }            }        }        $psco = Create-ObjectFromDSL $dsl    }    process{        if ($_ -is [xml]){            [xml](invoke-transform -inxml $_ -inxsl $xslt -extensionobjects @{"cjb"=$psco})        }    }    end{        if ($inxml -is [xml]){            [xml](invoke-transform -inxml $inxml -inxsl $xslt -extensionobjects @{"cjb"=$psco})        }    }} To use it you need the Create-ObjectFromDSL.ps1 script. Here it is Create-ObjectFromDSL.zip (953.00 bytes) Why you would want to do this and what you would use it for is up to you. Most people would use Java, JavaScript or C# to extend Xslt. In most cases I would generate Xml from the things that Powershell can do well and pass that into a transform. But this just shows that is is partly possible to use Powershell as an Xslt scripting language and it adds another arrow to your quiver. Since we have to do the Create-ObjectFromDSL step anyway we could change the Xslt to something like this <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"xmlns:cjbxsl="urn:schemas-bayes-co-uk:xslt"xmlns:ucjb="urn:schemas-bayes-co-uk:extension-object" version="1.0">    <cjbxsl:script language="Powershell" implements-prefix="ucjb">        <cjbxsl:method name="GetChecksum" returns="string" args="string">            <![CDATA[                $file = $args[0]                $stream = [System.IO.File]::OpenRead($file)                $sha256 = new-object System.Security.Cryptography.SHA256Managed                $checksum = $sha256.ComputeHash($stream)                [System.BitConverter]::ToString($checksum).Replace("-", [system.String]::Empty)            ]]>        </cjbxsl:method>    </cjbxsl:script>    <xsl:template match="node()|@*">        <xsl:copy>            <xsl:apply-templates select="@*|node()" />        </xsl:copy>    </xsl:template>    <xsl:template match="*[local-name()='file']">        <xsl:variable name="fname">            <xsl:call-template name="get-path" />        </xsl:variable>        <xsl:copy>            <xsl:apply-templates select="@*|node()" />            <xsl:attribute name="Checksum">                <xsl:value-of select="ucjb:GetChecksum(string($fname))" />            </xsl:attribute>        </xsl:copy>    </xsl:template>    <xsl:template name="get-path">        <xsl:for-each select="ancestor-or-self::*[not(@Root)]">            <xsl:value-of select="@Parent" />            <xsl:text>\</xsl:text>            <xsl:value-of select="@Name" />        </xsl:for-each>    </xsl:template></xsl:stylesheet> then we can pass the stylesheet through another translet to create the C# and the extension object :-D. The output from that translet would be the extension object that is actually passed as the extension object to the original stylesheet and used in the pipeline. Oh dizzy! function T-CreateObjectFromPSScriptXslt{    param ($inxml)    begin{        . pslib:\xml\invoke-transform.ps1        $xslt = New-Object Xml.Xmldocument        $xslt.PSBase.PreserveWhitespace = $true        $xslt.LoadXml(@' <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"xmlns:cjbxsl="urn:schemas-bayes-co-uk:xslt" version="1.0">    <xsl:output method="text" />    <xsl:template match="cjbxsl:script">$code=@"        using System;        using System.Collections;        using System.Management.Automation;        using System.Xml;        public class <xsl:value-of select="@assembly-name" />        {        <xsl:apply-templates select="cjbxsl:method" mode="declarations" />        public <xsl:value-of select="@assembly-name" />(        <xsl:apply-templates select="cjbxsl:method" mode="constructor-args" />        )        {        <xsl:apply-templates select="cjbxsl:method" mode="constructor-body" />        }        <xsl:apply-templates select="cjbxsl:method" mode="methods" />        }"@Add-Type -TypeDefinition $code -ReferencedAssemblies System.xml$NewObject = New-Object <xsl:value-of select="@assembly-name" />(<xsl:apply-templates select="cjbxsl:method" mode="new-object" />)$NewObject    </xsl:template>    <xsl:template match="cjbxsl:method" mode="declarations">        public ScriptBlock script<xsl:value-of select="count(preceding::cjbxsl:method)" />;    </xsl:template>    <xsl:template match="cjbxsl:method" mode="constructor-args">        ScriptBlock Script<xsl:value-of select="count(preceding::cjbxsl:method)" />        <xsl:if test="not(position()=last())">, </xsl:if>    </xsl:template>    <xsl:template match="cjbxsl:method" mode="constructor-body">        script<xsl:value-of select="count(preceding::cjbxsl:method)" /> = Script<xsl:value-of select="count(preceding::cjbxsl:method)" />;    </xsl:template>    <xsl:template match="cjbxsl:method" mode="methods">        public <xsl:value-of select="@returns" /> <xsl:text>          </xsl:text> <xsl:value-of select="@name" />(<xsl:value-of select="@args" />)        {        ScriptBlock Script = script<xsl:value-of select="count(preceding::cjbxsl:method)" />;        try        {        return (String)Script.Invoke(p0)[0].BaseObject;        }        catch (Exception ex)        {        throw new InvalidOperationException("Script failed***!", ex);        }        }    </xsl:template>    <xsl:template match="cjbxsl:method" mode="new-object">        {<xsl:value-of select="." />}<xsl:if test="not(position()=last())">,</xsl:if>    </xsl:template>    <xsl:template match="@* | node()">        <xsl:apply-templates select="@* | node()" />    </xsl:template></xsl:stylesheet>'@)    }    process{        if ($_ -is [xml]){            invoke-transform -inxml $_ -inxsl $xslt        }    }    end{        if ($inxml -is [xml]){            invoke-transform -inxml $inxml -inxsl $xslt        }    }} And here is a stylesheet that will use it function T-AddChecksum{    param ($inxml)    begin{        . pslib:\xml\xslt\T-CreateObjectFromPSScriptXslt.ps1        . pslib:\xml\invoke-transform.ps1        [xml]$xslt = @' <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:cjbxsl="urn:schemas-bayes-co-uk:xslt" xmlns:ucjb="urn:cjb" version="1.0">    <cjbxsl:script language="Powershell" implements-prefix="ucjb" assembly-name="ChecksumSHA3">        <cjbxsl:method name="GetChecksum" returns="string" args="string p0">            <![CDATA[                $file = $args[0]                $stream = [System.IO.File]::OpenRead($file)                $sha256 = new-object System.Security.Cryptography.SHA256Managed                $checksum = $sha256.ComputeHash($stream)                [System.BitConverter]::ToString($checksum).Replace("-", [system.String]::Empty)            ]]>        </cjbxsl:method>        <cjbxsl:method name="GetChecksum384" returns="string" args="string p0">            <![CDATA[                $file = $args[0]                $stream = [System.IO.File]::OpenRead($file)                $sha384 = new-object System.Security.Cryptography.SHA384Managed                $checksum = $sha384.ComputeHash($stream)                [System.BitConverter]::ToString($checksum).Replace("-", [system.String]::Empty)            ]]>        </cjbxsl:method>    </cjbxsl:script>    <xsl:template match="node()|@*">        <xsl:copy>            <xsl:apply-templates select="@*|node()" />        </xsl:copy>    </xsl:template>    <xsl:template match="*[local-name()='file']">        <xsl:variable name="fname">            <xsl:call-template name="get-path" />        </xsl:variable>        <xsl:copy>            <xsl:apply-templates select="@*|node()" />            <xsl:attribute name="Checksum">                <xsl:value-of select="ucjb:GetChecksum(string($fname))" />            </xsl:attribute>        </xsl:copy>    </xsl:template>    <xsl:template name="get-path">        <xsl:for-each select="ancestor-or-self::*[not(@Root)]">            <xsl:value-of select="@Parent" />            <xsl:text>\</xsl:text>            <xsl:value-of select="@Name" />        </xsl:for-each>    </xsl:template></xsl:stylesheet>'@        $psco = Invoke-Expression (T-CreateObjectFromPSScriptXslt $xslt)    }    process{        if ($_ -is [xml]){            [xml](invoke-transform -inxml $_ -inxsl $xslt -extensionObjects @{ucjb=$psco})        }    }    end{        if ($inxml -is [xml]){            [xml](invoke-transform -inxml $inxml -inxsl $xslt -extensionObjects @{ucjb=$psco})        }    }} Voila!!! Powershell as an xslt scripting language.
Another way to extend Xslt is to pass an extension object into the transform.  function T-AddChecksum{    param ($inxml)    begin{        . pslib:\xml\invoke-transform.ps1    [xml]$xslt = @' <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:msxsl="urn:schemas-microsoft-com:xslt"    xmlns:cjb="cjb" version="1.0">    <xsl:template match="node()|@*">        <xsl:copy>            <xsl:apply-templates select="@*|node()" />        </xsl:copy>    </xsl:template>    <xsl:template match="*[local-name()='file']">        <xsl:variable name="fname">            <xsl:call-template name="get-path" />        </xsl:variable>        <xsl:copy>            <xsl:apply-templates select="@*|node()" />            <xsl:attribute name="Checksum">                <xsl:value-of select="cjb:GetChecksum(string($fname))" />            </xsl:attribute>        </xsl:copy>    </xsl:template>    <xsl:template name="get-path">        <xsl:for-each select="ancestor-or-self::*[not(@Root)]">            <xsl:value-of select="@Parent" />            <xsl:text>\</xsl:text>            <xsl:value-of select="@Name" />        </xsl:for-each>    </xsl:template></xsl:stylesheet>'@$code = @'public class Checksum{    public System.String GetChecksum(System.String file) {        using (System.IO.FileStream stream = System.IO.File.OpenRead(file))        {            System.Security.Cryptography.SHA256Managed sha = new System.Security.Cryptography.SHA256Managed();            byte[] checksum = sha.ComputeHash(stream);            return System.BitConverter.ToString(checksum).Replace("-", System.String.Empty);        }   }}'@        Add-Type -TypeDefinition $code        $cs = new-object Checksum    }    process{        if ($_ -is [xml]){            [xml](invoke-transform -inxml $_ -inxsl $xslt -extensionobjects @{"cjb"=$cs})        }    }    end{        if ($inxml -is [xml]){            [xml](invoke-transform -inxml $inxml -inxsl $xslt -extensionobjects @{"cjb"=$cs})        }    }} It can be used the same way as before PS> . .\T-AddChecksum.ps1 #dot source transletPS> . .\Get-DirAsXml.ps1 #dot source Get-DirAsXml PS> Get-DirAsXml D:\powershell\test -props @{Length=""} | T-AddChecksum Here is the code T-AddChecksumCObject.zip (1.08 kb)
Here are ways to write translets that will do more than the just an Xslt transform. You might want to add an MD5 or SHA256 checksum to all the files within a tree. Xslt doesn't do this natively so you need to extend it. Here is one way it uses C# as the Xslt scripting language function T-AddChecksum{    param ($inxml)    begin{        . .\invoke-transform.ps1       [xml]$xslt = @' <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:msxsl="urn:schemas-microsoft-com:xslt" xmlns:ucjb="urn:cjb" version="1.0">    <msxsl:script language="C#" implements-prefix="ucjb"> public string GetChecksum(String file) { using (System.IO.FileStream stream = System.IO.File.OpenRead(file)) { System.Security.Cryptography.SHA256Managed sha = new System.Security.Cryptography.SHA256Managed(); byte[] checksum = sha.ComputeHash(stream); return System.BitConverter.ToString(checksum).Replace("-", System.String.Empty); } }    </msxsl:script>    <xsl:template match="node()|@*">        <xsl:copy>            <xsl:apply-templates select="@*|node()" />        </xsl:copy>    </xsl:template>    <xsl:template match="*[local-name()='file']">        <xsl:variable name="fname">            <xsl:call-template name="get-path" />        </xsl:variable>        <xsl:copy>            <xsl:apply-templates select="@*|node()" />            <xsl:attribute name="Checksum">                <xsl:value-of select="ucjb:GetChecksum(string($fname))" />            </xsl:attribute>        </xsl:copy>    </xsl:template>    <xsl:template name="get-path">        <xsl:for-each select="ancestor-or-self::*[not(@Root)]">            <xsl:value-of select="@Parent" />            <xsl:text>\</xsl:text>            <xsl:value-of select="@Name" />        </xsl:for-each>    </xsl:template></xsl:stylesheet>'@    }    process{        if ($_ -is [xml]){            [xml](invoke-transform -inxml $_ -inxsl $xslt)        }    }    end{        if ($inxml -is [xml]){            [xml](invoke-transform -inxml $inxml -inxsl $xslt)        }    }} It can be used like this PS> . .\T-AddChecksum.ps1 #dot source transletPS> . .\Get-DirAsXml.ps1 #dot source Get-DirAsXml PS> Get-DirAsXml D:\powershell\test -props @{Length=""} | T-AddChecksum or PS> T-AddChecksum [xml](gc .\tmp.xml ) and might produce <root Name="root" Root="True" Date="2009/11/03 05:45:37">    <folder Name="test" Base="D:\powershell\test">        <file Name="test.txt" Length="836" Checksum="0D7439F5894B4E8EFEC8FB409635D0D8EA7A450E902F6B30B335907B5867DF16" />        <file Name="test.ps1" Length="3330" Checksum="C47313D06C6AADA288AF6D61E03EFD7FA7C52DD73AB097E9D556535D330798D3" />        <file Name="test.zip" Length="1290" Checksum="7F2CCA02F17FF0E9458C0777C659D6D00B80F1C9D2921AEC971AE9A82D296AA5" />        <file Name="tmp.xml" Length="4383" Checksum="1351245F9834D0406C42DD5AF622FCA691A9A36F440A7C88F389927800292303" />    </folder></root> Here is the code T-AddChecksumCScript.zip (1.03 kb)