Going Out with a Bang!
I'm sure you'll agree that PowerShell is a terrific management and automation tool. The concept of passing objects through the pipeline is a powerful feature that makes it easy to work with almost anything in PowerShell. And, you don't need to be a developer. PowerShell handles everything for you, including how to present output.
PowerShell uses two special cmdlets to manage the pipeline: Out-Default
and Out-Host
. These cmdlets are responsible for formatting and displaying the output of a command. They are added automatically to every PowerShell expression. You do not need to specify them. Out-Default
is responsible for figuring out what type of output is being sent to the pipeline and then sending it to the appropriate formatting cmdlet using Out-Host
. You can read a description of this process from Jeffrey Snover himself at https://devblogs.microsoft.com/powershell/how-powershell-formatting-and-outputting-really-works/. The problem that introduces the article has long been fixed.
Normally, you can ignore these cmdlets. However, there is an intriguing opportunity with Out-Default
. If I remember correctly, Lee Holmes (one of the original PowerShell team members) told me that the Out-Default
command is a placeholder that allows users to customize the output experience. I thought we'd have a little fun with that idea.
Your Out-Default
When you run a command, there is an order of precedence. If you create a function with the same name as a cmdlet, your function will be invoked instead of the cmdlet. We can easily test this by creating a function called Out-Default
that does nothing. Here is the function:
Function Out-Default {
[CmdletBinding()]
Param (
[Parameter(ValueFromPipeline=$true)]
$InputObject
)
}
If you load this function into your PowerShell session, you essentially turn off all output.
PS C:\> Get-Service bits
PS C:\>
PowerShell is automatically directing the output of the Get-Service
command to Out-Default
, which does nothing which means nothing is sent to Out-Host
. I can specify the Out-Host
cmdlet directly using the fully qualified name to see the output.
PS C:\> Get-Service bits | Microsoft.PowerShell.Core\Out-Default
Status Name DisplayName
------ ---- -----------
Stopped bits Background Intelligent Transfer Servi…
Not very practical, but it does demonstrate the role of Out-Default
.
Let's create a wrapper function that is a little more useful.
Function Out-Default {
<#
.forwardhelptargetname Microsoft.PowerShell.Core\Out-Default
.forwardhelpcategory Function
#>
[cmdletbinding()]
Param(
[Parameter(ValueFromPipeline)]
[PSObject]$InputObject,
#added for compatibility with the native Out-Default command
[switch]$Transcript
)
Begin {
$out = [System.Collections.Generic.list[object]]::new()
}
Process {
$out.Add($InputObject)
}
End {
Set-Variable -Name LastOut -Value $out -Scope Global
$Out | Microsoft.PowerShell.Core\Out-Default | Out-Host
}
}
This function uses the same parameters as the original command. Pipeline input is collected in a generic list object. At the end of the pipeline, I'm passing the input to the original Out-Default
command and then sending it to Out-Host
. This may be unnecessary, but it doesn't seem to hurt anything or cause any problems.
The main purpose of this customized Out-Default
is to save the pipeline input to a global variable called $LastOut
. The idea is that I can automatically save the output of a command to a global variable.
PS C:\> Get-Process -id $PID | Select-Object name,StartTime,path
Name StartTime Path
---- --------- ----
pwsh 4/18/2024 1:26:12 PM C:\Program Files\PowerShell\7\pwsh.exe
PS C:\> $lastOut
Name StartTime Path
---- --------- ----
pwsh 4/18/2024 1:26:12 PM C:\Program Files\PowerShell\7\pwsh.exe
This will also capture exceptions.
PS C:\> 1/0
RuntimeException: Attempted to divide by zero.
PS C:\> $lastOut
RuntimeException: Attempted to divide by zero.
The other item to point out in this function is the use for forwarded help.
<#
.forwardhelptargetname Microsoft.PowerShell.Core\Out-Default
.forwardhelpcategory Function
#>
When I look at the Out-Default
command, I see my custom function.
PS C:\> Get-Command Out-Default
CommandType Name Version Source
----------- ---- ------- ------
Function Out-Default
I still may want to see the original help for Out-Default
, so I tell PowerShell to use the help from the original command, again using the fully qualified name. I'm also specifying the help category to help distinguish between the original and custom function.
PS C:\> Get-Command Out-Default*
CommandType Name Version Source
----------- ---- ------- ------
Function Out-Default
Cmdlet Out-Default 7.4.2.500 Microsoft.PowerShell.Core
A Proxy Function
As an alternative to a wrapper function, I can also create a proxy function version of Out-Default
. This function will call the original Out-Default
command as well as run any other code I add. The easiest way to create a proxy function is to use the Copy-Command
function from the PSScriptTools module.
Usually, we write proxy functions to change the behavior of a command by adding or removing parameters. In may case, I'm going to leave the pipeline input alone. Instead, because I know that Out-Default
will be invoked every time I run a command, I can use that to my advantage.
In my custom Out-Default
function, I'm going to add code that will get the last history item and write it to a CSV file. It will only do this, if there is a variable called myHistoryCSV
that points to a CSV file.
PS C:\> $myHistoryCSV = "D:\temp\{0:yyyyMMMdd}-log.csv" -f (Get-Date)
PS C:\> $myHistoryCSV
D:\temp\2024Apr18-log.csv
Here is the proxy function:
Function Out-Default {
<#
.forwardhelptargetname Microsoft.PowerShell.Core\Out-Default
.forwardhelpcategory Function
#>
[CmdletBinding(RemotingCapability = 'None')]
Param(
[switch]$Transcript,
[Parameter(ValueFromPipeline = $true)]
[PSObject]$InputObject
)
Begin {
Write-Verbose "[BEGIN ] Starting $($MyInvocation.MyCommand)"
Write-Verbose "[BEGIN ] Using parameter set $($PSCmdlet.ParameterSetName)"
Write-Verbose ($PSBoundParameters | Out-String)
if ($global:myHistoryCSV) {
$last = Get-History -Count 1
[PSCustomObject]@{
PSTypeName = 'PSHistoryItem'
Computername = [System.Environment]::MachineName
Username = "$([System.Environment]::UserDomainName)\$([System.Environment]::UserName)"
Host = $host.Name
PSVersion = $PSVersionTable.PSVersion
OS = If ($IsCoreClr) {$PSVersionTable.OS} else {$PSVersionTable.BuildVersion}
PSEdition = $PSVersionTable.PSEdition
Date = Get-Date
Path = (Get-Location).Path
Command = $last.CommandLine
Status = $last.ExecutionStatus
Start = $last.StartExecutionTime
End = $last.EndExecutionTime
Run = $last.EndExecutionTime - $last.StartExecutionTime
} | Export-Csv -Path $myHistoryCSV -Append
}
try {
$outBuffer = $null
if ($PSBoundParameters.TryGetValue('OutBuffer', [ref]$outBuffer)) {
$PSBoundParameters['OutBuffer'] = 1
}
$wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Microsoft.PowerShell.Core\Out-Default', [System.Management.Automation.CommandTypes]::Cmdlet)
$scriptCmd = { & $wrappedCmd @PSBoundParameters }
$steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin)
$steppablePipeline.Begin($PSCmdlet)
}
catch {
throw
}
} #begin
Process {
try {
$steppablePipeline.Process($_)
}
catch {
throw
}
} #process
End {
Write-Verbose "[END ] Ending $($MyInvocation.MyCommand)"
try {
$steppablePipeline.End()
}
catch {
throw
}
} #end
} #end proxy function Out-Default
I can't get the current command because it is technically still executing. But I can export information about the last command run. The function should work cross-platform and across PowerShell versions. If you define this function and CSV file in your profile scripts, you can have a single record of all activity.

This might be easier to read.
PS C:\> Import-Csv $myHistoryCSV | Format-Table
Computername Username Host PSVersion OS PSEdition Date
------------ -------- ---- --------- -- --------- ----
PROSPERO PROSPERO\Jeff ConsoleHost 7.4.2 Microsoft Windows 10.0.22631 Core 4/18/2024 2:…
PROSPERO PROSPERO\Jeff ConsoleHost 7.4.2 Microsoft Windows 10.0.22631 Core 4/18/2024 2:…
PROSPERO PROSPERO\Jeff ConsoleHost 7.4.2 Microsoft Windows 10.0.22631 Core 4/18/2024 2:…
PROSPERO PROSPERO\Jeff ConsoleHost 7.4.2 Microsoft Windows 10.0.22631 Core 4/18/2024 2:…
PROSPERO PROSPERO\Jeff ConsoleHost 7.4.2 Microsoft Windows 10.0.22631 Core 4/18/2024 2:…
PROSPERO PROSPERO\Jeff ConsoleHost 7.4.2 Microsoft Windows 10.0.22631 Core 4/18/2024 2:…
PROSPERO PROSPERO\Jeff ConsoleHost 7.4.2 Microsoft Windows 10.0.22631 Core 4/18/2024 2:…
PROSPERO PROSPERO\Jeff ConsoleHost 5.1.22621.2506 10.0.22621.2506 Desktop 4/18/2024 2:…
PROSPERO PROSPERO\Jeff ConsoleHost 5.1.22621.2506 10.0.22621.2506 Desktop 4/18/2024 2:…
PROSPERO PROSPERO\Jeff ConsoleHost 5.1.22621.2506 10.0.22621.2506 Desktop 4/18/2024 2:…
PROSPERO PROSPERO\Jeff ConsoleHost 5.1.22621.2506 10.0.22621.2506 Desktop 4/18/2024 2:…
PROSPERO PROSPERO\Jeff ConsoleHost 5.1.22621.2506 10.0.22621.2506 Desktop 4/18/2024 2:…
PROSPERO PROSPERO\Jeff ConsoleHost 5.1.22621.2506 10.0.22621.2506 Desktop 4/18/2024 2:…
PROSPERO PROSPERO\Jeff Visual Studio Code Host 7.4.2 Microsoft Windows 10.0.22631 Core 4/18/2024 2:…
PROSPERO PROSPERO\Jeff ConsoleHost 5.1.22621.2506 10.0.22621.2506 Desktop 4/18/2024 2:…
...
The reason I went with a CSV file an not a text log file, is that with structured data like a CSV, I can easily import it PowerShell and work with the object output. That makes it much easier to search, sort, filter or whatever I need to do.
Summary
This has definitely been a peek behind the PowerShell pipeline. Some of you many have never heard of Out-Default
and Out-Host
. And that's ok. For the most part, you shouldn't need to think about these commands. But there are some interesting possibilities should you want to experiment.
If you come up with a creative use for Out-Default
, I'd love to hear about it. You can reach me on Twitter or LinkedIn. Until next time, keep on scripting!