当 运行 在 powershell 中执行命令时,如何为 stdout/stderr 上的所有输出添加 date/time?

When running a command in powershell how can I prepend a date/time for all output on stdout/stderr?

当运行脚本为所有日志输出添加日期前缀时,是否可以在 powershell 中使用?

我知道可以这样做: Write-Host "$(Get-Date -format 'u') 我的日志输出"

但我不想每次输出一行时都调用一些函数。相反,我想在 运行 任何脚本或命令时修改所有输出,并为每一行添加时间前缀。

objects generated by Write-Host already come with a timestamp, you can use Update-TypeData to override the .ToString() Method from the InformationRecord Class and then redirect the output from the Information Stream to the Success Stream.

Update-TypeData -TypeName System.Management.Automation.InformationRecord -Value {
    return $this.TimeGenerated.ToString('u') + $this.MessageData.Message.PadLeft(10)
} -MemberType ScriptMethod -MemberName ToString -Force

'Hello', 'World', 123 | Write-Host 6>&1 

all输出前插入一个日期,即stdoutstderrPowerShell-specific streams,您可以使用重定向运算符 *>&1 重定向(合并)命令或脚本块的所有流,管道到 Out-String -Stream 以格式化流 objectstext 行,然后使用 ForEach-Object 处理每一行并在前面加上日期。

让我从一个简单的例子开始,可以在下面找到更完整的解决方案。

# Run a scriptblock
&{
    # Test output to all possible streams, using various formatting methods.
    # Added a few delays to test if the final output is still streaming.

    "Write $($PSStyle.Foreground.BrightGreen)colored`ntext$($PSStyle.Reset) to stdout"
    Start-Sleep -Millis 250
    [PSCustomObject]@{ Answer = 42; Question = 'What?' } | Format-Table
    Start-Sleep -Millis 250
    Get-Content -Path not-exists -EA Continue  # produce a non-terminating error
    Start-Sleep -Millis 250
    Write-Host 'Write to information stream'
    Start-Sleep -Millis 250
    Write-Warning 'Write to warning stream'
    Start-Sleep -Millis 250
    Write-Verbose 'Write to verbose stream' -Verbose
    Start-Sleep -Millis 250
    $DebugPreference = 'Continue'  # To avoid prompt, needed for Windows Powershell
    Write-Debug 'Write to debug stream'
        
} *>&1 | Out-String -Stream | ForEach-Object {
    # Add date in front of each output line

    $date = Get-Date -Format "yy\/MM\/dd H:mm:ss"

    foreach( $line in $_ -split '\r?\n' ) {
        "$($PSStyle.Reset)[$date] $line"
    }
}

在 PS 7.2 控制台中输出:

  • 使用 Out-String 我们使用标准的 PowerShell 格式化系统使输出看起来正常,因为它会在没有重定向的情况下出现(例如,表格之类的东西保持不变)。 -Stream 参数对于保持 PowerShell 的 streaming 输出行为至关重要。如果没有此参数,只有在整个脚本块完成后才会收到输出。
  • 虽然输出看起来已经很不错了,但还有一些小问题
    • 详细、警告和调试消息没有像往常一样着色。
    • 第 2 行中的“文本”一词应为绿色。由于使用 $PSStyle.Reset,这不起作用。删除后,错误消息的颜色会渗入日期列,看起来更糟。它可以被修复,但它不是微不足道的。
    • 换行不正确(换行到输出中间的日期列)。

作为一个更通用、可重用的解决方案,我创建了一个函数Invoke-WithDateLog,它运行一个脚本块,捕获它的所有输出,在前面插入一个日期每行并再次输出:

Function Invoke-WithDateLog {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] 
        [scriptblock] $ScriptBlock,
        
        [Parameter()]          
        [string] $DateFormat = '[yy\/MM\/dd H:mm:ss] ',
        
        [Parameter()]          
        [string] $DateStyle = $PSStyle.Foreground.BrightBlack,
        
        [Parameter()]          
        [switch] $CatchExceptions,
        
        [Parameter()]          
        [switch] $ExceptionStackTrace,
        
        [Parameter()]          
        [Collections.ICollection] $ErrorCollection
    )

    # Variables are private so they are not visible from within the ScriptBlock. 
    $private:ansiEscapePattern = "`e\[[0-9;]*m"
    $private:lastFmt = ''

    & {
        if( $CatchExceptions ) { 
            try   { & $scriptBlock }
            catch {
                # The common parameter -ErrorVariable doesn't work in scripted cmdlets, so use our own error variable parameter.
                if( $null -ne $ErrorCollection ) {
                    $null = $ErrorCollection.Add( $_ )
                }

                # Write as regular output, colored like an error message.
                "`n" + $PSStyle.Formatting.Error + "EXCEPTION ($($_.Exception.GetType().FullName)):`n  $_" + $PSStyle.Reset

                # Optionally write stacktrace. Using the -replace operator we indent each line.
                Write-Debug ($_.ScriptStackTrace -replace '^|\r?\n', "`n  ") -Debug:$ExceptionStackTrace
            }
        }
        else { 
            & $scriptBlock 
        }

    } *>&1 | ForEach-Object -PipelineVariable record {

        # Here the $_ variable is either:
        # - a string in case of simple output
        # - an instance of one of the System.Management.Automation.*Record classes (output of Write-Error, Write-Debug, ...)
        # - an instance of one of the Microsoft.PowerShell.Commands.Internal.Format.* classes (output of a Format-* cmdlet) 
        
        if( $_ -is [System.Management.Automation.ErrorRecord] ) {
            # The common parameter -ErrorVariable doesn't work in scripted cmdlets, so use our own error variable parameter.
            if( $null -ne $ErrorCollection ) {
                $null = $ErrorCollection.Add( $_ )
            }
        }

        $_ # Forward current record

    } | Out-String -Stream | ForEach-Object {

        # Here the $_ variable is always a (possibly multiline) string of formatted output.

        # Out-String doesn't add any ANSI escape codes to colorize Verbose, Warning and Debug messages, 
        # so we have to do it by ourselfs.
        $overrideFmt = switch( $record ) {
            { $_ -is [System.Management.Automation.VerboseRecord] } { $PSStyle.Formatting.Verbose; break }
            { $_ -is [System.Management.Automation.WarningRecord] } { $PSStyle.Formatting.Warning; break }
            { $_ -is [System.Management.Automation.DebugRecord]   } { $PSStyle.Formatting.Debug;   break }
        }

        # Prefix for each line. It resets the ANSI escape formatting before the date.
        $prefix = $DateStyle + (Get-Date -Format $DateFormat) + $PSStyle.Reset

        foreach( $line in $_ -split '\r?\n' ) {

            # Produce the final, formatted output.
            $prefix + ($overrideFmt ?? $lastFmt) + $line + ($overrideFmt ? $PSStyle.Reset : '')

            # Remember last ANSI escape sequence (if any) of current line, for cases where formatting spans multiple lines.
            $lastFmt = [regex]::Match( $line, $ansiEscapePattern, 'RightToLeft' ).Value
        }
    }
}

用法示例:

# To differentiate debug and verbose output from warnings 
$PSStyle.Formatting.Debug = $PSStyle.Foreground.Yellow
$PSStyle.Formatting.Verbose = $PSStyle.Foreground.BrightCyan

Invoke-WithDateLog -CatchExceptions -ExceptionStackTrace {
    "Write $($PSStyle.Foreground.Green)colored`ntext$($PSStyle.Reset) to stdout"
    [PSCustomObject]@{ Answer = 42; Question = 'What?' } | Format-Table
    Get-Content -Path not-exists -EA Continue  # produce a non-terminating error
    Write-Host    'Write to information stream'
    Write-Warning 'Write to warning stream'
    Write-Verbose 'Write to verbose stream' -Verbose
    Write-Debug   'Write to debug stream' -Debug
    throw 'Critical error'
}

在 PS 7.2 控制台中输出:

备注:

  • 代码需要PowerShell 7+.

  • 可以通过参数 -DateFormat 更改日期格式(参见 formatting specifiers) and -DateStyle (ANSI escape sequence for coloring)。

  • Script-terminating 错误,例如通过抛出异常或使用 Write-Error -EA Stop 创建的错误,不会被 default 记录。相反,它们像往常一样从脚本块中冒出来。您可以传递参数 -CatchExceptions 来捕获异常并像常规 non-terminating 错误一样记录它们。传递 -ExceptionStackTrace 还可以记录脚本堆栈跟踪,这对调试非常有用。

  • 像这样的脚本化 cmdlet 不会设置 automatic variable $? 也不会在出现错误时将错误添加到自动 $Error 变量通过 Write-Error 写入。公共参数 -ErrorVariable 都不起作用。为了仍然能够收集错误信息,我添加了参数 -ErrorCollection 可以像这样使用:

    $scriptErrors = [Collections.ArrayList]::new() 
    
    Invoke-WithDateLog -CatchExceptions -ExceptionStackTrace -ErrorCollection $scriptErrors {
        Write-Error 'Write to stderr' -EA Continue
        throw 'Critical error'
    }
    
    if( $scriptErrors ) {
        # Outputs "Number of errors: 2"
        "`nNumber of errors: $($scriptErrors.Count)"
    }