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?
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
ScriptBlock
-----------
'a'
'b'
'c'
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
> $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
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 aScriptBlockargument? 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.
Most helpful comment
Here is example of implicit
ScriptBlockinvocation:or
```powershell
filter f {
param(
[Parameter(ValueFromPipeline)]
[PSObject] $p
)
$p
}
1..3 | f -p { $_*2 }