1

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 Answers2

2

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' }
Santiago Squarzon
  • 41,465
  • 5
  • 14
  • 37
  • I tried with -scope 0, 1 and 2 and they all give me the error "cannot find a variable with name 'i'" – Andrea Bardelli Jul 26 '23 at 14:32
  • 1
    @AndreaBardelli hehe, with your function, then the scope is `3`, because `ternary` is passing the object to `valOf` which then executes the scriptblock adding +1 to the scope. What you should do instead is check if `$f` and `$t` are scriptblock in `ternary` itself, or at least, execute them in it – Santiago Squarzon Jul 26 '23 at 14:35
  • @AndreaBardelli I updated to clarify this. Hopefully it makes sense why for your function scope `3` is needed – Santiago Squarzon Jul 26 '23 at 14:40
  • Firstly ty for the answer. The top would be not having to use lengthy syntax, as the main purpose of a ternary operator is being concise. But for all other purposes it's fine – Andrea Bardelli Jul 26 '23 at 14:43
2

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

mklement0
  • 382,024
  • 64
  • 607
  • 775
  • This looks just like what i was looking for ^^ would it work with a normal .psm1 module as well? – Andrea Bardelli Jul 26 '23 at 15:13
  • @AndreaBardelli, yes, it should; please see my update, which also provides links to more information about (module) scopes in PowerShell. – mklement0 Jul 26 '23 at 15:27