What exactly is a PowerShell ScriptBlock

2019-04-05 07:48发布

问题:

A PowerShell ScriptBlock is not a lexical closure as it does not close over the variables referenced in its declaring environment. Instead it seems to leverage dynamic scope and free variables which are bound at run time in a lambda expression.

function Get-Block {
  $b = "PowerShell"
  $value = {"Hello $b"}
  return $value
}
$block = Get-Block
& $block
# Hello
# PowerShell is not written as it is not defined in the scope
# in which the block was executed.


function foo {
  $value = 5
  function bar {
    return $value
  }
  return bar
}
foo
# 5
# 5 is written $value existed during the evaluation of the bar function
# it is my understanding that a function is a named scriptblock
#  which is also registered to function:

Calling GetNewClosure() on a ScriptBlock returns a new ScriptBlock which closes over the variables referenced. But this is very limited in scope and ability.

What is a ScriptBlock's classification?

回答1:

Per the docs, a scriptblock is a "precompiled block of script text." So by default you just a pre-parsed block of script, no more, no less. Executing it creates a child scope, but beyond that it's as if you pasted the code inline. So the most appropriate term would simply be "readonly source code."

Calling GetNewClosure bolts on a dynamically generated Module which basically carries a snapshot of all the variables in the caller's scope at the time of calling GetNewClosure. It is not a real closure, simply a snapshot copy of variables. The scriptblock itself is still just source code, and variable binding does not occur until it is invoked. You can add/remove/edit variables in the attached Module as you wish.

function GetSB
{
   $funcVar = 'initial copy'

   {"FuncVar is $funcVar"}.GetNewClosure()

   $funcVar = 'updated value'  # no effect, snapshot is taken when GetNewClosure is called
}

$sb = GetSB

& $sb  # FuncVar is initial copy

$funcVar = 'outside'
& $sb  # FuncVar is initial copy

$sb.Module.SessionState.PSVariable.Remove('funcVar')
& $sb  # FuncVar is outside


回答2:

A PowerShell ScriptBlock is equivalent to a first-class, anonymous function. Most of the confusion I've seen is not with ScriptBlocks, but with the function keyword.

  • PowerShell does support function closures, however the function keyword does not.

Examples

Function:

PS> function Hello {
>>      param ([string] $thing)
>>      
>>      return ("Hello " + $thing)
>>  }

PS> Hello "World"
"Hello World"

ScriptBlock:

PS> $HelloSB = {
>>      param ([string] $thing)
>>      
>>      return ("Hello " + $thing)
>>  }

PS> & $HelloSB "World"
"Hello World"

PS> $HelloRef = $HelloSB
PS> & $HelloRef "Universe"
"Hello Universe"

Closure:

PS> $Greeter = {
>>      param ([string] $Greeting)
>>      
>>      return ( {
>>          param ([string] $thing)
>>          
>>          return ($Greeting + " " + $thing)
>>      }.GetNewClosure() )
>>  }

PS> $Ahoy = (& $Greeter "Ahoy")
PS> & $Ahoy "World"
"Ahoy World"

PS> $Hola = (& $Greeter "Hola")
PS> & $Hola "Mundo"
"Hola Mundo"

Although you can get around the limitation of the function keyword with the "Set-Item" cmdlet:

PS> function Greeter = { ... }  # ✕ Error
PS> function Greeter { ... }.GetNewClosure()  # ✕ Error

PS> Set-Item -Path "Function:Greeter" -Value $Greeter  # (defined above)  ✓ OK
PS> $Hola = Greeter "Hola"
PS> & $Hola "Mundo"
"Hola Mundo"

The Value parameter of the "Set-Item" cmdlet can be any ScriptBlock, even one returned by another function. (The "Greeter" function, for example, returns a closure, as shown above.)

PS> Set-Item -Path "Function:Aloha" -Value (Greeter "Aloha")
PS> Aloha "World"
"Aloha World"

Two other important points:

  • PowerShell uses dynamic scoping, not lexical scoping.

    A lexical closure is closed on its source-code environment, whereas a dynamic closure is closed based on the active/dynamic environment that exists when GetNewClosure() is called. (Which is more appropriate for a scripting language.)

  • PowerShell may have "functions" and "return" statements, but actually its input/output is based on streams and piping. Anything written out of a ScriptBlock with the "Write-Output" or "write" cmdlet will be returned.