4

I'm trying to understand how .GetNewClosure() works within the context of a script cmdlet in PowerShell 2.

In essence I have a function that returns an object like so:

function Get-AnObject {
param(
    [CmdletBinding()]
    [Parameter(....)]
    [String[]]$Id
    ..
    [ValidateSet('Option1','Option2')]
    [String[]]$Options
)

...

    $T = New-Object PSCustomObject -Property @{ ..... } 
    $T | Add-Member -MemberType ScriptProperty -Name ExpensiveScriptProperty -Value {
        $this | Get-ExpensiveStuff
    }.GetNewClosure() 

..
}

Providing I do not have the validate set options the closure appears to work fine. If it is included however the new closure fails with the following error.

Exception calling "GetNewClosure" with "0" argument(s): "Attribute cannot be added because it would cause the variable Options with value to become invalid."

Presumably the closure is trying to capture the context of the call to the Cmdlet. Since the parameter "Options" is not bound at all this is not nicely with the parameter validation.

I imagine it's possible to avoid this by placing validation as code within the body of the Cmdlet instead of making use of the [Validate*()] decorators -- but this seems nasty and quite obscure. Is there a way of fusing these two ideas?

thecoshman
  • 8,394
  • 8
  • 55
  • 77
user1383092
  • 507
  • 1
  • 7
  • 17

2 Answers2

5

The "Attribute cannot be added" message is (or was) a PowerShell bug, I've submitted it to Microsoft with this bug report. That particular issue seems to have been fixed, (perhaps around V5.1. but anyone interested in Powershell Closures may still find info below interesting.

There is a workaround which works in earlier versions, but first here's a simplified repro case that produces the same error:

function Test-ClosureWithValidation {
    [CmdletBinding()]
    param(
        [Parameter()]
        [ValidateSet('Option1','Option2')]
        [String[]]$Options
    )
    [scriptblock] $closure = {"OK"}.GetNewClosure();
    $closure.Invoke()
}

Test-ClosureWithValidation -Options Option1

The workaround depends on the fact that GetNewClosure() works by iterating over the local variables in the calling script's context, binding these local variables into the script's context. The bug occurs because its copying the $Options variable including the validation attribute. You can work around the bug by creating a new context with only the local variables you need. In the simple repro above, it is a one-line workaround:

    [scriptblock] $closure = &{ {"OK"}.GetNewClosure();}

The line above now creates a scope with no local variables. That may be too simple for your case; If you need some values from the outer scope, you can just copy them into local variables in the new scope, e.g:

    [scriptblock] $closure = &{ 
        $options = $options; 
        {"OK $options"}.GetNewClosure();
    }

Note that the second line above creates a new $options variable, assigning it the value of the outer variable, the attributes don't propagate.

Finally, I'm not sure in your example why you need to call GetNewClosure at all. The variable $this isn't a normal local variable, it will be available in your script property whether or not you create a closure. Example:

function Test-ScriptPropertyWithoutClosure {
    [CmdletBinding()]
    param(
        [Parameter()]
        [ValidateSet('Option1','Option2')]
        [String[]]$Options
    )
    [pscustomobject]@{ Timestamp= Get-Date} | 
        Add-Member ScriptProperty ExpensiveScriptProperty { 
            $this | get-member -MemberType Properties| % Name 
        } -PassThru
}

Test-ScriptPropertyWithoutClosure -Options Option1 | fl
Burt_Harris
  • 6,415
  • 2
  • 29
  • 64
  • Great sleuthing; I've updated the link to the bug report to point to its new home at windowsserver.uservoice.com - unfortunately, however, it is incorrectly marked as "completed" there, even though the problem still exists as of PowerShell v5.1.14393.693 / PowerShell Core v6.0.0-alpha.15. Therefore, as of this writing you cannot up-vote the bug report; you can only leave a comment. – mklement0 Feb 13 '17 at 03:31
  • Thanks @mklement0, glad you found this to be helpful. I ran the test case I had written for the bug report under 5.1.14393.693, The expected result ("OK") was output, with no errors, which is probably why it's been marked completed.. If there's been a bug reversion on this with PowerShell Core 6, it would be best to open a new issue rather than recycle the old one. – Burt_Harris Feb 13 '17 at 20:19
  • Yes, your test case passes, but I would have expected that to _always_ pass, because it passes a _valid_ `-Options` value. The problem - the bug - surfaces when you do _not_ pass any `-Options` value at all - both in Windows PowerShell and PowerShell Core. But I get your point about opening a new issue: where do you think it makes more sense to post it: windowsserver.uservoice.com or the open-source GitHub project? – mklement0 Feb 13 '17 at 20:25
  • OK, It so it sounds like the fix to my bug didn't change the fact that GetNewClosure() only captures local variables (not arguments.) If so, the other workaround I mentioned [putting GetNewClosure() inside a script-block with the argument reassigned to a local variable] is probably still be needed. I think that either place for filing it could work, but internally it may be resolved as "by design", so using the workaround is probably your best bet. Changing that might break existing scripts. – Burt_Harris Feb 13 '17 at 20:41
  • If I misunderstand, probably best to write a new test case and stackoverflow thread. I'm no longer working for Microsoft, but will look at it if you @ mention me in the text. – Burt_Harris Feb 13 '17 at 20:45
  • Thanks. I now realize that I may have misread your bug as exactly the same when it may not be - but they're at least closely related - my experience of the bug was always about _omitting_ `-Option` in the call, whereas a valid value is unproblematic. Please have a look at [this answer](http://stackoverflow.com/a/42189207/45375), which references yours; I've also just added a footnote with a minimal test case and brief explanation, which argues that it's a bug - at least I can't think of a good reason for this behavior. – mklement0 Feb 13 '17 at 21:03
1

I believe this might work:

function Get-AnObject {
param(
      [CmdletBinding()]
      [Parameter(....)]
      [String[]]$Id
      ..
      [ValidateSet('Option1','Option2')]
      [String[]]$Options
    )

...
$sb = [scriptblock]::create('$this | Get-ExpensiveStuff')
$T = New-Object PSCustomObject -Property @{ ..... } 
$T | Add-Member -MemberType ScriptProperty -Name ExpensiveScriptProperty -Value $sb 

.. }

That delays creation of the script block until run time.

mjolinor
  • 66,130
  • 7
  • 114
  • 135