【问题标题】:Write-Host vs Write-Information in PowerShell 5PowerShell 5 中的写入主机与写入信息
【发布时间】:2016-07-22 09:51:35
【问题描述】:

众所周知Write-Host 是邪恶的。 在PowerShell 5 中,添加了Write-Information,并考虑替换Write-Host

但是,真的,哪个更好?
Write-Host 是邪恶的,因为它不使用管道,所以输入消息不能被重用。
但是,Write-Host 所做的只是在控制台中显示一些内容,对吗?在什么情况下我们应该重用输入?
无论如何,如果我们真的想重用输入,为什么不写这样的东西:

$foo = "Some message to be reused like saving to a file"
Write-Host $foo
$foo | Out-File -Path "D:\foo.log"

Write-Host 的另一个缺点是,Write-Host 可以使用-ForegroundColor-BackgroundColor 指定消息在控制台中显示的颜色。

另一方面,通过使用Write-Information,输入消息可以通过No.6管道在任何我们想要的地方使用。并且不需要像我上面写的那样编写额外的代码。但这不好的一面是,如果我们想将消息写入控制台并保存到文件中,我们必须这样做:

# Always set the $InformationPreference variable to "Continue"
$InformationPreference = "Continue";

# if we don't want something like this:
# ======= Example 1 =======
# File Foo.ps1
$InformationPreference = "Continue";
Write-Information "Some Message"
Write-Information "Another Message"

# File AlwaysRunThisBeforeEverything.ps1
.\Foo.ps1 6>"D:\foo.log"
# ======= End of Example 1 =======

# then we have to add '6>"D:\foo.log"' to every lines of Write-Information like this:
# ======= Example 2 =======
$InformationPreference = "Continue";
Write-Information "Some Message" 6>"D:\foo.log"
Write-Information "Another Message" 6>"D:\foo.log"
# ======= End of Example 2 =======

我觉得有点多余。

我只知道这个“vs”这个东西的一小部分,我一定有什么东西不在我的脑海里。那么还有什么可以让我相信Write-InformationWrite-Host 更好,请在此处留下您的好答案。
谢谢。

【问题讨论】:

    标签: powershell powershell-5.0


    【解决方案1】:

    Write-* cmdlet 允许您以结构化方式引导 PowerShell 代码的输出,因此您可以轻松区分不同严重性的消息。

    • Write-Host:在控制台上向交互式用户显示消息。与其他 Write-* cmdlet 不同,此 cmdlet 既不适合也不用于自动化/重定向目的。不邪恶,只是与众不同。
    • Write-Output:将代码的“正常”输出写入默认(成功)输出流(“STDOUT”)。
    • Write-Error:将错误信息写入单独的流(“STDERR”)。
    • Write-Warning:将您认为是警告的消息(即不是失败的事情,但用户应该注意的事情)写入单独的流。
    • Write-Verbose:将您认为比“正常”输出更详细的信息写入单独的流。
    • Write-Debug:将您认为与调试代码相关的信息写入单独的流。

    Write-Information 只是这种方法的延续。它允许您在输出中实现日志级别(DebugVerboseInformationWarningError),并且仍然有可用于常规输出的成功输出流。

    至于为什么Write-Host 成为Write-Information 的包装:我不知道这个决定的实际原因,但我怀疑这是因为大多数人不了解Write-Host 的实际工作原理,即它可以用于什么,不应该用于什么。


    据我所知,没有普遍接受或推荐的登录 PowerShell 方法。例如,您可以实现一个单一的日志记录功能,如他的回答中建议的@JeremyMontgomery

    function Write-Log {
      Param(
        [Parameter(Mandatory=$true, Position=0)]
        [ValidateNotNullOrEmpty()]
        [string]$Message,
        [Parameter(Mandatory=$false, Position=1)]
        [ValidateSet('Error', 'Warning', 'Information', 'Verbose', 'Debug')]
        [string]$LogLevel = 'Information'
      )
    
      switch ($LogLevel) {
        'Error'       { ... }
        'Warning'     { ... }
        'Information' { ... }
        'Verbose'     { ... }
        'Debug'       { ... }
        default       { throw "Invalid log level: $_" }
      }
    }
    
    Write-Log 'foo'                    # default log level: Information
    Write-Log 'foo' 'Information'      # explicit log level: Information
    Write-Log 'bar' 'Debug'
    

    或一组日志功能(每个日志级别一个):

    function Write-LogInformation {
      Param(
        [Parameter(Mandatory=$true, Position=0)]
        [ValidateNotNullOrEmpty()]
        [string]$Message
      )
    
      ...
    }
    
    function Write-LogDebug {
      Param(
        [Parameter(Mandatory=$true, Position=0)]
        [ValidateNotNullOrEmpty()]
        [string]$Message
      )
    
      ...
    }
    
    ...
    
    Write-LogInformation 'foo'
    Write-LogDebug 'bar'
    

    另一种选择是创建自定义记录器对象:

    $logger = New-Object -Type PSObject -Property @{
      Filename = ''
      Console  = $true
    }
    $logger | Add-Member -Type ScriptMethod -Name Log -Value {
      Param(
        [Parameter(Mandatory=$true, Position=0)]
        [ValidateNotNullOrEmpty()]
        [string]$Message,
        [Parameter(Mandatory=$false, Position=1)]
        [ValidateSet('Error', 'Warning', 'Information', 'Verbose', 'Debug')]
        [string]$LogLevel = 'Information'
      )
    
      switch ($LogLevel) {
        'Error'       { ... }
        'Warning'     { ... }
        'Information' { ... }
        'Verbose'     { ... }
        'Debug'       { ... }
        default       { throw "Invalid log level: $_" }
      }
    }
    $logger | Add-Member -Type ScriptMethod -Name LogDebug -Value {
      Param([Parameter(Mandatory=$true)][string]$Message)
      $this.Log($Message, 'Debug')
    }
    $logger | Add-Member -Type ScriptMethod -Name LogInfo -Value {
      Param([Parameter(Mandatory=$true)][string]$Message)
      $this.Log($Message, 'Information')
    }
    ...
    
    Write-Log 'foo'                    # default log level: Information
    $logger.Log('foo')                 # default log level: Information
    $logger.Log('foo', 'Information')  # explicit log level: Information
    $logger.LogInfo('foo')             # (convenience) wrapper method
    $logger.LogDebug('bar')
    

    无论哪种方式,您都可以通过

    外部化日志记录代码
    • 将其放入单独的脚本文件并dot-sourcing该文件:

      . 'C:\path\to\logger.ps1'
      
    • 将其放入 module 并导入该模块:

      Import-Module Logger
      

    【讨论】:

    • 感谢您的详细解答。 Write-* cmdlet 非常适合记录消息,但缺少文件记录支持非常不方便,就像我在问题中写的那样,Write-Host 需要写多行,Write-Information 需要大量设置。是否有任何建议的方法来记录到控制台和文件?
    • @WonTasia:要同时登录到控制台和文件,请使用Start-Transcript / Stop-Transcript(但请注意我的回答中所述的注意事项)。
    • 根据这个答案,Write-Error 默认情况下并不总是写给stderr - stackoverflow.com/a/15669365/2801913
    【解决方案2】:

    补充Ansgar's helpful and comprehensive answer

    Write-Host 在 PSv5 中(本质上)成为
    Write-Information -InformationAction Continue
    的包装器,大概是因为:

    • 启用抑制或重定向Write-Host 消息,这在以前是不可能的(在 PowerShell 4 或更低版本中,Write-Host 绕过 PowerShell 的流并直接输出到主机),

    • 同时保持向后兼容性,因为消息默认输出 - 不像Write-Information,其默认行为是静默(因为它尊重偏好变量$InformationPreference,其默认值为SilentlyContinue)。

    虽然 Write-Host 现在 (PSv5+) 有点用词不当——它不再一定要写入 主机——它仍然具有 一个明显优于 Write-Information 的优势(如您所说):它可以使用-ForegroundColor-BackgroundColor 生成彩色 输出


    Ansgar 的答案涵盖了传统日志记录 视角,但 PowerShell 的 Start-Transcript cmdlet 可以作为内置替代方案(见下文)。

    至于您希望将消息输出到主机同时将它们捕获到日志文件中

    PowerShell 的会话记录 - 通过 Start-TranscriptStop-Transcript - 可能为您提供所需的内容。

    顾名思义,脚本捕获任何打印到屏幕上的内容(不着色),因此默认情况下包括成功输出,但是.
    应用于您的示例:

    $null = Start-Transcript "D:\foo.log"
    
    $InformationPreference = "Continue"
    Write-Information "Some Message"
    Write-Information "Another Message"
    
    $null = Stop-Transcript
    

    上面将向两个屏幕脚本文件打印消息;请注意,奇怪的是,只有在文件中它们才会以INFO:为前缀。
    (相比之下,Write-WarningWrite-VerboseWrite-Debug - 如果配置为产生输出 - 使用前缀 WARNING:VERBOSE:DEBUG: 在屏幕上和文件中;类似地,Write-Error 产生屏幕上和文件中的“嘈杂”多行输入。)

    请注意一个错误,它只影响Windows PowerShell(它一直是fixed in PowerShell [Core]谢谢JohnLBevan。): 即使$InformationPreference 设置为SilentlyContinue(默认值),Write-Information 的输出也会显示在脚本文件中(但不在屏幕上);排除Write-Information 输出(通过首选项变量或-InformationAction 参数)的唯一方法似乎是Ignore 的值 - 它绝对地使输出静音 - 或者,奇怪的是,Continue,它只打印到控制台,正如 PetSerAl 指出的那样。

    简而言之,您可以将Start-Transcript 用作日志工具的一种方便的内置近似,您可以通过首选项变量从外部控制其详细程度(@ 987654357@, $VerbosePreference, ...),与传统日志记录有以下重要区别

    • 一般来说,进入脚本文件的内容也是输出到控制台(这通常被认为是一个加分项)。

      李>
    • 但是,成功输出(数据输出)默认情况下发送到脚本 - 除非你捕获它或抑制 完全是 - 你不能选择性地将它排除在成绩单之外:

      • 如果您捕获或抑制它,它也不会显示在主机(默认为控制台)中[1]

      • 然而,相反的情况是可能的:您可以通过 Out-Default -Transcript 感谢 @ 987654324@;例如,
        'to transcript only' | Out-Default -Transcript;但是,从 PowerShell 7.0 开始,这似乎会在脚本中记录输出两次;另请注意,Out-Default 通常不会从用户代码中调用 - 请参阅this answer

    • 通常,外部重定向(将> 应用于内部执行转录的脚本的调用)将流排除在转录本之外,有两个例外,从 PowerShell 7.0 开始:

      • Write-Host 输出,即使使用了 6>*> 重定向。
      • 错误输出,即使使用了2>*> 重定向。
        但是,使用 $ErrorActionPreference = 'SilentlyContinue' / 'Ignore' 确实非终止错误排除在成绩单之外,但不会终止错误。
    • 脚本文件不是面向行的(有一个带有调用信息的标题行块,并且不能保证脚本产生的输出仅限于一行),因此您不能期望在逐行方式。


    [1] PetSerAl 提到了以下有限且有些麻烦的解决方法 (PSv5+),仅用于将成功输出发送到控制台,尤其是排除了通过管道发送输出或捕获它:
    'to console only' | Out-String -Stream | ForEach-Object { $Host.UI.WriteLine($_) }

    【讨论】:

    • 当我发现 $InformationPreference -eq 'Continue' 时,我以为我疯了,输出到控制台,而不是成绩单。感谢您让我知道这是预期的行为。
    • @PatrickFranchise:嗯,这是 PSv5.1 的实际行为 - 不确定它是否是预期的;坦率地说,我怀疑是一个错误。
    【解决方案3】:

    PowerShell 是关于自动化的。

    有时,您每天多次运行脚本,但不想一直看到输出。

    Write-Host 不可能隐藏输出。无论如何,它都会写在控制台上。

    使用Write-Information,您可以在脚本上指定-InformationAction 参数。使用此参数,您可以指定是否要查看消息(-InformationAction Continue)或不查看消息(-InformationAction SilentlyContinue

    编辑: 请使用"Some Message" | out-file D:\foo.log 进行日志记录,不要使用Write-HostWrite-Information

    【讨论】:

    • 谢谢。你能告诉我为什么不建议使用Write-Information 登录文件吗?
    • 从 PSv5 开始,Write-Host 输出可以通过输出流6 被抑制/重定向(因为Write-Host 现在是Write-Information -InformationAction Continue 的包装器)。例如,Write-Host "Some Message" 6>$null打印任何内容。
    • @WonTasia: 将Write-HostWrite-Information 输出重定向到文件本身并没有错 - 从外部使用6> (PSv5+) 按需,从外部;相反,如果您提前知道您将只写入一个文件,则没有理由使用这些 cmdlet。如果您想写入both 控制台(在 PSv5+ 中使用流 6 写入文件,请使用 'Some Message' | Tee-Object D:\foo.log | Write-Host
    • 还值得一提的是,如果您使用带有param(...) 参数声明语句的脚本定义了带有[CmdletBinding()] 属性修饰的脚本,那么您编写的任何脚本都只会尊重常见的-Information 参数。跨度>
    【解决方案4】:

    这是我最近用于我的脚本的更专业的日志记录功能的通用版本。

    这种情况是,当我需要将某事作为计划任务执行时,我通常会创建一个通用脚本,或在一个模块中创建一个执行“繁重工作”的函数,然后是一个处理特定细节的调用脚本工作,例如从 XML 配置中获取参数、日志记录、通知等。

    内部脚本使用 Write-ErrorWrite-WarningWrite-Verbose,调用脚本将所有输出流重定向到到此函数的管道,该函数将消息记录在 csv 文件中,其中包含时间戳、级别和消息。

    在这种情况下,它针对的是 PoSh v.4,所以我基本上使用 Write-Verbose 作为 Write-Information 的替代,但想法相同。如果我在 Some-Script.ps1(参见示例)中使用 Write-Host 而不是 Write-Verbose 或 Write-Information,则 Add-LogEntry 函数不会捕获和记录消息。如果您想使用它来适当地捕获更多流,请在 switch 语句中添加条目以满足您的需求。

    在这种情况下,-PassThru 开关基本上是一种解决您提到的关于写入日志文件以及输出到控制台(或另一个变量,或向下管道)。在这个实现中,我为对象添加了一个“Level”属性,但希望你能明白这一点。我的用例是将日志条目传递给一个变量,以便检查它们是否有错误,并在发生错误时用于 SMTP 通知。

    function Add-LogEntry {
    [CmdletBinding()]
    param (
        # Path to logfile
        [Parameter(ParameterSetName = 'InformationObject', Mandatory = $true, Position = 0)]
        [Parameter(ParameterSetName = 'Normal', Mandatory = $true, Position = 0)]
        [String]$Path,
    
        # Can set a message manually if not capturing an alternate output stream via the InformationObject parameter set.
        [Parameter(ParameterSetName = 'Normal', Mandatory = $true)]
        [String]$Message,
    
        # Captures objects redirected to the output channel from Verbose, Warning, and Error channels
        [ValidateScript({ @("VerboseRecord", "WarningRecord", "ErrorRecord") -Contains $_.GetType().name })]
        [Parameter(ParameterSetName = 'InformationObject', Mandatory = $true, ValueFromPipeline = $true)]
        $InformationObject,
    
        # If using the message parameter, must specify a level, InformationObject derives level from the object.
        [ValidateSet("Information", "Warning", "Error")]
        [Parameter(ParameterSetName = 'Normal', Mandatory = $true, Position = 2)]
        [String]$Level,
    
        # Forward the InformationObject down the pipeline with additional level property.
        [Parameter(ParameterSetName = 'InformationObject', Mandatory = $false)]
        [Switch]$PassThru
    )
    Process {
        # If using an information object, set log entry level according to object type.
        if ($PSCmdlet.ParameterSetName -eq "InformationObject") {
            $Message = $InformationObject.ToString()
    
            # Depending on the object type, set the error level, 
            # add entry to cover "Write-Information" output here if needed
            switch -exact ($InformationObject.GetType().name) {
                "VerboseRecord" { $Level = "Information" }
                "WarningRecord" { $Level = "Warning" }
                "ErrorRecord" { $Level = "Error" }
            }
        }
    
        # Generate timestamp for log entry
        $Timestamp = (get-date).Tostring("yyyy\-MM\-dd\_HH\:mm\:ss.ff")
        $LogEntryProps = @{
            "Timestamp" = $Timestamp;
            "Level" = $Level;
            "Message" = $Message
        }
    
        $LogEntry = New-Object -TypeName System.Management.Automation.PSObject -Property $LogEntryProps
        $LogEntry | Select-Object Timestamp, Level, Message | Export-Csv -Path $Path -NoTypeInformation -Append
    
        if ($PassThru) { Write-Output ($InformationObject | Add-Member @{Level = $Level } -PassThru) }
      }
    }
    

    示例用法是

    & $PSScriptRoot\Some-Script.ps1 -Param $Param -Verbose *>&1 | Add-LogEntry -Path $LogPath -PassThru
    

    -PassThru 开关本质上应该将信息对象写入控制台,如果您不捕获变量中的输出或将其通过管道传递给其他东西。

    【讨论】:

      【解决方案5】:

      我不得不承认我讨厌 PowerShell 日志记录和所有 Write-* 命令...所以我使用相同的功能启动所有脚本:

      function logto{  ## Outputs data to Folder tree
          Param($D,$P,$F,$C,$filename)
          $LogDebug = $false
          $FDomain =[System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest() 
          $SCRdir = $MyInvocation.ScriptName
          $FDNSName = $FDomain.Name 
          $RealFile = $F 
          if($ScriptName -eq $null){
              $ScriptName = "\LogTo\"
          }
          ## if there is a time stamp defined make it part of the directory
          if($GlobalRunTime){ 
              $Flocaldrive = $env:SystemDrive + "\" + $FDNSName + $ScriptName + $GlobalRunTime + "\"
              If ($LogDebug) {Write-host "Set path to $Flocaldrive" -foregroundcolor Magenta}
          }else{
              $Flocaldrive = $env:SystemDrive + "\" + $FDNSName + $ScriptName
              If ($LogDebug) {Write-host "Set path to $Flocaldrive" -foregroundcolor Magenta}
          }
          ## do not write null data
          if ($D -eq $null) {  
              If ($LogDebug) {Write-host "$RealFile :Received Null Data Exiting Function" -foregroundcolor Magenta}
              Return
          }
          ## if no path is chosen default to
          if ($P -eq $null) {  
              $PT = $Flocaldrive
              If ($LogDebug) {Write-host "Path was Null, setting to $PT" -foregroundcolor Magenta}
          }else{
              $PT = $Flocaldrive + $P
              If ($LogDebug) {Write-host "Path detected as $p, setting path to $PT" -foregroundcolor Magenta}
          }
          ## anything with no file goes to Catchall
          If ($RealFile-eq $null) { 
              If ($LogDebug) {Write-host "$D :attempting to write to Null file name, redirected out to Catchall" -foregroundcolor Magenta}
              $RealFile= "\Catchall.txt"
          }
          ##If color is blank DONT write to screen
          if ($C -eq $null) { 
              If ($LogDebug) {Write-host "Color was blank so not writing to screen" -foregroundcolor Magenta}
          }else{
              If ($LogDebug) {Write-host "Attempting to write to console in $C" -foregroundcolor Magenta}
              write-host $D -foregroundcolor $C
          }
          ###### Write standard format
          $DataFile = $PT + $RealFile## define path with File
          ## Check if path Exists if not create it
          If (Test-Path $PT) { 
              If ($LogDebug) {Write-host "$PT :Directory Exists" -foregroundcolor Magenta}
          }else{
              New-Item $PT -type directory | out-null ## if directory does not exist create it
              If ($LogDebug) {Write-host "Creating directory $PT" -foregroundcolor Magenta}
          } 
          ## If file exist if not create it
          If (Test-Path $DataFile) { ## If file does not exist create it
              If ($LogDebug) {Write-host "$DataFile :File Exists" -foregroundcolor Magenta}
          }else{
              New-Item $DataFile -type file | out-null ## if file does not exist create it, we cant append a null file
              If ($LogDebug) {Write-host "$DataFile :File Created" -foregroundcolor Magenta}
          } 
          ## Write our data to file
          $D | out-file -Filepath $DataFile -append  ## Write our data to file
          ## Write to color coded files 
          if ($C -ne $null) { 
              $WriteSumDir = $Flocaldrive + "Log\Sorted" 
              $WriteSumFile = $WriteSumDir + "\Console.txt"
              ## Check if path Exists if not create it
              If (Test-Path $WriteSumDir) { 
                  If ($LogDebug) {Write-host "$WriteSumDir :Directory Exists" -foregroundcolor Magenta}
              }else{
                  New-Item $WriteSumDir -type directory | out-null ## if directory does not exist create it
                  If ($LogDebug) {Write-host "Creating directory $WriteSumDir" -foregroundcolor Magenta}
              }
              ## If file does not exist create it
              If (Test-Path $WriteSumFile) { 
                  If ($LogDebug) {Write-host "$WriteSumFile :File Exists" -foregroundcolor Magenta}
              }else{
                  New-Item $WriteSumFile -type file | out-null ## if file does not exist create it, we cant append a null file
                  If ($LogDebug) {Write-host "$WriteSumFile :File Created" -foregroundcolor Magenta}
              } 
              ## Write our data to file
              $D | out-file -Filepath $WriteSumFile -append ## write everything to same file
              ## Write our data to color coded file
              $WriteColorFile = $WriteSumDir + "\$C.txt"
              If (Test-Path $WriteColorFile) { ## If file does not exist create it
                  If ($LogDebug) {Write-host "$WriteColorFile :File Exists" -foregroundcolor Magenta}
              }else{
                  New-Item $WriteColorFile -type file | out-null ## if file does not exist create it, we cant append a null file
                  If ($LogDebug) {Write-host "$WriteColorFile :File Created" -foregroundcolor Magenta}
              } 
              ## Write our data to Color coded file
              $D | out-file -Filepath $WriteColorFile -append ## write everything to same file
          }
          ## If A return was not specified
          If($filename -ne $null){
              Return $DataFile
          }
      }
      

      【讨论】:

      • 除了了解您的个人厌恶之外,我们没有学到太多东西。您发布的冗长功能 - 评论稀少且没有整体解释 - 没有帮助。
      • 我为我原来的评论中的恶作剧道歉;但是,我的意思是提供关于您的答案可能缺少什么的指针,以便对 others 有用(复数;即使它们显然是 my 指针):积极地说:告诉我们为什么您不喜欢Write-* cmdlet,并提供说明说明您的函数做了什么来解释它如何补偿cmdlet 的缺点。至于一般的我们观点:随着时间的推移,这会出现,反映在投票(或缺乏投票)中。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2018-11-28
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-11-04
      相关资源
      最近更新 更多