Public/Start-PoshLog.ps1

# Copyright (c) 2023 Anthony J. Raymond, MIT License (see manifest for details)

using namespace System.IO
using namespace System.Text
using namespace System.Collections.ObjectModel

function Start-PoshLog {
    [CmdletBinding(DefaultParameterSetName = "Path")]
    [OutputType([void])]

    ## PARAMETERS #############################################################
    param (
        [Parameter(
            Position = 0,
            ValueFromPipelineByPropertyName,
            ValueFromPipeline,
            ParameterSetName = "Path"
        )]
        [Parameter(
            Position = 0,
            ValueFromPipelineByPropertyName,
            ValueFromPipeline,
            ParameterSetName = "PathAppend"
        )]
        [Parameter(
            Position = 0,
            ValueFromPipelineByPropertyName,
            ValueFromPipeline,
            ParameterSetName = "PathNoClobber"
        )]
        [ValidateScript({
                if ((Resolve-PoshPath -Path $_ -Provider).Name -eq "FileSystem") {
                    $true
                }
                throw "The argument specified must resolve to a valid path on the FileSystem provider."
            })]
        [ValidateScript({
                if (Test-Path -Path $_ -IsValid) {
                    $true
                }
                throw "The argument specified must resolve to a valid file or folder path."
            })]
        [string[]]
        $Path = [Environment]::GetFolderPath("MyDocuments"),

        [Parameter(
            Mandatory,
            ValueFromPipelineByPropertyName,
            ParameterSetName = "LiteralPath"
        )]
        [Parameter(
            Mandatory,
            ValueFromPipelineByPropertyName,
            ParameterSetName = "LiteralPathAppend"
        )]
        [Parameter(
            Mandatory,
            ValueFromPipelineByPropertyName,
            ParameterSetName = "LiteralPathNoClobber"
        )]
        [Alias("PSPath")]
        [ValidateScript({
                if ((Resolve-PoshPath -LiteralPath $_ -Provider).Name -eq "FileSystem") {
                    $true
                }
                throw "The argument specified must resolve to a valid path on the FileSystem provider."
            })]
        [ValidateScript({
                if (Test-Path -LiteralPath $_ -IsValid) {
                    $true
                }
                throw "The argument specified must resolve to a valid file or folder path."
            })]
        [string[]]
        $LiteralPath,

        [Parameter(ParameterSetName = "PathAppend")]
        [Parameter(ParameterSetName = "LiteralPathAppend")]
        [switch]
        $Append,

        [Parameter(ParameterSetName = "PathNoClobber")]
        [Parameter(ParameterSetName = "LiteralPathNoClobber")]
        [switch]
        $NoClobber,

        [Parameter()]
        [switch]
        $AsUtc,

        [Parameter()]
        [switch]
        $PassThru
    )

    ## BEGIN ##################################################################
    begin {
        $DateTime = [datetime]::Now

        $Format = $AsUtc | ?: { "yyyy\-MM\-dd HH:mm:ss\Z", "ToUniversalTime", "yyyyMMdd\-HHmmss\Z" } { "yyyy\-MM\-dd HH:mm:ss", "ToLocalTime", "yyyyMMdd\-HHmmss" }
        $FileMode = $Append | ?: { [FileMode]::Append } { $NoClobber | ?: { [FileMode]::CreateNew } { [FileMode]::Create } }

        $Template = {
            "**********************"
            "Windows PowerShell log start"
            "Version: {0} ({1})" -f $PSVersionTable.PSVersion, $PSVersionTable.PSEdition
            "Start time: {0:$( $Format[0] )}" -f $DateTime.($Format[1]).Invoke()
            "**********************"
        }
    }

    ## PROCESS ################################################################
    process {
        $Process = ($PSCmdlet.ParameterSetName -cmatch "^LiteralPath") | ?: { Resolve-PoshPath -LiteralPath $LiteralPath } { Resolve-PoshPath -Path $Path }

        foreach ($Object in $Process) {
            try {
                if (-not ($LogDir = [DirectoryInfo] (Split-Path $Object -Parent)).Exists) {
                    $LogDir = [Directory]::CreateDirectory($LogDir.FullName)
                }

                if (-not ($LogFile = [FileInfo] (Split-Path $Object -Leaf)).Extension) {
                    $LogDir = [Directory]::CreateDirectory($LogDir.FullName + "\" + $LogFile.Name)

                    $LogFile = [FileInfo] ("PowerShell_log.{0}.{1:$( $Format[2] )}.txt" -f ([guid]::NewGuid() -isplit "-")[0], $DateTime.($Format[1]).Invoke())
                }

                Use-Object ($File = [File]::Open($LogDir.FullName + "\" + $LogFile.Name, $FileMode)) {
                    if ($Append) {
                        $NewLine = [Encoding]::UTF8.GetBytes([Environment]::NewLine)
                        $File.Write($NewLine, 0, $NewLine.Length)
                    }

                    foreach ($Line in $Template.Invoke()) {
                        $Bytes = [Encoding]::UTF8.GetBytes($Line + [Environment]::NewLine)
                        $File.Write($Bytes, 0, $Bytes.Length)
                    }
                }

                $Global:PSLogDetails += @(@{ Path = $File.Name; Utc = $AsUtc })
                Write-Information -InformationAction Continue -MessageData ("Log started, output file is '{0}'" -f $File.Name) -InformationVariable null
                ## EXCEPTIONS #################################################
            } catch [MethodInvocationException] {
                $PSCmdlet.WriteError((New-MethodInvocationException -Exception $_.Exception.InnerException))
            } catch {
                $PSCmdlet.WriteError($_)
            }
        }
    }

    ## END ####################################################################
    end {
        if (-not (Get-EventSubscriber | Where-Object SourceIdentifier -CMatch "^PSLog") -and $PSLogDetails) {
            $Global:PSLogInformation = [ObservableCollection[InformationRecord]]::new()
            $Global:PSLogWarning = [ObservableCollection[WarningRecord]]::new()
            $Global:PSLogError = [ObservableCollection[ErrorRecord]]::new()

            $Action = { Write-PoshLog -PSEventArgs $Event }

            $null = Register-ObjectEvent -EventName CollectionChanged -InputObject $PSLogInformation -SourceIdentifier PSLogInformation -Action $Action
            $null = Register-ObjectEvent -EventName CollectionChanged -InputObject $PSLogWarning -SourceIdentifier PSLogWarning -Action $Action
            $null = Register-ObjectEvent -EventName CollectionChanged -InputObject $PSLogError -SourceIdentifier PSLogError -Action $Action

            $Global:PSDefaultParameterValues["Write-Information:InformationVariable"] = "+PSLogInformation"
            $Global:PSDefaultParameterValues["Write-Warning:WarningVariable"] = "+PSLogWarning"
            $Global:PSDefaultParameterValues["Write-Error:ErrorVariable"] = "+PSLogError"
        }

        if ($PassThru) {
            Write-Output $PSLogDetails.Path -NoEnumerate
        }
    }
}