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)