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 withWrite-Output
(whose built-in alias isecho
): 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 usingWrite-Output
in that scenario just using$input
by itself, taking advantage of PowerShell's implicit output behavior, is both more concise and more efficient.
- equally useless if
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 '^' }