Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Add custom powershell parameter attribute to enable filtering

Tags:

powershell

I'm writing a function that's basically a convenience wrapper around an external program that it'll invoke. Several of the parameters will be forwarded to the external program, but not all.

I was writing

$ArgsToFwd = @()
switch ($PSBoundParameters.Keys) {
  '--server' {$ArgsToFwd += @('--server',$server)}
  '--userid' {$ArgsToFwd += @('--userid',$userid)}
  ...
}

but then I thought it might be better to define a custom parameter attribute that could let me do something like:

params(
[Parameter()]
[IsExternal()]
[string]$Server
)
#...
foreach ($key in $PSBoundParameters.Keys) {
    if (<#a test for the custom [IsExternal] attribute#>) {
        $ArgsToFwd += @($key, $PSBoundParameters[$key])
    }
}

But I can't quite figure it out. Can that be done?

like image 869
alazyworkaholic Avatar asked Feb 01 '26 15:02

alazyworkaholic


1 Answers

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.

like image 92
Mathias R. Jessen Avatar answered Feb 04 '26 05:02

Mathias R. Jessen



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!