What defines the boundary of a local scope in PowerShell? [duplicate] - powershell

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.

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.

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.

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

Related

Modifying variable in parent script using child script in powershell

I have two powershell scripts.
I have to assign the parent variable using child script
The child powershell script is called using parent powershell script
parent.ps1
$count = $Null
child.ps1
$count = 10
How do I make sure that the change in child script gets reflected in parent script?
Manuel Batsching's answer shows you how to use dot-sourcing to solve your problem indirectly: by executing the child script directly in the parent script's scope, all of the child script's (script-level) variables (among other definitions, namely functions and aliases) are created directly in the parent script's scope, which may be undesirable.
PowerShell does offer mechanisms to selectively modify variables in other scopes, but it's best to avoid them, because you're creating a tight coupling between your scripts that makes them difficult to maintain; instead, use other mechanism to communicate information between your scripts, in the simplest form via output.
If you still want to solve your problem by modifying a variable in the parent scope, you can use the following:
# When run in child.ps1 that was invoked (normally) by parent.ps1,
# sets $count to 1 in the scope of *parent.ps1*.
Set-Variable -Scope 1 -Name count -Value 10
Scope -1 refers to the parent scope of the calling scope (2 would refer to the grandparent scope, and so on) - see Set-Variable.
Complementarily, scope modifiers $script: and $global: may be used with variable names to target variables in the (same) script scope and the global scope (for instance, $script:foo = 'bar' could be used to set a script-level variable from inside of a function defined in the same script; creating or modifying global variables should be avoided, as they linger even after a script exits) - see about_Scopes.
For the sake of completeness:
Scope modifier $local: allows you to refer to the current (local) scope explicitly; it is rarely used, because the current scope is implied when you assign to a variable by mere name (e.g., $foo = 'bar' creates a local $foo variable).
However, on getting a variable $local:foo isn't necessarily the same as just $foo: due to PowerShell's dynamic scoping, $foo returns the value of a variable in an ancestral scope, if any, if no such variable exists in the current scope; by contrast, $local:foo strictly returns the value of a variable by that name in the current scope, if defined there.
To suppress dynamic scoping for a given variable, i.e. to prevent descendant scopes from seeing it (by default), create it with the $private: scope modifier.
See this answer for more information about PowerShell's dynamic scoping and the $private: scope.
Scope modifier $using: is used in the context of remoting and jobs; it doesn't actually reference a variable per se in the caller's context, but its value - see about_Remote_Variables.
By using the dot sourcing operator . you can run your child.ps1 script in the same scope as your parent.ps1 script. That will preserve all changes to variables, that your child script does (see: script scope and dot sourcing).
Let the content of your parent.ps1 be like:
$count = $null
. .\child.ps1
$count
This will return 10.

Global variable not being picked up by function [duplicate]

If I declare and initialize a PowerShell variable with $global:MyVariable = "123" do I then need to use $global: anywhere I use the variable or can I just use $MyVariable?
In principle you can use just $MyVariable to reference the value of a global variable by that name, but you may see a different variable's value, situationally.
Specifically, if any intervening ancestral (parent) scope or even the calling scope itself has also created a $MyVariable variable (which typically happens implicitly by simple assigmnent; e.g., $MyVariable = ...), you will see that variable's value instead.
You're only guaranteed to see the global value if you do use the global scope specifier, namely: $global:MyVariable (or Get-Variable -ValueOnly -Scope Global MyVariable)[1]
By default, variables are visible, but not directly modifiable in all descendant (child) scopes.
Therefore you must use $global:MyVariable / Set-Variable -Scope Global in order to modify (set) a global variable; without $global: you'll implicitly create a local variable by the same name.
For more on PowerShell's scoping rules, see the last section of this answer.
[1] In real-world scenarios, even code in PowerShell modules sees global variables with scope specifier $global:, and the implicit visibility of global variables always applies.
For in-memory modules, there is a way to make $global / -Scope global refer to a different scope, namely the module's own top-level scope, but the technique is obscure, not documented, and its real-world utility is unknown.
Read on, if you want to know more.
Optional reading: Creating an in-memory module that sees its own top-level scope as the global scope:
PetSerAl has discovered a little-known way to create an in-memory module in a manner that makes it see $global: / -Scope global as its own top-level scope rather than the true global scope - curiously, not using an explicit scope reference makes the code still see (non-shadowed) global variables:
$global:MyVariable = 42 # Create a true global variable.
# Create an in-memory module for which its *own top-level scope*
# becomes the "global" scope, by virtue of passing $false to the
# [psmoduleinfo] constructor:
& ([psmoduleinfo]::new($false)) {
#"
"$global:MyVariable",
"$(Get-Variable -ValueOnly -Scope global MyVariable)",
"$MyVariable"
"#
}
yields:
Get-Variable : Cannot find a variable with the name 'MyVariable'.
# ...
"", # $global:MyVariable didn't find the variable
", # Neither did Get-Variable -Scope Global (see error above)
"42" # OK - implicit visibility of true global variables
Note that neither New-Module nor Import-Module (for persisted modules) offer this functionality.
The obscure & <module-info> { ... } technique used above for invoking a script block in a module's scope is explained in this excellent blog post by Patrick Meinecke.
Run this and see that it is best to always specify, if you don't you will not alwys get what you might expect
function func {
"0. $myvar"
$myvar='funclocal'
"1. $myvar"
"2. $Global:myvar"
}
$Global:myvar='global'
func
"3. $Global:myvar"
you do not need to use $global: when calling

Powershell "private" scope seems not useful at all

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

Any example to show "Numbered Scopes" concept? [duplicate]

A sad thing about PowerShell is that function and scriptblocks are dynamically scoped.
But there is another thing that surprised me is that variables behave as a copy-on-write within an inner scope.
$array=#("g")
function foo()
{
$array += "h"
Write-Host $array
}
& {
$array +="s"
Write-Host $array
}
foo
Write-Host $array
The output is:
g s
g h
g
Which makes dynamic scoping a little bit less painful. But how do I avoid the copy-on-write?
The PowerShell scopes article (about_Scopes) is nice, but too verbose, so this is quotation from my article:
In general, PowerShell scopes are like .NET scopes. They are:
Global is public
Script is internal
Private is private
Local is current stack level
Numbered scopes are from 0..N where each step is up to stack level (and 0 is Local)
Here is simple example, which describes usage and effects of scopes:
$test = 'Global Scope'
Function Foo {
$test = 'Function Scope'
Write-Host $Global:test # Global Scope
Write-Host $Local:test # Function Scope
Write-Host $test # Function Scope
Write-Host (Get-Variable -Name test -ValueOnly -Scope 0) # Function Scope
Write-Host (Get-Variable -Name test -ValueOnly -Scope 1) # Global Scope
}
Foo
As you can see, you can use $Global:test like syntax only with named scopes, $0:test will be always $null.
You can use scope modifiers or the *-Variable cmdlets.
The scope modifiers are:
global used to access/modify at the outermost scope (eg. the interactive shell)
script used on access/modify at the scope of the running script (.ps1 file). If not running a script then operates as global.
(For the -Scope parameter of the *-Variable cmdlets see the help.)
Eg. in your second example, to directly modify the global $array:
& {
$global:array +="s"
Write-Host $array
}
For more details see the help topic about_scopes.
Not just varibles. When this says "item" it means variables, functions, aliases, and psdrives. All of those have scope.
LONG DESCRIPTION
Windows PowerShell protects access to variables, aliases, functions, and
Windows PowerShell drives (PSDrives) by limiting where they can be read and
changed. By enforcing a few simple rules for scope, Windows PowerShell
helps to ensure that you do not inadvertently change an item that should
not be changed.
The following are the basic rules of scope:
- An item you include in a scope is visible in the scope in which it
was created and in any child scope, unless you explicitly make it
private. You can place variables, aliases, functions, or Windows
PowerShell drives in one or more scopes.
- An item that you created within a scope can be changed only in the
scope in which it was created, unless you explicitly specify a
different scope.
The copy on write issue you're seeing is because of the way Powershell handles arrays. Adding to that array actually destroys the original array and creates a new one. Since it was created in that scope, it is destroyed when the function or script block exits and the scope is disposed of.
You can explicitly scope varibles when you update them, or you can use [ref] objects to do your updates, or write your script so that you're updating a property of an object or a hash table key of an object or hash table in a parent scope. This does not create a new object in the local scope, it modifies the object in the parent scope.
While other posts give lots of useful information they seem only to save you from RTFM.
The answer not mentioned is the one I find most useful!
([ref]$var).value = 'x'
This modifies the value of $var no matter what scope it happens to be in. You need not know its scope; only that it does in fact already exist. To use the OP's example:
$array=#("g")
function foo()
{
([ref]$array).Value += "h"
Write-Host $array
}
& {
([ref]$array).Value +="s"
Write-Host $array
}
foo
Write-Host $array
Produces:
g s
g s h
g s h
Explanation:
([ref]$var) gets you a pointer to the variable. Since this is a read operation it resolves to the most recent scope that actually did create that name. It also explains the error if the variable doesn't exist because [ref] can't create anything, it can only return a reference to something that already exists.
.value then takes you to the property holding the variable's definition; which you can then set.
You may be tempted to do something like this because it sometimes looks like it works.
([ref]$var) = "New Value"
DON'T!!!!
The instances where it looks like it works is an illusion because PowerShell is doing something that it only does under some very narrow circumstances such as on the command line. You can't count on it. In fact it doesn't work in the OP example.

Variable scoping in PowerShell

A sad thing about PowerShell is that function and scriptblocks are dynamically scoped.
But there is another thing that surprised me is that variables behave as a copy-on-write within an inner scope.
$array=#("g")
function foo()
{
$array += "h"
Write-Host $array
}
& {
$array +="s"
Write-Host $array
}
foo
Write-Host $array
The output is:
g s
g h
g
Which makes dynamic scoping a little bit less painful. But how do I avoid the copy-on-write?
The PowerShell scopes article (about_Scopes) is nice, but too verbose, so this is quotation from my article:
In general, PowerShell scopes are like .NET scopes. They are:
Global is public
Script is internal
Private is private
Local is current stack level
Numbered scopes are from 0..N where each step is up to stack level (and 0 is Local)
Here is simple example, which describes usage and effects of scopes:
$test = 'Global Scope'
Function Foo {
$test = 'Function Scope'
Write-Host $Global:test # Global Scope
Write-Host $Local:test # Function Scope
Write-Host $test # Function Scope
Write-Host (Get-Variable -Name test -ValueOnly -Scope 0) # Function Scope
Write-Host (Get-Variable -Name test -ValueOnly -Scope 1) # Global Scope
}
Foo
As you can see, you can use $Global:test like syntax only with named scopes, $0:test will be always $null.
You can use scope modifiers or the *-Variable cmdlets.
The scope modifiers are:
global used to access/modify at the outermost scope (eg. the interactive shell)
script used on access/modify at the scope of the running script (.ps1 file). If not running a script then operates as global.
(For the -Scope parameter of the *-Variable cmdlets see the help.)
Eg. in your second example, to directly modify the global $array:
& {
$global:array +="s"
Write-Host $array
}
For more details see the help topic about_scopes.
Not just varibles. When this says "item" it means variables, functions, aliases, and psdrives. All of those have scope.
LONG DESCRIPTION
Windows PowerShell protects access to variables, aliases, functions, and
Windows PowerShell drives (PSDrives) by limiting where they can be read and
changed. By enforcing a few simple rules for scope, Windows PowerShell
helps to ensure that you do not inadvertently change an item that should
not be changed.
The following are the basic rules of scope:
- An item you include in a scope is visible in the scope in which it
was created and in any child scope, unless you explicitly make it
private. You can place variables, aliases, functions, or Windows
PowerShell drives in one or more scopes.
- An item that you created within a scope can be changed only in the
scope in which it was created, unless you explicitly specify a
different scope.
The copy on write issue you're seeing is because of the way Powershell handles arrays. Adding to that array actually destroys the original array and creates a new one. Since it was created in that scope, it is destroyed when the function or script block exits and the scope is disposed of.
You can explicitly scope varibles when you update them, or you can use [ref] objects to do your updates, or write your script so that you're updating a property of an object or a hash table key of an object or hash table in a parent scope. This does not create a new object in the local scope, it modifies the object in the parent scope.
While other posts give lots of useful information they seem only to save you from RTFM.
The answer not mentioned is the one I find most useful!
([ref]$var).value = 'x'
This modifies the value of $var no matter what scope it happens to be in. You need not know its scope; only that it does in fact already exist. To use the OP's example:
$array=#("g")
function foo()
{
([ref]$array).Value += "h"
Write-Host $array
}
& {
([ref]$array).Value +="s"
Write-Host $array
}
foo
Write-Host $array
Produces:
g s
g s h
g s h
Explanation:
([ref]$var) gets you a pointer to the variable. Since this is a read operation it resolves to the most recent scope that actually did create that name. It also explains the error if the variable doesn't exist because [ref] can't create anything, it can only return a reference to something that already exists.
.value then takes you to the property holding the variable's definition; which you can then set.
You may be tempted to do something like this because it sometimes looks like it works.
([ref]$var) = "New Value"
DON'T!!!!
The instances where it looks like it works is an illusion because PowerShell is doing something that it only does under some very narrow circumstances such as on the command line. You can't count on it. In fact it doesn't work in the OP example.