Search code examples
functionpowershellparametersvariadic-functions

PowerShell pass all parameters received in function, and handle parameters with spaces


I am a novice with PowerShell. In Msys2 (or Lnux), I have defined a function npp

npp ()
{
    ${NPP_PATH} "$@"
}

such that if I call from the command prompt npp it launches Notepad++ (as defined in ${NPP_PATH}). If I call npp "mydir/stage 1/a.txt" it opens that file for editing. Generally speaking, it allows:

  1. Any number of parameters.
  2. Parameters containing spaces, if suitably escaped.

What would be the equivalent in PowerShell?

I guess in PS I should also go for a function to obtain a similar behavior. So far, I could receive an undefined number of parameters, and use them in a foreach loop, see code below. But I could not find the equivalent of the simple "$@" to pass all parameters as they are received. Moreover, if I use quotes in one of the arguments, they are removed, so it will probably have problems with file paths including blanks.

function multi_params {
    param(
        [Parameter(
            ValueFromRemainingArguments=$true
        )][string[]]
        $listArgs
    )
    $count = 0
    foreach($listArg in $listArgs) {
        '$listArgs[{0}]: {1}' -f $count, $listArg
        $count++
    }
}

Solution

  • Assuming that NPP_PATH is an environment variable, the equivalent PowerShell function is:

    function npp {
      & $env:NPP_PATH $args
    }
    

    If NPP_PATH is the name of a regular PowerShell variable, use & $NPP_PATH $args.

    & is the call operator, which is needed for syntactic reasons whenever you want to invoke a command whose name/path is specified in quotes and/or via a variable.

    In simple functions (as opposed to advanced functions) such as the above (use of neither [CmdletBinding()] nor [Parameter()] attributes), you can use the automatic $args variable to pass any arguments through to another command.

    • If the target command is not an external program, such as here, but a PowerShell command, use the form @args to ensure that all arguments - including those preceded by their parameter names - are properly passed through - see about_Splatting.
      Note that the form @args works with external programs too, where it is generally equivalent to $args (the only difference is that only @args recognizes and removes --%, the stop-parsing token)

    • Note that passing arguments with embedded " chars. and empty arguments to external programs is broken in Windows PowerShell and in PowerShell (Core) 7 up v7.2.x - see this answer.


    Passing arguments through in simple vs. advanced functions (scripts):

    • In simple functions only, $args contains all arguments that did not bind to declared parameters, if any, on invocation.

      • If your simple function doesn't declare any parameters, as in the example above, $args contains all arguments passed on invocation.

      • If your simple function does declare parameters (typically via param(...)), $args contains only those arguments that didn't bind to declared parameters; in short: it collects any arguments your function did not declare parameters for.

      • Therefore, $args is a simple mechanism for collecting arguments not declared or known in advance, either to be used in the function itself - notably if declaring parameters isn't worth the effort - or to pass those arguments through to another command.

      • To pass arguments that comprise named arguments (e.g., -Path foo instead of just foo) through to another PowerShell command, splatting is needed, i.e. the form @args.

        • Note that while $args is technically a regular PowerShell array ([object[]]), it also has built-in magic to support passing named arguments through; a custom array can not be used for this, and the hash-table form of splatting is then required - see about_Splatting
    • In advanced functions, $args is not available, because advanced functions by definition only accept arguments for which parameters have been declared.

      • To accept extra, positional-only arguments, you must define a catch-all ValueFromRemainingArguments parameter, as shown in the question, which collects such arguments in an array-like[1] data structure by default.

      • To also support named pass-through arguments, you have two basic option:

        • If you know the set of potential pass-through parameters, declare them as part of your own function.

          • You can then use splatting with the $PSBoundParameters dictionary (hash table) - see below - to pass named arguments through, possibly after removing arguments meant for your function itself from the dictionary.
        • This technique is used when writing proxy (wrapper) functions for existing commands; the PowerShell SDK makes duplicating the pass-through parameters easier by allowing you to scaffold a proxy function based on an existing command - see this answer.

      • Otherwise, there is only a suboptimal solution where you emulate PowerShell's own parameter parsing to parse the positional arguments into parameter-name/value pairs - see this answer.


    The automatic $PSBoundParameters variable is a dictionary that is available in both simple and advanced functions:

    • $PSBoundParameters applies only if your function declares parameters, and contains entries only for those among the declared parameters to which arguments were actually bound (passed) on invocation; the dictionary keys are the parameter names, albeit without the initial -.
      Note that parameters bound by a default value are not included - see this GitHub issue for a discussion.
      Again, note that in advanced functions you can only pass a given argument if a parameter was declared for it, so any argument passed in a given invocation is by definition reflected in $PSBoundParameters.

    • Because it is a dictionary (hash table), it can be used with hash-table based splatting - @PSBoundParameters - to pass named arguments through to other PowerShell commands and, since it is mutable, you have the option of adding or removing named arguments (such as the ones intended for your function itself).


    [1] That type is [System.Collections.Generic.List[object]]; however, you can specify a collection type explicitly, such as [object[]] to get a regular PowerShell array.