PSWriteMessage.psm1

function Write-Message {
    <#
    .SYNOPSIS
        Writes a formatted, timestamped message to the host.
    .DESCRIPTION
        Writes a formatted, timestamped message to the host.

        'Write-Message' includes functionality for writing PowerShell's
        default messages, including warnings, errors, verbose, and debug
        messages, while also including generic "info" messages and
        "success" messages when needed. Additionally, 'Write-Message'
        includes a formatted date parameter that prefixes every call for
        easy timestamping.

        This function can be utilized in place of the various builtin
        'Write-*' cmdlets. Output goes to the host via Write-Host (the
        information stream, #6) -- never the success stream -- so it will
        not contaminate a function's return value or a captured pipeline.
        Redirect 6> or use -OutFile to capture it.
    .PARAMETER Message
        Any message to be sent, must be enclosed in quotes.
    .PARAMETER Type
        The type of message to be sent. Valid options are Debug, Verbose,
        Info, Success, Warning, and Error. Debug and Verbose only produce
        output when the corresponding preference is not 'SilentlyContinue'
        -- pass the common -Debug or -Verbose switch (on this call or any
        ancestor in the call chain), or set $DebugPreference /
        $VerbosePreference. Write-Message only prints; it does not honor
        the prompt/throw behavior of 'Inquire' or 'Stop'.
    .PARAMETER Clean
        Removes all ANSI formatting from the message before output. Also
        enforced automatically on PowerShell versions below 7, and on any
        7.x release that predates $PSStyle (7.0-7.1).
    .PARAMETER NoPrefix
        Omits the leading type label (e.g. '[INFO]', '[ERROR]') from the
        message.
    .PARAMETER OutFile
        Tee the message to a specified file. -OutFile enforces -Clean on the
        file only to prevent ANSI definitions from interfering with
        readability.
    .NOTES
        The "Warning" and "Error" types do not write to any builtin variable
        and do not terminate or throw. Error handling must be done via other
        methods, 'Write-Message' is purely an informational utility.

        Nothing is written to the success stream. Assigning the result of a
        'Write-Message' call ($x = Write-Message ...) yields $null; use 6>&1
        or -OutFile to capture the rendered text.
    .EXAMPLE
        Write-Message "Hello world!"
        [Tue May 01 12:00:00 2023] [INFO] Hello world!

        Writes a general message. With no other parameters, the type is
        assumed to be "Info".
    .EXAMPLE
        Write-Message "An error occurred." -Type Error
        [Tue May 01 12:00:00 2023] [ERROR] An error occurred.

        Writes an error message to the host.
    .EXAMPLE
        Write-Message "Hello world!" -NoPrefix
        [Tue May 01 12:00:00 2023] Hello world!

        Writes a message without its type label.
    #>


    [CmdletBinding()]
    [OutputType([void])]
    param(
        [Parameter(Mandatory, Position = 0)] [string]$Message,
        [ValidateSet('Debug', 'Verbose', 'Info', 'Success', 'Warning', 'Error')] [string]$Type = 'Info',
        [switch]$Clean,
        [switch]$NoPrefix,
        [string]$OutFile
    )

    begin {
        # ┌───I.────────────────────────────────────────┐
        # │ Enforce $Clean when using older PowerShell. │
        # └─────────────────────────────────────────────┘

        if ($PSVersionTable.PSVersion.Major -lt 7) { $Clean = $True }
        
        # ┌───II.───────────────────────────┐
        # │ Stage parameters and variables. │
        # └─────────────────────────────────┘

        $Date = $(Get-Date -UFormat "[%a %b %d %T %Y] ")

        # An unqualified $VerbosePreference / $DebugPreference read inside a
        # module function resolves against THIS module's scope chain, so
        # -Verbose / -Debug on an ancestor caller in another module or script
        # is invisible here. Resolve the effective preference once: honor a
        # switch bound directly on Write-Message (PowerShell sets the local
        # preference for that call), otherwise fall back to the caller's
        # scope via $PSCmdlet.GetVariableValue, which walks the calling scope
        # chain and reflects a switch passed to an ancestor.
        $EffectiveVerbose = if ($PSBoundParameters.ContainsKey('Verbose')) { $VerbosePreference }
                            else { $PSCmdlet.GetVariableValue('VerbosePreference') }
        $EffectiveDebug   = if ($PSBoundParameters.ContainsKey('Debug'))   { $DebugPreference }
                            else { $PSCmdlet.GetVariableValue('DebugPreference') }

        # Show the message for any non-silent preference, mirroring how the
        # builtin Write-Verbose / Write-Debug gate visibility. This also lets
        # -Debug work on Windows PowerShell 5.1, where the switch sets
        # $DebugPreference to 'Inquire' (7.x sets 'Continue'). Write-Message
        # only prints -- it does not reproduce the Inquire prompt or Stop
        # throw -- so those degrade to print-and-continue.
        $ShowVerbose = $EffectiveVerbose -and $EffectiveVerbose -ne 'SilentlyContinue'
        $ShowDebug   = $EffectiveDebug   -and $EffectiveDebug   -ne 'SilentlyContinue'

        # $PSStyle (PowerShell 7.2+) supplies ANSI styling. Degrade to
        # no color -- same as -Clean -- on PowerShell versions where
        # it isn't defined, rather than erroring on Foreground.* access.
        $UseColor = (-not($Clean)) -and $null -ne $PSStyle
        if ($UseColor) { $ColorDefault = $PSStyle.Reset }

        # Per-Type color/bold/prefix lookup, keyed by -Type. Color and
        # Bold are $null when color is disabled; Prefix is $null when
        # -NoPrefix is set, so they interpolate as empty strings below.
        $TypeInfo = @{
            Debug   = @{
                Color  = $(if ($UseColor) { $PSStyle.Foreground.FromRgb(0x585858) })
                Bold   = $(if ($UseColor) { $PSStyle.Bold + $PSStyle.Foreground.FromRgb(0x585858) })
                Prefix = $(if (-not($NoPrefix)) { '[DEBUG] ' })
            }
            Verbose = @{
                Color  = $(if ($UseColor) { $PSStyle.Foreground.FromRgb(0x8A8A8A) })
                Bold   = $(if ($UseColor) { $PSStyle.Bold + $PSStyle.Foreground.FromRgb(0x8A8A8A) })
                Prefix = $(if (-not($NoPrefix)) { '[VERBOSE] ' })
            }
            Info    = @{
                Prefix = $(if (-not($NoPrefix)) { '[INFO] ' })
            }
            Success = @{
                Color  = $(if ($UseColor) { $PSStyle.Bold + $PSStyle.Foreground.BrightGreen })
                Prefix = $(if (-not($NoPrefix)) { '[SUCCESS] ' })
            }
            Warning = @{
                Color  = $(if ($UseColor) { $PSStyle.Bold + $PSStyle.Foreground.BrightYellow })
                Prefix = $(if (-not($NoPrefix)) { '[WARNING] ' })
            }
            Error   = @{
                Color  = $(if ($UseColor) { $PSStyle.Bold + $PSStyle.Foreground.BrightRed })
                Prefix = $(if (-not($NoPrefix)) { '[ERROR] ' })
            }
        }
    }
    
    process {

        # ┌───III.─────────────────────────────────────┐
        # │ Format the message for output to the host. │
        # └────────────────────────────────────────────┘

        switch ($Type) {
            'Debug' {
                if ($ShowDebug) {
                    $t = $TypeInfo.Debug
                    $MessageContent = "$($t.Color)$Date$($t.Bold)$($t.Prefix)$($t.Color)$Message$ColorDefault"
                }
            }
            'Verbose' {
                if ($ShowVerbose) {
                    $t = $TypeInfo.Verbose
                    $MessageContent = "$($t.Color)$Date$($t.Bold)$($t.Prefix)$($t.Color)$Message$ColorDefault"
                }
            }
            'Info' { $MessageContent = "$Date$($TypeInfo.Info.Prefix)$Message$ColorDefault" }
            default {
                $t = $TypeInfo[$Type]
                $MessageContent = "$Date$($t.Color)$($t.Prefix)$ColorDefault$Message"
            }
        }

        # ┌───IV.────────────────────────────────────────┐
        # │ Format the message for output to a log file. │
        # └──────────────────────────────────────────────┘

        if ($OutFile) {
            if ($ShowDebug) {
                $t = $TypeInfo.Debug
                Write-Host "$($t.Color)$Date$($t.Bold)$($t.Prefix)$($t.Color)`$OutFile detected as $OutFile."
            }
            try {
                switch ($Type) {
                    'Debug' { if ($ShowDebug) { Add-Content -Path $OutFile -Value "$Date$($TypeInfo.Debug.Prefix)$Message" } }
                    'Verbose' { if ($ShowVerbose) { Add-Content -Path $OutFile -Value "$Date$($TypeInfo.Verbose.Prefix)$Message" } }
                    default { Add-Content -Path $OutFile -Value "$Date$($TypeInfo[$Type].Prefix)$Message" }
                }
            }
            catch {
                $t = $TypeInfo.Error
                Write-Host "$Date$($t.Color)$($t.Prefix)$($ColorDefault)An error occurred while attempting to write to the output file. `n $($_.Exception.Message)"
            }
        }
    }

    end {
        # Write-Host routes to the information stream (#6), not the success
        # stream, so log lines never leak into a caller's return value or a
        # captured pipeline. Redirect 6> or use -OutFile to capture output.
        if ($null -ne $MessageContent) { Write-Host $MessageContent }
    }
}

Export-ModuleMember -Function Write-Message