Output Multiple Colors on a Single Line

Archived Version

function Write-Out {
<#
.SYNOPSIS
	Writes multi-color console output with advanced formatting and optional multi-file logging.

.DESCRIPTION
	This function prints formatted and colorized output to the PowerShell console. It supports:
	- Multiple foreground and background colors per line
	- Pre/post spacing, line breaks, and tabulation
	- Optional timestamps and text alignment (left, right, center)
	- Logging output to one or more files with support for overwriting or appending

	Console output is handled via `Write-Host`, while logging uses `New-Item`, `Set-Content`, and `Add-Content`.

	Multi-color support works by accepting an array of strings (`Text`) and matching each entry with optional arrays 
	of `ForegroundColor` and `BackgroundColor`. The value `Default` can be passed to use the terminal’s current color settings.

.PARAMETER Text
	The text to display or log. Can be an array of multiple strings.

.PARAMETER ForegroundColor
	Array of foreground colors corresponding to the text array.

.PARAMETER BackgroundColor
	Array of background colors corresponding to the text array.

.PARAMETER ForegroundColorDefault
	Fallback color used if ForegroundColor is not specified or set to Default.

.PARAMETER BackgroundColorDefault
	Fallback color used if BackgroundColor is not specified or set to Default.

.PARAMETER PreLine
	Number of blank lines to add before the text.

.PARAMETER PreSpace
	Number of spaces to add before the text.

.PARAMETER PreTab
	Number of tabs to add before the text.

.PARAMETER TimeStamp
	Adds a timestamp before the text.

.PARAMETER TimeStampFormat
	Format used for the timestamp.

.PARAMETER ClearHost
	Clears the console screen before printing.

.PARAMETER PostSpace
	Number of spaces to add after the text.

.PARAMETER PostTab
	Number of tabs to add after the text.

.PARAMETER PostLine
	Number of blank lines to add after the text.

.PARAMETER NoNewLine
	Suppress newline at the end of the text.

.PARAMETER PadLeft
	Pads the string to the left up to this number of characters.

.PARAMETER PadRight
	Pads the string to the right up to this number of characters.

.PARAMETER PadCenter
	Centers the string in a field of this width.

.PARAMETER LogFile
	Array of file paths to log output to.

.PARAMETER LogOnly
	If specified, skips console output and writes only to the log file.

.PARAMETER Overwrite
	If specified and the file does not exist, it creates a new one. If the file exists, it overwrites it.
	
.EXAMPLE 
Write-Out -Text "Clear all text on console" -Clearhost

.EXAMPLE
Write-Out -Text "Start with Red Text ","Then Switch to Blue Text ","Now Magenta" -ForegroundColor Red,Blue,Magenta 

.EXAMPLE
Write-Out -Text "White on Black ","Black on White ","Dark Cyan on Cyan ","Yellow on Green ","Default Color" -ForegroundColor White,Black,DarkCyan,Yellow -BackgroundColor Black,White,Cyan,Green

.EXAMPLE
Write-Out -Text "Make this"," entire line"," the same color by setting defaults" -ForegroundColorDefault Yellow -BackgroundColorDefault Magenta

.EXAMPLE
Write-Out -Text "Add a blank line and two tabs ","before ","my text" -ForegroundColor Green,Cyan,White -PreLine 1 -PreTab 2

.EXAMPLE
Write-Out -Text "Add two blank ","lines ","after my text" -ForegroundColor White,Green,White -PostLine 2

.EXAMPLE
Write-Out -Text "Add 3 spaces before my text" -ForegroundColor Gray -Prespace 3

.EXAMPLE
Write-Out -Text "White text and a tab after" -ForegroundColor White -NoNewLine -PostTab 1
Write-Out -Text "Black text on Yellow ","and then back to white" -ForegroundColor Black,White -BackgroundColor Yellow

.EXAMPLE
Write-Out -Text "An easy way to ","highlight ","text in the middle" -BackgroundColor Default,Yellow

.EXAMPLE
Write-Out -Text "You can even add a ","time stamp ","before your output" -ForegroundColor White,Green,White -TimeStamp -PreLine 3

.EXAMPLE
Write-Out -Text "You can change the ","time stamp format" -ForegroundColor White,Yellow -TimeStamp -TimeStampFormat "dd-MM-yyy HH:mm" -PreLine 1 -PostLine 1

.EXAMPLE
Write-Out -Text "An"," Error"," occurred let's write overwrite/create a new log file" -ForegroundColor White,Red,White -TimeStamp -LogFile "script.log" -Overwrite

.EXAMPLE
Write-Out -Text "Now you can ","Append ","this line to your log file" -ForegroundColor Cyan,Magenta -TimeStamp -LogFile "script.log"

.EXAMPLE
Write-Out -Text "You can write this line to two log files now" -LogFile "script.log","second.txt"

.EXAMPLE
Write-Out -Text "Pad Right:", "20 Spaces" -ForegroundColor Yellow,White -PadRight 20

.EXAMPLE
Write-Out -Text "Pad Left:", "10 Spaces" -ForegroundColor Cyan,Green -PadLeft 10

.EXAMPLE
Write-Out -Text "Pad Center 30 Spaces" -ForegroundColor Green -PadCenter 30

.EXAMPLE
Write-Out -Text "Padding is useful to lineup text like a table" -ForegroundColor Gray -PreLine 1
Write-Out -Text "Name:", "Size:", "Date:" -ForegroundColor Yellow,Green,Cyan -PadRight 20
Write-Out -Text "windows.iso", "1.0 GB", "1/1/1995" -ForegroundColor Yellow,Green,Cyan -PadRight 20
Write-Out -Text "text.txt", "3.5 MB", "8/16/2024" -ForegroundColor Yellow,Green,Cyan -PadRight 20

.INPUTS
	[string[]] Text can be piped or passed as an argument.

.OUTPUTS
	None. This function writes to the console and/or log files.

.NOTES
	Author: Brian Steinmeyer
	URL: http://sigkillit.com/
	Created: 5/23/2023
	Requires: PowerShell 5.1 or higher
	Version 1.6
	- Rewrote code for max efficiency and speed
	- Added PadLeft, PadRight, and PadCenter Parameters
	Version 1.5
	- Removed Unnecessary loop in 1.4 for log files since Test-Path, New-Item, Set-Content, and Add-Content all support -Path as string array
	Version 1.4
	- Changed LogFile from a String to String Array so you can log the same line to multiple files or provide a single value so it' backwards compatible.
	Version 1.3
	- Added LogOnly Option for only writing text to a log file
	Version 1.2
	- Set Text, ForegroundColor, and BackgroundColor to default value of @() to fix errors checking counts in some circumstances
	- Fixed an issue where ForegroundColorDefault and BackgroundColorDefault were not working properly in some circumstances
	- Added Requires -Version 4.0
	Version 1.1
	- Completely rewrote the "Main Text" section
	- Added "Default" as a color option, which allows you to use the default values for foreground/background 
	  - Useful when you want to specify a backgroundcolor in certain parts of a line like the middle
	  - Ex: Write-Out -Text "How to ","highlight ","the middle text" -BackgroundColor Default,Yellow,Default
	Version: 1.0
	- Initial Creation inspired by PSWriteColor (https://github.com/EvotecIT/PSWriteColor)
	  - Improved upon by switching Foreground and Background Colors to default values if colors are not specifid for all strings.
		Will also ignore extra colors if more colors are specified than strings specified.
#>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false)]
        [string[]]$Text = @(),

        [Parameter(Mandatory = $false)]
        [ValidateSet("Default","Black","DarkBlue","DarkGreen","DarkCyan","DarkRed","DarkMagenta","DarkYellow","Gray","DarkGray","Blue","Green","Cyan","Red","Magenta","Yellow","White")]
        [string[]]$ForegroundColor = @(),

        [Parameter(Mandatory = $false)]
        [ValidateSet("Default","Black","DarkBlue","DarkGreen","DarkCyan","DarkRed","DarkMagenta","DarkYellow","Gray","DarkGray","Blue","Green","Cyan","Red","Magenta","Yellow","White")]
        [string[]]$BackgroundColor = @(),

        [Parameter(Mandatory = $false)]
        [ValidateSet("Default","Black","DarkBlue","DarkGreen","DarkCyan","DarkRed","DarkMagenta","DarkYellow","Gray","DarkGray","Blue","Green","Cyan","Red","Magenta","Yellow","White")]
        [string]$ForegroundColorDefault = [ConsoleColor]::White,
		
        [Parameter(Mandatory = $false)]
        [ValidateSet("Default","Black","DarkBlue","DarkGreen","DarkCyan","DarkRed","DarkMagenta","DarkYellow","Gray","DarkGray","Blue","Green","Cyan","Red","Magenta","Yellow","White")]
        [string]$BackgroundColorDefault = "Default",

		[Parameter(Mandatory = $false)]
        [int]$PreLine = 0,

        [Parameter(Mandatory = $false)]
        [int]$PreSpace = 0,

        [Parameter(Mandatory = $false)]
        [int]$PreTab = 0,

        [Parameter(Mandatory = $false)]
        [switch]$TimeStamp,
		
        [Parameter(Mandatory = $false)]
        [string]$TimeStampFormat = 'yyyy-MM-dd HH:mm:ss',

		[Parameter(Mandatory = $false)]
        [switch]$ClearHost,        

        [Parameter(Mandatory = $false)]
        [int]$PostSpace = 0,

        [Parameter(Mandatory = $false)]
        [int]$PostTab = 0,
		
		[Parameter(Mandatory = $false)]
        [int]$PostLine = 0,

        [Parameter(Mandatory = $false)]
        [switch]$NoNewLine = $false,

		[Parameter(Mandatory = $false)]
        [int]$PadLeft = 0,

		[Parameter(Mandatory = $false)]
        [int]$PadRight = 0,    

        [Parameter(Mandatory = $false)]
        [int]$PadCenter = 0,

        [Parameter(Mandatory = $false)]
        [string[]]$LogFile = @(),

        [Parameter(Mandatory = $false)]
        [switch]$LogOnly = $false,

        [Parameter(Mandatory = $false)]
        [switch]$Overwrite = $false
    )

	begin {
        function Pad-Center {
            param (
                [string]$Text,
                [int]$Width
            )
            $totalPadding = $Width - $Text.Length
            if ($totalPadding -le 0) { return $Text }
            $padLeft = [math]::Floor($totalPadding / 2)
            $padRight = $totalPadding - $padLeft
            return (' ' * $padLeft) + $Text + (' ' * $padRight)
        }

        if ($ClearHost) { Clear-Host }

        $prefixBuilder = [System.Text.StringBuilder]::new()
        if ($PreLine)  { $null = $prefixBuilder.Append("`n" * $PreLine) }
        if ($PreSpace) { $null = $prefixBuilder.Append(' ' * $PreSpace) }
        if ($PreTab)   { $null = $prefixBuilder.Append("`t" * $PreTab) }
        if ($TimeStamp) { $null = $prefixBuilder.Append("[$([datetime]::Now.ToString($TimeStampFormat))]") }
        $prefix = $prefixBuilder.ToString()

        $postfixBuilder = [System.Text.StringBuilder]::new()
        if ($PostSpace) { $null = $postfixBuilder.Append(' ' * $PostSpace) }
        if ($PostTab)   { $null = $postfixBuilder.Append("`t" * $PostTab) }
        if ($PostLine)  { $null = $postfixBuilder.Append("`n" * $PostLine) }
        $postfix = $postfixBuilder.ToString()
    }

    process {
        if (-not $LogOnly) {
            if ($prefix) { Write-Host -NoNewline $prefix }

            $currentFG = $null
            $currentBG = $null
            $buffer = [System.Text.StringBuilder]::new()

            for ($i = 0; $i -lt $Text.Count; $i++) {
                $txt = $Text[$i]

                if ($PadRight -gt 0 -and $txt.Length -lt $PadRight) { $txt = $txt.PadRight($PadRight) }
                if ($PadLeft -gt 0 -and $txt.Length -lt $PadLeft) { $txt = $txt.PadLeft($PadLeft) }
                if ($PadCenter -gt 0 -and $txt.Length -lt $PadCenter) { $txt = Pad-Center -Text $txt -Width $PadCenter }

                $fg = if ($ForegroundColor.Count -gt $i) { $ForegroundColor[$i] } else { $ForegroundColorDefault }
                $bg = if ($BackgroundColor.Count -gt $i) { $BackgroundColor[$i] } else { $BackgroundColorDefault }

                if ($buffer.Length -gt 0 -and ($fg -ne $currentFG -or $bg -ne $currentBG)) {
                    if ($currentBG -eq 'Default') {
                        Write-Host -NoNewline $buffer.ToString() -ForegroundColor $currentFG
                    } else {
                        Write-Host -NoNewline $buffer.ToString() -ForegroundColor $currentFG -BackgroundColor $currentBG
                    }
                    $buffer.Clear() | Out-Null
                }

                $null = $buffer.Append($txt)
                $currentFG = $fg
                $currentBG = $bg
            }

            if ($buffer.Length -gt 0) {
                if ($currentBG -eq 'Default') {
                    Write-Host -NoNewline $buffer.ToString() -ForegroundColor $currentFG
                } else {
                    Write-Host -NoNewline $buffer.ToString() -ForegroundColor $currentFG -BackgroundColor $currentBG
                }
            }

            if ($postfix) { Write-Host -NoNewline $postfix }
            if (-not $NoNewLine) { Write-Host }
        }

        if ($LogFile.Count -gt 0) {
            $logLine = $prefix + ($Text -join '') + $postfix
            foreach ($file in $LogFile) {
                if (-not (Test-Path $file)) {
                    #New-Item -Path $file -ItemType File -Force -Value ($logLine + ($NoNewLine ? '' : "`r`n")) | Out-Null
					if ($NoNewLine) {
						$logValue = $logLine
					} else {
						$logValue = "$logLine`r`n"
					}
					New-Item -Path $file -ItemType File -Force -Value $logValue | Out-Null
                } elseif ($Overwrite) {
                    Set-Content -Path $file -Value $logLine -NoNewline:$NoNewLine
                } else {
                    try {
                        Add-Content -Path $file -Value $logLine -NoNewline:$NoNewLine -ErrorAction Stop
                    } catch {
                        Start-Sleep -Seconds 3
                        Add-Content -Path $file -Value $logLine -NoNewline:$NoNewLine
                    }
                }
            }
        }
    }
	
	end {
		#Nothing to Do	
	}
}