40

It is well known that Write-Host is evil. In PowerShell 5, Write-Information is added and is considered to replace Write-Host.

But, really, which is better?
Write-Host is evil for it does not use pipeline, so the input message can't get reused.
But, what Write-Host do is just to show something in the console right? In what case shall we reuse the input?
Anyway, if we really want to reuse the input, why not just write something like this:

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

Another Cons of Write-Host is that, Write-Host can specified in what color the messages are shown in the console by using -ForegroundColor and -BackgroundColor.

On the other side, by using Write-Information, the input message can be used wherever we want via the No.6 pipeline. And doesn't need to write the extra codes like I write above. But the dark side of this is that, if we want to write messages to the console and also saved to the file, we have to do this:

# 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 =======

A little bit redundant I think.

I only know a little aspect of this "vs" thing, and there must have something out of my mind. So is there anything else that can make me believe that Write-Information is better than Write-Host, please leave your kind answers here.
Thank you.

wontasia
  • 522
  • 1
  • 6
  • 10

6 Answers6

56

The Write-* cmdlets allow you to channel the output of your PowerShell code in a structured way, so you can easily distinguish messages of different severity from each other.

  • Write-Host: display messages to an interactive user on the console. Unlike the other Write-* cmdlets this one is neither suitable nor intended for automation/redirection purposes. Not evil, just different.
  • Write-Output: write the "normal" output of the code to the default (success) output stream ("STDOUT").
  • Write-Error: write error information to a separate stream ("STDERR").
  • Write-Warning: write messages that you consider warnings (i.e. things that aren't failures, but something that the user should have an eye on) to a separate stream.
  • Write-Verbose: write information that you consider more verbose than "normal" output to a separate stream.
  • Write-Debug: write information that you consider relevant for debugging your code to a separate stream.

Write-Information is just a continuation of this approach. It allows you to implement log levels in your output (Debug, Verbose, Information, Warning, Error) and still have the success output stream available for regular output.

As for why Write-Host became a wrapper around Write-Information: I don't know the actual reason for this decision, but I'd suspect it's because most people don't understand how Write-Host actually works, i.e. what it can be used for and what it should not be used for.


To my knowledge there isn't a generally accepted or recommended approach to logging in PowerShell. You could for instance implement a single logging function like @JeremyMontgomery suggested in his answer:

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'

or a set of logging functions (one for each log level):

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'

Another option is to create a custom logger object:

$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')

Either way you can externalize the logging code by

  • putting it into a separate script file and dot-sourcing that file:

    . 'C:\path\to\logger.ps1'
    
  • putting it into a module and importing that module:

    Import-Module Logger
    
Community
  • 1
  • 1
Ansgar Wiechers
  • 193,178
  • 25
  • 254
  • 328
  • Thanks for you detailed answer. `Write-*` cmdlets are great to log messages, but lacking file logging support is very inconvenient, like I wrote in the question, `Write-Host` needs write multiple lines, and `Write-Information` needs lots of setups. Is there any suggested approach to log to both console and files? – wontasia Jul 25 '16 at 06:25
  • @WonTasia: To log to both the console and a file, use `Start-Transcript` / `Stop-Transcript` (but note the caveats stated in my answer). – mklement0 Jul 27 '16 at 02:11
  • According to this answer, `Write-Error` does not always write to `stderr` by default - https://stackoverflow.com/a/15669365/2801913 – Graeme Jan 07 '20 at 09:49
28

To complement Ansgar's helpful and comprehensive answer:

Write-Host became (in essence) a wrapper for
Write-Information -InformationAction Continue
in PSv5, presumably because:

  • it enables suppressing or redirecting Write-Host messages, which was not previously possible (in PowerShell 4 or below, Write-Host bypassed PowerShell's streams and output directly to the host),

  • while preserving backward compatibility in that the messages are output by default - unlike with Write-Information, whose default behavior is to be silent (because it respects preference variable $InformationPreference, whose default value is SilentlyContinue).

While Write-Host is therefore now (PSv5+) a bit of a misnomer — it doesn't necessarily write to the host anymore — it still has one distinct advantage over Write-Information (as you state): it can produce colored output with -ForegroundColor and -BackgroundColor.


Ansgar's answer has the conventional logging perspective covered, but PowerShell's Start-Transcript cmdlet may serve as a built-in alternative (see below).

As for your desire to output messages to the host while also capturing them in a log file:

PowerShell's session transcripts - via Start-Transcript and Stop-Transcript - may give you what you want.

As the name suggests, transcripts capture whatever prints to the screen (without coloring), which therefore by default includes success output, however.
Applied to your example:

$null = Start-Transcript "D:\foo.log"

$InformationPreference = "Continue"
Write-Information "Some Message"
Write-Information "Another Message"

$null = Stop-Transcript

The above will print messages to both the screen and the transcript file; note that, curiously, only in the file will they be prefixed with INFO:.
(By contrast, Write-Warning, Write-Verbose and Write-Debug - if configured to produce output - use prefix WARNING:, VERBOSE:, DEBUG: both on-screen and in the file; similarly, Write-Error produces "noisy" multiline input both on-screen and in the file.)

Note one bug that only affects Windows PowerShell (it has been fixed in PowerShell [Core].Thanks, JohnLBevan.): output from Write-Information shows up in the transcript file (but not on the screen) even when $InformationPreference is set to SilentlyContinue (the default); the only way to exclude Write-Information output (via the preference variable or -InformationAction parameter) appears to be a value of Ignore - which silences the output categorically - or, curiously, Continue, in which it only prints to the console, as PetSerAl points out.

In a nutshell, you can use Start-Transcript as a convenient, built-in approximation of a logging facility, whose verbosity you can control from the outside via the preference variables ($InformationPreference, $VerbosePreference, ...), with the following important differences from conventional logging:

  • Generally, what goes into the transcript file is also output to the console (which could generally be considered a plus).

  • However, success output (data output) is by default also sent to the transcript - unless you capture it or suppress it altogether - and you cannot selectively keep it out of the transcript:

    • If you capture or suppress it, it won't show in in the host (the console, by default) either[1].

    • The inverse, however, is possible: you can send output to the transcript only (without echoing it in the console), by way of Out-Default -Transcript Thanks, PetSerAl; e.g.,
      'to transcript only' | Out-Default -Transcript; however, as of PowerShell 7.0 this appears to log the output twice in the transcript; also note that Out-Default is generally not meant to be called from user code - see this answer.

  • Generally, external redirections (applying > to a call to a script that internally performs transcription) keep streams out of the transcript, with two exceptions, as of PowerShell 7.0:

    • Write-Host output, even if 6> or *> redirections are used.
    • Error output, even if 2> or *> redirections are used.
      However, using $ErrorActionPreference = 'SilentlyContinue' / 'Ignore' does keep non-terminating errors out of the transcript, but not terminating ones.
  • Transcript files aren't line-oriented (there's a block of header lines with invocation information, and there's no guarantee that output produced by the script is confined to a line), so you cannot expect to parse them in a line-by-line manner.


[1] PetSerAl mentions the following limited and somewhat cumbersome workaround (PSv5+) for sending success output to the console only, which notably precludes sending the output through the pipeline or capturing it:
'to console only' | Out-String -Stream | ForEach-Object { $Host.UI.WriteLine($_) }

mklement0
  • 382,024
  • 64
  • 607
  • 775
  • 1
    I thought I was crazy when I found out if $InformationPreference -eq 'Continue', the output goes to the console, but not the transcript. Thanks for letting me know it's the expected behavior. – PatrickFranchise Apr 05 '17 at 19:54
  • 3
    @PatrickFranchise: Well, it's the _actual_ behavior as of PSv5.1 - not sure if it's _expected_; frankly, I suspect a bug. – mklement0 Apr 05 '17 at 20:05
  • "… `Write-Host` … still has one distinct advantage over `Write-Information` …: it can produce colored output with `-ForegroundColor` and `-BackgroundColor`." Seeing that there are different `Write-*` "levels", is it possible to set a default color for `Write-Information` messages, `Write-Warning` messages, etc.? – Garret Wilson Oct 06 '22 at 15:31
  • 1
    @GarretWilson, I'm not aware of a way to modify the colors used by the built-in, stream-specific `Write-*` cmdlets; as far as I know, it is indeed just `Write-Host` that supports custom colors; however, you can also output strings with embedded VT (ANSI) escape sequences (both to the success output stream with (implied) `Write-Output` and via `Write-Host`); e.g.: `"It ain't easy being $([char] 27)[32mgreen$([char] 27)[m."`; alternatively, in PowerShell (Core) 7.2, via the automatic `$PSStyle` variable: `"It ain't easy being $($PSStyle.Foreground.Green)green$($PSStyle.Reset)."` – mklement0 Oct 06 '22 at 15:58
5

PowerShell is about automation.

Sometimes, you run a script multiple times a day and you don't want to see the output all the time.

Write-Host has no possibility of hiding the output. It gets written on the Console, no matter what.

With Write-Information, you can specify the -InformationAction Parameter on the Script. With this parameter, you can specify if you want to see the messages (-InformationAction Continue) or not (-InformationAction SilentlyContinue)

Edit: And please use "Some Message" | out-file D:\foo.log for logging, and neither Write-Host or Write-Information

MicroScripter
  • 148
  • 2
  • 9
  • Thanks. Could you tell me why using `Write-Information` to log to files is not suggested? – wontasia Jul 25 '16 at 03:47
  • 1
    Since PSv5, `Write-Host` output _can_ be suppressed / redirected via output stream `6` (because `Write-Host` is now a wrapper for `Write-Information -InformationAction Continue`). E.g., `Write-Host "Some Message" 6>$null` will _not_ print anything. – mklement0 Jul 26 '16 at 02:55
  • 1
    @WonTasia: There's nothing wrong with redirecting `Write-Host` and `Write-Information` output to a file per se - on demand, from the outside, using `6>` (PSv5+); by contrast, if you know ahead of time that you'll be writing to a _file only_, there's no reason to use these cmdlets. If you want to write to _both_ the console (using stream `6` in PSv5+) _and_ to a file, use `'Some Message' | Tee-Object D:\foo.log | Write-Host` – mklement0 Jul 27 '16 at 02:22
  • It's also worth mentioning that any script you write will only respect the common `-Information` parameter if you define the script with a `param(...)` parameter-declaration statement decorated with a `[CmdletBinding()]` attribute. – mklement0 Jul 27 '16 at 02:44
0

Here's a generic version of a more specialized logging function that I used for a script of mine recently.

The scenario for this is that when I need to do something as a scheduled task, I typically create a generic script, or function in a module that does the "heavy lifting" then a calling script that handles the specifics for the particular job, like getting arguments from an XML config, logging, notifications, etc.

The inner script uses Write-Error, Write-Warning, and Write-Verbose, the calling script redirects all output streams down the pipeline to this function, which captures records the messages in a csv file with a Timestamp, Level, and Message.

In this case, it was targeted at PoSh v.4, so I basically am using Write-Verbose as a stand-in for Write-Information, but same idea. If I were to have used Write-Host in the Some-Script.ps1 (see example) instead of Write-Verbose, or Write-Information, the Add-LogEntry function wouldn't capture and log the message. If you want to use this to capture more streams appropriately, add entries to the switch statement to meet your needs.

The -PassThru switch in this case was basically a way to address exactly what you mentioned about both writing to a log file in addition to outputting to the console (or to another variable, or down the pipeline). In this implementation, I've added a "Level" property to the object, but hopefully you can see the point. My use case for this was to pass the log entries to a variable so that they could be checked for errors, and used in an SMTP notification if an error did occur.

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) }
  }
}

Example usage would be

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

The -PassThru switch should essentially write the information object to the console if you don't capture the output in a variable or pass it down the pipe to something else.

webward
  • 21
  • 2
0

Interesting: while the preference variables have been mentioned in the earlier answers, nobody has mentioned the common parameters -InformationVariable, -ErrorVariable and -WarningVariable. They are designed to take the output from the corresponding Write-* commands in the script or command so you can do with it what you want.

They're not really suitable to use as a log of an entire session, because they need to be added to every command you run, and the command/script needs to support it. (can be as easy as adding [CmdletBinding()] at the top of you scipt if you don't have it already. See the documentation)

But it could be a good way to separate the warnings/error from the real output of a command. Just run it again with the proper parameters added.
For example -ErrorVariable ErrorList -ErrorAction SilentlyContinue. Now all the errors are stored in the $ErrorList variable and you can write them in a file. $ErrorList|Out-File -FilePath errors.txt

Niels
  • 66
  • 4
-1

I have to admit that I hate PowerShell logging and all the Write-* commands... So I start all my scripts with the same function:

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
    }
}
mklement0
  • 382,024
  • 64
  • 607
  • 775
  • 5
    Other than learning about your personal dislikes we haven't learned much. The lengthy function you posted - with sparse commenting and without an overall explanation - didn't help. – mklement0 Jul 23 '16 at 02:59
  • 10
    I apologize for the snark in my original comment; however, I did mean to provide pointers as to what your answer may be lacking in order to be useful to _others_ (plural; even though they're obviously _my_ pointers): To put it in positive terms: tells us _why_ you don't like the `Write-*` cmdlets, and provide an _explanation_ for what your function does to explain how it compensates for the cmdlets' shortcomings. As for the _we_ perspective in general: that'll emerge over time, reflected in the votes (or lack thereof). – mklement0 Jul 24 '16 at 14:55