Search code examples
powershellpowershell-5.0

Powershell 5 - Multicolored text on the same line, logged on text file on the same line


I'm currently running a ps1 script in Powershell 5, where I need two things to happen at the same time:

  • display (on the console) a text with different colors on the same line;
  • save (log) everything as shown on the console to a txt file.

An example of the code used is the following:

Write-Host "Name: " -ForegroundColor "DarkMagenta" -NoNewLine
Write-Host "John"   -ForegroundColor "DarkBlue"

Unfortunately, the different methods I've tried to achieve the above goals do not work, since:

  • Using Start-Transcript "C:\Log.txt", the -NoNewLine are ignored and every Write-Host command gets logged on the txt file on a separate line.
  • Calling the script as & "Script.ps1" | Tee-Object -FilePath "C:\Log.txt" or as POWERSHELL -Command "& { ""Script.ps1"" | Tee-Object ""C:\Log.txt"" }"does not save the Write-Host outputs, only the Write-Output ones.
  • Calling the script as POWERSHELL -File "Script.ps1" | Tee-Object -FilePath "C:\Log.txt" does not show the colors on the console (everything is white on black, as usual, and logged correctly).
  • Using ANSI escape sequences like Write-Output "`e[5;36mMyText`e[0m" does not work with PS5 (or, at least, in my environment, where the raw string is simply printed).

Any suggestion about how could I achieve the desired result (i.e. colored output on the console and correctly formatted log on the text file)?

Thank you!


Solution

  • Do use ANSI (VT) escape sequences, which Windows PowerShell (the legacy, ships-with-Windows, Windows-only edition of PowerShell whose latest and last version is 5.1) also supports.

    The only pitfall is that PowerShell's `e escape sequence only exists in PowerShell (Core) 7, whereas in Windows PowerShell it becomes a verbatim e - use $([char] 27) or similar techniques instead; e.g.:

    # Write-Output implied
    '{0}[5;36mMyText{0}[0m' -f [char] 27
    
    # Alternative, with helper variable (you can also embed $([char] 27) every time).
    $e = [char] 27; "$e[5;36mMyText$e[0m"
    
    • Start-Transcript in Windows PowerShell should then record something like the following correctly, i.e. as a single output line with the escape sequences invariably preserved (which renders as expected when you print the file's content to the terminal, e.g. with Get-Content, but may be unwanted noise when viewing in a text editor):[1]

      Write-Host ('The following is colored and blinking: {0}[5;36mMyText{0}[0m' -f [char] 27)
      
      • By contrast, in PowerShell 7, as of v7.5.x, Start-Transcript the escape sequences are invariably removed - you'll invariably get monochrome text; see GitHub issue #11567 for a discussion.
    • As for the Tee-Object attempts:

      • If you use (implied) Write-Output rather than Write-Host, your Tee-Object approach should work as-is - and the ANSI escape sequences are invariably preserved.

      • If you want to stick with Write-Host (so as not to "pollute" the success output stream with the colored messages by default), use a *> or - information stream-specifically, 6> - redirection to (also) capture Write-Host messages with the escape sequences in Windows PowerShell:
        & "Script.ps1" *>&1 | Tee-Object -FilePath "C:\Log.txt"

        • By contrast, PowerShell 7, in the Write-Host + *> or 6>) case, removes the ANSI escape sequences by default if the redirection target is a file or, if it is &1, i.e. merged into the success output stream, and piped to Out-File, Tee-Object or Out-String; however, there is an opt-in for preserving them, namely if you first (temporarily) set the $PSStyle preference variable's .OutputRendering property as follows:
          $PSStyle.OutputRendering = 'Ansi'

    Optional reading: ANSI escape-sequence handling in Windows PowerShell vs. PowerShell (Core) 7:
    • Windows PowerShell:

      • itself never produces strings with ANSI escape sequences
      • always uses user-supplied strings as-is, even if they contain such sequences.
    • PowerShell 7:

      • itself uses ANSI escape sequences to colorize and style for-display output, as part of its formatting system; e.g. outputting $PSVersionTable to the display produces a tabular display with colored column headers.

        • Whether and under what circumstances this coloring is applied or not is controlled via the .OutputRendering property of the object stored in the $PSStyle preference variable.
      • mostly preserves ANSI escape sequences in user-supplied strings, with the following exceptions:

        • When a string with embedded ANSI escape sequences is passed to Write-Host and Write-Information and a *> or - information stream-specifically - 6> redirection is applied and the redirection target is either a file or, if it is &1, i.e. merged into the success output stream, piped to Out-File, Tee-Object, or Out-String, the escape sequences are only preserved if
          $PSStyle.OutputRendering = 'Ansi' is in effect.[2]

        • However, if you use *>&1 or 6>&1, you can retrieve the string intact even when $PSStyle.OutputRendering = 'Host' (the default) or $PSStyle.OutputRendering = 'PlainText' are in effect, namely if you explicitly stringify the [System.Management.Automation.InformationRecord] instances that Write-Host and Write-Information output, before sending the result to Out-File, Tee-Object, or Out-String; e.g., the following always preserves the escape sequences, as evidenced by the -match "`e" operation yielding $true:

          (
             Write-Host "It ain't easy being `e[32mgreen`e[m." *>&1 |
               ForEach-Object ToString # explicit stringification
          ) -match "`e"` # -> always $true
          
        • Note that escape sequences are always preserved in strings sent directly output to the success output stream (implicitly or as command output, as is typical, or via Write-Output). However, with $PSStyle.OutputRendering = 'PlainText' in effect, strings with escape sequences print to the display uncolored (due the formatting system removing the colors for display); yet, the escape sequences are still present in programmatic processing.

      • However, the above, in-session rules are overruled in the following scenarios:

        • As of v7.5.x, when Start-Transcript is used to create a session transcript, escape sequences are never preserved, i.e. they are actively stripped, whether or not they originate from the formatting system or the success output stream; see GitHub issue #11567 for a discussion that asks for a future version to allow coloring to be preserved in some fashion, on an opt-in basis.

        • When outside callers receive output from pwsh, the PowerShell (Core) 7 CLI, they receive the for-display output that the code would produce in-session as a string via stdout.

          • The upshot is that, when $PSStyle.OutputRendering = 'PlainText' is in effect - possibly via having set environment variable NO_COLOR to 1 prior to invocation - escape sequences are stripped even from strings directly output to PowerShell's success output stream.

          • Also, as of v7.5.x, there's arguably a bug: $PSStyle.OutputRendering = 'Host', which is the default, isn't honored as expected, in that even when outside callers capture or redirect a CLI call's output, the formatting system unexpectedly preserves the escape sequences: see GitHub issue #20170.


    [1] If preserving the ANSI sequences is undesired, you'll have to strip (remove) them yourself in Windows PowerShell after the fact; you yourself came up with the following regex, though note that this doesn't cover all possible VT (ANSI) escape sequences:
    (Get-Content -Raw C:\Log.txt) -replace '\e\[[0-9;]*m' | Set-Content -NoNewLine C:\Log.txt
    Note that PowerShell 7 now offers a helper class, System.Management.Automation.Internal.StringDecorated, to achieve the same, which (presumably) covers all escape sequences:
    ([System.Management.Automation.Internal.StringDecorated] (Get-Content -Raw C:\Log.txt)).ToString('PlainText') | Set-Content -NoNewLine C:\Log.txt

    [2] These cmdlets use the for-display formatting system to create string representations of their input objects (in the case of Tee-Object, this applies to use with the -FilePath parameter, not with -Variable). While .NET primitive types as well as strings are considered out-of-band by the formatting system and are stringified with a simple .ToString() call, the [System.Management.Automation.InformationRecord] instances that Write-Host / Write-Information wrap their string arguments in do go through the formatting system and are therefore subject to $PSStyle.OutputRendering.