Home > Net >  Not quoting $input in Powershell, and its value is "Current"
Not quoting $input in Powershell, and its value is "Current"

Time:12-20

I was writing a PowerShell script, and forgot to quote $input when piping to a command. The command unexpectedly appeared to receive the string Current on stdin, so I investigated further and typed the following:

PS C:\> echo $input

Current
-------



PS C:\> echo "$input"

The difference in output between these two statements confuses me, and I'm not sure what to search for to understand this better. I would have expected both to output nothing, like the second command.

What does "Current" mean? Is this a general quote behaviour, or something specific to $input?

CodePudding user response:

Building on the helpful comments:

  • The automatic $input variable is managed by PowerShell, and represents input from the pipeline in appropriate contexts.

  • As such, use of $input as a user variable, for custom purposes, should be avoided.

  • To use $input, do not use it with Write-Output (whose built-in alias is echo): either use $input as is in the pipeline, or, to force instant enumeration, use @($input).


As for what you tried:

echo $input

This is effectively the same as Write-Output $input, given that echo is a built-in alias of the Write-Output cmdlet.

PowerShell's for-display output-formatting system uses a reflection-based approach for types that (a) do not have predefined formatting data associated with them and (b) are neither strings no (quasi) primitive .NET types such as numbers.

Instances of such types are represented by their public properties, and instances up to 4 properties are implicitly formatted with Format-Table, instances with 5 or more properties implicitly with Format-List.

The automatic $input variable is an enumerator that represents the current context's pipeline input.

$input.GetType().FullName reveals that $input is an instance of the System.Collections.ArrayList ArrayListEnumeratorSimple class.

It isn't directly obvious, but this - non-public - class is what the System.Collections.ArrayList's .GetEnumerator() method returns, which is an enumerator class that implements the System.Collections.IEnumerator interface, which you can verify as follows: $input.GetType().GetInterfaces()

This interface has one public property, .Current, which you can verify as follows, using PowerShell's intrinsic psobject property: $input.psobject.Properties.Name

Therefore, Write-Output $input implicitly uses Format-Table to render the $input instance's one and only property, .Current, which is what you saw.

Irrespective of how many elements the enumerator represents - none outside a pipeline - the .Current property value is $null and therefore not showing in the output, since the .MoveNext() method isn't being called in this case.

Example:

function foo { Write-Output $input }
1..3 | foo  # !! Still prints a table with an empty "Current" column.

In short: Passing $input as-is to Write-Output is virtually pointless.

  • The to-display output is always unhelpful, as shown above.

  • In the pipeline: Write-Output $input | & someCommand is:

    • equally useless if someCommand is an external program (executable), because it is also the for-display representation that is sent (as a stream of strings (lines)), given that PowerShell only "speaks text" when communicating with external programs, as of PowerShell 7.3.1
    • does work if someCommand is a PowerShell command (whether built-in or custom), but there is no benefit to using Write-Output in that scenario just using $input by itself, taking advantage of PowerShell's implicit output behavior, is both more concise and more efficient.

Given the above, the current behavior (which analogously also affects Write-Host) - as of PowerShell 7.3.1 - could be considered a bug, and has been reported in GitHub issue #18826.

Examples:

# !! USELESSS, because findstr.exe receives the *for-display*
# !! representation of the enumerator, line by line.
# Note: The findstr.exe call simply passes each line it receives through.
1..3 | & { Write-Output $input | findstr.exe '^'  }

# OK, due to using a *PowerShell* command,
# but there's no good reason to use Write-Output here.
function foo { Write-Output $input | ForEach-Object { "[$_]" } }
1..3 | foo # -> '[1]', '[2]', '[3]'

# Preferred approach, because it is both simpler and more efficient:
# *implicit* output of $input, which causes enumeration.
function foo { $input | ForEach-Object { "[$_]" } }
1..3 | foo # same as above

echo "$input"

Enclosing $input in "...", i.e. in an expandable string, forces it to be stringified.

Unlike when you pass $input to Write-Output, using it inside "...":

  • causes enumeration of the enumerator (in essence, repeatedly calling .MoveNext() on it while it returns $true, and then accessing its .Current property)

  • with the resulting (stringified) objects joined with spaces, which is how PowerShell stringifies arrays (collections)

Outside of a pipeline, $input is an enumerator without elements, which causes "$input" to evaluate to the empty string, which, in terms of display output results in an empty line.

An example of how it acts in a pipeline:

'one', 2 | & { "[$input]" }

Output:

[one 2]

That is, the (stringified) input objects were joined with a single space.

(A single space is the default separator. Technically, you can specify a different one via the $OFS preference variable, although that is rarely done in pratice).

In short: While "$input" does force enumeration, it may not do what you want, given that:

  • for non-string input, the original type identity is lost.

  • even for string input, multiple input strings end up as one, single-line string, with the input strings separated with spaces, as discussed above.

Examples:

# OK: Input strings are passed one by one (as lines) to findstr.exe
'foo', 'bar' | & { $input | findstr.exe '^' }

# !! DIFFERENT: "$input" forces collecting the inputs in an array,
# !! which is then stringified; therefore a *single string* containing
# !! 'foo bar' is sent.
# !! Using Write-Output "$input"` would behave the same.
'foo', 'bar' | & { "$input" | findstr.exe '^' }
  • Related