Powershell "private" scope seems not useful at all
Asked Answered
G

3

8

I've got the script below, from internet:

$private:a = 1
Function test  {
    "variable a contains $a"
    $a = 2
    "variable a contains $a"
}
test

It prints 2. No problem. If I delete "private", like below:

$a = 1
Function test  {
    "variable a contains $a"
    $a = 2
    "variable a contains $a"
}

Still it prints "2". Seems no difference. Could you provide an quick sample of how "private" scope affects the result?

Thanks.

Gillan answered 1/4, 2016 at 2:1 Comment(0)
I
14

Private scope can be useful when writing a function that invokes a user-supplied callback. Consider this simple example:

filter Where-Name {
    param(
        [ScriptBlock]$Condition
    )
    $FirstName, $LastName = $_ -split ' '
    if(&$Condition $FirstName $LastName) {
        $_
    }
}

Then, if someone calls it like this:

$FirstName = 'First2'
'First1 Last1', 'First2 Last2', 'First3 Last3' |
  Where-Name {param($a, $b) $a -eq $FirstName}

they'll expect to see only the First2 Last2 row, but actually this will print all three rows. This is because of a collision on the $FirstName variable. To prevent such collisions, you can declare variables in Where-Name as private:

filter Where-Name {
    param(
        [ScriptBlock]$private:Condition
    )
    $private:FirstName, $private:LastName = $_ -split ' '
    if(&$Condition $FirstName $LastName) {
        $_
    }
}

Now $FirstName in Where-Name does not hide $FirstName in the outer scope when referenced from the $Condition script block.

Implicit answered 1/4, 2016 at 9:26 Comment(9)
Helpful, but there's a problem when applying private: to parameters; consider: function foo([ScriptBlock] $private:sb) { & $private:sb }; $sb = 'hi'; This works fine when invoked positionally: foo { $sb } # -> 'hi'; but when you try to use the parameter name, you can't: foo -sb { $sb } FAILS, because PS now thinks the parameter name is -private:sb, which, however cannot be used, because PS then interprets the part after : - sb - as the parameter value: foo -private:sb { $sb } FAILS too. Is there a way around this?Shandrashandrydan
@Shandrashandrydan If you use v2+, then you better to use modules. IMHO, them do that better then private scope. And you can use splatting to supply such weird parameter name: $p = @{ 'private:sb' = { $sb } }; foo @p.Implicit
Great idea to use modules, thanks for the tip; in v3+, this can even be done dynamically: $null = New-Module { function foo([ScriptBlock] $sb) { & $sb } }; $sb = 'hi'; foo { $sb }. Btw, it seems that if you do want to use a $private: parameter after all, you must keep using $private: when referencing that parameter later (variables don't seem to have that requirement); i.e.: & $private:Condition ....Shandrashandrydan
@Shandrashandrydan It work for me as it is: with out adding private: when I reference $Condition inside function body.Implicit
It works for your specific example, but that's because $Condition doesn't exist in the outer scope. Contrast these examples: function foo([ScriptBlock] $private:sb) { & $private:sb }; $sb = 'hi'; foo { $sb } - works (outputs 'hi'); if I remove the 2nd $private:, it doesn't work ($sb then refers to the value in the outer scope rather than the variable): function foo([ScriptBlock] $private:sb) { & $sb }; $sb = 'hi'; foo { $sb } (PS version 5.0.10586.63)Shandrashandrydan
@Shandrashandrydan For me it look like a bug in v5 (guessing it actually since v3, IMHO, it is DLR optimization bug). In v2 it print hi and if I write it like this: function foo([ScriptBlock] $private:sb) { & $sb; if($false){$private:sb} }; $sb = 'hi'; foo { $sb } — it print hi in v5.Implicit
@Shandrashandrydan $a=1; &{$a;$a+=2;$a;(gv a).GetType().FullName}; '-'; &{$a;$a+=2;$a;(gv a).GetType().FullName;if($false){rv}}; '-'; &{$a;$a+=2;$a;(gv a).GetType().FullName;rv a;$a}; '-'; sal x rv; &{$a;$a+=2;$a;(gv a).GetType().FullName;x a;$a}Implicit
Astounding stuff, thanks for digging deeper; as your last command shows, PS clearly tries to anticipate DLR optimization troubles, but there are bugs (which aren't in PSv2) - have you notified the PS team? As for the answer: can I suggest removing the $private from the Condition parameter to avoid confusion, and adding the tip to use modules instead directly to the answer?Shandrashandrydan
@Shandrashandrydan IIRC, I post about this on Microsoft Connect, not sure if it got migrated to UserVoice. Actually, even putting parameters into param block fix this: function foo { param([ScriptBlock] $private:sb) & $sb }; $sb = 'hi'; foo { $sb }. Although, if you think that removing $private will clarify the answer, feel free to do that.Implicit
S
18

Note:
* This answer explains why the OP's code behaves the way it does (and that it behaves as designed); additionally, it provides some general information about variable scopes in PowerShell.
* For an important real-world use of scope private, see PetSerAl's helpful answer.

Your first snippet prints:

variable a contains
variable a contains 2

Your second snippet prints:

variable a contains 1
variable a contains 2

In the first snippet, using scope private causes the parent (script) scope's a variable to be hidden from the child (function) scope, as designed, so the first output line shows that $a has no value
(an undefined variable has value $null, which evaluates to the empty string in a string context).

In the second snippet, by contrast, without the private scope modifier, variable a from the parent scope is visible to the child scope.

In PowerShell, functions execute in child scopes by default.

Therefore, in both snippets above, assigning to variable $a inside the function implicitly creates a local $a variable there, whose scope is limited to the enclosing function.

In other words:

  • Assigning to $a in the function creates a function-local variable named $a, which then shadows (hides) the script-level $a variable (if it wasn't already hidden by having been declared as $private:a) - though note that local in PowerShell means that child scopes do see its value; see next section.
  • On leaving the function, $a again has its original, script-level value.

General information about variable scopes in PowerShell

Note:

  • The discussion focuses on variables, but in principle it applies to all scoped entities, namely also to functions, aliases, PowerShell drives, and modules. However, only variables allow modification of instances in ancestral scopes.

  • The discussion is limited to code that runs in a given session's main runspace (its main thread) and therefore does not apply to out-of-runspace contexts, namely:

    • Remote calls (such as via Invoke-Command -ComputerName), background jobs (started with Start-Job), thread jobs (Start-ThreadJob, PSv6+) and thread-based parallel processing (ForEach-Object -Parallel, PSv7+).

    • Such contexts do not share state with the main runspace and only the values of variables can be passed to them, via the $using: scope; see this answer for details.

  • 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. To give a simplified example: if function A defines a variable and then calls function B, B sees that variable by default. In other words: what variables a given function or script sees depends on who called it.
  • Global variables (e.g., $global:foo) are session-global in PowerShell, and therefore best avoided, unless they are truly needed throughout an entire session.

  • See also: The conceptual about_Scopes help topic.

Overview:

  • Unless a variable is explicitly hidden with scope private, descendant scopes see that variable and read its value using the variable name without a scope qualifier (e.g., $a) or the need for Get-Variable -Scope.

    • E.g., $foo = 'bar'; function Get-Foo { $foo }; Get-Foo outputs 'bar', because the child scope in which the function runs sees the caller's $foo variable..

    • Note that while descendant scopes do not see the values of variables created with $private: by default, they can still refer to them with relative cross-scope access, using Get-Variable -Scope or Set-Variable -Scope.
      Non-relative scope modifiers ($script, $global, $local) generally do not work - except if the reference happens in the same scope in which the private variable was created and the scope modifier happens to effectively target that same scope, which is always true for $local:privateVarName, for instance.

  • Assigning to an unqualified variable, however, implicitly creates a new variable in the current (local) scope, which can shadow a variable of the same name in an ancestral scope.

    • That is, $a = 2 is implicitly the same as $local:a = 2.
    • E.g., $foo = 'bar'; function Get-Foo { $foo = 'bar2'; $foo }; Get-Foo; $foo outputs bar2 and bar, because the unqualified assignment $foo = 'bar2' created a local $foo variable inside the function (which then shadows the caller's $foo inside the function), leaving the caller's $foo untouched.
  • To explicitly get / modify a variable in an ancestral scope, use Get-Variable / Set-Variable -Scope <n> <name>, where <n> represents the scope level, with 0 representing the current scope, 1 the parent scope, and so on.
    Note that Get-Variable returns a [System.Management.Automation.PSVariable] instance by default, so in order to get only the value, access its .Value property, or use the -ValueOnly switch, which only returns the value to begin with.

    • In functions and trap handlers, before creating a local copy of a variable, you can alternatively modify a variable in the most immediate ancestral scope where it is defined as follows:

      • ([ref] $var).Value = ...
      • (If and once a local variable by the same name is created, the above will modify only the local variable, however.)
    • Variables in the script scope and the global scope can also be accessed - and modified - by using the $script: and $global: scope modifiers; e.g., $script:a and $global:a.
      Note that $script: refers to the (immediately) enclosing script file's top-level scope.

  • Modules each have their own scope domain (aka session state), which is linked only to the global scope. That is, modules see outside variables only from the global scope, not from a caller in any other scope, such as from a script (the exception is if the caller is from the same module); this can cause unexpected behavior with preference variables, as discussed in this GitHub issue.

    • In short:
      • All non-module code runs in a (the same) scope domain.
      • Each module has its own scope domain.
      • The only scope shared by all scope domains is the one and only global one, which functions as the root scope for all scope domains.
  • Declaring a variable with Set-Variable -Option AllScope allows it to be read and modified in any descendant scope without needing to qualify the name; to put it differently: only a single variable by that name exists then, which any scope can directly read and write using the unqualified variable name.

    • Without a separate -Scope parameter, -Option AllScope is applied to the variable in the current scope (e.g., the script scope at the script's top level, a function's local scope inside a function). Thus, to safely create a script-global variable that you can access unqualified for reading and writing, use Set-Variable -Scope Script -Option AllScope.

    • -Scope Global is distinct from -Option AllScope: while -Scope Global creates a globally accessible variable, reading it may, and modifying it does, require the $global: scope modifier, and, without -Option AllScope, global variables can be shadowed by variables of the same name in descendant scopes. Also note that a global variable is session-global, so it persists even after the script that defined it has terminated.

    • By combining -Scope Global with -Option AllScope you effectively create a session-global singleton variable that can be read and written from any scope without qualifier; as stated, however, such a variable lives on even after your script exits.

Shandrashandrydan answered 1/4, 2016 at 3:28 Comment(0)
I
14

Private scope can be useful when writing a function that invokes a user-supplied callback. Consider this simple example:

filter Where-Name {
    param(
        [ScriptBlock]$Condition
    )
    $FirstName, $LastName = $_ -split ' '
    if(&$Condition $FirstName $LastName) {
        $_
    }
}

Then, if someone calls it like this:

$FirstName = 'First2'
'First1 Last1', 'First2 Last2', 'First3 Last3' |
  Where-Name {param($a, $b) $a -eq $FirstName}

they'll expect to see only the First2 Last2 row, but actually this will print all three rows. This is because of a collision on the $FirstName variable. To prevent such collisions, you can declare variables in Where-Name as private:

filter Where-Name {
    param(
        [ScriptBlock]$private:Condition
    )
    $private:FirstName, $private:LastName = $_ -split ' '
    if(&$Condition $FirstName $LastName) {
        $_
    }
}

Now $FirstName in Where-Name does not hide $FirstName in the outer scope when referenced from the $Condition script block.

Implicit answered 1/4, 2016 at 9:26 Comment(9)
Helpful, but there's a problem when applying private: to parameters; consider: function foo([ScriptBlock] $private:sb) { & $private:sb }; $sb = 'hi'; This works fine when invoked positionally: foo { $sb } # -> 'hi'; but when you try to use the parameter name, you can't: foo -sb { $sb } FAILS, because PS now thinks the parameter name is -private:sb, which, however cannot be used, because PS then interprets the part after : - sb - as the parameter value: foo -private:sb { $sb } FAILS too. Is there a way around this?Shandrashandrydan
@Shandrashandrydan If you use v2+, then you better to use modules. IMHO, them do that better then private scope. And you can use splatting to supply such weird parameter name: $p = @{ 'private:sb' = { $sb } }; foo @p.Implicit
Great idea to use modules, thanks for the tip; in v3+, this can even be done dynamically: $null = New-Module { function foo([ScriptBlock] $sb) { & $sb } }; $sb = 'hi'; foo { $sb }. Btw, it seems that if you do want to use a $private: parameter after all, you must keep using $private: when referencing that parameter later (variables don't seem to have that requirement); i.e.: & $private:Condition ....Shandrashandrydan
@Shandrashandrydan It work for me as it is: with out adding private: when I reference $Condition inside function body.Implicit
It works for your specific example, but that's because $Condition doesn't exist in the outer scope. Contrast these examples: function foo([ScriptBlock] $private:sb) { & $private:sb }; $sb = 'hi'; foo { $sb } - works (outputs 'hi'); if I remove the 2nd $private:, it doesn't work ($sb then refers to the value in the outer scope rather than the variable): function foo([ScriptBlock] $private:sb) { & $sb }; $sb = 'hi'; foo { $sb } (PS version 5.0.10586.63)Shandrashandrydan
@Shandrashandrydan For me it look like a bug in v5 (guessing it actually since v3, IMHO, it is DLR optimization bug). In v2 it print hi and if I write it like this: function foo([ScriptBlock] $private:sb) { & $sb; if($false){$private:sb} }; $sb = 'hi'; foo { $sb } — it print hi in v5.Implicit
@Shandrashandrydan $a=1; &{$a;$a+=2;$a;(gv a).GetType().FullName}; '-'; &{$a;$a+=2;$a;(gv a).GetType().FullName;if($false){rv}}; '-'; &{$a;$a+=2;$a;(gv a).GetType().FullName;rv a;$a}; '-'; sal x rv; &{$a;$a+=2;$a;(gv a).GetType().FullName;x a;$a}Implicit
Astounding stuff, thanks for digging deeper; as your last command shows, PS clearly tries to anticipate DLR optimization troubles, but there are bugs (which aren't in PSv2) - have you notified the PS team? As for the answer: can I suggest removing the $private from the Condition parameter to avoid confusion, and adding the tip to use modules instead directly to the answer?Shandrashandrydan
@Shandrashandrydan IIRC, I post about this on Microsoft Connect, not sure if it got migrated to UserVoice. Actually, even putting parameters into param block fix this: function foo { param([ScriptBlock] $private:sb) & $sb }; $sb = 'hi'; foo { $sb }. Although, if you think that removing $private will clarify the answer, feel free to do that.Implicit
C
0

Good software design means minimized coupling (among other things). Within Powershell, that includes using private ON EVERY VARIABLE YOU CAN. If you want to make a value available in some subsequently called module, pass that information EXPLICITLY. There should be a very good EXCEPTION reason for not doing this, because each time you rely on implicit knowledge (e.g. the kind that happens in Powershell when you don't use private variables), you increase the chance something will go unexpectedly wrong later (maybe months later when the software has a lot more code in it).

Crud answered 9/6, 2017 at 21:42 Comment(1)
Good recommendation, but it means always having to be explicit on creation ($private:var = ... instead of $var = ...). Also, given that the question is about how the private scope works, not whether or not it's a good idea to use it, I suggest framing your answer accordingly.Shandrashandrydan

© 2022 - 2024 — McMap. All rights reserved.