access parent scriptblock scope in powershell

79 Views Asked by At

As part of learning its basics i am implementing a ternary operator cmdlet in pws. I have it taking scriptblocks, to emulate the conditional evaluation ternary operators usually have. And in most instances it works fine.

function valOf($var){
    if($var -is [scriptblock]){
        return & $var   }
    else {
        return $var    }
}

function ternary([bool]$condition, $t, $f){
    if($condition){
        return valOf($t)    }
    else{
        return valOf($f)    }
    #return @($t,$f)[!$condition]
}

I got in trouble when i started nesting scriptblocks:

$i=56;
&{
   $i=0
   ternary($true) {$script:i+=2} {write-host "onFalse"}
   $i #wanted:2 #reality: 58
   <# without '$script: $i' is indeed 0, but cannot be edited #>
}      
$i #wanted:56 #reality:58

How can i access the middle scope?

browsing the documentation as well as the forum this seems to be quite a common issue, but the theme is anything but clear x.x
Perhaps an invokeCommand that optsOut from the copyOnWrite behaviour..?

2

There are 2 best solutions below

2
mklement0 On BEST ANSWER

An alternative to Santiago's helpful answer that makes it unnecessary to use special syntax in the script-block arguments passed to your ternary function:

You can combine dynamic modules with dot-sourcing:

Note: For brevity:

  • The ternary function below doesn't handle the case where the arguments aren't script blocks, but that's easy to add.

  • The [CmdletBinding()] attribute is omitted; you don't strictly need an advanced function to make the solution work, though it's certainly advisable, and adding something like [Parameter(Mandatory)] would implicitly make your function an advanced one.

# Create (and implicitly import) a dynamic module that
# hosts the ternary function.
$null = New-Module {
  # Define the ternary function.
  function ternary {
    param(
      [bool] $Condition,
      [scriptblock] $trueBlock,
      [scriptblock] $falseBlock
    )
    # Dot-source the appropriate script block,
    # which runs directly in the *caller's* scope,
    # given that's where it was created and given that the
    # module's code runs in a separate scope domain ("session state")
    if ($Condition) { . $trueBlock } else { . $falseBlock }
  }
}

$i=56;
& {
   $i=0
   # Now you can use $i as-is
   # in order to refer to the current scope's $i.
   ternary $true { $i+=2 } {write-host "onFalse"}
   $i # -> 2 
}
$i # -> 56

Note:

  • While a dynamic module is used above, the technique equally works with persisted modules (*.psm1)

  • This answer provides a comprehensive overview of scopes in PowerShell, including the separate scope domains (trees) for modules, somewhat unfortunately called session states in the official documentation

4
Santiago Squarzon On

Assuming your ternary function is an advanced function, for instance (notice the [cmdletbinding()] decoration):

function ternary {
    [CmdletBinding()]
    param(
        [bool] $condition,
        [scriptblock] $ifTrue,
        [scriptblock] $ifFalse
    )

    if ($condition) {
        return & $ifTrue
    }

    & $ifFalse
}

Then you can leverage $PSCmdlet to get and update the value $i in the inner scope:

$i = 56

& {
    $i = 0
    ternary $true { $PSCmdlet.SessionState.PSVariable.Get('i').Value += 2 } { Write-Host 'onFalse' }
    $i # 2
}

$i # 56

If the ternary is not advanced, the you can use Get-Variable targeting scope 2 for the function in this answer:

ternary $true { (Get-Variable i -Scope 2).Value += 2 } { Write-Host 'onFalse' }

For the function in your question you would need to use scope 3 because the scriptblock is passed to and executed by valOf adding +1 to the scope.

Personally, I would change your function to:

function ternary([bool] $condition, $t, $f) {
    if ($condition) {
        if ($t -is [scriptblock]) {
            return & $t
        }

        return $t
    }

    if ($f -is [scriptblock]) {
        return & $f
    }

    $f
}

Then you can use -Scope 2 without issues.

In both cases, advanced or non-advanced, using $ExecutionContext should also work:

ternary $true { $ExecutionContext.SessionState.PSVariable.Get('i').Value += 2 } { Write-Host 'onFalse' }