Proxy Problems
In this issue:
Creating PowerShell proxy functions can serve a useful purpose. But it is not without its challenges. As you create proxy functions, there are a few things you should keep in mind. These are also items to take into account if you want to create a wrapper function instead of a proxy function. Here's what I have in mind.
Documentation
When you use the code I've been showing you to generate a proxy function, it will automatically insert pointers to the original function's help.
<#
.ForwardHelpTargetName Microsoft.PowerShell.Management\Get-Service
.ForwardHelpCategory Cmdlet
#>
This means when the user calls for help on the proxy function, they will get the help for the original function. The downside, it the presumably you have modified the original function in some way by adding or removing parameters. This makes the help in accurate.
If you delete the forward links, then the only help will be the syntax and automatically generated parameter help.
Building Your Own
One thing you could do is create your own help documentation. But you don't have to re-invent the wheel. You can use the original help as a starting point.
#do this BEFORE loading your proxy function into your session
$cmd = Get-Command Get-Service
#define the fully qualified command name
$fqdn = "{0}\{1}" -f $cmd.Source,$cmd.Name
$help = [System.Management.Automation.ProxyCommand]::GetHelpComments((Get-Help $fqdn))
This code snippet will get the help comments from the original function and format them as a commented help block.
If you are creating a wrapper function instead of a proxy function, you can use the same code snippet to get the help comments from the original function and use them as a starting point for your own help documentation. The wrapper function will have a separate name.
$help = $help -replace $Command,$NewName
In any event, copy the original help to the Windows clipboard and paste it into your proxy function.
$help | Set-Clipboard
Then you can modify it as needed. Don't forget to delete any forward help links in your code.
Creating Custom Comment-Based Help
If you don't want to use the proxy generated help, you can create your own comment-based help. You might want to do this for wrapper functions. First, get the original help content.
$h = Get-Help $fqdn
This is a rich object that you can use to construct your own help content. Here's an example:
#I'm removing Markdown formatting
$custom = @"
<#
.SYNOPSIS
$($h.Synopsis)
.DESCRIPTION
$($h.description.text -replace "``|\*\*","")
.NOTES
This is a customized version of $fqdn.
$($h.alertSet.alert.Text -replace "``|\*\*","")
.INPUTS
$($h.inputTypes.inputType.Type.Name | Out-String)
.OUTPUTS
$($h.ReturnValues.ReturnValue.Type.Name | Out-String)
#>
"@
I can paste this into my function and modify it further as needed.
<#
.SYNOPSIS
Gets the services on the computer.
.DESCRIPTION
> This cmdlet is only available on the Windows platform.
The Get-Service cmdlet gets objects that represent the services on a computer, including running and stopped services. By default, when Get-Service is run without parameters, all the local computer's services are returned.
You can direct this cmdlet to get only particular services by specifying the service name or the display name of the services, or you can pipe service objects to this cmdlet.
.NOTES
This is a customized version of Microsoft.PowerShell.Management\Get-Service.
PowerShell includes the following aliases for Get-Service:
- Windows:
- gsv
This cmdlet is only available on Windows platforms.
Beginning in PowerShell 6.0, the following properties are added to the ServiceController
objects: UserName, Description, DelayedAutoStart, BinaryPathName, and
StartupType .
This cmdlet can display services only when the current user has permission to see them. If this
cmdlet does not display services, you might not have permission to see them.
To find the service name and display name of each service on your system, type Get-Service. The
service names appear in the Name column, and the display names appear in the DisplayName
column.
> [!NOTE]
> Typically, Get-Service returns information about services and not driver. However, if you
> specify the name of a driver, Get-Service returns information about the driver.
>
> - Enumeration doesn't include device driver services
> - When a wildcard is specified, the cmdlet only returns Windows services
> - If you specify the Name or DisplayName that is an exact match to a device service name,
> then the device instance is returned
When you sort in ascending order by status value, Stopped services appear before Running
services. The Status property of a service is an enumerated value in which the names of the
statuses represent integer values. The sort is based on the integer value, not the name. Running
appears before Stopped because Stopped has a value of 1, and Running has a value of 4. For
more information, see [ServiceControllerStatus](xref:System.ServiceProcess.ServiceControllerStatus).
.INPUTS
System.ServiceProcess.ServiceController
System.String
.OUTPUTS
System.ServiceProcess.ServiceController
#>
Dynamic Parameters
Perhaps the biggest challenge with proxy functions is dynamic parameters. These are parameters that only exist under certain conditions, typically when the command is used in a given context. Get-ChildItem has dynamic parameters depending if you are using the FileSystem or Certificate provider. What should you do when creating a proxy function that might have dynamic parameters?
If you run the code I've been showing you, the dynamic parameters will be included in the proxy function.
$cmd = Get-Command Get-Item
$meta = [System.Management.Automation.CommandMetadata]::New($cmd)
[System.Management.Automation.ProxyCommand]::Create($meta)
Running this code will also create this definition:
dynamicparam
{
try {
$targetCmd = $ExecutionContext.InvokeCommand.GetCommand('Microsoft.PowerShell.Management\Get-Item', [System.Management.Automation.CommandTypes]::Cmdlet, $PSBoundParameters)
$dynamicParams = @($targetCmd.Parameters.GetEnumerator() | Microsoft.PowerShell.Core\Where-Object { $_.Value.IsDynamic })
if ($dynamicParams.Length -gt 0)
{
$paramDictionary = [Management.Automation.RuntimeDefinedParameterDictionary]::new()
foreach ($param in $dynamicParams)
{
$param = $param.Value
if(-not $MyInvocation.MyCommand.Parameters.ContainsKey($param.Name))
{
$dynParam = [Management.Automation.RuntimeDefinedParameter]::new($param.Name, $param.ParameterType, $param.Attributes)
$paramDictionary.Add($param.Name, $dynParam)
}
}
return $paramDictionary
}
} catch {
throw
}
}
Here's the problem. The code is dynamically generating the dynamic parameters. You have no way of knowing what parameters are being generated or under what conditions. This means you can't *selectively() remove or modify dynamic parameters. It also makes it extremely complicated to define your own dynamic parameters in your proxy function. This is on top of the challenge of documenting dynamic parameters.
My recommendation is to avoid creating a proxy function with dynamic parameters when the dynamic parameter is the thing you want to customize. Use syntax like this to generate the proxy function without the dynamic parameters.
[System.Management.Automation.ProxyCommand]::Create($meta," ",$false)
> This syntax will also delete the forward help links, so that you can create your own help documentation.
If you want any of the original dynamic parameters, you can add them manually to your proxy function. You can use the code snippet above as a starting point to see what dynamic parameters are being generated and then add the ones you want to your proxy function.