对于 PowerShell cmdlet,我是否可以始终将脚本块传递给字符串参数?

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

我正在查看 PowerShell Rename-Item cmdlet 的文档,其中有一个这样的示例。

Get-ChildItem *.txt | Rename-Item -NewName { $_.name -Replace '\.txt','.log' }

This example shows how to use the Replace operator to rename multiple files, even though the NewName parameter does not accept wildcard characters.

This command renames all of the .txt files in the current directory to .log.

The command uses the Get-ChildItem cmdlet to get all of the files in the current folder that have a .txt file name extension. Then, it uses the pipeline operator (|) to send those files to Rename-Item .

The value of NewName is a script block that runs before the value is submitted to the NewName parameter.

注意最后一句:

NewName的值是一个脚本块,在值被提交给NewName参数之前运行

其实NewName是一个字符串:

[-NewName] <String>

这是否意味着当所需的参数类型是字符串时我总是可以使用脚本块?

So does that means I can always use a script block when the required parameter type is a string? : NO

这里的技术叫做Delay Binding,在这种情况下非常有用。

延迟绑定会怎样?

PowerShell ParameteBinder 会理解延迟绑定的用法,会先执行ScriptBlock,然后将输出转换为相应参数的预期类型,这里是字符串。

下面是一个例子。

#Working one
'Path'|Join-Path -Path {$_} -ChildPath 'File'  

#Not working one
Join-Path -Path {'path'} -ChildPath 'File'
Join-Path : Cannot evaluate parameter 'Path' because its argument is specified as a script block and there is no input. A script block cannot be evaluated without input.

要了解有关 ParameterBinding 的更多信息,您可以执行如下操作 Trace-Command

Trace-Command ParameterBinding -Expression {'Path'|Join-Path -Path {$_} -ChildPath 'File'} -PSHost

使用延迟绑定,参数可以使用脚本块而不是参数的实际数据类型从管道接收值。

在脚本块中,$_ 代表管道值。

仅当管道中有输入时才可用。

# 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 属性 name (ValueFromPipelineByPropertyName), is irrelevant.

    • See 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 属性 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 属性 - 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 运行 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 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.