Scriptblocks seem to be PowerShell's analog to other languages' anonymous functions. I'm finding scriptblock's role as anonymous anonymous functions critical to writing maintainable code. Unlike anonymous functions in C#, for example, how a scriptblock is invoked seems to affect its behavior. The method of invocation seems affect behavior in (at least) the following ways:
param() can be used.Consider the following code:
New-Module m {
$v = 'c'
function f {
param($sb)
% $sb
}
function g {
param($sb)
& $sb -p1 'p1 is bound'
}
} | Out-Null
'--- & ---'
& {
$v = 'v is not modified by g'
g -sb {
param( $p1 )
process
{
$p1
$v = 'modified by g'
}
}
$v
}
'--- % ---'
& {
$v = 'v is not modified by f'
f -sb {
param( $p1 ) # <== these line are unused because % does not
$p1 # <== use parameter binding on this scriptblock
$v = 'v is modified by f'
}
$v
}
which outputs
--- & ---
p1 is bound
v is not modified by g
--- % ---
v is modified by f
This seems to demonstrate the following:
& do not have side effects in the definition's scope% do have side effects in the definition's scope& supports param()Judging from its help topic, % does not support using param().
The following table summarizes the behavior of % and & in these respects:
| Invocation Method | supports using param() | variable assignment has side-effects |
|-------------------|--------------------------|--------------------------------------|
| % | no | yes |
| & | yes | no |
Is there a way to invoke scriptblocks that supports using both param() and variable assignments with side effects?
ForEach-Object isn't really meant to be used that way, though it is sometimes abused for it's side effects.
You probably want the dot source operator, like . $sb -p1 'p1 is bound'
ForEach-Object isn't really meant to be used that way, though it is sometimes abused for it's side effects.
Could you elaborate on why relying on ForEach-Object's side effects is abuse?
Here is an example typical of how I rely on its side effects:
$in = @{
a1 = 'ayeone'
a2 = 'ayetwo'
b1 = 'beone'
b2 = 'betwo'
}
$out = @{}
$in.Keys |
where {$_ -match 'a'} |
foreach { $out.$_ = $in.$_ }
Is there something problematic about this use?
You probably want the dot source operator, like
. $sb -p1 'p1 is bound'
Interesting. I think you are probably right. I hadn't considered this because neither about_Operators nor the language spec makes any mention of . having an effect on the scope where the scriptblock is defined. Instead the (rather terse) dot-source sections in those documents espouses the dot-source operator's effect on the "current scope". Affecting the "current scope" at the . call site would be awkward in this usage. It seems, however, that is not what . does.
My prior understanding of . seems to have been based on the statements
Runs a script in the current scope so that any functions, aliases, and variables that the script creates are added to the current scope.
in about_Operators, and
However, when dot source notation is used, no new scope is created before the command is executed, so additions/changes it would have made to its own local scope are made to the current scope instead.
in the language spec.
Both of those statements are inconsistent with the behavior of
New-Module n {
$u = 'module'
function a {
param($sb)
$u = 'function'
'input_object' | . $sb -p1 'p1 is bound'
"u: $u"
"t: $t"
}
} | Out-Null
$u = 'root'
& {
$u = 'u is not modified by a'
a -sb {
param($p1)
process
{
"DollarBar: $_"
$p1
$u = 'u is modified by a'
$t = 'created variable'
}
}
$u
}
which outputs
DollarBar: input_object
p1 is bound
u: function
t:
u is modified by a
That seems to demonstrate that neither "any...variables that the script creates are added to the current scope" nor "additions/changes it would have made to its own local scope are made to the current scope instead" are true, in general. It seems that if those statements were true, the output would have included u: u is modified by a and t: created variable which it did not.
I'm surprised by the behavior of . here. It seems contrary to the documentation and to the behavior when dot-sourcing files. Could you shed some light on what is happening with respect to scope when using the . dot-source operator in this manner?
Could you elaborate on why relying on ForEach-Object's side effects is abuse?
Maybe I overstated it, but if you aren't using ForEach-Object in the pipeline, you are using it in a way that it wasn't designed for, so surprising things could happen, that's all. It's also not intuitive for readers of the script.
Could you shed some light on what is happening with respect to scope when using the . dot-source operator in this manner?
A script block is associated with a SessionState which is closely related to a module, but there is a SessionState associated with script blocks not in a module, we could call that the global SessionState.
A script block is bound to a SessionState immediately if you use the { ... } syntax, or upon the first invocation if the script block was created some other way, e.g. [ScriptBlock]::Create(). The binding is to the active SessionState.
Invoking a script block may involve changing the active SessionState if the current SessionState doesn't match the one bound to the script block.
Each SessionState has it's own scope stack.
Normally, when invoking a script block (including a function or ps1 script file), a new scope is created after the SessionState has been updated, unless you use the dot source invocation operator, in which case, the current scope in the active SessionState is used instead.
One last detail - there are at most 2 relevant SessionState instances involved when looking for scoped items like a variable. If the scoped item is not found in the current SessionState, there may be a parent SessionState that links directly to the global scope. This is to prevent accidentally finding something in an unexpected scope as this example demonstrates:
$null = New-Module M {
$v = "M.v" # Never found
function a { b }
}
$null = New-Module N {
function b { $v <# Finds global.v #> }
}
$v = "global.v"
function f { $v }
function g {
$v = "g.v"
a # Finds global.v
f # Finds g.v
}
a
b
g
I think I see how this works now. Here is my understanding of what is happening to scope and session state:
New-Module p {
$mpss = $ExecutionContext.SessionState # module p session state
$v = 'p.v' <# This is never accessed because it is associated with
$mpss, and no other scriptblock associated with
$mpss accesses $v #>
function d {
<# the session state associated with this scriptblock is $mpss #>
param($sb)
# active session state is $mpss
. $sb # changes active session state to $gss
# active session state is $mpss again
}
function e {
param($sb)
& $sb <# changes active session state to $gss,
and adds a new scope onto $gss's scope stack #>
}
} | Out-Null
$gss = $ExecutionContext.SessionState # "global" session state
$v = 'global.v'
& { <# this pushes a new scope onto $gss's scope stack #>
$v = 'global.v-1'
d -sb {
<# The session state associated with this scriptblock is $gss.
So when this scriptblock is invoked from module p, the
active session state is switched to $gss.#>
<# Because this is invoked using . no scope is added to $gss's
scope stack, so an assignment to $v overwrites 'global.v-1' #>
$v = 'modified global.v-1'
<# When this scriptblock completes its execution after being
invoked from module p, the active session state is switched back
to $mpss.#>
}
$v # 'modified global.v-1'
} <# this pops the last scope from $gss's scope stack #>
& {
$v = 'global.v-1'
e -sb {
<# Because this is invoked using & a new scope is added to $gss's
scope stack, so an assignment to $v creates $v in the new scope
without overwriting 'global.v-1'#>
$v = 'global.v-2'
}
$v # 'global.v-1'
}
Does this look correct @lzybkr?
@alx9r - yes, that looks correct.
Thanks very much for your help @lzybkr.