tl;dr
The automatic $_
variable and its alias, $PSItem
, only ever have meaningful values inside script blocks ({ ... }
), in specific contexts.
The bottom section lists all relevant contexts.
- Update: A new conceptual help topic, about_PSItem, is now available, which covers most of what is in the bottom section.
I would have expected both pipelines to be equivalent.
They're not:
'foo', 'bar', 'baz' | write-host
It is the pipeline-based equivalent of the following (equivalent in ultimate effect, not technically):
foreach ($str in 'foo', 'bar', 'baz') { Write-Host -Object $str }
That is, in your command Write-Host
receives input from the pipeline that implicitly binds to its -Object
parameter for each input object, by virtue of parameter -Object
being declared as accepting pipeline input via attribute [Parameter(ValueFromPipeline=$true)]
'foo', 'bar', 'baz' | write-host $_
Before pipeline processing begins, arguments - $_
in your case - are bound to parameters first:
Since $_
isn't preceded by a parameter name, it binds positionally to the - implied - -Object
parameter.
Then, when pipeline processing begins, pipeline parameter binding finds no pipeline-binding Write-Host
parameter to bind to anymore, given that the only such parameter, -Object
has already been bound, namely by an argument $_
.
In other words: your command mistakenly tries to bind the -Object
parameter twice; unfortunately, the error message doesn't exactly make that clear.
The larger point is that using $_
only ever makes sense inside a script block ({ ... }
) that is evaluated for each input object.
Outside that context, $_
(or its alias, $PSItem
) typically has no value and shouldn't be used - see the bottom section for an overview of all contexts in which $_
/ $PSItem
inside a script block is meaningfully supported.
While $_
is most typically used in the script blocks passed to the ForEach-Object
and Where-Object
cmdlets, there are other useful applications, most typically seen with the Rename-Item
cmdlet: a delay-bind script-block argument:
# Example: rename *.txt files to *.dat files using a delay-bind script block:
Get-ChildItem *.txt | Rename-Item -NewName { $_.BaseName + '.dat' } -WhatIf
That is, instead of passing a static new name to Rename-Item
, you pass a script block that is evaluated for each input object - with the input object bound to $_
, as usual - which enables dynamic behavior.
As explained in the linked answer, however, this technique only works with parameters that are both (a) pipeline-binding and (b) not [object]
or [scriptblock]
typed; therefore, given that Write-Object
's -Object
parameter is [object]
typed, the technique does not work:
# Try to enclose all inputs in [...] on output.
# !! DOES NOT WORK.
'foo', 'bar', 'baz' | write-host -Object { "[$_]" }
Therefore, a pipeline-based solution requires the use of ForEach-Object
in this case:
# -Object is optional
PS> 'foo', 'bar', 'baz' | ForEach-Object { write-host -Object "[$_]" }
[foo]
[bar]
[baz]
Contexts in which $_
(and its alias, $PSItem
) is meaningfully defined:
What these contexts have in common is that the $_
/ $PSItem
reference must be made inside a script block ({ ... }
), namely one passed to / used in:
... the ForEach-Object
and Where-Object
cmdlets; e.g.:
1..3 | ForEach-Object { 1 + $_ } # -> 2, 3, 4
... the intrinsic .ForEach()
and intrinsic .Where()
methods; e.g.:
(1..3).ForEach({ 1 + $_ }) # -> 2, 3, 4
... a parameter, assuming that parameter allows a script block to act as a delay-bind script-block parameter; e.g.:
# Rename all *.txt files to *.dat files.
Get-ChildItem *.txt | Rename-Item -NewName { $_.BaseName + '.dat' } -WhatIf
... conditionals and associated script blocks inside a switch
statement; e.g.:
# -> 'is one or three: one', 'is one or three: three'
switch ('one', 'two', 'three') {
{ $_ -in 'one', 'three' } { 'is one or three: ' + $_ }
}
... simple function
and filters
; e.g.:
# -> 2, 3
function Add-One { process { 1 + $_ } }; 1..2 | Add-One
# -> 2, 3
filter Add-One { 1 + $_ }; 1..2 | Add-One
... direct subscriptions to an object's event (n/a to the script block that is passed to the -Action
parameter of a Register-ObjectEvent
call); e.g::Tip of the hat to Santiago Squarzon.
# In a direct event-subscription script block used in the
# context of WinForms; e.g:
$txtBox.Add_KeyPress({
param($sender, $eventArgs)
# The alternative to the explicitly defined parameters above is:
# $this ... implicitly the same as $sender, i.e. the event-originating object
# $_ / $PSItem ... implicitly the same as $eventArgs, i.e. the event-arguments object.
})
... the [ValidateScript()]
attribute in parameter declarations; note that for array-valued parameters the script block is called for each element; e.g.,
function Get-Foo {
param(
[ValidateScript({ 0 -eq ($_ % 2) })]
[int[]] $Number
)
"All numbers are even: $Number"
}
PowerShell (Core) only: ... the substitution operand of the -replace
operator; e.g.:
# -> 'a10, 'a20'
'a1', 'a2' -replace '\d+', { 10 * [int] $_.Value }
... in the context of <ScriptBlock>
elements in formatting files (but not in the context of script block-based ETS members, where $this
is used instead).
... in the context of using PowerShell SDK methods such as .InvokeWithContext()
# -> 43
{ 1 + $_ }.InvokeWithContext($null, [psvariable]::new('_', 42), $null)
ForEach-Object
, like:'foo', 'bar', 'baz' | ForEach-Object {write-host $_}
– Katsuyama