Is there any sane, reliable contract that dictates whether Write-Host
is supported in a given PowerShell host implementation, in a script that could be run against any reasonable host implementation?
(Assume that I understand the difference between Write-Host
and Write-Output
/Write-Verbose
and that I definitely do want Write-Host
semantics, if supported, for this specific human-readable text.)
I thought about trying to interrogate the $Host
variable, or $Host.UI
/$Host.UI.RawUI
but the only pertinent differences I am spotting are:
in
$Host.Name
:- The Windows powershell.exe commandline has
$Host.Name = 'ConsoleHost'
- ISE has
$Host.Name = 'Windows PowerShell ISE Host'
- SQL Server Agent job steps have
$Host.Name = 'Default Host'
- I have none of the non-Windows versions installed, but I expect they are different
- The Windows powershell.exe commandline has
in
$Host.UI.RawUI
:- The Windows powershell.exe commandline returns values for all properties of
$Host.UI.RawUI
- ISE returns no value (or
$null
) for some properties of$Host.UI.RawUI
, e.g.$Host.UI.RawUI.CursorSize
- SQL Server Agent job steps return no values for all of
$Host.UI.RawUI
- Again, I can't check in any of the other platforms
- The Windows powershell.exe commandline returns values for all properties of
Maintaining a list of $Host.Name
values that support Write-Host
seems like it would be bit of a burden, especially with PowerShell being cross-platform now. I would reasonably want the script to be able to be called from any host and just do the right thing.
Background
I have written a script that can be reasonably run from within the PowerShell command prompt, from within the ISE or from within a SQL Server Agent job. The output of this script is entirely textual, for human reading. When run from the command prompt or ISE, the output is colorized using Write-Host
.
SQL Server jobs can be set up in two different ways, and both support capturing the output into the SQL Server Agent log viewer:
via a CmdExec step, which is simple command-line execution, where the Job Step command text is an executable and its arguments, so you invoke the powershell.exe executable. Captured output is the stdout/sterr of the process:
powershell.exe -Command x:\pathto\script.ps1 -Arg1 -Arg2 -Etc
via a PowerShell step, where the Job Step command text is raw PS script interpreted by its own embedded PowerShell host implementation. Captured output is whatever is written via
Write-Output
orWrite-Error
:#whatever Do-WhateverPowershellCommandYouWant x:\pathto\script.ps1 -Arg1 -Arg2 -Etc
Due to some other foibles of the SQL Server host implementation, I find that you can emit output using either Write-Output
or Write-Error
, but not both. If the job step fails (i.e. if you throw
or Write-Error 'foo' -EA 'Stop'
), you only get the error stream in the log and, if it succeeds, you only get the output stream in the log.
Additionally, the embedded PS implementation does not support Write-Host
. Up to at least SQL Server 2016, Write-Host
throws a System.Management.Automation.Host.HostException
with the message A command that prompts the user failed because the host program or the command type does not support user interaction.
To support all of my use-cases, so far, I took to using a custom function Write-Message
which was essentially set up like (simplified):
$script:can_write_host = $true
$script:has_errors = $false
$script:message_stream = New-Object Text.StringBuilder
function Write-Message {
Param($message, [Switch]$iserror)
if ($script:can_write_host) {
$private:color = if ($iserror) { 'Red' } else { 'White' }
try { Write-Host $message -ForegroundColor $private:color }
catch [Management.Automation.Host.HostException] { $script:can_write_host = $false }
}
if (-not $script:can_write_host) {
$script:message_stream.AppendLine($message) | Out-Null
}
if ($iserror) { $script:has_errors = $true }
}
try {
<# MAIN SCRIPT BODY RUNS HERE #>
}
catch {
Write-Message -Message ("Unhandled error: " + ($_ | Format-List | Out-String)) -IsError
}
finally {
if (-not $script:can_write_host) {
if ($script:has_errors) { Write-Error ($script:message_stream.ToString()) -EA 'Stop' }
else { Write-Output ($script:message_stream.ToString()) }
}
}
As of SQL Server 2019 (perhaps earlier), it appears Write-Host
no longer throws an exception in the embedded SQL Server Agent PS host, but is instead a no-op that emits nothing to either output or error streams. Since there is no exception, my script's Write-Message
function can no longer reliably detect whether it should use Write-Host
or StringBuilder.AppendLine
.
The basic workaround for SQL Server Agent jobs is to use the more-mature CmdExec step type (where Write-Output
and Write-Host
both get captured as stdout), but I do prefer the PowerShell step type for (among other reasons) its ability to split the command reliably across multiple lines, so I am keen to see if there is a more-holistic, PowerShell-based approach to solve the problem of whether Write-Host
does anything useful for the host I am in.
$Host.Name
to detect known scenarios where Write-Host is supported, default to Write-Output in other cases; 2) use a switch to specify whether to use Write-Host and coloration, and rely on the caller to specify mode – Eltonelucidate$script:can_write_host = $true
with$script:can_write_host = (-not $BufferOutput)
, where BufferOutput is a Switch parameter. – Gaming