14

I’m trying to write a wrapper function in PowerShell that basically evaluates the first parameter and based on that runs a program on the computer. All the remaining parameters to the wrapper function should then be passed to the program that is ran as well.

So it should look something like this:

function test ( [string] $option )
{
    if ( $option -eq 'A' )
    {
        Write-Host $args
    }
    elseif ( $option -eq 'B' )
    {
        . 'C:\Program Files\some\program.exe' $args
    }
}

Now just adding $args does not work, so what do I have to do to make it work? Another option would probably be using Invoke-Expression, but it feels a bit like eval so I want to avoid if possible, and in addition I think doing it like that would limit me to string-only parameters right? If possible I would want to have the full support for the wrapped program/cmdlet - basically like a dynamic alias. Is that even possible?

poke
  • 369,085
  • 72
  • 557
  • 602

4 Answers4

17

This sort of does what you ask. You may run into trouble if you need to pass dash-prefixed options to the executable that conflict or cause ambiguity with the PowerShell common parameters. But this may get you started.

function Invoke-MyProgram
{
    [CmdletBinding()]
    Param
    (
        [parameter(mandatory=$true, position=0)][string]$Option,
        [parameter(mandatory=$false, position=1, ValueFromRemainingArguments=$true)]$Remaining
    )

    if ($Option -eq 'A')
    {
        Write-Host $Remaining
    }
    elseif ($Option -eq 'B')
    {
        & 'C:\Program Files\some\program.exe' @Remaining # NOTE: @ not $ (splatting)
    }
}
x0n
  • 51,312
  • 7
  • 89
  • 111
OldFart
  • 2,411
  • 15
  • 20
  • While recommending splatting is the crucial pointer for enabling support for passing through arguments to any (type of) command, _array_ (list)-based splatting, as in your solution, is limited to _positional_ (unnamed) parameter passing to other PowerShell commands, which is a severe limitation. Due to built-in magic, using a non-advanced function with `@args` - even though the _automatic_ `$args` variable is also an array - does _not_ have this limitation. To also support _named_ parameter pass-through in an _advanced_ function (such as yours), more work is needed. – mklement0 Jun 28 '20 at 14:16
6
  • Your solution works as-is for external programs (such as your C:\Program Files\some\program.exe example): you can always pass an array of values (which is what $args is) to an external program, and its elements will be passed as individual arguments (stringified, if necessary).

  • You can make your solution work with any command if you change $args to @args[1], to take advantage of a PowerShell parameter-passing technique called splatting:

function test ( [string] $option )
{
    if ( $option -eq 'A' )
    {
        Write-Host $args
    }
    elseif ( $option -eq 'B' )
    {
        # Use @args to also support passing *named* arguments
        # through to *PowerShell* commands.
        & $someCommand @args
    }
}

Caveats:

  • 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.

  • PowerShell has built-in magic that makes the automatic array variable $args also support passing named parameters through via splatting, which no custom array or collection supports.

    • By contrast, custom arrays and collections only support splatting positional (unnamed) arguments, which, however, covers all calls to external programs.
    • When calling PowerShell commands, this limitation is problematic, however: For instance, if you wanted to pass the named argument -Path C:\ through to the Set-Location cmdlet via splatting, using a custom collection parameter declared via ValueFromRemaining Arguments, as shown in OldFart's answer (Set-Location @Remaining), would not work; to support passing through named arguments (other than via @args, if available), you must use hashtable-based splatting.

Therefore, if your function is an advanced one and you need to support passing named arguments through to other PowerShell commands, a different approach is required: this answer shows two alternatives.


[1] With external programs, there is a corner case where @args behaves differently from $args, namely if the $args array contains --%, the stop-parsing symbol: @args recognizes it, $args treats it as a literal.

mklement0
  • 382,024
  • 64
  • 607
  • 775
4

What you have written does work. Note that what is there is $args is the unnamed arguments that are over and above the parameters expected by the function.

So if you call test as

test -option "A" 1 2 3

$args will have 1,2,3

Note that if you call test as

test -option "A" -other "B" 1 2 3

$args will have -other,B,1,2,3

mklement0
  • 382,024
  • 64
  • 607
  • 775
manojlds
  • 290,304
  • 63
  • 469
  • 417
0

I'd like to improve this answer and suggest a solution that works both for positional and named arguments:

function Invoke-MyProgram
{
    [CmdletBinding()]
    Param
    (
        [parameter(mandatory=$true, position=0)][string]$Option,
        [parameter(mandatory=$false, position=1, ValueFromRemainingArguments=$true)]$Remaining
    )

    if ($Option -eq 'A')
    {
        Write-Host $Remaining
    }
    elseif ($Option -eq 'B')
    {
        Invoke-Expression "& `"C:\Program Files\some\program.exe`" $Remaining"
    }
}

Caution: Be aware of the security issues of 'Invoke-Expression'!

Mehrdad Mirreza
  • 984
  • 12
  • 20