Why do these two code snippets behave differently? Is this a bug? (PSVersion = 5.1.22621.2506, PSEdition = Desktop)
## Code Snippet #1
rmdir -recurse org
mkdir org\dir1
Get-ChildItem org -Directory | foreach { $_.GetType() }
vs
## Code Snippet #2
rmdir -recurse org
mkdir org\dir1 | Out-Null
Get-ChildItem org -Directory | foreach { $_.GetType() }
The only difference between these two is that in snippet 2, the output of mkdir
is piped to Out-Null. This second snippet (and its variants like $null = mkdir org/dir1
) work as I would expect:
Get-ChildItem org -Directory | foreach { $_.GetType() }
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True DirectoryInfo System.IO.FileSystemInfo
But when mkdir output is not captured (as in snippet 1), Get-ChildItem does something unexpected:
Get-ChildItem org -Directory | foreach { $_.GetType() }
Directory: C:\Users\Phil\ps\as1\org
Mode LastWriteTime Length Name
---- ------------- ------ ----
d----- 4/10/2024 12:56 AM dir1
Module : CommonLanguageRuntimeLibrary
Assembly : mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
TypeHandle : System.RuntimeTypeHandle
DeclaringMethod :
BaseType : System.IO.FileSystemInfo
UnderlyingSystemType : System.IO.DirectoryInfo
FullName : System.IO.DirectoryInfo
AssemblyQualifiedName : System.IO.DirectoryInfo, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
[: : :]
The same results are seen if you use New-Item
instead of mkdir
. I don't understand why (or even how) a directory structure can be different depending upon whether the output was captured during its creation.
The behavior - which is only a display problem - may be surprising, but it is by design:
If the first output object in a pipeline triggers table-based display formatting based on explicitly defined formatting definitions (see below for why that matters), subsequent output objects of different type are coerced to list formatting.[1]
Specifically, mkdir
's output object - a System.IO.DirectoryInfo
instance - triggered Format-Table
formatting, because a table view (with predefined columns) is defined as the default view in the formatting definitions associated with that type.
Therefore, the subsequent System.Type
instances output by the .GetType()
calls in your Get-ChildItem
were implicitly forced to use Format-List
formatting.
At the expense of producing data output from your script, you can apply explicit formatting to each command; in the simplest case, pipe to Out-Host
, which applies each command's default formatting in isolation, and sends it directly to the host, preventing it from being captured; alternatively, use Format-*
cmdlet calls explicitly; while they do produce output that can be captured, it isn't data anymore - it is objects representing formatting instructions (which is why Format-*
cmdlets should only ever be used for to-display output).
Presumably, the underlying design rationale for this behavior is:
PowerShell's tabular display formatting is designed for objects of the same type.
If subsequent objects are of a different type, they may have completely different properties that may not contribute any values to the table's columns. So as not to make these objects effectively invisible, list formatting[1] is switched to as soon as the first object of a different type is received.
Unfortunately, if the initial, table format-triggering does not have associated formatting definitions[2] and only triggers table-formatting incidentally, due to having 4 or fewer public properties, subsequent objects that do not share any properties do in effect become invisible; e.g.:
# !! Get-Item output is *invisible*, because the [pscustomobject]
# instance incidentally triggers table view with just a 'foo' column.
[pscustomobject] @{ foo=1 }; Get-Item .
This problematic behavior is the subject of GitHub issue #7871 and GitHub issue #12825.
[1] Strictly speaking, if a given object has associated formatting data that defines a custom view as its default, the latter is used; an example is Get-Date
's output.
[2] For a given object, you can use Get-FormatData
to test whether its type has associated formatting definitions; for instance (non-empty output implies the presence of definitions):
$obj = Get-Item .; Get-FormatData -TypeName $obj.GetType().FullName