ShoutOut.psm1


# START: source\_buildBasicDirectoryLogger.ps1

function _buildBasicDirectoryLogger {
    param(
        [Parameter(Mandatory=$true, HelpMessage="Path to the direcory where log files should be created.")]
        [string]$DirectoryPath,
        [Parameter(Mandatory=$false, HelpMessage="Name prefix that will be used for log files craeted by this handler.")]
        [string]$name="shoutout",
        [Parameter(Mandatory=$false, HelpMessage="Minimum amount of time between cleanups of the log directory")]
        [timespan]$CleanupInterval = "0:15:0.0",
        [Parameter(Mandatory=$false, HelpMessage="Minimum amount of time that log files will be retained.")]
        [timespan]$RetentionTime = "14:0:0:0.0"
    )

    $filename = '{0}.{1}.{2}.{3}.log' -f $name, $env:USERNAME, $PID, [datetime]::Now.ToString('o').replace(':', '')
    $filePath = '{0}/{1}' -f $DirectoryPath, $filename

    $cleanupState = @{
        Id = [guid]::NewGuid().guid
    }

    $Cleanup = {
        param(
            $DirectoryPath,
            $RetentionTime
        )
        
        Get-ChildItem -Path $DirectoryPath -Filter *.log -File | Where-Object {
            ([datetime]::Now - $_.LastWriteTime) -gt $RetentionTime
        } | Remove-Item
    }

    $startCleanup = {
        $jobName = 'DirectoryLoggerCleanup.{0}' -f $cleanupState.Id
        $job = Start-Job -ScriptBlock $Cleanup -Name $jobName -ArgumentList $DirectoryPath, $RetentionTime
        
        Register-ObjectEvent -InputObject $job -EventName StateChanged -Action {
            Unregister-Event $EventSubscriber.SourceIdentifier
            Remove-Job $EventSubscriber.SourceIdentifier
            Remove-Job $EventSubscriber.SourceObject.Id
        } | Out-Null
        $cleanupState.LastCleanup = [datetime]::Now
    }.GetNewClosure()

    & $startCleanup

    return {
        param($Record)
        
        # Ensure that the directory exists:
        $item = Get-Item -Path $DirectoryPath -ErrorAction SilentlyContinue
        if ($item -isnot [System.IO.DirectoryInfo]) {
            $item = New-Item -Path $DirectoryPath -ItemType Directory -Force -ErrorAction SilentlyContinue
        
            if (($null -eq $item) -or ($item -isnot [System.IO.DirectoryInfo])) {
                # "Failed to log in directory {0}. Directory does not exist and we cannot create it." -f $DirectoryPath | shoutOut -MessageType Error
                return
            }
        }

        # Write record to file:
        $Record | Out-File -FilePath $filePath -Encoding utf8 -Append
        
        # Perform log directory cleanup:
        if (([datetime]::Now - $cleanupState.LastCleanup) -ge $CleanupInterval) {
            & $startCleanup
        }

    }.GetNewClosure()
 
}
# END: source\_buildBasicDirectoryLogger.ps1


# START: source\_buildBasicFileLogger.ps1
function _buildBasicFileLogger {
    param(
        [string]$FilePath
    )

    return {
        param($Record)

        if (-not (Test-Path $FilePath -PathType Leaf)) {
            New-Item -Path $FilePath -ItemType File -Force -ErrorAction Stop | Out-Null
        }

        $Record | Out-File $FilePath -Encoding utf8 -Append -Force
    }.GetNewClosure()
}
# END: source\_buildBasicFileLogger.ps1


# START: source\_ensureShoutOutLogFile.ps1
function _ensureShoutOutLogFile {
    param(
        [string]$logFile,
        [string]$MsgType = "*"
    )

    if (!(Test-Path $logFile -PathType Leaf)) {
        try {
            return new-Item $logFile -ItemType File -Force -ErrorAction Stop
        } catch {
            "Unable to create log file '{0}' for '{1}'." -f $logFile, $msgType | shoutOut -MsgType Error
            "Messages marked with '{0}' will be redirected." -f $msgType | shoutOut -MsgType Error
            shoutOut $_ Error
            throw ("Unable to use log file '{0}', the file cannot be created." -f $logFile)
        }
    }

    return gi $logFile
}
# END: source\_ensureShoutOutLogFile.ps1


# START: source\_injectLogHandler.ps1
<#
.SYNOPSIS
Injects a log handler into the callstack at the specified scope
#>

function _injectLogHandler {
    param(
        [Parameter(Mandatory=$true)]
        [scriptblock]$Handler,
        [Parameter(Mandatory=$false)]
        [ValidateNotNull()]
        [System.Management.Automation.CallStackFrame]$InjectionFrame,
        [Parameter(Mandatory=$false)]
        [String]$MessageType="*",
        [Parameter(Mandatory=$false)]
        [bool]$StopPropagation=$false
    )

    # $script:logRegistry | Out-String | Write-Host -ForegroundColor DarkRed
<#
    $callStack = Get-PSCallStack
 
    $callStack | Write-Host -ForegroundColor DarkCyan
 
    $InjectionFrame = $callStack | Where-Object {
        "-" * 80 | Write-Host -ForegroundColor DarkGreen
        $_.GetFrameVariables().Keys | Sort-Object | Write-Host -ForegroundColor Magenta
        $_.GetFrameVariables().ContainsKey($script:hashCodeAttribute)
    } | Select-Object -First 1
 
    $InjectionFrame | Write-Host -ForegroundColor Cyan
#>

    # Generate GUID $logId
    $logid = [guid]::newGuid().guid

    # Generate a record $logRecord of the handler:
    $record = @{
        Id = $logId
        Handler = $Handler
        MessageType = $MessageType
        StopPropagation = $StopPropagation
    }

    if ($InjectionFrame) {
        $hash = $InjectionFrame.GetFrameVariables().$script:hashCodeAttribute.value.getHashCode()
    } else {
        $hash = 'global'
    }

    $record.frame = $hash

    if (-not $script:logRegistry.containsKey($hash)) {
        $script:logRegistry[$hash] = New-Object System.Collections.ArrayList
    }

    # "Adding handler {0} for frame {1}" -f $logId, $hash | Write-Host -ForegroundColor Cyan

    $script:logRegistry[$hash].Add($record) | Out-Null

    return $logId
}
# END: source\_injectLogHandler.ps1


# START: source\_removeLogHandler.ps1
<#
.SYNOPSIS
Attempts to remove the log handler with the specified Id from the callstack.
 
Returns $true if a handler was removed, $false otherwise.
 
#>

function _removeLogHandler {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [string]$LogId
    )

    foreach($handlers in $script:logRegistry.Values) {
        $handler = $handlers | Where-Object { $_.Id -eq $LogId }
        if ($null -ne $handler) {
            $handlers.remove($handler)
            return $true
        }
    }

    return $false
}
# END: source\_removeLogHandler.ps1


# START: source\_resolveLogHandler.ps1
<#
.SYNOPSIS
Scans the current callstack for ShoutOut log handers macthing the provided MessageType (wildcard expression).
#>

function _resolveLogHandler {
    [CmdletBinding(DefaultParameterSetName="All")]
    param(
        [Parameter(Mandatory=$false)]
        [string]$MessageType,
        [Parameter(Mandatory=$false, ParameterSetName="TargetFrame")]
        [System.Management.Automation.CallStackFrame]$TargetFrame
    )

    # $PSCmdlet.ParameterSetName | Write-Host -ForegroundColor DarkYellow

    # $script:logRegistry | Out-String | Write-Host -ForegroundColor DarkGreen

    $callstack = if ($TargetFrame) {
        @($TargetFrame)
    } else {
        Get-PSCallStack
    }
    
    $foundHandlers = New-Object System.Collections.Queue
    $liveHashes = new-Object System.Collections.ArrayList
    $liveHashes.Add('global') | Out-Null

    foreach($frame in $callstack){
        # "-" * 80 | Write-Host -ForegroundColor Magenta
        $fv = $frame.GetFrameVariables()
        # $fv | Out-String | Write-Host
        if (-not $fv.ContainsKey($script:hashCodeAttribute)) {
            continue
        }
        $hash = $fv.$script:hashCodeAttribute.value.getHashCode()
        $liveHashes.Add($hash) | Out-Null
        # "Looking for handler on frame '{0}'..." -f $hash | Write-Host -ForegroundColor Gray
        
        if ($handlers = $script:logRegistry[$hash]) {
            foreach ($handler in $handlers) {
                if (($null -eq $Messagetype) -or ($MessageType -like $handler.MessageType)) {
                    # "Found handler {1} on frame '{0}'" -f $hash, $handler.Id | Write-Host -ForegroundColor Green
                    $foundHandlers.Enqueue($handler)

                    if ($handler.StopPropagation) {
                        return $foundHandlers
                    }
                }
            }
        }
    }

    if (-not $PSBoundParameters.containsKey('TargetFrame')) {
        foreach($handler in $script:logRegistry['global']) {
            # "{0} -like {1} => {2}" -f $MessageType, $handler.MessageType, ($MessageType -like $handler.MessageType) | Write-Host
            if (($null -eq $Messagetype) -or ($MessageType -like $handler.MessageType)) {
                # "Found handler {1} on frame '{0}'" -f $hash, $handler.Id | Write-Host -ForegroundColor Green
                $foundHandlers.Enqueue($handler)

                if ($handler.StopPropagation) {
                    return $foundHandlers
                }
            }
        }
    }

    if ($PSCmdlet.ParameterSetName -eq 'All') {
        # Garbage collection
        $hashes = [array]$script:logRegistry.Keys
        foreach ($hash in $hashes) {
            if ($hash -notin $liveHashes) {
                $script:logRegistry.remove($hash)
            }
        }
    }

    return $foundHandlers
}
# END: source\_resolveLogHandler.ps1


# START: source\_validateShoutOutLogHandler.ps1
function _validateShoutOutLogHandler {
    param(
        [scriptblock]$LogHandler,
        [string]$msgType = "*"
    )

    # Valid/recognizable parameters and their expected typing:
    $validParams = @{
        '$Message' = [Object]
        '$Record'  = [String]
        '$details' = [hashtable]
    }

    $params = $LogHandler.Ast.ParamBlock.Parameters

    if ($params.count -eq 0) {
        "Invalid handler, no parameters found: {0}" -f $LogHandler | shoutOut -MsgType Error
        "Messages marked with '{0}' will not be redirected using this handler." -f $msgType | shoutOut -MsgType Error
        throw "No parameters declared by the given handler."
    }

    $recognizedParams = $params | Where-Object { $_.Name.Extent.Text -in $validParams.Keys }

    if ($null -eq $recognizedParams) {
        "Invalid handler, none of the expeted parameters found (expected any of {0}): {1}" -f ($paramNames -join ', '), $LogHandler | shoutOut -MsgType Error
        "Messages marked with '{0}' will not be redirected using this handler." -f $msgType | shoutOut -MsgType Error
        throw ("None of {0} parameters declared by the given handler." -f ($paramNames -join ', '))
    }

    foreach ($param in $recognizedParams) {
        $paramType = $validParams[$param.Name.Extent.Text]
        if (($t = $param.StaticType) -and !($t.IsAssignableFrom($paramType)) ) {
            "Invalid handler, the '{0}' parameter should accept values of type '{1}' (found '{2}' which is not assignable from '{1}')." -f $param.Name, $paramType.Name, $t.Name | shoutOut -MsgType Error
            "Messages marked with '{0}' will not be redirected using this handler." -f $msgType | shoutOut -MsgType Error
            throw ("'{0}' parameter on the given handler is of invalid type (not assignable from [string])." -f $paramNames)
        }
    }

    return $LogHandler
}
# END: source\_validateShoutOutLogHandler.ps1


# START: source\Add-ShoutOutLog.ps1
function Add-ShoutOutLog {
    [CmdletBinding(DefaultParameterSetName="DirectoryPath")]
    param(
        [parameter(Mandatory=$true, Position=1, HelpMessage="Message type to redirect.")]
        [Alias('MsgType')]
        [string]$MessageType,
        [Parameter(ParameterSetName="FilePath", ValueFromPipeline=$true, Mandatory=$true, Position=2, HelpMessage="Path to log file.")]
        [string]$LogFilePath,
        [Parameter(ParameterSetName="FileInfo", ValueFromPipeline=$true, Mandatory=$true, Position=2, HelpMessage="FileInfo object.")]
        [System.IO.FileInfo]$LogFile,
        [Parameter(ParameterSetName="DirectoryPath", ValueFromPipeline=$true, Mandatory=$true, Position=2, HelpMessage="Path to log file.")]
        [string]$LogDirectoryPath,
        [Parameter(ParameterSetName="DirectoryInfo", ValueFromPipeline=$true, Mandatory=$true, Position=2, HelpMessage="Path to log file.")]
        [System.IO.DirectoryInfo]$LogDirectory,
        [Parameter(ParameterSetName="Scriptblock", ValueFromPipeline=$true, Mandatory=$true, Position=2, HelpMessage="ScriptBlock to use as log handler.")]
        [scriptblock]$LogHandler,
        [Parameter(Mandatory=$false, HelpMessage="Causes the log handler to be registered on the global frame.")]
        [switch]$Global,
        [Parameter(Mandatory=$false, HelpMessage="Clear all log handlers for this message type on the current frame. If used with -Global this will remove all log handlers for the message type up to and including the global frame.")]
        [switch]$Reset
    )

    switch ($PSCmdlet.ParameterSetName) {
        "FileInfo" {
            try {
                _ensureShoutOutLogFile $LogFile.FullName $MessageType | Out-Null
                $LogHandler = _buildBasicFileLogger $LogFile.FullName
            } catch {
                return $_
            }
        }
        "FilePath" {
            try {
                _ensureShoutOutLogFile $LogFilePath $MessageType | Out-Null
                $LogHandler = _buildBasicFileLogger $LogFilePath
            } catch {
                return $_
            }
        }
        "DirectoryInfo" {
            try {
                $LogHandler = _buildBasicDirectoryLogger $LogDirectory.FullName
            } catch {
                return $_
            }
        }
        "DirectoryPath" {
            try {
                $LogHandler = _buildBasicDirectoryLogger $LogDirectoryPath
            } catch {
                return $_
            }
        }
    }

    try {
        $cs = Get-PSCallStack
        $logHandler = _validateShoutOutLogHandler $LogHandler $MessageType
        $injectArgs = @{
            InjectionFrame = $cs[1]
            MessageType = $MessageType
            Handler = $logHandler
        }
        if ((Get-PSCallStack)[1].Command -in 'Set-ShoutOutDefaultLog', 'Set-ShoutOutRedirect') {
            $injectArgs.InjectionFrame = $cs[2]
        }
        if ($PSBoundParameters.ContainsKey('Global')) {
            $injectArgs.remove('InjectionFrame')
        }
        if ($PSBoundParameters.ContainsKey('Reset')) {
            $handlers = Get-ShoutOutLog
            if (-not $PSBoundParameters.ContainsKey('Global')) {
                $hash = if ($InjectArgs.InjectionFrame) {
                    $InjectArgs.InjectionFrame.GetFrameVariables().$script:hashCodeAttribute.value.getHashCode()
                } else {
                    'global'
                }
                $handlers = $handlers | Where-Object { $_.frame -eq $hash }
            }
            $handlers | Where-Object { $_.MessageType -eq $MessageType } | ForEach-Object {
                _removeLogHandler $_.Id | Out-Null
            }
        }
        _injectLogHandler @injectArgs 
    } catch {
        return $_
    }
}
# END: source\Add-ShoutOutLog.ps1


# START: source\Clear-ShoutOutLog.ps1

<#
.SYNOPSIS
Removes all log handlers in the current scope for the given message type.
 
If the '-Global' switch is specified, all log handlers for the given message type will be removed
#>

function Clear-ShoutOutLog {
    param(
        [Parameter(Mandatory=$true, Position=1, HelpMessage="Mesage Type to remove handlers for.")]
        [Alias('MsgType')]
        [string]$MessageType,
        [Parameter(Mandatory=$false, HelpMessage="Causes all log handlers for the given message type to be removed")]
        [Switch]$Global
    )

    $resolveArgs = @{
        MessageType = $MessageType
    }

    if (-not $Global) {
        $resolveArgs.TargetFrame = (Get-PSCallStack)[1]
    }

    _resolveLogHandler @resolveArgs | ForEach-Object {
        _removeLogHandler $_.Id
    }

}
# END: source\Clear-ShoutOutLog.ps1


# START: source\Get-ShoutOutConfig.ps1
function Get-ShoutOutConfig {
  return $_ShoutOutSettings
}
# END: source\Get-ShoutOutConfig.ps1


# START: source\Get-ShoutOutDefaultLog.ps1
function Get-ShoutOutDefaultLog {
    param(
        [Parameter(HelpMessage="Specifies that log handlers in all context should be removed, instead of just the current context.")]
        [switch]$Global
    )

    $getArgs = @{
        MessageType = '*'
    }

    if ($Global) {
        $getArgs.Global = $true
    }

    return Get-ShoutOutLog @getArgs
}
# END: source\Get-ShoutOutDefaultLog.ps1


# START: source\Get-ShoutOutLog.ps1

function Get-ShoutOutLog {
    [CmdletBinding(DefaultParameterSetName="MessageType")]
    param(
        [Parameter(Mandatory=$false, ParameterSetName="MessageType", HelpMessage="Message Type to retrieve log handlers for.")]
        [Alias('MsgType')]
        [string]$MessageType,
        [Parameter(Mandatory=$true, ParameterSetName="LogId", HelpMessage="ID of the log handler to retrieve")]
        [guid]$LogId
    )

    switch ($PSCmdlet.ParameterSetName) {
        MessageType {
            $foundHandlers = New-Object System.Collections.ArrayList

            foreach ($context in $script:logRegistry.Keys) {

                $handlers = $script:logRegistry[$context]

                $handlers | Where-Object {
                    ('' -eq $MessageType) -or ($_.MessageType -eq $MessageType)
                } | ForEach-Object {
                    $foundHandlers.Add($_) | Out-Null
                }
            }

            return $foundHandlers
        }

        LogId {
            foreach ($context in $script:logRegistry.Keys) {
                $handlers = $script:logRegistry[$context]

                $id = $LogId.Guid

                foreach ($handler in $handlers) {
                    if ($handler.id -eq $id) {
                        return $handlers[$id]
                    }
                }
            }
        }
    }
} 
# END: source\Get-ShoutOutLog.ps1


# START: source\Invoke-ShoutOut.ps1

<#
.WISHLIST
    - Update so that that output is fed to shoutOut as it is generated rather than using the result output.
      The goal is to generate logging data continuously so that it's clear whether the script has hung or not.
      [Done]
.SYNOPSIS
    Helper function to execute commands (strings or blocks) with error-handling/reporting.
.DESCRIPTION
    Helper function to execute commands (strings or blocks) with error-handling/reporting.
 
Invokes a command in a new context so that all output and errors can be captured and recorded by ShoutOut.
 
If a scriptblock is passed as the operation, the function will attempt make any variables referenced by the
scriptblock available to the scriptblock when it is resolved (using variables available in the scope that
called Run-Operation).
 
The variables used in the command are identified using [scriptblock].Ast.FindAll method, and are imported
from the parent scope using $PSCmdlet.SessionState.PSVariable.Get.
 
The following variable-names are restricted and may cause errors if they are used in the operation:
 - $__thisOperation: The operation being run.
 - $__inputVariables: List of the variables being imported to run the operation.
 
.NOTES
   - Transforms ScriptBlocks to Strings prior to execution because of a quirk in iex where it will not allow the
     evaluation of ScriptBlocks without an input (a 'param' statement in the block). iex is used because it yields
     output as each line is evaluated, rather than waiting for the entire $OPeration to complete as would be the
     case with <ScriptBlock>.Invoke().
#>

function Invoke-ShoutOut {
    param(
        [parameter(ValueFromPipeline=$true, position=1, HelpMessage="Operation to perform. Should be a scripblock or a string describing a command to run.")]
        $Operation,
        [parameter(HelpMessage="Suppreses all output from the call.")]
        [Switch] $OutNull,
        [parameter(HelpMessage="Do not treat exceptions as fatal.")]
        [Switch] $NotStrict,
        [parameter(HelpMessage="Only log errors.")]
        [Switch] $LogErrorsOnly,
        [parameter(HelpMessage="Suppress output to console.")]
        [Switch] $Quiet
    )
    $shoutOutArgs = @{
        Quiet = $PSBoundParameters.ContainsKey('Quiet')
    }
    
    if (!$NotStrict) {
        # Switch error action preference to catch any errors that might pop up.
        # Works so long as the internal operation doesn't also change the preference.
        $OldErrorActionPreference = $ErrorActionPreference
        $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop
    }

    if ($Operation -is [string]) {
        $Operation = [scriptblock]::create($Operation)
    }

    if (!$LogErrorsOnly) {
        $msg = "Invoke-ShoutOut: '$Operation'..."
        $msg | shoutOut -MsgType Invocation -ContextLevel 2 @shoutOutArgs
    }

    $r = try {
        
        # Step 1: Get any variables in the parent scope that are referenced by the operation.
        if ($Operation -is [scriptblock]) {
            $variableNames = $Operation.Ast.FindAll(
                {param($o) $o -is [System.Management.Automation.Language.VariableExpressionAst]},
                $true
            ) | ForEach-Object {
                $_.VariablePath.UserPath
            }

            $variables = foreach ($vn in $variableNames) {
                $PSCmdlet.SessionState.PSVariable.Get($vn)
            }
        }

        # Step 2: Convert the scriptblock if necessary.
        if ($Operation -is [scriptblock]) {
            # Under certain circumstances the iex cmdlet will not allow
            # the evaluation of ScriptBlocks without an input. However it will evaluate strings
            # just fine so we perform the transformation before evaluation.
            $Operation = $Operation.ToString()
        }

        # Step 3: inject the operation and the variables into a new isolated scope and resolve
        # the operation there.
        & {
            param(
                $thisOperation,
                $inputVariables
            )

            $__thisOperation = $thisOperation
            $__inputVariables = $inputVariables

            Remove-Variable "thisOperation"
            Remove-Variable "inputVariables"

            $__ = $null

            foreach ( $__ in $__inputVariables ) {
                if ($null -ne $__) {
                    if ($v = Get-Variable -Name $__.Name -Scope 0 -ea SilentlyContinue) {
                        if (-not $v.Options.HasFlag([System.Management.Automation.ScopedItemOptions]::Constant)) {
                            Set-Variable $__.Name $__.Value
                        }
                    } else {
                        Set-Variable $__.Name $__.Value
                    }
                }
            }

            Remove-Variable "__"

            # Invoke-Expression allows us to receive
            # and handle output as it is generated,
            # rather than wait for the operation to finish
            # as opposed to <[scriptblock]>.invoke().
            Invoke-Expression $__thisOperation | ForEach-Object {
                if (!$LogErrorsOnly) {
                    shoutOut "`t| $_" "Result" -ContextLevel 2 @shoutOutArgs
                }
                return $_
            }
        } $Operation $variables

    } catch {
        "An error occured while executing the operation:" | shoutOUt -MsgType Error -ContextLevel 2 @shoutOutArgs
        $_ | shoutOut -MsgType Error -ContextLevel 2 @shoutOutArgs
        
        $_
    }

    if (!$NotStrict) {
        $ErrorActionPreference = $OldErrorActionPreference
    }

    if ($OutNull) {
        return
    }
    return $r
}
# END: source\Invoke-ShoutOut.ps1


# START: source\Remove-ShoutOutLog.ps1
function Remove-ShoutOutLog {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true, Position=1, ValueFromPipelineByPropertyName=$true, ValueFromPipeline=$true, HelpMessage="Id of the log handler to remove")]
        [guid[]]$LogId
    )

    process {
        foreach ($id in $logId) {
            foreach ($context in $script:logRegistry.Keys) {

                $context | Write-Host -Fore Magenta

                $handlers = $script:logRegistry[$context]

                foreach ($handler in $handlers) {

                    if ($handler.id -eq $id) {
                        $handlers.remove($handler)
                        break
                    }
                }
            }
        }
    }
}
# END: source\Remove-ShoutOutLog.ps1


# START: source\Set-ShoutOutConfig.ps1
function Set-ShoutOutConfig {
    param(
        [Parameter(HelpMessage="The default Message Type that ShoutOut should apply to messages.")]
        [string]$DefaultMsgType,
        [Parameter(HelpMessage="Enable/Disable Context logging.")]
        [Alias("LogContext")]
        [bool]$EnableContextLogging,
        [Parameter(HelpMessage="Disable/Enable ShoutOut.")]
        [Alias("Disabled")]
        [bool]$DisableLogging
    )

    if ($PSBoundParameters.ContainsKey("DefaultMsgType")) {
        $_shoutOutSettings.DefaultMsgType = $DefaultMsgType
    }

    if ($PSBoundParameters.ContainsKey("LogContext")) {
        $_shoutOutSettings.LogContext = $LogContext
    }
    
    if ($PSBoundParameters.ContainsKey("Disabled")) {
        $_shoutOutSettings.Disabled = $Disabled
    }

}
# END: source\Set-ShoutOutConfig.ps1


# START: source\Set-ShoutOutDefaultLog.ps1
function Set-ShoutOutDefaultLog {
    [CmdletBinding(DefaultParameterSetName="DirectoryPath")]
    param(
        [parameter(Mandatory=$true, ValueFromPipeline=$true, Position=1, ParameterSetName="LogFilePath", HelpMessage="Path to log file.")]
        [String]$LogFilePath,
        [parameter(Mandatory=$true, ValueFromPipeline=$true, Position=1, ParameterSetName="LogFile", HelpMessage="FileInfo object.")]
        [System.IO.FileInfo]$LogFile,
        [Parameter(Mandatory=$true, ValueFromPipeline=$true, Position=1, ParameterSetName="DirectoryPath", HelpMessage="Path to log file.")]
        [string]$LogDirectoryPath,
        [Parameter(Mandatory=$true, ValueFromPipeline=$true, Position=1, ParameterSetName="DirectoryInfo", HelpMessage="Path to log file.")]
        [System.IO.DirectoryInfo]$LogDirectory,
        [parameter(Mandatory=$true, ValueFromPipeline=$true, Position=1, ParameterSetName="LogHandler", HelpMessage="ScriptBlock to use as log handler.")]
        [scriptblock]$LogHandler,
        [Parameter(Mandatory=$false, HelpMessage="Causes the log handler to be registered on the global frame.")]
        [switch]$Global
    )

    $redirectArgs = @{
        MsgType = '*'
        Reset   = $true
    }

    $PSBoundParameters.GetEnumerator() | ForEach-Object {
        $redirectArgs[$_.Key] = $_.Value 
    }

    return Add-ShoutOutLog @redirectArgs
    
}
# END: source\Set-ShoutOutDefaultLog.ps1


# START: source\Set-ShoutOutRedirect.ps1
function Set-ShoutOutRedirect {
    [CmdletBinding(DefaultParameterSetName="DirectoryPath")]
    param(
        [parameter(Mandatory=$true, Position=1, HelpMessage="Message type to redirect.")]
        [Alias('MsgType')]
        [string]$MessageType,
        [Parameter(ParameterSetName="FilePath", ValueFromPipeline=$true, Mandatory=$true, Position=2, HelpMessage="Path to log file.")]
        [string]$LogFilePath,
        [Parameter(ParameterSetName="FileInfo", ValueFromPipeline=$true, Mandatory=$true, Position=2, HelpMessage="FileInfo object.")]
        [System.IO.FileInfo]$LogFile,
        [Parameter(ParameterSetName="DirectoryPath", ValueFromPipeline=$true, Mandatory=$true, Position=2, HelpMessage="Path to log file.")]
        [string]$LogDirectoryPath,
        [Parameter(ParameterSetName="DirectoryInfo", ValueFromPipeline=$true, Mandatory=$true, Position=2, HelpMessage="Path to log file.")]
        [System.IO.DirectoryInfo]$LogDirectory,
        [Parameter(ParameterSetName="Scriptblock", ValueFromPipeline=$true, Mandatory=$true, Position=2, HelpMessage="ScriptBlock to use as log handler.")]
        [scriptblock]$LogHandler,
        [Parameter(Mandatory=$false, HelpMessage="Causes the log handler to be added to the global frame.")]
        [switch]$Global
    )

    $redirectArgs = @{
        Reset   = $true
    }

    $PSBoundParameters.GetEnumerator() | ForEach-Object {
        $redirectArgs[$_.Key] = $_.Value 
    }

    return Add-ShoutOutLog @redirectArgs
}
# END: source\Set-ShoutOutRedirect.ps1


# START: source\ShoutOut.ps1
# ShoutOut.ps1

# First-things first: Logging function (is the realest, push the message and let the harddrive feel it.)

<#
.SYNOPSIS
Pushes a message of the given $MessageType to the appropriate log handlers.
.DESCRIPTION
Logging function, used to push a message to a corresponding log handlers.
 
The default log handler type is a Record handler, in thich The message is prepended with meta data about
the invocation to shoutOut as:
 
<MessageType>|<Computer name>|<PID>|<calling Context>|<Date & time>|<Message object type>|<$Message as a string>
 
The other types of log handlers are 'Message' (just receives the raw message object) and 'Details' (the raw message
along with the same metadata that is summarized for the 'Record' type).
 
If an ErrorRecord or Exception object is passed to shoutout, will attempt to expand the object
to make the output of 'Record' type handlers more detailed.
 
The default values for the parameters can be set using the Set-ShoutOutConfig,
Set-ShoutOutDefaultLog, Set-ShotOutRedirect and Add-ShoutOutLog functions.
 
.PARAMETER Message
Message object to log.
 
.PARAMETER MessageType
The type of message to log. By default ShoutOut is intended to handle the following types:
 - Success: Indicating a positive outcome.
 - Error: Indicating that the message is or is related to an Error (typically an [ErrorRecord] object). Comparable with Write-Error.
 - Exception: Indicates that the message is or is related to an Exception.
 - Warning: Indicates that the message relates to a non-fatal irregularity in the system.
 - Info: Indicates that the message is purely informational. This is the standard default message type. Comparable with Write-Host.
 - Result: Indicates that the message is related to an output value from an operation. Comparable with write output.
 
 Each of these types have standard output color presets.
  
 In practice ShoutOut will accept any given string.
 
.PARAMETER Log
Overrides the standard log-selection process and forces shoutout to use the provided log.
 
If this parameter is a string it will be interpreted as the path to a file where log records
should be written.
 
If this is a ScriptBlock, it should have one of the accepted parameters:
- Message
- Details
- Record
 
See the overall ShoutOut README.md for details on Log Handlers.
 
.PARAMETER ContextLevel
The number of steps to climb up the callstack when reporting context.
 
0: Include the call to shoutout.
1: Include the callstack from the call to the context where shoutout was called.
 
Default is 1.
 
When using a Record-type log handler the last element in the stack will be reported as the calling context.
 
.PARAMETER LogContext
If set to $false, no context information will be included in the log (no 'Callstack' for Details, no calling context for Record handlers).
 
.PARAMETER NoNewLine
Omits the newline when writing to console/host.
 
.PARAMETER Quiet
Disables all writing to console/host.
 
#>

function shoutOut {
    [CmdletBinding()]
    param(
        [Alias('Msg')]
        [parameter(Mandatory=$false,  position=1, ValueFromPipeline=$true, ParameterSetName="Message", HelpMessage="Message object to log")]
        [Object]$Message,
        [Alias("ForegroundColor")]
        [Alias("MsgType")]
        [parameter(Mandatory=$false, position=2, HelpMessage="The type of message log. If this is not specified it will be calculated based on the input type and shoutout configuration.")]
        [String]$MessageType=$null,
        [parameter(Mandatory=$false, position=3, HelpMessage="Path to a file or a Scriptblock to use to log the message.")]
        $Log=$null,
        [parameter(Mandatory=$false, position=4, HelpMessage="How many levels to remove from the callstack when reporting the caller context. 0 will include the call to ShoutOut. Default is 1")]
        [Int32]$ContextLevel=1, # The number of levels to proceed up the call
                                # stack when reporting the calling script.
        [parameter(Mandatory=$false, HelpMessage="Determines if context information should be logged.")]
        [bool] $LogContext=$true,
        [parameter(Mandatory=$false, HelpMessage="If set, omits the newline when writing to console/host.")]
        [Switch] $NoNewline,
        [parameter(Mandatory=$false, HelpMessage="If set, no output will be printed to console/host.")]
        [Switch] $Quiet
    )
    
    begin {

        $settings = $_ShoutOutSettings

        # If shoutOut is disabled, return to caller.
        if ($settings.ContainsKey("Disabled") -and ($settings.Disabled)) {
            Write-Debug "Call to Shoutout, but Shoutout is disabled. Turn back on with 'Set-ShoutOutConfig -Disabled `$false'."
            return
        }

        <# Applying global variables #>

        if (!$PSBoundParameters.ContainsKey('LogContext') -and $_ShoutOutSettings.ContainsKey("LogContext")) {
            $LogContext = $_ShoutOutSettings.LogContext
        }
    }

    process {

        $details = @{
            Message    = $Message
            Computer    = $env:COMPUTERNAME
            LogTime     = [datetime]::Now
            PID         = $pid
        }

        $msgObjectType = if ($null -ne $Message) {
            $Message.GetType()
        } else {
            $null
        }

        $details.ObjectType = if ($null -ne $msgObjectType) {
            $msgObjectType.Name
        } else {
            "NULL"
        }
        
        if ( (-not $PSBoundParameters.ContainsKey("MessageType")) -or ($null -eq $PSBoundParameters["MessageType"]) ) {
            
            switch ($details.ObjectType) {

                "ErrorRecord" {
                    $MessageType = "Error"
                }

                default {
                    if ([System.Exception].IsAssignableFrom($msgObjectType)) {
                        $MessageType = "Exception"
                    } else {
                        $MessageType = $script:_ShoutOutSettings.DefaultMsgType
                    }
                }
            }
        }

        $details.MessageType = $MessageType

        $logHandlers = if ($null -eq $Log) {
            _resolveLogHandler -MessageType $MessageType | ForEach-Object Handler
        } else {
            Switch ($log.GetType().Name) {
                String {
                    @{ Handler = @(_buildBasicFileLogger $Log) }
                }
                ScriptBlock {
                    @{ Handler = $Log }
                }
            }
        }

        $recurseArgs = @{}
        $PSBoundParameters.Keys | Where-Object { $_ -notin "Message", "MsgType", "MessageType" } | ForEach-Object {
            $recurseArgs[$_] = $PSBoundParameters[$_]
        }
        if ($recurseArgs.ContainsKey('ContextLevel')) {
            $recurseArgs.ContextLevel += 1
        } else {
            $recurseArgs.ContextLevel = 2
        }

        $messageString = $null
        # Apply formatting to make output more readable.
        switch ($details.ObjectType) {

            "String" {
                # No transformation necessary.
                $messageString = $details.Message
            }

            "NULL" {
                # No Transformation necessary.
                $messageString = ""
            }

            "ErrorRecord" {
                if ($null -ne $details.Message.Exception) {
                    shoutOut -Message $details.Message.Exception @recurseArgs
                }

                $m = $details.Message
                $MessageString = 'Exception', 'CategoryInfo', 'InvocationInfo', 'ScriptStackTrace' | ForEach-Object { $m.$_ } | Out-string | ForEach-Object Split "`n`r" | Where-Object { $_ }
                $MessageString = $MessageString -join "`n"
            }

            default {
                $t = $details.Message.GetType()
                if ([System.Exception].IsAssignableFrom($t)) {
                    if ($null -ne $details.Message.InnerException) {
                        shoutOut $details.Message.InnerException @recurseArgs
                    }
                    $m = $details.Message
                    $MessageString = 'message', 'Source', 'Stacktrace', 'TargetSite' | ForEach-Object { $m.$_ } | Out-string | ForEach-Object Split "`n`r" | Where-Object { $_ }
                    $MessageString = $MessageString -join "`n"
                } else {
                    $messageString = $Message | Out-String | ForEach-Object TrimEnd "`n`r"
                }
            }
        }

        $details.MessageString = $MessageString

        # Print to console if necessary
        if ([Environment]::UserInteractive -and !$Quiet) {

            if ($settings.containsKey("MsgStyles") -and ($settings.MsgStyles -is [hashtable]) -and $settings.MsgStyles.containsKey($details.MessageType)) {
                $msgStyle = $settings.MsgStyles[$details.MessageType]
            }
            
            if (!$msgStyle) {
                if ($details.MessageType -in [enum]::GetNames([System.ConsoleColor])) {
                    $msgStyle = @{ ForegroundColor=$details.MessageType }
                } else {
                    $msgStyle = @{ ForegroundColor="White" }
                }
            }

            $p = @{
                Object = $details.MessageString
                NoNewline = $NoNewline
            }
            if ($msgStyle.ForegroundColor) { $p.ForegroundColor = $msgStyle.ForegroundColor }
            if ($msgStyle.BAckgroundColor) { $p.BackgroundColor = $msgStyle.BackgroundColor }

            Write-Host @p
        }
        
        # Calculate parent/calling context
        $details.Caller = if ($LogContext) {

            
            # Calculate the callstack.

            $cs = Get-PSCallStack
            # Adjust ContextLevel if it is greater than the total size of the callstack:
            if ($cs.Length -le $ContextLevel) {
                $ContextLevel = $cs.Length - 1
            }
            $cs = $cs[$ContextLevel..($cs.length - 1)]

            # Record the callstack on details:
            $details.CallStack = $cs

            # Calculate caller context:
            $l  = if ($null -eq $cs[0].ScriptName) {
                "<No file>"
            } else {
                '{0}:{1}' -f $cs[0].ScriptName, $cs[0].ScriptLineNumber
            }
            "[{0}]{1}" -f ($cs.length), $l
        } else {
            "[context logging disabled]"
        }

        $createRecord = {
            param($details)

            "{0}|{1}|{2}|{3}|{4}|{5}|{6}" -f @( 
                $details.MessageType,
                $details.Computer,
                $details.PID,
                $details.Caller,
                $details.LogTime.toString('o'),
                $details.ObjectType,
                $details.MessageString
            )
        }

        foreach ($handler in $logHandlers) {
            try {
                $handlerArgs = @{}

                $handler.Ast.ParamBlock.Parameters | ForEach-Object {
                    $n = $_.Name.Extent.Text.TrimStart('$')
                    switch ($n) {
                        Message {
                            $handlerArgs.$n = $details.Message
                        }
                        Record {
                            $handlerArgs.$n = & $createRecord $details
                        }
                        Details {
                            $handlerArgs.$n = $details
                        }
                    }
                }

                & $handler @handlerArgs

            } catch {
                "Failed to log: {0}" -f ($handlerArgs | Out-String) | Write-Error
                "using log handler: '{0}'" -f $handler | Write-Error
                $_ | Out-string | Write-Error
            }
        }
    }
}
# END: source\ShoutOut.ps1

# ShoutOut..setup

# START: source\.setup\.bootstrap.ps1

# Default Configuration:
$script:_ShoutOutSettings = @{
    DefaultMsgType="Info"
    MsgStyles=@{
        Success =       @{ ForegroundColor="Green" }
        Exception =     @{ ForegroundColor="Red"; BackgroundColor="Black" }
        Error =         @{ ForegroundColor="Red" }
        Warning =       @{ ForegroundColor="Yellow"; BackgroundColor="Black" }
        Info =          @{ ForegroundColor="Cyan" }
        Result =        @{ ForegroundColor="White" }
    }
    LogContext=$true
    Disabled=$false
}

$script:logRegistry = @{
    global = New-Object System.Collections.ArrayList
}
$script:hashCodeAttribute = 'MyInvocation'

New-Alias 'Get-ShoutOutRedirect' -Value 'Get-ShoutOutLog'
New-Alias 'Clear-ShoutOutRedirect' -Value 'Clear-ShoutOutLog'

# Setting up default logging:
$defaultLogFolder = "{0}\AppData\local\ShoutOut" -f $env:USERPROFILE
$script:DefaultLog = _buildBasicDirectoryLogger $defaultLogFolder

Set-ShoutOutDefaultLog -LogHandler $script:DefaultLog -Global

# $script:logRegistry.values | Out-String | Write-Host
# END: source\.setup\.bootstrap.ps1