5

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
  • 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

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:

  1. 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
    
  2. 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 or Write-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.

mklement0
  • 245,023
  • 45
  • 419
  • 492
jimbobmcgee
  • 1,341
  • 8
  • 28
  • PowerShell output coloring feels like a feature that was tacked on, then neglected. Workarounds (not solutions) that I can think of: 1) use `$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 – Tydaeus Aug 04 '20 at 15:29
  • @Tydaeus :: Indeed, so far, the Switch parameter is the only reliable way I've come across. In most of my scripts, now, I have replaced the `$script:can_write_host = $true` with `$script:can_write_host = (-not $BufferOutput)`, where *BufferOutput* is a Switch parameter. – jimbobmcgee Aug 04 '20 at 21:07

0 Answers0