Summary: Microsoft MVP, Shay Levy, shows how to use Windows PowerShell proxy functions to extend the capability of Windows PowerShell cmdlets.
Hey, Scripting Guy! I need to be able to modify the behavior of existing Windows PowerShell cmdlets. Is this something that can be accomplished by using “normal” tools, or do I need to reverse engineer them by using Visual Studio?
—LS
Hello LS,
Microsoft Scripting Guy, Ed Wilson, here. Today our guest blogger is Shay Levy.
Here is a bit of information about Shay: Windows PowerShell MVP Blog: http://PowerShay.com Follow Shay on Twitter: https://twitter.com/ShayLevy Download the PowerShell Community Toolbar: http://tinyurl.com/PSToolbar
Have you ever had a thought like, “I wish that cmdlet had that parameter?”
I have had several thoughts like that one, and with proxy functions, I was able to implement them on my system. For example, take a look at a snippet that reads XML file content and casts the content to an XML document object:
PS > $xml = [xml](Get-Content -Path c:\test.xml)
Wouldn’t it be nice if the Get-Content Windows PowerShell cmdlet had an AsXml parameter? We could write the same command as shown here.
PS > Get-Content -Path c:\test.xml -AsXml
Windows PowerShell 2.0 includes a new and exciting feature called proxy commands. Proxy commands are used primarily by the remoting mechanism in Windows PowerShell. We can use it in restricted runspace configurations where we can modify sessions by adding or removing certain commands, providers, cmdlet parameters, and every other component of a Windows PowerShell session. For instance, we can remove the ComputerName parameter from all cmdlets that support that parameter.
Another major use for proxy commands is performed in implicit remoting. In a nutshell, implicit remoting lets you use commands from a remote session inside your local session (by using the Import-PSSession cmdlet). For example, say you manage an Exchange Server 2010 server and you don’t have the Exchange Server admin tools installed on your admin computer. Normally, you would create a Remote Desktop session, launch the Exchange Management Shell, and perform your tasks.
With implicit remoting, you can create a PSSession to the Exchange Server and import the Exchange commands into your local session. When you do that, Windows PowerShell dynamically creates a module in your TEMP folder, and the imported commands are converted to proxy functions. From now on, each time you run an Exchange command in your local session, it will run on the remote server. This feature enables us to manage multiple servers without installing admin tools locally on each server!
So what is a proxy function? It is a wrapper function around a cmdlet (or another function). When we proxy a command, we get access to its parameters (which we can add or remove). We also get control over the command steppable pipeline, which is the three script blocks of a Windows PowerShell function: Begin, Process, and End.
In this post, I want to show you how easy it is to create a proxy function and how you can harness its power to extend existing cmdlets with new functionality by adding new parameters.
The process of creating a proxy function is relatively easy (you can read more about it here). You decide which cmdlet you want to wrap and then get the cmdlet metadata (command line syntax). In the following example, we get the code for the Get-ChildItem cmdlet.
PS > $MetaData = New-Object System.Management.Automation.CommandMetaData (Get-Command ConvertTo-Html) PS > [System.Management.Automation.ProxyCommand]::Create($MetaData)
[CmdletBinding(DefaultParameterSetName=’Items’, SupportsTransactions=$true)] param( [Parameter(ParameterSetName=’Items’, Position=0, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)] [System.String[]] ${Path},
[Parameter(ParameterSetName=’LiteralItems’, Mandatory=$true, Position=0, ValueFromPipelineByPropertyName=$true)] [Alias(‘PSPath’)] [System.String[]] ${LiteralPath},
[Parameter(Position=1)] [System.String] ${Filter},
[System.String[]] ${Include},
[System.String[]] ${Exclude},
[Switch] ${Recurse},
[Switch] ${Force},
[Switch] ${Name})
begin { try { $outBuffer = $null if ($PSBoundParameters.TryGetValue(‘OutBuffer’, [ref]$outBuffer)) { $PSBoundParameters[‘OutBuffer’] = 1 } $wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand(‘Get-ChildItem’, [System.Management.Automation.Command Types]::Cmdlet) $scriptCmd = {& $wrappedCmd @PSBoundParameters } $steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin) $steppablePipeline.Begin($PSCmdlet) } catch { throw } }
process { try { $steppablePipeline.Process($_) } catch { throw } }
end { try { $steppablePipeline.End() } catch { throw } } <#
.ForwardHelpTargetName Get-ChildItem .ForwardHelpCategory Cmdlet
#>
Don’t get intimidated by the result. This is what a proxy function looks like. As you can see, the generated code starts by including all the cmdlet parameters and then defines the script blocks of the command. At the end, there are instructions for the Get-Help cmdlet.
In the Begin block, the Get-ChildItem command ($wrappedCmd) is retrieved and the parameters are passed to it (@PSBoundParameters). A steppable pipeline is initialized ($steppablePipeline), which invokes the cmdlet ($scriptCmd), and finally the Begin starts. At the end of the code sample, you can find a multiline comment that is used to instruct the Help that Get-Help should display when you type Get-Help <ProxyFunction>.
After this long introduction, it is time to introduce the proxy function that we are going to work on. For this blog, I chose to wrap the ConvertTo-Html cmdlet. It can do everything that the ConvertTo-Html cmdlet is capable of with two additional enhancements: It will be able to save the generated markup language (HTML) to a file and invoke the file in the default browser.
Consider the following command:
PS > Get-Process | Select-Object -Property Name,Id | ConvertTo-Html -FilePath gps.html -Invoke
Note the FilePath and Invoke parameters. These parameters are not a part of the default ConvertTo-Html parameters; they are added by our proxy function.
If we wanted to use the default Windows PowerShell core cmdlets to achieve the same result, we would have to write it as follows:
PS > Get-Process | Select-Object -Property Name,Id | ConvertTo-Html | Out-File -FilePath gps.html PS > Invoke-Item -Path gps.html
So, in essence we are creating a shortcut of some sort, with the added advantage that the proxy command is easier to read and understand. The output results for both of the previous commands is the same.
So let’s get started. First, we need to generate the proxy command for the ConvertTo-Html cmdlet. We will redirect the code to a new file and edit the file in the Windows PowerShell Integrated Scripting Environment (ISE) as seen here.
PS > $MetaData = New-Object System.Management.Automation.CommandMetaData (Get-Command ConvertTo-Html) PS > [System.Management.Automation.ProxyCommand]::Create($MetaData) | Out-File -FilePath C:\ConvertTo-Html.ps1 PS > ise C:\ConvertTo-Html.ps1
The Windows PowerShell ISE is shown here with the code generated by the previous command.
Now let’s add a new parameter to the parameters section in the script file. (The two new parameters FilePath and Invoke are shown in bold text.)
[ValidateNotNullOrEmpty()] [System.String[]] ${PreContent},
[ValidateNotNullOrEmpty()] [System.String] ${FilePath},
[Switch] ${Invoke} )
The main logic of the following code is inside the Begin script block (comments are inline, and code modifications are in bold text).
begin { try { $outBuffer = $null if ($PSBoundParameters.TryGetValue(‘OutBuffer’, [ref]$outBuffer)) { $PSBoundParameters[‘OutBuffer’] = 1 } # make sure we are wrapping a core cmdlet by specifiyng the command qualified name $wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand(‘Microsoft.PowerShell.Utility\ConvertTo-Html’, [System.Management.Automation.CommandTypes]::Cmdlet)
# remove the FilePath parameter if it was specified, otherwise it will get to the core cmdlet and we’ll get an error if($PSBoundParameters[‘FilePath’]) { $null = $PSBoundParameters.Remove(‘FilePath’) # redirect the output to the a file $scriptCmd = {& $wrappedCmd @PSBoundParameters | Out-File $FilePath} # if Invoke has been specified – remove it and invoke the file if($PSBoundParameters[‘Invoke’]) { $null = $PSBoundParameters.Remove(‘Invoke’) Invoke-Item $FilePath } } else { $scriptCmd = {& $wrappedCmd @PSBoundParameters } } $steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin) $steppablePipeline.Begin($PSCmdlet) } catch { throw } }
Finally, the code is wrapped in a function declaration that has the same name as the cmdlet, as shown here:
function ConvertTo-Html {
proxy function generated code
}
By choosing the same name as the cmdlet, we can take advantage of the command discovery process in Windows PowerShell. Basically, if you have a cmdlet and a function with the same name, the function takes precedence. Here is the order of command precedence that is used by the command discovery process in Windows PowerShell:
· Alias: All Windows PowerShell aliases in the current session
· Filter/Function: All Windows PowerShell functions
· Cmdlet: The cmdlets in the current session (“Cmdlet” is the default)
· ExternalScript: All .ps1 files in the paths that are listed in the Path environment variable ($env:PATH)
· Application: All non-Windows-PowerShell files in paths that are listed in the Path environment variable
· Script: Script blocks in the current session
There is one gotcha to using the same name. If you need to call the core ConvertTo-Html cmdlet, you need to specify its module/snap-in fully qualified name. For the ConvertTo-Html cmdlet, that name is seen here.
PS > Microsoft.PowerShell.Utility\ConvertTo-Html ….
More than likely, you will not know what that module/snap-in fully qualified name is. Luckily, you can use Windows PowerShell to get the full name of a cmdlet by using the following command.
PS > $cmd = Get-Command -Name ConvertTo-Html PS > Join-Path -Path $cmd.ModuleName -ChildPath $cmd.Name Microsoft.PowerShell.Utility\ConvertTo-Html
The same technique applies for getting Help. To get the Help for the core cmdlet and specify its full name, you would use the command shown here.
PS > Get-help Microsoft.PowerShell.Utility\ConvertTo-Html
If you do not like this behavior, you can give the proxy function a different name.
Another gotcha occurs when you are using the Get-Help cmdlet. By default, Get-Help displays the Help content of a cmdlet, but if we have two commands with the same name, Get-Help lists the two commands (note the Category column):
PS > Get-Help ConvertTo-Html
Name Category Synopsis
—- ——– ——–
ConvertTo-Html Function ConvertTo-Html [[-Property] ] [[-Head] ] [[-Title] ] [[-Body] < browser.>
ConvertTo-Html Cmdlet Converts Microsoft .NET Framework objects into HTML that can be…
To load the proxy command into your session, simply “dot-source” it as follows.
PS > . C:\ConvertTo-Html.ps1
Now, let us try the command that we used previously with the new proxy command.
Get-Process | Select-Object -Property Name,Id | ConvertTo-Html -FilePath gps.html -Invoke
You can see that it works great, as shown here.
That is it. We now have a proxy function that wraps a cmdlet with additional functionality. You can download the function file here.
With regard to the AsXml parameter mentioned earlier…I already have this functionality in my profile, but I strongly recommend that you file a suggestion on Microsoft’s Connect website so we all can get the benefit of having new parameters in future releases of Windows PowerShell. (Here’s my suggestion for AsXml support—feel free to add your vote.) Until then, we can add this kind of functionality to core Windows PowerShell cmdlets right now!
LS, that is all there is to using Windows PowerShell proxy functions to extend the capabilities of Windows PowerShell cmdlets. Thank you, Shay, for your hard work and for sharing it with us. Guest Blogger Week will continue tomorrow when Rob Campbell will join us.
I invite you to follow me on Twitter and Facebook. If you have any questions, send email to me at scripter@microsoft.com, or post your questions on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.
Ed Wilson, Microsoft Scripting Guy
0 comments