Add custom powershell parameter attribute to enable filtering

Solution 1:

If you want to use custom attributes, there are 3 things you need to do:

  1. Define a custom Attribute type
  2. Decorate your function's parameters with instances of said attribute
  3. Write a mechanism to discover the attribute decorations and do something with them at runtime

Let's start with step 1, by defining a custom class that inherits from System.Attribute:

class ProxyParameterAttribute : Attribute
{
  [string]$Target

  ProxyParameterAttribute([string]$Target){
    $this.Target = $Target
  }
}

Attribute annotations map directly back to the target attribute's constructor, so in this case, we'll be using it like [ProxyParameter('someValue')] and 'someValue' will then be stored in the $Target property.

Now we can move on to step 2, decorating our parameters with the new attribute. You can omit the Attribute part of the name when applying it in the param block, PowerShell is expecting all annotations to be attribute-related anyway:

function Invoke-SomeProgram
{
  param(
    [ProxyParameter('--server')]
    [Parameter()]
    [string]$Server
  )

  # ... code to resolve ProxyParameter goes here ...
}

For step 3, we need a piece of code that can discover the attribute annotations on the parameters of the current command, and use them to map the input parameter arguments to the appropriate target names.

To discover declared parameter metadata for the current command, the best entry point is $MyInvocation:

# Create an array to hold all the parameters you want to forward
$argumentsToFwd = @()

# Create mapping table for parameter names
$paramNameMapping = @{}

# Discover current command
$currentCommandInfo = $MyInvocation.MyCommand

# loop through all parameters, populate mapping table with target param names
foreach($param in $currentCommandInfo.Parameters.GetEnumerator()){
  # attempt to discover any [ProxyParameter] attribute decorations
  $proxyParamAttribute = $param.Value.Attributes.Where({$_.TypeId -eq [ProxyParamAttribute]}, 'First') |Select -First 1
  if($proxyParamAttribute){
    $paramNameMapping[$param.Name] = $proxyParamAttribute.Target
  }
}

# now loop over all parameter arguments that were actually passed by the caller, populate argument array while taking ProxyParameter mapping into account
foreach($boundParam in $PSBoundParameters.GetEnumerator()){
  $name = $boundParam.Name
  $value = $boundParam.Value
  if($paramNameMapping.ContainsKey[$name]){
    $argumentsToFwd += $paramNameMapping[$name],$value
  }
}

Now that the parameters how been filtered and renamed appropriately, you can invoke the target application with the correct arguments via splatting:

.\externalApp.exe @argumentsToFwd

Putting it all together, you end up with something like:

class ProxyParameterAttribute : Attribute
{
  [string]$Target

  ProxyParameterAttribute([string]$Target){
    $this.Target = $Target
  }
}
function Invoke-SomeProgram
{
  param(
    [ProxyParameter('--server')]
    [Parameter()]
    [string]$Server
  )

  # Create an array to hold all the parameters you want to forward
  $argumentsToFwd = @()

  # Create mapping table for parameter names
  $paramNameMapping = @{}

  # Discover current command
  $currentCommandInfo = $MyInvocation.MyCommand

  # loop through all parameters, populate mapping table with target param names
  foreach($param in $currentCommandInfo.Parameters.GetEnumerator()){
    # attempt to discover any [ProxyParameter] attribute decorations
    $proxyParamAttribute = $param.Value.Attributes.Where({$_.TypeId -eq [ProxyParamAttribute]}, 'First') |Select -First 1
    if($proxyParamAttribute){
      $paramNameMapping[$param.Name] = $proxyParamAttribute.Target
    }
  }

  # now loop over all parameter arguments that were actually passed by the caller, populate argument array while taking ProxyParameter mapping into account
  foreach($boundParam in $PSBoundParameters.GetEnumerator()){
    $name = $boundParam.Name
    $value = $boundParam.Value
    if($paramNameMapping.ContainsKey[$name]){
      $argumentsToFwd += $paramNameMapping[$name],$value
    }
  }

  externalApp.exe @argumentsToFwd
}

You can add other properties and constructor arguments to the attributes to store more/different data (a flag to indicate whether something is just a switch, or a scriptblock that transforms the input value for example).

If you need this for multiple different commands, extract the step 3 logic (discovering attributes and resolving parameter names) to a separate function or encapsulate it in a static method on the attribute class.