Powershell: Why does parameter binding impose the ScriptBlockArgumentNoInput restriction?

Created on 17 Mar 2018  路  7Comments  路  Source: PowerShell/PowerShell

I was a bit surprised by the ScriptBlockArgumentNoInput parameter binding restriction. The special-casing of a [scriptblock] argument with a command-line-bound pipeline parameter seems to be implemented in BindParameter and looks rather deliberate.

The comments include the following statements:

... If the argument is of type ScriptBlock and the parameter takes pipeline input, then the ScriptBlock is saved off in the delay-bind ScriptBlock container for further processing of pipeline input and is not bound as the argument to the parameter.
...
Now we need to check to see if the argument value is a ScriptBlock. If it is and the parameter type is not ScriptBlock and not Object, then we need to delay binding until a pipeline object is provided to invoke the ScriptBlock.
...
We treat the parameter as bound, but really the script block gets run for each pipeline object and the result is bound.
...

This suggests there can be some sort of implicit script block invocation happening by way of the parameter binding, but I haven't been able to reproduce that. Or maybe this is happening in cases but I haven't noticed.

I'd like to understand what is happening here. Is there an example that demonstrates this delayed parameter binding and/or implicit scriptblock invocation?

Steps to reproduce

Add-Type '
using System.Management.Automation;
public class ConvertibleFromScriptblock
{
    public ScriptBlock ScriptBlock { get; private set; }

    public ConvertibleFromScriptblock
    (
        ScriptBlock scriptBlock
    )
    {
        ScriptBlock = scriptBlock;
    }
}
'

function f
{
    param
    (
        [Parameter(ValueFromPipeline,Position=1,Mandatory)]
        [ConvertibleFromScriptblock]
        $ScriptBlock
    )
    process
    {
        $ScriptBlock
    }
}

f ([ConvertibleFromScriptblock]{'a'}) # succeeds
{'b'} | f                             # succeeds
f {'c'}                               # fails

Expected behavior

ScriptBlock
-----------
'a'       
'b'        
'c'

Actual behavior

ScriptBlock
-----------
'a'        
'b'        
f : Cannot evaluate parameter 'ScriptBlock' because its argument is specified as a script block 
and there is no input. A script block cannot be evaluated without input.
At C:\Users\un1\Desktop\test.ps1:33 char:3
+ f {'c'}                               # fails
+   ~~~~~
    + CategoryInfo          : MetadataError: (:) [f], ParameterBindingException
    + FullyQualifiedErrorId : ScriptBlockArgumentNoInput,f

Environment data

> $PSVersionTable

Name                           Value                                            
----                           -----                                            
PSVersion                      6.0.0                                            
PSEdition                      Core                                             
GitCommitId                    v6.0.0                                           
OS                             Microsoft Windows 6.3.9600                       
Platform                       Win32NT                                          
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0...}                          
PSRemotingProtocolVersion      2.3                                              
SerializationVersion           1.1.0.1                                          
WSManStackVersion              3.0                                              
Issue-Question Resolution-Answered

Most helpful comment

Here is example of implicit ScriptBlock invocation:

1..3 | Select-Object -InputObject { $_*2 }

or
```powershell
filter f {
param(
[Parameter(ValueFromPipeline)]
[PSObject] $p
)
$p
}
1..3 | f -p { $_*2 }

All 7 comments

Here is example of implicit ScriptBlock invocation:

1..3 | Select-Object -InputObject { $_*2 }

or
```powershell
filter f {
param(
[Parameter(ValueFromPipeline)]
[PSObject] $p
)
$p
}
1..3 | f -p { $_*2 }

A great use of this feature is with Rename-Item:

dir -Recurse *.ps1 | Rename-Item -NewName { $_.Name -replace '.ps1','.ps1.bak' }

Thanks @lzybkr and @PetSerAl. I can see how this enables some more expressive constructions.

@lzybkr Can you comment on why [object] parameters are excluded from this delayed-binding treatment?

For example, this produces errors

function f {
    param ([Parameter(ValueFromPipeline)]$x)
    process { "value: $x" }
}

1,2 | f -x {$_ * 2}

while replacing $x with [int]$x succeeds.

If the parameter type was object - how should PowerShell bind a ScriptBlock argument? In a way, it's ambiguous, maybe you meant a ScriptBlock as is, or maybe you meant to use delayed binding.

For other parameter types, it's much safer to assume you did not mean to pass a ScriptBlock.

Right. If the parameter type is [object] or [scriptblock], then the scriptblock can be bound to the parameter. If it's any other type, then you can't bind the scriptblock to the parameter so we use the scriptblock for a computed argument. @lzybkr 's example with renaming files is the canonical usecase for this feature.

If the parameter type was object - how should PowerShell bind a ScriptBlock argument? In a way, it's ambiguous, maybe you meant a ScriptBlock as is, or maybe you meant to use delayed binding.

@lzybkr I see. I expected the rule to be "use delayed binding for scriptblock argument values for all parameter types other than scriptblock". The distinction between a psobject and object parameter type, for example, seems rather subtle yet the difference in parameter binding behavior for scriptblocks is rather dramatic.

I see, though, that the built-in commands on this computer seem to use object parameter types for pipeline parameters sparingly (here is the search code I used). I also see that object parameter types are discouraged by SD03. So binding a scriptblock to an object parameter type ought to be a rare occurrence anyway.

@alx9r:

The name given to this feature by Jeffrey Snover in a blog post from 2006 is _ScriptBlock Parameter_.

Unfortunately, as far as I know, not only is this feature not _named_ in the documentation, it doesn't appear to be documented at all.

A while ago I've described the current behavior and asked for it to be documented in https://github.com/PowerShell/PowerShell-Docs/issues/2338.

On a more general note:

Giving features names is important for them to catch on.

A similar case is _member enumeration_: while it _is_ documented, it doesn't have a name either - except in an old blog post.

Was this page helpful?
0 / 5 - 0 ratings