PowerShell Proxy Functions


In a previous article, I've mentioned "proxy functions" in PowerShell. Essentially, a proxy function is something that you write, often as part of a script module, and load into the shell. The proxy function's name matches the name of some existing cmdlet. By loading the proxy function, you are "hiding," or "shadowing," the original function. When someone runs the cmdlet, they get your function instead.

This is a neat trick, because it allows you to override certain portions of the original cmdlet, or even add to its functions.

There's a great example at http://blogs.technet.com/b/heyscriptingguy/archive/2011/03/01/proxy-functions-spice-up-your-powershell-core-cmdlets.aspx, and I'll give you another.

Normally, the Export-CSV cmdlet defaults to using a comma as its delimiter. You can change that using the -Delimiter parameter, and one common alternate delimiter is a tab character. It can be tricky to figure out how to type a tab character, though, and so I want to modify the Export-CSV cmdlet to have a -UseTab parameter. Instead of running:

Get-Service | Export-CSV "services.tdf" -Delimiter "`t"


I want to run:

Get-Service | Export-CSV "services.tdf" -UseTab


To do so, I'll need to create a brand-new function named Export-CSV. It'll need to expose all of the original cmdlets' parameters, which sounds like a pain in the neck - except that PowerShell gives you a tricky way of getting that information. I'll start with this:

$Metadata = New-Object System.Management.Automation.CommandMetaData (Get-Command Export-CSV)
[System.Management.Automation.ProxyCommand]::Create($Metadata) | Out-File c:\mine.ps1

Run that from the command-line, and it'll spew out a proxy function for you, saved into C:\Mine.ps1. Just open that file in a script editor to start working on it. I rearranged the output a bit to move the comment-based help to the front, and I surrounded PowerShell's code with a function declaration. So I've got this:

function Export-CSV {
<#

.ForwardHelpTargetName Export-Csv
.ForwardHelpCategory Cmdlet

#>
    [CmdletBinding(DefaultParameterSetName='Delimiter', SupportsShouldProcess=$true, ConfirmImpact='Medium')]
    param(
        [Parameter(Mandatory=$true, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)]
        [System.Management.Automation.PSObject]
        ${InputObject},

        [Parameter(Mandatory=$true, Position=0)]
        [Alias('PSPath')]
        [System.String]
        ${Path},

        [Switch]
        ${Force},

        [Switch]
        ${NoClobber},

        [ValidateSet('Unicode','UTF7','UTF8','ASCII','UTF32','BigEndianUnicode','Default','OEM')]
        [System.String]
        ${Encoding},

        [Parameter(ParameterSetName='Delimiter', Position=1)]
        [ValidateNotNull()]
        [System.Char]
        ${Delimiter},

        [Parameter(ParameterSetName='UseCulture')]
        [Switch]
        ${UseCulture},

        [Alias('NTI')]
        [Switch]
        ${NoTypeInformation})

    begin
    {
        try {
            $outBuffer = $null
            if ($PSBoundParameters.TryGetValue('OutBuffer', [ref]$outBuffer))
            {
                $PSBoundParameters['OutBuffer'] = 1
            }
            $wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Export-Csv', [System.Management.Automation.CommandTypes]::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
        }
    }
}

Now I'll make a few changes. See if you can spot them - but I'll go over them. I'm adding one parameter, -UseTab, and I'm modifying the contents of the "begin" block:

function Export-CSV {
<#

.ForwardHelpTargetName Export-Csv
.ForwardHelpCategory Cmdlet

#>
    [CmdletBinding(DefaultParameterSetName='Delimiter', SupportsShouldProcess=$true, ConfirmImpact='Medium')]
    param(
        [Parameter(Mandatory=$true, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)]
        [System.Management.Automation.PSObject]
        ${InputObject},

        [Parameter(Mandatory=$true, Position=0)]
        [Alias('PSPath')]
        [System.String]
        ${Path},

        [Switch]
        ${Force},

        [Switch]
        ${NoClobber},

        [ValidateSet('Unicode','UTF7','UTF8','ASCII','UTF32','BigEndianUnicode','Default','OEM')]
        [System.String]
        ${Encoding},

        [Parameter(ParameterSetName='Delimiter', Position=1)]
        [ValidateNotNull()]
        [System.Char]
        ${Delimiter},

        [Parameter(ParameterSetName='UseCulture')]
        [Switch]
        ${UseCulture},

        [Alias('NTI')]
        [Switch]
        ${NoTypeInformation},
        
        [Switch]
        $UseTab)
        

    begin
    {
        try {
            $outBuffer = $null
            if ($PSBoundParameters.TryGetValue('OutBuffer', [ref]$outBuffer))
            {
                $PSBoundParameters['OutBuffer'] = 1
            }
            $wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Export-Csv', [System.Management.Automation.CommandTypes]::Cmdlet)

            if ($PSBoundParameters['UseTab']) {
                $PSBoundParameters.Remove('UseTab') | Out-Null
                If ($PSBoundParameters['Delimiter']) { 
                    $PSBoundParameters.Remove('Delimiter') | Out-Null
                }
                $scriptCmd = {& $wrappedCmd @PSBoundParameters -Delimiter "`t"}
            } else {
                $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
        }
    }
}

Note that I didn't include the UseTab parameter name in curly brackets. I could have done so, and it would have been consistent with the way PowerShell generated the other parameter names, but I don't need to. 

Within the "begin" block is where the magic happens:

        if ($PSBoundParameters['UseTab']) {
            $PSBoundParameters.Remove('UseTab') | Out-Null
            If ($PSBoundParameters['Delimiter']) { 
                $PSBoundParameters.Remove('Delimiter') | Out-Null
            }
            $scriptCmd = {& $wrappedCmd @PSBoundParameters -Delimiter "`t"}
        } else {
            $scriptCmd = {& $wrappedCmd @PSBoundParameters }
        } 

Let's walk through that logic.

First, a built-in variable called $PSBoundParameters contains all of the parameters that the user typed with they ran the function. I'm checking to see if my -UseTab was one of them. If it is, I remove it from $PSBoundParameters, because I don't want my -UseTab being passed through to the real Export-CSV. I'm also going to check for the -Delimiter parameter and, if it exist, remove it so that I can specify my own. Finally, I'm using the provid $wrappedCmd variable (you can actually see in the Begin block where this variable is created), which represents the real Export-CSV, to run the command with the parameters typed by the user and my own -Delimiter parameter.

If the user didn't specify -UseTab, then I just run the real Export-CSV and pass along whatever parameters the user typed.

In either case, $scriptCmd is being filled with the resulting cmdlet, which is then started at the end of the Begin block.

This is a real cool trick. You can add parameters, take them away, and so forth, meaning you can kind of customize the interface that someone has to accomplish tasks. This is definitely an advanced trick; you need to know a bit about how cmdlets work, and each cmdlet's proxy function will be slightly different. But with a bit of practice, you can do some really neat stuff!

Want to ask a question about this article? I'll answer at http://powershell.com/cs/forums/230.aspx!



Please or Register to post comments.

What's PowerShell with a Purpose Blog?

Don Jones demystifies Windows PowerShell.

Blog Archive

Sponsored Introduction Continue on to (or wait seconds) ×