1

I typically do something like this:

function func1 {
    param (
        $a
    )
    function func2 {
        param (
            $a
        )
        return $a
    }
    return func2 -a $a
}

But it gets tedious repeating the param block for every nested function and including/removing the applicable parameters.

I've done this:

function func1 {
    param (
        $a
    )
    function func2 {
        return $a
    }
    return func2
}

But it seems to me this could invite issues since func2 assumes the $a variable exists without it being explicitly defined as a parameter for func2.

So which is the best method, or is there a different method that's better?

jeremywat
  • 181
  • 7
  • 1
    Look for PSBoundParameters. – Santiago Squarzon Aug 04 '23 at 21:34
  • 1
    I don't know if it really offers any benefits, but this would work... ```function func1 { param($a) function func2 { param($p) return $p.a } return func2 -p $PSBoundParameters``` - i.e. pass ```$PSBoundParameters``` as a hashtable *object* rather than *splatting* it down into ```func2``` - it still relies on magic strings for key names, but at least the variable scope is local to ```func2``` rather than relying on inheriting variables like ```$a``` from the *parent* scope in ```func1```. I'm not sure I'd use *any* of the suggestions, tbh - I'd just cut&paste the ```params``` block, but ymmv... – mclayton Aug 04 '23 at 23:27
  • Good points, @mclayton. There is a compromise: you can avoid duplication of parameter declarations as well as the need for argument-passing in / to the nested functions via defining a local variable in the enclosing function that stores a reference to `$PSBoundParameters`, which the nested functions can access via dynamic scoping. Referring to the enclosing function's parameter values that way avoids accidentally referencing unrelated variables from higher ancestral scopes. Similarly, however, I'm not sure if this approach - which requires a lot of discipline to implement - is worthwhile. – mklement0 Aug 05 '23 at 20:21

1 Answers1

1

Note:

  • Powershell: inspect, consume and pass through arguments? deals with the complementary scenario: How to relay arguments (parameter values) to a nested or separate function or script having the same (or least compatible) parameter declarations (param(...) block), using the automatic $PSBoundParameters variable. While this ultimately makes for the most robust solution, your intent is to avoid this duplication of parameter declarations.

  • The answer below contains a solution that may or may not be worth the trouble; if not, I hope that at least the background information that is provided is useful.


PowerShell's dynamic scoping makes all variables in ancestral scopes (in the same scope domain aka "session state")[1] on the call stack visible to a given scope.[2]

  • Only variables defined with the $private: (pseudo) scope are exempt from dynamic scoping; that is, this scope specifier makes a variable visible to the local scope only, but not to any descendent ones.

Therefore, your nested func2 function implicitly sees any variables - including parameter variables - defined in the enclosing func1 function, as well as - potentially - any variables from ancestral scopes higher up on the call stack.

If you want to ensure that your nested func2 function only operates on parameter variables passed to the enclosing function - i.e. only on arguments (parameter values) passed to func1 , as opposed to incidental variables of the same name having been defined in a higher ancestral scope - you can combine dynamic scoping with the automatic $PSBoundParameters variable, which is a dictionary of the names of all explicitly bound parameters and their values.

Specifically, you can define a local variable in the enclosing function that stores a reference to its $PSBoundParameters value, which any nested scope can then reference thanks to dynamic scoping. (Note that each nested function sees a separate $PSBoundParameters instance in its own scope, reflecting its bound parameters, if any; if a function declares no parameters, the dictionary is empty.)

The advantage of this approach is that it neither requires parameter declarations nor passing arguments to the nested functions while still ensuring that only parameter values passed to the parent function are operated on. However, in the nested functions this requires disciplined access to the enclosing function's parameter values via the local variable referencing the enclosing function's $PSBoundParameters dictionary.

Note that $PSBoundParameters fundamentally only tells you which parameters were bound explicitly, i.e. with arguments (values) supplied by the caller; it doesn't reflect parameter default values - see GitHub issue #3285 for a discussion.

However, parameters with default values invariably result in a local (parameter) variable with that default value, which can therefore be referenced directly in a nested scope. If you know a variable with a given name to be such a parameter variable with default value, its value is guaranteed to be the one from the enclosing scope (barring shadowing via a local variable in a nested scope).

To spell the solution out in the context of your example:

function func1 {
  param (
      $a,
      $b = 42
  )

  # Store a reference to this function's explicitly bound parameters
  # in a local variable.
  $parentBoundParameters = $PSBoundParameters
  
  function func2 {
    # This nested function now sees the local variable from the enclosing
    # scope.
    # Output the value passed to parameter -a of the enclosing function.
    # Make sure that you reference all parameter variables that
    # don't have default values this way.
    '$a parameter value (if bound): ' + $parentBoundParameters.a
    
    # A parameter bound with a *default* value is not stored in 
    # $PSBoundParameters, but is guaranteed to have a value in its scope,
    # which this nested scope sees too:
    '$b parameter value (possibly bound by default value): ' + $b
  }

  # Invoke func2 *without arguments*.
  return func2

}

# Invoke the outer function with (only) an -a argument
func1 -a hi

Note that func2 does not use return and instead relies on PowerShell's implicit output behavior to "return" two strings. return is only needed for flow control in PowerShell, though, as syntactic sugar, it can optionally be combined with producing output - see this answer.

The above produces the following output:

$a parameter value (if bound): hi
$b parameter value (possibly bound by default value): 42

[1] All code executing outside of modules executes in the same scope domain, whereas each module operates in its own scope domain that is connected to the global scope only - see the bottom section of this answer for details.

[2] However, with name-only variable references (e.g. $var), any scope can shadow an ancestral scope's variables of the same name (and in the same scope domain) by defining a local variable with the same name, which is happens by default on assignment (e.g. $var = ...) - see the bottom section of this answer for a concise overview of variable scopes in PowerShell.

mklement0
  • 382,024
  • 64
  • 607
  • 775