For PowerShell cmdlets, can I always pass a script block to a string parameter?

# Delay-bind script-block argument:
# The code inside { ... } is executed for each input object ($_) and
# the output is passed to the -NewName parameter.
... | Rename-Item -NewName { $_.Name -replace '\.txt$','.log' }

The call above shows an application of a delay-bind script-block ({ ... }) argument, which is an implicit feature that:

  • only works with parameters that are designed to take pipeline input,

    • of any type except the following, in which case regular parameter binding happens[1]:

      • [scriptblock]
      • [object] ([psobject], however, does work, and therefore the equivalent [pscustomobject] too)
      • (no type specified), which is effectively the same as [object]
    • Whether such parameters accept pipeline input by value (ValueFromPipeline) or by property name (ValueFromPipelineByPropertyName), is irrelevant.

    • See this answer for how to discover a given cmdlet’s pipeline-binding parameters; in the simplest case, e.g.:

      Get-Help Rename-Item -Parameter * | Where pipelineInput -like True*
      
  • enables per-input-object transformations via a script block passed instead of a type-appropriate argument; the script block is evaluated for each pipeline object, which is accessible inside the script block as $_, as usual, and the script block’s output – which is assumed to be type-appropriate for the parameter – is used as the argument.

    • Since such ad-hoc script blocks by definition do not match the type of the parameter you’re targeting, you must always use the parameter name explicitly when passing them.

    • Delay-bind script blocks unconditionally provide access to the pipeline input objects, even if the parameter would ordinarily not be bound by a given pipeline object, if it is defined as ValueFromPipelineByPropertyName and the object lacks a property by that name.

    • This enables techniques such as the following call to Rename-Item, where the pipeline input from Get-Item is – as usual – bound to the -LiteralPath parameter, but passing a script block to -NewName – which would ordinarily only bind to input objects with a .NewName property – enables access to the same pipeline object and thus deriving the destination filename from the input filename:

      • Get-Item file | Rename-Item -NewName { $_.Name + '1' } # renames 'file' to 'file1'; input binds to both -LiteralPath (implicitly) and the -NewName script block.
    • Note: Unlike script blocks passed to ForEach-Object or Where-Object, for example, delay-bind script blocks run in a child variable scope[2], which means that you cannot directly modify the caller’s variables, such as incrementing a counter across input objects.
      As a workaround, use Get-Variable to gain access to a caller’s variable and access its .Value property inside the script block – see this answer for an example.


[1] Error conditions:

  • If you mistakenly attempt to pass a script block to a parameter that is either not pipeline-binding or is [scriptblock]– or [object]-typed (untyped), regular parameter-binding occurs:

    • The script block is passed once, before pipeline-input processing begins, if any.
      That is, the script block is passed as a (possibly converted) value, and no evaluation happens.

      • For parameters of type [object] or [scriptblock] / a delegate type such as System.Func that is convertible to a script block, the script block will bind as-is.
      • In the case of a (non-pipeline-binding) [string]-typed parameter, the script block’s literal contents is passed as the string value.
      • For all other types, parameter binding – and therefore the command as a whole – will simply fail, since conversion from a script block is not possible.
  • If you neglect to provide pipeline input while passing a delay-bind script block to a pipeline-binding parameter that does support them, you’ll get the following error:

    • Cannot evaluate parameter '<name>' because its argument is specified as a script block and there is no input. A script block cannot be evaluated without input.

[2] This discrepancy is being discussed in GitHub issue #7157.

Leave a Comment