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..?

1
  • 3
    the inner $i would be Get-Variable i -Scope 0 the outer would be Get-Variable i -Scope 1 Commented Jul 26, 2023 at 14:05

2 Answers 2

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

Sign up to request clarification or add additional context in comments.

2 Comments

This looks just like what i was looking for ^^ would it work with a normal .psm1 module as well?
@AndreaBardelli, yes, it should; please see my update, which also provides links to more information about (module) scopes in PowerShell.
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' }

4 Comments

I tried with -scope 0, 1 and 2 and they all give me the error "cannot find a variable with name 'i'"
@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
@AndreaBardelli I updated to clarify this. Hopefully it makes sense why for your function scope 3 is needed
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

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.