Skip to content

Do delay-bind scriptblocks / scriptblocks in calculated properties run in a child scope by design? #7157

Closed

Description

The current behavior may well be by design, but it would be helpful to understand why - and document it.

  • Script blocks passed to dedicated [scriptblock]-typed parameters such as in the context of ForEach-Object and Where-Object run directly in the caller's scope.

  • Delay-bind script-block arguments / script blocks in calculated properties, by contrast, run in a child scope, so that direct attempts to modify variables in the caller's scope create new local instances instead.

Contrast the following two commands:

# $i in the script block is the *caller's* $i, so modifying it works across invocations.
PS> $i = 0; 'a', 'b' | ForEach-Object { (++$i) }
1
2
# The script block runs in a *child* scope, so modifying $i modifies a local copy that
# goes out of scope with the script block.
PS> $i = 0; 'a', 'b' | Select-Object { (++$i) }

 (++$i) 
--------
       1
       1

Note how $i didn't increment across calls.


If you do want $i to increment across calls, you currently need a somewhat obscure workaround based on [ref]:

PS> $iRef = [ref] 0; 'a', 'b' | Select-Object { (++$iRef.Value) }

 (++$iRef.Value) 
-----------------
                1
                2

Changing the behavior to consistently execute script blocks in the caller's would be a breaking change, though presumably of type Bucket 3: Unlikely Grey Area.

Environment data

Written as of:

PowerShell Core v6.1.0-preview.3

Activity

  1. added
    Issue-Questionideally support can be provided via other mechanisms, but sometimes folks do open an issue to get a
    WG-Enginecore PowerShell engine, interpreter, and runtime
    on Jun 24, 2018
  2. changed the title [-]Do ad-hoc script-block arguments / script blocks in calculated properties run in a child variable scope by design?[/-] [+]Do delay-bind scriptblocks / scriptblocks in calculated properties run in a child variable scope by design?[/+] on Nov 17, 2018
  3. GeeLaw commented on Mar 14, 2019

    @GeeLaw

    This behavior is so subtle that people might have been implicitly taking advantage of the fact that delay-bind scripts doesn't pollute the caller's scope.

    My understanding is that pipeline-extraction scripts (ValueFromPipeline(ByPropertyName)) are mapping that are supposed to be pure (free of side-effect), while clearly ForEach-Object is more on the side of imperative paradigm, resembling the foreach statement. The choice for Where-Object is somehow arbitrary, but is useful for the case

    $i = 0;
    Get-Items | Where-Object { (++$i) % 2 }

    One might ask why such usage wouldn't benefit the pipeline-extraction scenario, i.e., what's wrong if we execute the extraction scripts in the caller's scope. The answer is that there might be multiple parameters delay-bound. If the extraction script blocks have side effects, the order of extraction would affect the result. E.g., consider

    # Imaginary
    $i = 0;
    Get-SomeItem | Use-SomePipeline -IndexMod2 { <# ??? #> } -IndexMod3 { <# ??? #> }
    
    # Currently possible version
    $i = [pscustomobject]@{ 'Value' = 0 };
    Get-SomeItem | Use-SomePipeline -IndexMod2 { <# ??? #> } -IndexMod3 { <# ??? #> }

    It is impossible to write the script correctly without knowing the order of delay-binding -- should I increase $i in the script for IndexMod2 or IndexMod3? I'm not sure whether PowerShell has the order of which parameter is bound before which documented. If that's undocumented, that's undefined to me, and if the extraction scripts had side effects, the whole script wouldn't be useful because of undefined behavior.

    The invocation order for ForEach-Object and Where-Object is clear, so it makes sense to allow execution in the caller's scope.

  4. daxian-dbw commented on Mar 15, 2019

    @daxian-dbw
    Member

    clearly ForEach-Object is more on the side of imperative paradigm

    Agreed. For Foreach-Object and Where-Object, running in the caller's scope allows us to do more than just writing results to the pipeline.
    Also, Foreach-Object has -Begin, -Process and -End three script blocks, and they need to run in the same scope.

    As for delay-bind script blocks in parameter binding, the purpose is simply to do some quick transformation/calculation on the value from pipeline, and it makes sense to isolate the execution to not pollute the caller's scope.

  5. mklement0 commented on Mar 15, 2019

    @mklement0
    ContributorAuthor

    This behavior is so subtle

    The behavioral distinction is indeed so subtle that it invites everlasting confusion.
    Even if you understand the rationale - and the multiple-delay-bind-script-block scenario is a good argument for the distinction in principle, but, due to its exoticness, to my mind not in practice - remembering it is a challenge.

    it makes sense to isolate the execution to not pollute the caller's scope.

    To paraphrase a common saying: One person's pollution is another person's treasure.

    Given:

    PS> $i = 0; 'a', 'b' | ForEach-Object { (++$i) }
    1
    2

    why shouldn't a delay-bind script block with Rename-Item work the same?

    # Does NOT work as intended, because a *local* $i variable is created on every invocation.
    PS>  $i = 0; $null = New-Item -Force tf-a, tf-b; Get-Item tf-? |
               Rename-Item -NewName { '{0}-{1}' -f ++$i, $_.Name  } -WhatIf
    What if: Performing ... "Rename File" on target "Item: .../tf-a Destination: .../1-tf-a".
    What if: Performing ... "Rename File" on target "Item: .../tf-b Destination: .../1-tf-b".

    That is not a hypothetical example, by the way: it's a real-world scenario for renaming files with sequence numbers that I've come across multiple times on Stack Overflow.

  6. mklement0 commented on Mar 15, 2019

    @mklement0
    ContributorAuthor

    P.S.: The flip side of this issue is that it's currently extremely cumbersome to implement your own cmdlet / advanced function that processes a given script block in the caller's scope ("dot-sourced") with $_ support, the way that ForEach-Object / Where-Object / Measure-Command do - see #3581

  7. vexx32 commented on Mar 15, 2019

    @vexx32
    Collaborator

    Yeah, I've hit that one too Michael Klement (@mklement0). Takes me a few attempts to realize what I'm doing wrong, and thankfully the solution is relatively easy -- just scope-bind the variable properly (either to $script: or $global: usually works OK.) 😄

  8. wisemoth commented on Jun 17, 2020

    @wisemoth

    the solution is relatively easy -- just scope-bind the variable properly (either to $script: or $global: usually works OK.) 😄

    Rain Sallow (/u/ta11ow) (@vexx32) Would you please be able to expand on this? Pretty sure I'm up against this issue, so wanted to explore your suggestion further.

  9. vexx32 commented on Jun 17, 2020

    @vexx32
    Collaborator

    Michael Klement (@mklement0)'s not-working example was:

    $i = 0
    $null = New-Item -Force tf-a, tf-b; Get-Item tf-? |
        Rename-Item -NewName { '{0}-{1}' -f ++$i, $_.Name  } -WhatIf

    In that case you can workaround it with something like:

    $script:i = 0
    $null = New-Item -Force tf-a, tf-b; Get-Item tf-? |
        Rename-Item -NewName { '{0}-{1}' -f ++$script:i, $_.Name  } -WhatIf

    Generally you should only run into this kind of issue if you've a need to set or change the variable value from inside the delay-bind scriptblock. 🙂

  10. mklement0 commented on Jun 17, 2020

    @mklement0
    ContributorAuthor

    Thanks, Rain Sallow (/u/ta11ow) (@vexx32). Note that there's also the more obscure [ref]-based workaround from the OP, which, however, does have that advantage that it predictably targets the parent scope (the caller's specific scope), whatever it may be (you could alternatively implement this via Get-Variable / New-Variable -PassThru in the calling scope):

    [ref] $iRef = 0; $null = New-Item -Force tf-a, tf-b; Get-Item tf-? |
               Rename-Item -NewName { '{0}-{1}' -f ++$iRef.Value, $_.Name  } -WhatIf
  11. changed the title [-]Do delay-bind scriptblocks / scriptblocks in calculated properties run in a child variable scope by design?[/-] [+]Do delay-bind scriptblocks / scriptblocks in calculated properties run in a child scope by design?[/+] on Jun 24, 2021
  12. microsoft-github-policy-service commented on Nov 16, 2023

    @microsoft-github-policy-service
    Contributor

    This issue has not had any activity in 6 months, if this is a bug please try to reproduce on the latest version of PowerShell and reopen a new issue and reference this issue if this is still a blocker for you.

  13. microsoft-github-policy-service commented on Nov 16, 2023

    @microsoft-github-policy-service
    Contributor

    This issue has not had any activity in 6 months, if this is a bug please try to reproduce on the latest version of PowerShell and reopen a new issue and reference this issue if this is still a blocker for you.

  14. microsoft-github-policy-service commented on Nov 16, 2023

    @microsoft-github-policy-service
    Contributor

    This issue has not had any activity in 6 months, if this is a bug please try to reproduce on the latest version of PowerShell and reopen a new issue and reference this issue if this is still a blocker for you.

  15. microsoft-github-policy-service commented on Nov 23, 2023

    @microsoft-github-policy-service
    Contributor

    This issue has been marked as "No Activity" as there has been no activity for 6 months. It has been closed for housekeeping purposes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Issue-Questionideally support can be provided via other mechanisms, but sometimes folks do open an issue to get aResolution-No ActivityIssue has had no activity for 6 months or moreWG-Enginecore PowerShell engine, interpreter, and runtime

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions