1

I'm writing a function that wraps a cmdlet using ValueFromRemainingArguments (as discussed here).

The following simple code demonstrates the problem:

  • works
function Test-WrapperArgs {
    Set-Location @args
}
Test-WrapperArgs -Path C:\ 
  • does not work
function Test-WrapperUnbound {
    Param(
        [Parameter(ValueFromRemainingArguments)] $UnboundArgs
    )

    Set-Location @UnboundArgs
}
Test-WrapperUnbound -Path C:\
Set-Location: F:\cygwin\home\thorsten\.config\powershell\test.ps1:69
Line |
  69 |      Set-Location @UnboundArgs
     |      ~~~~~~~~~~~~~~~~~~~~~~~~~
     | A positional parameter cannot be found that accepts argument 'C:\'.

I tried getting to the issue with GetType and EchoArgs from the PowerShell Community Extensions to no avail. At the moment I'm almost considering a bug (maybe related to this ticket??).

Thorsten
  • 196
  • 4
  • 8

1 Answers1

4

The best solution for an advanced function (one that uses a [CmdletBinding()] attribute and/or a [Parameter()] attribute) is to scaffold a proxy (wrapper) function via the PowerShell SDK, as shown in this answer. This involves essentially duplicating the target command's parameter declarations (albeit in an automatic, but static fashion).

If you do not want to use this approach, your only option is to perform your own parsing of the $UnboundArgs array (technically, it is an instance of [System.Collections.Generic.List[object]]), which is cumbersome, however, and not foolproof:

function Test-WrapperUnbound {
  Param(
      [Parameter(ValueFromRemainingArguments)] $UnboundArgs
  )

  # (Incompletely) emulate PowerShell's own argument parsing by 
  # building a hashtable of parameter-argument pairs to pass through
  # to Set-Location via splatting.
  $htPassThruArgs = @{}; $key = $null
  switch -regex ($UnboundArgs) {
    '^-(.+)' { if ($key) { $htPassThruArgs[$key] = $true } $key = $Matches[1] }
    default {  $htPassThruArgs[$key] = $_; $key = $null }
  }
  if ($key) { $htPassThruArgs[$key] = $true } # trailing switch param.

  # Pass the resulting hashtable via splatting.
  Set-Location @htPassThruArgs

}

Note:

  • This isn't foolproof in that your function won't be able to distinguish between an actual parameter name (e.g., -Path) and a string literal that happens to look like a parameter name (e.g., '-Path')

  • Also, unlike with the scaffolding-based proxy-function approach mentioned at the top, you won't get tab-completion for any pass-through parameters and the pass-through parameters won't be listed with -? / Get-Help / Get-Command -Syntax.

If you don't mind having neither tab-completion nor syntax help and/or your wrapper function must support pass-through to multiple or not-known-in-advance target commands, using a simple (non-advanced) function with @args (as in your working example; see also below) is the simplest option, assuming your function doesn't itself need to support common parameters (which requires an advanced function).
Using a simple function also implies that common parameters are passed through to the wrapped command only (whereas an advanced function would interpret them as meant for itself, though their effect usually propagates to calls inside the function; with a common parameter such as -OutVariable, however, the distinction matters).


As for what you tried:

While PowerShell does support splatting via arrays (or array-like collections such as [System.Collections.Generic.List[object]]) in principle, this only works as intended if all elements are to be passed as positional arguments and/or if the target command is an external program (about whose parameter structure PowerShell knows nothing, and always passes arguments as a list/array of tokens).

In order to pass arguments with named parameters to other PowerShell commands, you must use hashtable-based splatting, where each entry's key identifies the target parameter and the value the parameter value (argument).

Even though the automatic $args variable is technically also an array ([object[]]), PowerShell has built-in magic that allows splatting with @args to also work with named parameters - this does not work with any custom array or collection.

Note that the automatic $args variable, which collects all arguments for which no parameter was declared - is only available in simple (non-advanced) functions and scripts; advanced functions and scripts - those that use the [CmdletBinding()] attribute and/or [Parameter()] attributes - require that all potential parameters be declared.

mklement0
  • 382,024
  • 64
  • 607
  • 775
  • 1
    Very useful on the point explanation regarding the difference between splatting arrays and hashtables and the special case for params in simple and advanced functions. In my case I can actually use `@args` because it does not contain all arguments but only those not assigned (using a simple function). Thanks a lot. – Thorsten Jun 28 '20 at 13:39