Alter script variable value from inside PowerShell function - powershell

I like C# so much better than PS but this time I'm forced to write a script...
I need to alter "global" variable from inside a function without passing the variable as parameter. Is it even possible?
Here is an abstract:
$a = 0
$b = 0
$c = 0
function Increment($var1, $var2)
{
$a = $var1
$b = $var2
$c++
}
Unlike C# in PS function variable are function scope bound and they remain unchanged outside of the scope of the function if used as described above. How can I make this work so the script variable are accessed by reference?

Try using the PowerShell global variable definition:
$global:a = 0
$global:b = 0
$global:c = 0
function Increment($var1, $var2)
{
$global:a = $var1
$global:b = $var2
$global:c++
}

If you really need to modify by reference, you can use [Ref], e.g.:
$a = 1
function inc([Ref] $v) {
$v.Value++
}
inc ([Ref] $a)
$a # outputs 2

Related

How does Measure-Command invoke ScriptBlock in parent scope?

In the example below after Measure-Command variable x is updated, but in example with my own version of command x remains the same.
$x = 0
Measure-Command -Expression { $x++ } | Out-Null
$x # outputs 1
function Measure-Command2
{
param([ScriptBlock]$Expression)
. $Expression
}
$x = 0
Measure-Command2 -Expression { $x++ }
$x # outputs 0
Can I use the same magic in my own functions?
The most user-friendly solution is to define your function in a dynamic module, using New-Module:
$null = New-Module {
function Measure-Command2 {
param([ScriptBlock]$Expression)
. $Expression
}
}
$x = 0
Measure-Command2 -Expression { $x++ }
$x # outputs *1* now, as desired.
Note:
Modules run in their own scope domain aka session state that is separate from the caller's. Thus, unlike with non-module functions, no child scope of the caller's scope is created on invocation.
The script block passed as an argument, due to having been created as a literal ({ ... }) in the caller's scope, executes there, not in the function's.
Without a module, you'd need to dot-source the invocation of Measure-Command2 itself, given that functions and scripts run in a child scope by default:
function Measure-Command2 {
param([ScriptBlock]$Expression)
. $Expression
}
$x = 0
# Note the dot-sourcing
. Measure-Command2 -Expression { $x++ }
$x # outputs *1* now, as desired.

Get-Variable defined in a scriptblock from a psm function

I have the following piece of code:
$x = 'xyz'
& {
$y = 'abc'
foo
}
The foo function is defined in the foo.psm1 module which is imported before the script block is started.
Inside the foo function, I call Get-Variable which shows me x but it doesn't show y. I tried playing with the -Scope parameter: Local, Script, Global, 0 - which is the local scope from what I understood from the docs, 1 - which is the parent scope.
How could I get the y variable inside the foo function?
I'm not looking for a solution such as passing it as an argument. I want something as Get-Variable but sadly it doesn't see it for some reason.
UP
Based on the comments received, probably more context is needed.
Say that foo receives a ScriptBlock which is using the $using: syntax.
$x = 'xyz'
& {
$y = 'abc'
foo -ScriptBlock {
Write-Host $using:x
Write-Host $using:y
}
}
I'm 'mining' these variables as follows:
$usingAsts = $ScriptBlock.Ast.FindAll( { param($ast) $ast -is [System.Management.Automation.Language.UsingExpressionAst] }, $true) | ForEach-Object { $_ -as [System.Management.Automation.Language.UsingExpressionAst] }
foreach ($usingAst in $usingAsts) {
$varAst = $usingAst.SubExpression -as [System.Management.Automation.Language.VariableExpressionAst]
$var = Get-Variable -Name $varAst.VariablePath.UserPath -ErrorAction SilentlyContinue
}
This is how I'm using Get-Variable and in the case presented above, y cannot be found.
Modules run in their own scope domain (aka session state), which means they generally do not see the caller's variables - unless (a module-external) caller runs directly in the global scope.
For an overview of scopes in PowerShell, see the bottom section of this answer.
However, assuming that you define the function in your module as an advanced one, there is a way to access the caller's state, namely via the automatic $PSCmdlet variable.
Here's a simplified example, using a dynamic module created via the New-Module cmdlet:
# Create a dynamic module that defines function 'foo'
$null = New-Module {
function foo {
# Make the function and advanced (cmdlet-like) one, via
# [CmdletBinding()].
[CmdletBinding()] param()
# Access the value of variable $bar in the
# (module-external) caller's scope.
# To get the variable *object*, use:
# $PSCmdlet.SessionState.PSVariable.Get('bar')
$PSCmdlet.GetVariableValue('bar')
}
}
& {
$bar = 'abc'
foo
}
The above outputs verbatim abc, as desired.

How does the compound assignment operator behave in child scope?

What happens to the following two function? I expect they are the same.
PS C:\> $i = 5
PS C:\> function Test1 { $i += 1; $i }
PS C:\> function Test2 { $i = $i + 1; $i }
PS C:\> Test1
1 # why?
PS C:\> Test2
6
PS C:\> $i
5 # will not change
I'm aware the $i in the function has local scope, hence will not be changed in the global scope, this is intentional. This question is simply about why the following 2 assignment statements behave differently here, as far as I know, they should be equivalent.
$i = $i + 1
$i += 1
Explanation:
In Test1 the variable $i is assigned a value (using the compound assignment operator). Because you cannot change the variable from the global scope, a new local variable is created (hiding the global variable), which does not have a value initially (basically 0), which is then increased by 1. When returning the value, the local variable is used.
function Test1 {
# because it's an assignment, a new local variable
# is created (with initial value = 0)
$local:i += 1
$local:i
}
In Test2 the value of the global variable $i is used (because the variable is visible in this child scope), 1 is added, and the result (6) is assigned to a new local variable. As in Test1, the value of the local variable is returned at the end.
function Test2 {
# a new local variable is created, but because the global
# variable is visible here, its value is used initially
$local:i = $global:i + 1
$local:i
}
Read more about scopes here: about_scopes
As for ...
I want the behavior of function Test2
($i = 5)
# Results
<#
5
#>
<#
To save the time/code in calling the variable separately, you can use PowerShell
variable squeezing to assign to the variable and output to the screen at the same
time.
This is more direct if you are always going to output to the screen.
It's a choice and has no effect on what you are doing
function Test1 { ($i += 1) }
function Test2 { ($i = $i + 1) }
Test1
Test2
# Results
<#
1
6
#>
($i = 5)
# Note the small refactor in the operator
function Test1 { ($i + 1) }
function Test2 { ($i = $i + 1) }
Test1
Test2
# Results
<#
6
6
#>
This still is a scope issue, local vs global, but the above gives you the matching results in both calls.

Powershell function creation with Set-Item and GetNewClosure

I am attempting to build a function that can itself create functions through the Set-Item command where I pass a scriptblock for the new function to the -Value parameter of Set-Item. I'm running into an issue where using GetNewClosure on the scriptblock doesn't seem to be working and I just don't know what I'm doing wrong.
In the code below, first I'm creating a function manually (testFunc) which works as intended, in that setting $x to 2 after the function creation will not cause the function to return 2; instead it returns 1 because that was the value of $x at the time the function was created. But when I try to do the same through the make-function function, the behavior changes.
I'm sure I'm overlooking something small.
> $x = 1
> $block = {$x}.GetNewClosure()
> Set-Item function:global:testFunc -Value $block
> testFunc
1
> $x = 2
> testFunc
1 # still 1 because of GetNewClosure - this is expected behavior
> $x = 1
> function make-function { $block2 = {$x}.GetNewClosure()
Set-Item function:global:testFunc2 -Value $block2
}
> make-function
> testFunc2
1
> $x = 2
> testFunc2
2 # Why is it not returning 1 in this case?
The clue is in the MSDN docs but it is subtle:
Any local variables that are in the context of the caller are copied
into the module.
GetNewClosure() appears to capture "only" local variables i.e those from the caller's current scope. So try this:
function Make-Function {
$x = $global:x
$function:global:testFunc2 = {$x}.GetNewClosure()
}
BTW you can see what variables are captured by GetNewClosure() into the newly created dynamic module by executing:
$m = (Get-Command testFunc2).Module
& $m Get-Variable -Scope 0

Setting a global PowerShell variable from a function where the global variable name is a variable passed to the function

I need to set a global variable from a function and am not quite sure how to do it.
# Set variables
$global:var1
$global:var2
$global:var3
function foo ($a, $b, $c)
{
# Add $a and $b and set the requested global variable to equal to it
$c = $a + $b
}
Call the function:
foo 1 2 $global:var3
End result:
$global:var3 is set to 3
Or if I called the function like this:
foo 1 2 $global:var2
End result:
$global:var2 is set to 3
I hope this example makes sense. The third variable passed to the function is the name of the variable it is to set.
You can use the Set-Variable cmdlet. Passing $global:var3 sends the value of $var3, which is not what you want. You want to send the name.
$global:var1 = $null
function foo ($a, $b, $varName)
{
Set-Variable -Name $varName -Value ($a + $b) -Scope Global
}
foo 1 2 var1
This is not very good programming practice, though. Below would be much more straightforward, and less likely to introduce bugs later:
$global:var1 = $null
function ComputeNewValue ($a, $b)
{
$a + $b
}
$global:var1 = ComputeNewValue 1 2
As simple as:
$A="1"
function changeA2 () { $global:A="0"}
changeA2
$A
I ran across this question while troubleshooting my own code.
So this does NOT work...
$myLogText = ""
function AddLog ($Message)
{
$myLogText += ($Message)
}
AddLog ("Hello")
Write-Host $myLogText
This APPEARS to work, but only in the PowerShell ISE:
$myLogText = ""
function AddLog ($Message)
{
$global:myLogText += ($Message)
}
AddLog ("Hello")
Write-Host $myLogText
This is actually what works in both ISE and command line:
$global:myLogText = ""
function AddLog ($Message)
{
$global:myLogText += ($Message)
}
AddLog ("Hello")
Write-Host $global:myLogText
You'll have to pass your arguments as reference types.
#First create the variables (note you have to set them to something)
$global:var1 = $null
$global:var2 = $null
$global:var3 = $null
#The type of the reference argument should be of type [REF]
function foo ($a, $b, [REF]$c)
{
# add $a and $b and set the requested global variable to equal to it
# Note how you modify the value.
$c.Value = $a + $b
}
#You can then call it like this:
foo 1 2 [REF]$global:var3
The first suggestion in latkin's answer seems good, although I would suggest the less long-winded way below.
PS c:\temp> $global:test="one"
PS c:\temp> $test
one
PS c:\temp> function changet() {$global:test="two"}
PS c:\temp> changet
PS c:\temp> $test
two
His second suggestion however about being bad programming practice, is fair enough in a simple computation like this one, but what if you want to return a more complicated output from your variable? For example, what if you wanted the function to return an array or an object? That's where, for me, PowerShell functions seem to fail woefully. Meaning you have no choice other than to pass it back from the function using a global variable. For example:
PS c:\temp> function changet([byte]$a,[byte]$b,[byte]$c) {$global:test=#(($a+$b),$c,($a+$c))}
PS c:\temp> changet 1 2 3
PS c:\temp> $test
3
3
4
PS C:\nb> $test[2]
4
I know this might feel like a bit of a digression, but I feel in order to answer the original question we need to establish whether global variables are bad programming practice and whether, in more complex functions, there is a better way. (If there is one I'd be interested to here it.)
#zdan. Good answer. I'd improve it like this...
I think that the closest you can come to a true return value in PowerShell is to use a local variable to pass the value and never to use return as it may be 'corrupted' by any manner of output situations
function CheckRestart([REF]$retval)
{
# Some logic
$retval.Value = $true
}
[bool]$restart = $false
CheckRestart( [REF]$restart)
if ( $restart )
{
Restart-Computer -Force
}
The $restart variable is used either side of the call to the function CheckRestart making clear the scope of the variable. The return value can by convention be either the first or last parameter declared. I prefer last.
Set the variable as a global variable outside of the function and then set the value inside of the function.