Powershell function creation with Set-Item and GetNewClosure - powershell

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

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.

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.

Why does the scope of variables change depending on if it's a .ps1 or .psm1 file, and how can this be mitigated?

I have a function that executes a script block. For convenience, the script block does not need to have explicitly defined parameters, but instead can use $_ and $A to refer to the inputs.
In the code, this is done as such:
$_ = $Value
$A = $Value2
& $ScriptBlock
This whole thing is wrapped in a function. Minimal example:
function F {
param(
[ScriptBlock]$ScriptBlock,
[Object]$Value
[Object]$Value2
)
$_ = $Value
$A = $Value2
& $ScriptBlock
}
If this function is written in a PowerShell script file (.ps1), but imported using Import-Module, the behaviour of F is as expected:
PS> F -Value 7 -Value2 1 -ScriptBlock {$_ * 2 + $A}
15
PS>
However, when the function is written in a PowerShell module file (.psm1) and imported using Import-Module, the behaviour is unexpected:
PS> F -Value 7 -Value2 1 -ScriptBlock {$_ * 2 + $A}
PS>
Using {$_ + 1} instead gives 1. It seems that $_ has a value of $null instead. Presumably, some security measure restricts the scope of the $_ variable or otherwise protects it. Or, possibly, the $_ variable is assigned by some automatic process. Regardless, if only the $_ variable was affected, the first unsuccessful example would return 1.
Ideally, the solution would involve the ability to explicitly specify the environment in which a script block is run. Something like:
Invoke-ScriptBlock -Variables #{"_" = $Value; "A" = $Value2} -InputObject $ScriptBlock
In conclusion, the questions are:
Why can't script blocks in module files access variables defined in functions from which they were called?
Is there a method for explicitly specifying the variables accessible by a script block when invoking it?
Is there some other way of solving this that does not involve including an explicit parameter declaration in the script block?
Out of order:
Is there some other way of solving this that does not involve including an explicit parameter declaration in the script block?
Yes, if you just want to populate $_, use ForEach-Object!
ForEach-Object executes in the caller's local scope, which helps you work around the issue - except you won't have to, because it also automatically binds input to $_/$PSItem:
# this will work both in module-exported commands and standalone functions
function F {
param(
[ScriptBlock]$ScriptBlock,
[Object]$Value
)
ForEach-Object -InputObject $Value -Process $ScriptBlock
}
Now F will work as expected:
PS C:\> F -Value 7 -ScriptBlock {$_ * 2}
Ideally, the solution would involve the ability to explicitly specify the environment in which a script block is run. Something like:
Invoke-ScriptBlock -Variables #{"_" = $Value; "A" = $Value2} -InputObject $ScriptBlock
Execute the scripblock using ScriptBlock.InvokeWithContext():
$functionsToDefine = #{
'Do-Stuff' = {
param($a,$b)
Write-Host "$a - $b"
}
}
$variablesToDefine = #(
[PSVariable]::new("var1", "one")
[PSVariable]::new("var2", "two")
)
$argumentList = #()
{Do-Stuff -a $var1 -b two}.InvokeWithContext($functionsToDefine, $variablesToDefine, $argumentList)
Or, wrapped in a function like your original example:
function F
{
param(
[scriptblock]$ScriptBlock
[object]$Value
)
$ScriptBlock.InvokeWithContext(#{},#([PSVariable]::new('_',$Value)),#())
}
Now you know how to solve your problem, let's get back to the question(s) about module scoping.
At first, it's worth noting that you could actually achieve the above using modules, but sort of in reverse.
(In the following, I use in-memory modules defined with New-Module, but the module scope resolution behavior describe is the same as when you import a script module from disk)
While module scoping "bypasses" normal scope resolution rules (see below for explanation), PowerShell actually supports the inverse - explicit execution in a specific module's scope.
Simply pass a module reference as the first argument to the & call operator, and PowerShell will treat the subsequent arguments as a command to be invoked in said module:
# Our non-module test function
$twoPlusTwo = { return $two + $two }
$two = 2
& $twoPlusTwo # yields 4
# let's try it with explicit module-scoped execution
$myEnv = New-Module {
$two = 2.5
}
& $myEnv $twoPlusTwo # Hell froze over, 2+2=5 (returns 5)
Why can't script blocks in module files access variables defined in functions from which they were called?
If they can, why can't the $_ automatic variable?
Because loaded modules maintain state, and the implementers of PowerShell wanted to isolate module state from the caller's environment.
Why might that be useful, and why might one preclude the other, you ask?
Consider the following example, a non-module function to test for odd numbers:
$two = 2
function Test-IsOdd
{
param([int]$n)
return $n % $two -ne 0
}
If we run the above statements in a script or an interactive prompt, subsequently invocating Test-IsOdd should yield the expected result:
PS C:\> Test-IsOdd 123
True
So far, so great, but relying on the non-local $two variable bears a flaw in this scenario - if, somewhere in our script or in the shell we accidentally reassign the local variable $two, we might break Test-IsOdd completely:
PS C:\> $two = 1 # oops!
PS C:\> Test-IsOdd 123
False
This is expected since, by default, variable scope resolution just wanders up the call stack until it reaches the global scope.
But sometimes you might require state to be kept across executions of one or more functions, like in our example above.
Modules solve this by following slightly different scope resolution rules - module-exported functions defer to something we call module scope (before reaching the global scope).
To illustrate how this solves our problem from before, considering this module-exported version of the same function:
$oddModule = New-Module {
function Test-IsOdd
{
param([int]$n)
return $n % $two -ne 0
}
$two = 2
}
Now, if we invoke our new module-exported Test-IsOdd, we predictably get the expected result, regardless of "contamination" in the callers scope:
PS C:\> Test-IsOdd 123
True
PS C:\> $two = 1
PS C:\> Test-IsOdd 123 # still works
True
This behavior, while maybe surprising, basicly serves to solidify the implicit contract between the module author and the user - the module author doesn't need to worry too much about what's "out there" (the callers session state), and the user can expect whatever going on "in there" (the loaded module's state) to work correctly without worrying about what they assign to variables in the local scope.
Module scoping behavior poorly documented in the help files, but is explained in some depth in chapter 8 of Bruce Payette's "PowerShell In Action" (ISBN:9781633430297)

Is it possible to have variable after a loop in PowerShell?

I was wondering is it possible to have variables after a conditions, e.g. when normally a while sentence would be like that:
$i = 5
while ($i -le 5) {Write-Host $i; $i++}
But because I have so many variables in my script I thought may be I could write the variables at the end of the code so I could read the script's logic first not the variables, something like that:
while ($i -le 5) {Write-Host $i; $i++}
$i = 10
$Variable2 = 5
...
$Variable100 = 25
Powershell executes the lines in your script in order from top to bottom except where a loop, condition, or a function call make the execution jump somewhere else.
So, no, you cannot initialise a variable after you used it, that just wouldn't make sense.
Your solution here would be to move all the logic inside one or more functions, and then at the end of your script you can initialise variables and call the function(s). That does roughly what you are asking for.
You don't need to expressly declare a variable in PowerShell, if that's what you're asking. Variables that weren't defined before are automatically defined and initialized with default value the first time they're used.
However, depending on how the variable is used, you may inadvertently get different variables with the same name in different scopes:
PS C:\> function Foo { $i; $i = 23; $i }
PS C:\> Foo
23
PS C:\> $i = 42
PS C:\> $i
42
PS C:\> Foo
42
23
PS C:\> $i
42
PS C:\> function Bar { $i; $global:i = 23; $i }
PS C:\> Bar
42
23
PS C:\> $i
23
Also, if you want a variable to start with a specific value, you need to initialize it before you use it. If you have numerous initializations, you could put them into a separate PowerShell script and dot-source that second script in your "worker" script:
. 'C:\path\to\config.ps1'

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.