Proxy Functions
In PowerShell, a proxy function is a specific kind of wrapper function. That is, it “wraps” around an existing command, usually with the intent of either:
- Removing functionality
- Hard-coding functionality and removing access to it
- Adding functionality
In some cases, a proxy command is meant to “replace” an existing command. This is done by giving the proxy the same name as the command it wraps; since the proxy gets loaded into the shell last, it’s the one that gets run when you run the command name.
For Example
You’re probably familiar with PowerShell’s ConvertTo-HTML command. We’d like to make a version that “replaces” the existing command, providing full access to it but always injecting a particular CSS style sheet, so that the resulting HTML can be a bit prettier.
Creating the Proxy Base
PowerShell automates the first step, which is generating a “wrapper” that exactly duplicates whatever command you’re wrapping. Here’s how to use it (we’ll put our results into a Step1 subfolder in this chapter’s sample code):
$cmd = New-Object System.Management.Automation.CommandMetaData (Get-Command ConvertT\
o-HTML)
[System.Management.Automation.ProxyCommand]::Create($cmd) |
Out-File ConvertToHTMLProxy.ps1
Here’s the rather lengthy result (once again, apologies for the backslashes, which represent line-wrapping; it’s unavoidable in this instance, but the downloadable sample code won’t show them):
[CmdletBinding(DefaultParameterSetName='Page',
HelpUri='http://go.microsoft.com/fwlink/?LinkID=113290',
RemotingCapability='None')]
param(
[Parameter(ValueFromPipeline=$true)]
[PSObject]
${InputObject},
[Parameter(Position=0)]
[System.Object[]]
${Property},
[Parameter(ParameterSetName='Page', Position=3)]
[string[]]
${Body},
[Parameter(ParameterSetName='Page', Position=1)]
[string[]]
${Head},
[Parameter(ParameterSetName='Page', Position=2)]
[ValidateNotNullOrEmpty()]
[string]
${Title},
[ValidateNotNullOrEmpty()]
[ValidateSet('Table','List')]
[string]
${As},
[Parameter(ParameterSetName='Page')]
[Alias('cu','uri')]
[ValidateNotNullOrEmpty()]
[uri]
${CssUri},
[Parameter(ParameterSetName='Fragment')]
[ValidateNotNullOrEmpty()]
[switch]
${Fragment},
[ValidateNotNullOrEmpty()]
[string[]]
${PostContent},
[ValidateNotNullOrEmpty()]
[string[]]
${PreContent})
begin
{
try {
$outBuffer = $null
if ($PSBoundParameters.TryGetValue('OutBuffer', [ref]$outBuffer))
{
$PSBoundParameters['OutBuffer'] = 1
}
$wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Microsoft.PowerShe\
ll.Utility\ConvertTo-Html',
[System.Management.Automation.CommandTypes]::Cmdlet)
$scriptCmd = {& $wrappedCmd @PSBoundParameters }
$steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOr\
igin)
$steppablePipeline.Begin($PSCmdlet)
} catch {
throw
}
}
process
{
try {
$steppablePipeline.Process($_)
} catch {
throw
}
}
end
{
try {
$steppablePipeline.End()
} catch {
throw
}
}
<#
.ForwardHelpTargetName Microsoft.PowerShell.Utility\ConvertTo-Html
.ForwardHelpCategory Cmdlet
#>
This isn’t wrapped in a function, so that’s the first thing we’ll do in the next step (which we’ll put into a file in Step 2, so you can differentiate).
Modifying the Proxy
In addition to wrapping our proxy code in a function, we’re going to play with the -Head parameter. We’re not going to remove access to it; we want users to be able to pass content to -Head. We just want to intercept it, and add our stylesheet to it, before letting the underlying ConvertTo-HTML command take over. So we’ll need to test and see if our command was even run with -Head or not, and if it was, grab that content and concatenate our own. The final result:
function NewConvertTo-HTML {
[CmdletBinding(DefaultParameterSetName = 'Page',
HelpUri = 'http://go.microsoft.com/fwlink/?LinkID=113290',
RemotingCapability = 'None')]
param(
[Parameter(ValueFromPipeline = $true)]
[PSObject]$InputObject,
[Parameter(Position = 0)]
[System.Object[]]$Property,
[Parameter(ParameterSetName = 'Page', Position = 3)]
[string[]]$Body,
[Parameter(ParameterSetName = 'Page', Position = 1)]
[string[]]$Head,
[Parameter(ParameterSetName = 'Page', Position = 2)]
[ValidateNotNullOrEmpty()]
[string]$Title,
[ValidateNotNullOrEmpty()]
[ValidateSet('Table', 'List')]
[string]$As,
[Parameter(ParameterSetName = 'Page')]
[Alias('cu', 'uri')]
[ValidateNotNullOrEmpty()]
[uri]$CssUri,
[Parameter(ParameterSetName = 'Fragment')]
[ValidateNotNullOrEmpty()]
[switch]$Fragment,
[ValidateNotNullOrEmpty()]
[string[]]$PostContent,
[ValidateNotNullOrEmpty()]
[string[]]$PreContent
begin {
try {
$outBuffer = $null
if ($PSBoundParameters.TryGetValue('OutBuffer', [ref]$outBuffer)) {
$PSBoundParameters['OutBuffer'] = 1
}
$wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Microsoft.Powe\
rShell.Utility\ConvertTo-Html',
[System.Management.Automation.CommandTypes]::Cmdlet)
# create our css
$css += @'
<style>
th { color:white; background-color: black;}
body { font-family: Calibri; padding: 2px }
</style>
'@
# was -head specified?
if ($PSBoundParameters.ContainsKey('head')) {
$PSBoundParameters.head += $css
}
else {
$PSBoundParameters += @{'Head' = $css }
}
$scriptCmd = { & $wrappedCmd @PSBoundParameters }
$steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.Comma\
ndOrigin)
$steppablePipeline.Begin($PSCmdlet)
}
catch {
throw
}
}
process {
try {
$steppablePipeline.Process($_)
}
catch {
throw
}
}
end {
try {
$steppablePipeline.End()
}
catch {
throw
}
}
<#
.ForwardHelpTargetName Microsoft.PowerShell.Utility\ConvertTo-Html
.ForwardHelpCategory Cmdlet
#>
}
Our changes begin at around line 50, with the #create our css comment.
# create our css
$css += @'
<style>
th { color:white; background-color: black;}
body { font-family: Calibri; padding: 2px }
</style>
'@
Under that, we check to see if -head had been specified; if it was, we append our CSS to it. If not, we add a “head” parameter to $PSBoundParameters. Then we let the proxy function continue just as normal.
Adding or Removing Parameters
You’re likely to run into occasions when you do want to add or remove a parameter. For example, a new parameter might simplify usage or unlock functionality; removing a parameter might enable you to hard-code a value that the user shouldn’t be changing. The real key is the $PSBoundParametersCollection.
Adding a Parameter
Adding a parameter is as easy as declaring it in your proxy function’s Param() block. Add whatever attributes you like, and you’re good to go. You just want to remove the added parameter from $PSBoundParameters before the underlying command executes, since that command won’t know what to do with your new parameter.
$PSBoundParameters.Remove('MyNewParam')
$scriptCmd = {& $wrappedCmd @PSBoundParameters }
Just remove it before that $scriptCmd line, and you’re good to go.
Removing a Parameter
This is even easier - just delete the parameter from the Param() block! If you’re removing a mandatory parameter, you’ll need to internally provide a value with it. For example:
$PSBoundParameters += @{'RemovedParam'=$MyValue}
$scriptCmd = {& $wrappedCmd @PSBoundParameters }
This will re-connect the -RemovedParam parameter, feeding it whatever’s in $MyValue, before running the underlying command.
Your Turn
Now it’s your turn to create a proxy function.
Start Here
In this exercise, you’ll be extending the Export-CSV command. However, you’re not going to “overwrite” the existing command. Instead, you’ll be creating a new command that uses Export-CSV under the hood.
Your Task
Create a proxy function named Export-TDF. This should be a wrapper around Export-CSV, and should not include a -Delimiter parameter. Instead, it should hard-code the delimiter to be a tab. Hint: you can specify a tab by putting a backtick, followed by the letter “t,” inside double quotes.
Our Take
Here’s what we came up with - also in the lab results folder in the downloadable code.
function Export-TDF {
[CmdletBinding(DefaultParameterSetName = 'Delimiter',
SupportsShouldProcess,
ConfirmImpact = 'Medium')]
param(
[Parameter(
Mandatory,
ValueFromPipeline,
ValueFromPipelineByPropertyName
)]
[PSObject]$InputObject,
[Parameter(Position = 0)]
[ValidateNotNullOrEmpty()]
[string]$Path,
[Alias('PSPath')]
[ValidateNotNullOrEmpty()]
[string]$LiteralPath,
[switch]$Force,
[Alias('NoOverwrite')]
[switch]$NoClobber,
[ValidateSet('Unicode', 'UTF7', 'UTF8', 'ASCII', 'UTF32',
'BigEndianUnicode', 'Default', 'OEM')]
[string]$Encoding,
[switch]$Append,
[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('Microsoft.Powe\
rShell.Utility\Export-Csv',
[System.Management.Automation.CommandTypes]::Cmdlet)
$PSBoundParameters += @{'Delimiter' = "`t" }
$scriptCmd = { & $wrappedCmd @PSBoundParameters }
$steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.Comma\
ndOrigin)
$steppablePipeline.Begin($PSCmdlet)
}
catch {
throw
}
}
process {
try {
$steppablePipeline.Process($_)
}
catch {
throw
}
}
end {
try {
$steppablePipeline.End()
}
catch {
throw
}
}
} #close function
We just removed one parameter definition and added one line of code to hard-code the delimiter. We removed the {} around the parameter names and lined things up in the Param() block the way we would normally write code. We also removed the forwarded help links. We would still need to create new comment-based help for this command. Probably by copying a lot of the help from the original command.
Let’s Review
See if you can answer a couple of questions on proxy functions:
- The boilerplate proxy function behaves exactly like what?
- If you define an additional parameter in a proxy function, what must you do before the wrapped command is allowed to run?
- If you delete a non-mandatory parameter definition in a proxy function, what must you do before the wrapped command is allowed to run?
Review Answers
Here are our answers:
- The command it wraps.
- Remove the new parameter from
$PSBoundParameters. - You don’t need to do anything since the wrapped command can run without the removed parameter.