Why can't I use $_ in write-host?

2.9k views Asked by At

I am trying to pipe an array of strings to write-host and explicitly use $_ to write those strings:

'foo', 'bar', 'baz' | write-host $_

However, it fails with:

The input object cannot be bound to any parameters for the command either because the command does not take pipeline input or the input and its properties do not match any of the parameters that take pipeline input.

This error message makes no sense to me because I am perfectly able to write

'foo', 'bar', 'baz' | write-host

I would have expected both pipelines to be equivalent. Apparently, they're not. So, what's the difference?

3

There are 3 answers

0
mklement0 On BEST ANSWER

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)
    
0
js2010 On

$_ will never work outside a scriptblock. How about write-output instead:

'hi' | Write-Output -InputObject { $_ + ' there' } 

hi there
0
AdminOfThings On

You can use it as iRon has indicated in the comments. $_ or $PSItem is the current object in the pipeline that is being processed. Typically, you see this with commands that require a processing or script block. You would have to contain your Write-Host command within a similar processing block.

'foo', 'bar', 'baz' | ForEach-Object {write-host $_}

Here is an example using the process block of a function:

function write-stuff {
    process { write-host $_ }
}

'foo', 'bar', 'baz' | write-stuff
bar
foo
hi