Possible to specify private scope on param and support Get-Help+tab-completion?

#TL;DR;

On Jeffrey Snover’s blog post Variable Scopes and $PRIVATE: | Jeffrey Snover's blog, Niklas Bergius commented with a good question about scoping on parameters.

Does anyone know if there is a good way to scope parameters to the private scope without ruining usage of Get-Help and tab-completion for the function parameters?

#Longer description of the question

First, let’s create a script similar to the one mentioned in Mr Snover’s blog post:

function Test-Scopes
{
    PARAM(
        [scriptblock]$ScriptBlock
    )

    & $ScriptBlock
}

This function only takes a single parameter named ScriptBlock. In an outer scope, which will call the Test-Stuff function, let’s create two script block variables, where the second script block calls the first script block as part of its execution, and the first script block has the same name as the parameter to our Test-Scopes function above.

[scriptblock]$ScriptBlock = {
    Write-Host "My script block"
}

[scriptblock]$myScriptBlock = {
    Write-Host "Outer script block"
    & $ScriptBlock
}

Finally, let’s call the function, providing the second scriptblock as parameter.

Test-Scopes -ScriptBlock $myScriptBlock

Executing that line will end up in an infinite loop, constantly writing the output “Outer script block”. The reason for this is of course that the -ScriptBlock parameter will be the one called during the execution of the script block we passed in, not the $ScriptBlock variable which we created in a scope above.

So, as mentioned in Niklas Bergius’ comment, we can fix this behaviour by changing the scope of the parameter to Private.

function Test-Scopes
{
    PARAM(
        [scriptblock]$Private:ScriptBlock
    )

    & $Private:ScriptBlock
}

If we now, using the same script block variables as previously, try to write Get-Help for the command, it will display the following syntax:

Test-Scopes [[-Private:ScriptBlock] <scriptblock>]

Also, the tab-completion functionality in the PowerShell ISE and the PowerShell console will both want to use the -Private:ScriptBlock parameter name, but trying to do this fails with the following error message:

PS C:> Test-Scopes -Private:ScriptBlock $myScriptBlock
Test-Scopes : Cannot process argument transformation on parameter 'Private:ScriptBlock'. Cannot convert the "ScriptBlock" value of type "System.String" to type "System.Management.Automation.ScriptBlock".
At line:19 char:22
+ Test-Scopes -Private:ScriptBlock $myScriptBlock
+                      ~~~~~~~~~~~
    + CategoryInfo          : InvalidData: (:) [Test-Scopes], ParameterBindingArgumentTransformationException
    + FullyQualifiedErrorId : ParameterArgumentTransformationError,Test-Scopes

The error message seems do try to indicate that it is actually trying to pass “ScriptBlock” as a string (the underlined part in the error), not the “$myScriptBlock” variable value, as a parameter value. Almost seems a bit like a PowerShell parser bug, since it seems like it first correctly identifies the parameter name as being “Private:ScriptBlock” (since it correctly identifies the expected type to be ScriptBlock. Another interpretation could be that it is trying to pass the parameter by index instead of name, but that would lead to a lot of other questions regarding parsing of that line) but when it checks for the value to bind to the parameter it tries to use “ScriptBlock” for the name. This could, of course, be a missconception on my part, but leaving out the $myScriptBlock part of that line generates the same error, supporting the hypothesis at least a little.

We can, instead, provide the parameter by index instead, calling the Test-Scopes function like:

Test-Scopes $myScriptBlock

This works correctly and prints the expected “Outer script block” followed by “My script block” lines. In order to use a named parameter, we could set an alias for the parameter, like so:

function Test-Scopes
{
    PARAM(
        [Alias("ScriptBlock")]
        [scriptblock]$Private:ScriptBlock
    )

    & $Private:ScriptBlock
}

Test-Scopes -ScriptBlock $myScriptBlock

This works correctly, executing the functions just as we want. However, the Get-Help syntax shown, as well as the tab-completion, will still be suggesting that we use the parameter named -Private:ScriptBlock, which, as mentioned, fails.

Does anyone know if it is possible to specify the scope on a parameter to a function and still retain a useful Get-Help and tab-completion functionality for the function?

Not really… the parsing PowerShell uses for tab completion doesn’t really have the smarts to understand scope. It’s just a string parsing routine to look up parameters, so it’s not processing variables or anything like that.

I’m not entirely sure that, from a design perspective, this use of $private “fixes” something. “Fix” implies “broke,” and the original behavior is absolutely what I’d expect the shell to do. Parameters are already scoped to their function, and that’s the way they’re meant to work. You’re meant to use disciplined programming, and an understanding of scope, to avoid problems. For example, it’s considered a poor practice for a function to rely on out-of-scope items, with one exception being module-level “preference” variables. In other words, in your example, I’d argue that by passing in a script block that in turn relied on an out-of-scope variable, you were exhibiting poor design, and that you shouldn’t want to “fix” that. Not suggesting that what you’re doing doesn’t have use… but you’re in a shell, not a strict programming language, so you have to have more care on the design side.

But to directly answer the question - no; the shell’s tab completion parser just isn’t built to support this scenario. But I think you’re running into a scoping situation that the shell just wasn’t designed to make straightforward, and without the design intent to begin with, you’re going to have a rough time doing what you’re asking.

To be honest, I have never stumbled into that scenario in the real world. It was a fictitious scenario created as part of learning and understanding how PowerShell handles different things. I do them every now and then, when I stumble upon something I’m not sure of, to investigate and learn.

I can actually only see this scenario happening with badly closed scriptblocks, so just as you say it is not much of an issue (especially since scriptblocks are generally passed for things running in other runscopes, e.g. Invoke-Command and Start-Job). Thanks for the response, it’s good to know that I wasn’t just missing something when investigating the comment on Mr Snover’s blog post.