René Nyffenegger
René Nyffenegger

Reputation: 40523

How 'local' are function parameters in a PowerShell module

I have found a behavior in PowerShell that surprises me and for which I am looking for an explanation. Please consider the following PowerShell module named testParameter:

testParameter.psd1:

@{
   RootModule         = 'testParameter.psm1'
   ModuleVersion      = '0.1'

   FunctionsToExport  = @(
      'get-embeddedString'
   )
}

testParameter.psm1:

set-strictMode -version 3

function embed-string {

   param (
      [string] $pre,
      [string] $post
   )

  "$($pre)$text$($post)"
}


function get-embeddedString {

   param (
      [string] $text
   )

   return embed-string '>>> ' ' <<<'
}

When I call get-embeddedString foo, the function returns (or the console prints):

>>> foo <<<

I am surprised because this string is rendered in the function embed-string which does not declare a local variable named $text, does not have a parameter with that name and does not assign a value to it before it is used and I have expected the function to throw a The variable '$text' cannot be retrieved because it has not been set. error.

Apparently, embed-string chooses to use the value that $text has in get-embeddedString which I assume is in a different and unrelated scope.

I don't understand what's going on here and where the relevant documentation for this behavior is found.

Upvotes: 3

Views: 839

Answers (2)

mklement0
mklement0

Reputation: 438823

about_Scopes is the official conceptual help topic describing scopes in PowerShell (which, typically, but not exclusively, relate to variables), but let me provide a concise explanation:

  • As most shells do, PowerShell uses dynamic rather than lexical scoping.

    • That is, rather than variables being visible only to the enclosing lexical construct, such as a function definition, variable visibility depends on the runtime call stack. In other words: what variables a given function or script sees depends on who called it.
    • Unless a caller explicitly hides variables with the $private: scope, all descendant scopes see them by default.
  • Therefore, because your embed-string function was called from your get-embeddedString function and the latter defines a $text variable (via a parameter declaration in this case), that $text variable is also visible to embed-string - and would also be visible to functions called from it (and so on).

Notable pitfalls:

  • While unqualified (non-scoped) read access to an ancestral variable gets its value, assigning a value implicitly creates a new, local variable that shadows the ancestral one.

    • E.g., "$($pre)$text$($post)" in your embed-string function gets the value of the ancestral $text variable, but if you were to execute, say, $text = 'value' there, you'd create a local variable by the same name, which is then visible to any embed-strings descendants on the call stack, but unrelated to the original get-embeddedstring $text variable.
  • Only scopes in the same scope domain (aka session state), in the same runspace, exhibit this relationship; each module has its own scope domain, and all non-module code shares a single, separate one. The only one scope shared by all scope domains is the global scope, which is the root scope of all scope domains.

See the bottom section of this answer for a comprehensive overview of PowerShell scopes.

Upvotes: 3

Ash
Ash

Reputation: 3246

Nested/Child functions have access to all the parent function's variables. You can even modify them in the child scope and they are returned to the original value in the parent.

function embed-string {
   param (
      [string] $pre,
      [string] $post
   )
    Write-Host $MyInvocation.BoundParameters
    Write-Host $MyInvocation.UnboundArguments

    Write-Host $text
    $text = "bar"

    "$($pre)$text$($post)"
}

function get-embeddedString {

    param (
       [string] $text
    )
    
    embed-string '>>> ' ' <<<'

    Write-Host $text
}

Output

[pre, >>> ] [post,  <<<]

foo
>>> bar <<<
foo

If you want to persist a change to a variable in the child function, you can use Set-Variable -Name text -Option AllScope.

Link about Nested Functions, applies to Child Functions too

Upvotes: 0

Related Questions