How to determine if Write-Host will work for the current host
Asked Answered
G

2

7

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.

Gaming answered 30/1, 2020 at 0:28 Comment(5)
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 modeEltonelucidate
@Eltonelucidate :: 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.Gaming
Jeffrey Snover told us back in 2013 to stop using Write-Host all together :) Write-Host Considered HarmfulTortious
@Tortious :: until a viable alternative for colourised, human-readable, non-pipelined output exists, I suspect most people aren't going to careGaming
Well, if you want non-pipelined output, then Write-Host is the cmdlet for you... But once you start writing Advanced Functions, that is out of the question.Tortious
C
1

Just check if your host is UserInteractive or an service type environment.

$script:can_write_host = [Environment]::UserInteractive
Churchy answered 28/7, 2022 at 11:18 Comment(4)
Have a +1 for informing me about [Environment]::UserInteractive. I'll test this shortly. After two years of looking, it can't be that simple, can it...?Gaming
Yeah sure i thought you might be on the total wrong track searching the answer inside this $Host Variables. I use this [Environment]::UserInteractive inside powershell script to determinate if the .ps1 is run by a task scheduler (SYSTEM). And the Task scheduler is a kind of service like the Job Agent.Churchy
I think it's something that solves my actual problem, but not necessarily the problem as described in the title; i.e. I might be able use it to differentiate between PS/ISE usage and SQL Agent usage; but it doesn't actually determine if Write-Host would fail. For my use-case, it is very likely to be good enough (hence the +1) but, technically one can still schedule cmd /s /c "powershell -command "Write-Host 'test'" > out.txt" and, while [Environment]::UserInteractive would return $false, Write-Host calls would still work.Gaming
Maybe this article can be helpful devblogs.microsoft.com/scripting/… as you stated correctly Write-Host uses the default of the underlying PSHostChurchy
B
0

Another way to track the output of a script in real time is to push that output to a log file and then monitor it in real time using trace32. This is just a workaround, but it might work out for you.

Add-Content -Path "C:\Users\username\Documents\PS_log.log" -Value $variablewithvalue
Blanket answered 29/9, 2021 at 13:54 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.