Understanding Streams, Redirection, and Write-Host in PowerShell

Summary: June Blender explains how to understand and use streams in Windows PowerShell.

Microsoft Scripting Guy, Ed Wilson, is here. Today guest blogger, June Blender, explains how to understand and use streams in Windows PowerShell. To read more from June, see these Hey, Scripting Guy! Blog posts.

Note  Since the writing of this post, some of the information has changed with the introduction of the information stream in Windows PowerShell 5.0. Read about it here: Welcome to the Powershell Information Stream.

And now, here's June…

My first introduction to streams in computing was a really positive one. A friend emailed me a short video of my son and his friends helping clear debris from a nearby stream after a flood. I clicked the attachment in the email, and I got the most insightful error I've ever seen:

Error: Cannot render the stream

"Wow," I thought. "That is a really sophisticated error message. I wonder if it can render the trees or just my adorable son."

It turns out that the error referred to a video stream, not the watery one near the school. But that experience prepared me to learn about streams in Windows PowerShell.

Windows PowerShell has multiple message streams. The engine sends different types of messages down each stream. As shown in the following image, the streams are parallel, so they don't interfere with each other.  

Streams are critical in a language that uses piping. When you pipe objects from one cmdlet to another, you don't want the receiving cmdlet to receive errors, warnings, debugging messages, or verbose messages along with the objects that it's designed to process.

So, the pipe operator (|) actually pipes objects down the output stream (stream #1). The receiving cmdlet doesn't see errors, warnings, verbose messages, or debugging messages, because they're sent in other streams.

Windows PowerShell also uses the output stream for assignments, like assigning (or saving) a value in a variable. This explains why you can save an output message in a variable, for example:

PS C:\> $message = Write-Output -Message "Output message"

PS C:\> $message

Output message

 But you can't save a verbose message in a variable:

PS C:\> $message = Write-Verbose -Message "Verbose message" -Verbose

VERBOSE: Verbose message

PS C:\> $message

PS C:\>

To save a verbose message in a variable, you need to redirect the message from the verbose stream (stream #4) to the output stream (stream #1). To do this, you use the "from-4-to-1" (4>&1) redirection operator, as shown here:

PS C:\ps-test> $message = Write-Verbose -Message "Verbose message" -Verbose 4>&1

PS C:\ps-test> $message

VERBOSE: Verbose message

If you haven't yet memorized the redirection operators, you can find them in the about_Redirection Help topic.

I was reminded of this lesson when discussing the infamous Write-Host cmdlet with some colleagues. We all know that you're not supposed to use Write-Host. Windows PowerShell MVP, Don Jones, says that Write-Host kills puppies. Windows PowerShell inventor, Jeffrey Snover, says that Write-Host is harmful. But they both agree that it's fine to use it under very limited conditions.

One of those conditions is when you want to talk to your user, but you don't want the output to interfere with or "pollute" your object stream. If you use the Write-Output cmdlet, the output messages are sent down the output stream (stream #1) along with the objects that you're piping to the next cmdlet. The receiving cmdlet better know how to handle the message strings and how to distinguish them from other strings that you pipe to it.

Write-Host does not pollute the output stream. But which stream does Write-Host use?

To find out, I wrote a little function that writes a host message and a message to each stream:

function Write-Messages

Write-Host "Host message"

Write-Output "Output message"

Write-Verbose "Verbose message"

Write-Warning "Warning message"

Write-Error "Error message"

Write-Debug "Debug message"


When I used the standard redirection operator to write the messages to a file, all of the messages were display on the console, except for the output message, which was written to the output file.

PS C:\> Write-Messages > OutputFile.txt

Host message

Write-Messages : Error message

At line:1 char:1

+ Write-Messages > OutputFile.txt

+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

+ CategoryInfo          : NotSpecified: (:) [Write-Error], WriteErrorException

+ FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,Write-Messages


WARNING: Warning message

VERBOSE: Verbose message

DEBUG: Debug message


PS C:\> Get-Content OutputFile.txt

Output message

Then, I ran the same function, but I used the redirection operation that directs all streams to a file (*>). This time, only the host message appeared on the console. All of the other messages (those in message streams) were written to the output file.

PS C:\ps-test> Write-Messages *> .\OutputFile.txt

Host message


PS C:\ps-test> Get-Content OutputFile.txt

Output message

Write-Messages : Error message

At line:1 char:1

+ Write-Messages *> .\OutputFile.txt

+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    + CategoryInfo          : NotSpecified: (:) [Write-Error], WriteErrorException

    + FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,Write-Messages


Warning message

Verbose message

Debug message

This happens because Write-Host is not written to any stream. It's sent to the host program, and the host program decides what to do with it. The Windows PowerShell console and Windows PowerShell ISE display host messages on the console. But other host programs might do something entirely different. The behavior can vary with the program that is hosting Windows PowerShell.

You cannot redirect a Write-Host message or assign it to a variable, even if you're very clever with redirection operators.

So, how do you talk to the user in a way that doesn't pollute the output stream but can be run in background without interaction?

The best way to talk to the user is by using Write-Verbose. Verbose messages go to the verbose stream (stream #4), but the user can redirect them. Of course, users see verbose messages only if they run your script with the Verbose common parameter or when they change VerbosePreference to Continue. But that's a much better alternative than writing to the output stream (which can mess up your piping), and it is better than writing messages to the host program (which can't be suppressed or redirected).

My first impression of streams was that they're very useful and sophisticated. That's my current impression, too!


function Write-Messages




    Write-Host "Host message"

    Write-Output "Output message"

    Write-Verbose "Verbose message"

    Write-Warning "Warning message"

    Write-Error "Error message"

    Write-Debug "Debug message"



# Writes all messages to console.

Write-Messages -Verbose -Debug

# Writes output to the file

# Writes all other messages to console.

Write-Messages -Verbose -Debug > .\OutputFile.txt

# Writes all output except Host messages to file

Write-Messages -Verbose -Debug *> .\OutputFile.txt

# Writes all output (except Host messages) to output stream, then saves them in a file.

Write-Messages -Verbose -Debug *>&1 | Out-File -FilePath .\OutputFile.txt

# Writes all messages to console and then saves all but host messages in a file.

Write-Messages -Verbose -Debug *>&1 | Tee-Object -FilePath .\OutputFile.txt


Thanks, June!

I invite you to follow me on Twitter and Facebook. If you have any questions, send email to me at scripter@microsoft.com, or post your questions on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.

Ed Wilson, Microsoft Scripting Guy

Comments (11)

  1. Anonymous says:

    Hi June,

    I didn’t read this post on the day you posted it but I still would like to say it is a great article.
    It gave me a better understanding of the PowerShell internals.

    Thanks a lot.

  2. June Blender says:

    Chris H: I know this isn’t helpful, but I can’t reproduce the error. When I copy your command from this post, I get the expected result.

    The advanced redirection operators were added in PowerShell 3.0. Are you using 2.0?

    PS C:ps-test> $message = Write-Verbose -Message "Verbose message" -Verbose
    VERBOSE: Verbose message
    PS C:ps-test> $message
    PS C:ps-test>
    PS C:ps-test> $message = Write-Verbose -Message "Verbose message" -Verbose 4>&1
    PS C:ps-test> $message
    VERBOSE: Verbose message

  3. Berbigier says:

    To avoid output stream pollution, I use
    Write-Host $string | Out-Host
    This permits to send messages to the user while keeping the output stream clean

  4. Keith says:

    If I wanted to hide multiple messages (Host, Verbose, Warning, Error, Debug) from the console except the successful output, how would I do that?

  5. June Blender says:

    Thanks for the nice comments, gaff-jakobs.

    Keith, that’s a great question. You can’t block Write-Host messages, because they don’t go to any stream. Debug and verbose messages are off by default. To silence errors and warning messages, use the following command:

    dir variable:*preference | where {$_.Value -eq "Continue"} | Set-Variable -Value "SilentlyContinue"

    BTW, this is a "you can," not a "you should." I never recommend silencing error messages. Let me know if you have any other questions.

  6. June Blender says:

    Again, I would avoid silencing the error stream unless you have a very good reason to do so. Instead of changing those preferences, consider changing them for one script. To do that, use the ErrorAction and WarningAction common parameters of every script.

  7. Chris H says:

    When running this example in PowerShell:
    $message = Write-Verbose -Message "Verbose message" -Verbose 4>&1

    I get the following error message:
    Ampersand not allowed. The & operator is reserved for future use; use "&" to pass ampersand as a string.

    Not sure what I’m missing.

  8. BrianS says:

    Nope, I tired Write-Verbose and I could NOT redirect either using > or >> or piping to a file using Out-File The only one I could redirect is when I used Write-Output but it screws up my formatting!!! Little disappointing powershell is not all that powerful,
    times like this it looks like WeakShell

  9. CodeMaster Bob says:

    Here’s a tricky one (in several parts). (Colors indicated by , repeated error messages abbreviated to … .)

    PS l:> 5/0

    Attempted to divide by zero.
    At line:1 char:1
    + 5/0
    + ~~~
    + CategoryInfo : NotSpecified: (:) [], RuntimeException
    + FullyQualifiedErrorId : RuntimeException

    As expected. But:

    PS l:> 5/0 2>file

    Attempted to divide by zero.
    At line:1 char:1

    and file is empty. However:

    PS l:> &{ 5/0 } 2> file
    PS l:> type file
    Attempted to divide by zero.
    At line:1 char:4
    + &{ 5/0 } 2> file

    I suspect this is to do with Terminating Errors (which throw exceptions) vs. Non-Terminating Errors (which use Write-Error). In the first two cases, 5/0 only throws an exception and there is nothing in the error stream. The Powershell Host catches the exception
    and displays the error. However, in the last case, it is the Call Operator (&) which catches the exception and changes the error from Terminating to Non-Terminating by using Write-Error.

    Now we enter "The Twilight Zone". (Rod Serling voice) Consider the following:
    (note carefully, this is a 3 element array, 2 ints and a float)

    PS l:> 1,2,3.4 | foreach { "{0:X}" -f $_ }

    The following exception occurred while retrieving the string: "Format specifier was invalid."
    At line:1 char:21
    + 1,2,3.4 | foreach { "{0:X}" -f $_ }
    + ~~~~~~~~~~~~~
    + CategoryInfo : NotSpecified: (:) [], ExtendedTypeSystemException
    + FullyQualifiedErrorId : ToStringPSObjectBasicException

    Obviously, you can’t format a [Double] as a hexadecimal integer. But this problem manifested in the display of a much larger array with calculated members which were all [Uint32] but were actually being stored in [Object]. Due to a fully predictable overflow
    (when the value wouldn’t fit in the default integer object [Int32]), the above error occurred several times during the (long) output. Trying to narrow down the fault, I tried (the equivalent of):

    PS l:> 1,2,3.4 | foreach { "{0:X}" -f $_ } | more.com # use more.com for direct access to native command. function:more gives same behavior.

    The following exception occurred while retrieving the string: "Format specifier was invalid."
    At line:1 char:21
    + 1,2,3.4 | foreach { "{0:X}" -f $_ }


    Oops, the error stream is output first, so there is no longer any connection to the point of error. Try this:

    PS l:> 1,2,3.4 | foreach { "{0:X}" -f $_ } 2>&1 | more.com

    PS l:>

    (Cue Peter, Paul and Mary or Pete Seeger) Where have all the errors gone?

    Apparently into a black hole (or another dimension G#,A,G#,E,G#,A,G#,E see lead-in comment). Does this mean that you can’t convert error messages into text for piping to native commands? What about these?

    PS l:> &{5/0} 2>&1

    Attempted to divide by zero.
    At line:1 char:3
    + &{5/0} 2>&1

    PS l:> &{5/0} 2>&1 | more.com

    Attempted to divide by zero.
    At line:1 char:3
    + &{5/0} 2>&1 | more


  10. CodeMaster Bob says:

    Thus, error objects sent into the success stream are still error objects (c.f. "&{5/0} 2>&1 | gm" vs "&{5/0} | gm") but

    they can become text for native commands. Is the problem in Foreach-Object? A similar unexpected(?) behavior occurs in:

    PS l:> 1,2,3.4 | select @{expression={ "{0:X}" -f $_ }}

    "{0:X}" -f $_

    PS l:>

    Now there are no error messages even without redirection!

    Maybe it will all be clearly explained in the ANSI Powershell specification. (IEEE? ECMA?)

    @BrianS: Assuming that your version of Powershell accepts more than just > and 2> (and that you were actually using 4> to redirect the verbose stream), remember that Powershell is an "object" passing shell. Thus,

    Write-Verbose "some text" -verbose

    puts a verbose message object (containing "some text") into the verbose stream. (The use of -verbose is to override the normal setting of $VerbosePreference, vis. SilentlyContinue. The effect of which is the same as 4>$null, i.e discard.) When the verbose message
    object reaches the end of the pipeline it is displayed AS a verbose message irrespective of which stream it was in. Conversion to plain text only occurs when an object is passed to a native command. As in:

    $(Write-Verbose -verbose "verbose"; Write-Output "message") 4>&1 | more.com


    $(Write-Verbose -verbose "verbose" 4>&1; Write-Output "message") | more.com

    Note: subexpression used because | has higher precedence than ;

    A further comment that was prompted by another author’s blog post concerns the effect of redirection at a particular point in the pipeline. For example:

    Get-Volume h | Get-Partition 2>$null | Get-Disk

    Get-Volume : No MSFT_Volume objects found with property ‘DriveLetter’ equal to ‘h’. Verify the value of the property and retry.
    At line:1 char:1
    + Get-Volume h | Get-Partition 2>$null | Get-Disk
    + ~~~~~~~~~~~~
    + CategoryInfo : ObjectNotFound: (h:Char) [Get-Volume], CimJobException
    + FullyQualifiedErrorId : CmdletizationQuery_NotFound_DriveLetter,Get-Volume

    Commands can add message objects to a particular stream but cannot affect the objects already in the stream. Thus, the above command will only discard errors generated by Get-Partition.

    (continues again)

  11. CodeMaster Bob says:

    (continued again)

    Finally, the various streams appear to be serially synchronised. This means that it doesn’t matter how the objects get to the end of the pipeline, they still arrive in the same order as they were generated. Thus, there is no difference between:

    $(Write-Output "message 1"; Write-Verbose -verbose "verbose"; Write-Output "message 2") 4>&1 | more.com
    # Sub-expression has 2 objects in success stream and 1 in verbose stream before redirection


    $(Write-Output "message 1"; Write-Verbose -verbose "verbose" 4>&1; Write-Output "message 2") | more.com
    # Sub-expression has 3 objects in success stream

    Same output from both:
    message 1
    message 2

    I am guessing that there are not 5 streams but only one and that the objects are simply tagged as belonging to a particular stream so that they can be handled by the appropriate routine at the end of the pipeline, somewhat analogous to the port number on an
    IP packet. Thus, there are 5 serially synchronised virtual streams within the actual pipeline stream. This also explains why the error messages in my erroneous pipeline became separated from the text. Even though the original text/errors were generated in
    sequence, when the success stream was sent to more.com its objects were removed from the pipeline, converted to text and piped into more.com. The output from more.com was then re-inserted at the back of the pipeline, i.e. after the error messages, and then
    piped into Out-Default. Thus, my earlier statement that "the error stream is output first" did not mean that it had some sort of preferential treatment but rather that the interposition of more.com had the effect of sorting the success stream objects to the
    tail of the pipeline.