Powershell output capturing and text wrapping: strange quirks... solved!

Summary

To capture (transcript) all output of a Powershell script, and control the way textwrapping is done, use the following approach from for exmaple a batch file or your Powershell code:

PowerShell -Command  "`$host.UI.RawUI.BufferSize = new-object System.Management.Automation.Host.Size(512,50); `"c:\temp\testoutputandcapture.ps1`" -argument `"A value`"" >c:\temp\out.txt 2>&1

Note the '`' (backtick characters).

The problem: output capture and text wrap

Powershell is a great language, but I have been fighting and fighting with Powershell on two topics:

  • to capture the output
  • to get it output text in the max width that I want without unexpected wrapping

After reading the manuals you would think that the PowerShell transcripting possibilities are your stairways to heaven. You just start capturing output as follows:

  • start-transcript "c:\temp\transcript.txt"
  • do your thing
  • stop-transcript

Try this with the following sample file c:\temp\testoutputandcapture.ps1:

Start-Transcript "c:\temp\transcript.txt"
function Output
{
    Write-Host "Write-Host"
    Write-Output "Write-Output"
    cmd.exe /c "echo Long long long long long long long long yes very long output from external command"
    PowerShell -Command "Write-Error 'error string'"
}
Output
Stop-Transcript

When you execute this script you get the following output:

PS C:\temp> C:\temp\testoutputandcapture.ps1
Transcript started, output file is c:\temp\transcript.txt
Write-Host
Write-Output
Long long long long long long long long yes very long output from external command
Write-Error 'error string' : error string Transcript stopped, output file is C:\temp\transcript.txt

If we now look at the generated transcript file c:\temp\transcript.txt:

**********************
Windows PowerShell Transcript Start
Start time: 20081209001108
Username  : VS-D-SVDOMOSS-1\Administrator 
Machine	  : VS-D-SVDOMOSS-1 (Microsoft Windows NT 5.2.3790 Service Pack 2) 
**********************
Transcript started, output file is c:\temp\transcript.txt
Write-Host
Write-Output
**********************
Windows PowerShell Transcript End
End time: 20081209001108
**********************

The output from the external command (the texts Long long long long long long long long yes very long output from external command and Write-Error 'error string' : error string)  is not captured!!!

Step one: piping output of external commands

This can be solved by appending | Write-Output to external commands:

Start-Transcript 'c:\temp\transcript.txt'
function Output
{
    Write-Host "Write-Host"
    Write-Output "Write-Output"
    cmd.exe /c "echo Long long long long long long long long yes very long output from external command" | Write-Output
    PowerShell -Command "Write-Error 'error string'" | Write-Output
}
Output
Stop-Transcript

This will result in the following output:

PS C:\temp> C:\temp\testoutputandcapture.ps1
Transcript started, output file is c:\temp\transcript.txt
Write-Host
Write-Output
Long long long long long long long long yes very long output from external command
Write-Error 'error string' : error string Transcript stopped, output file is C:\temp\transcript.txt

Note that the error string Write-Error 'error string' : error string is not in red anymore.

The resulting transcript file c:\temp\transcript.txt now looks like:

**********************
Windows PowerShell Transcript Start
Start time: 20081209220137
Username  : VS-D-SVDOMOSS-1\Administrator 
Machine	  : VS-D-SVDOMOSS-1 (Microsoft Windows NT 5.2.3790 Service Pack 2) 
**********************
Transcript started, output file is c:\temp\transcript.txt
Write-Host
Write-Output
Long long long long long long long long yes very long output from external command
Write-Error 'error string' : error string ********************** Windows PowerShell Transcript End End time: 20081209220139 **********************

This is what we want in the transcript file, everything is captured, but we need to put | Write-Output after every external command. This is way to cumbersome if you have large scripts. For example in our situation there is a BuildAndPackageAll.ps1 script that includes a lot of files and cosists in totla of thousands of lines of Powershell code. There must be a better way...

Transcripting sucks, redirection?

Ok, so transcript just does not do the job of capturing all output. Lets look at another method: good old redirection.

We go back to our initial version of the script, and do c:\temp\testoutputandcapture.ps1 > c:\temp\out.txt. This results in a c:\temp\out.txt file with the following contents:

Transcript started, output file is c:\temp\transcript.txt
Write-Output
Long long long long long long long long yes very long output from external command
Write-Error 'error string' : error string Transcript stopped, output file is C:\temp\transcript.txt

We are missing the Write-Host output! Actually, the Write-Host is the only line ending up in the transcript file;-)

This is not good enough, so another try, but now using the Powershell command-line host on the little modified script c:\temp\testoutputandcapture.ps1 that will showcase our next problem:

function Output
{
    Write-Host "Write-Host"
    Write-Output "Write-Output Long long long long long long long long yes very long output from Write-Output"
    cmd.exe /c "echo Long long long long long long long long yes very long output from external command"
    PowerShell -Command "Write-Error 'error string'"
}
Output

We now do: Powershell -Command "c:\temp\testoutputandcapture.ps1" > c:\temp\out.txt 2>&1 (2>&1 means: redirect stderr to stdout). We capture stderr as well, you never know where it is good for. This results in:

Write-Host
Write-Output Long long long long long long long l
ong yes very long output from Write-Output
Long long long long long long long long yes very long output from external command
Write-Error 'error string' : error string

I hate automatic text wrapping

As you may notice some strange things happen here:

  • A long output line generated by PowerShell code is wrapped (at 50 characters in the above case)
  • A long line generated by an external command, called from Powershell is not truncated

The reason the Powershell output is wrapped at 50 characters is because the dos box I started the command from is set to 50 characters wide. I did this on purpose to proof a case. Ok, so you can set your console window really wide, so no truncation is done? True. But you are not always in control of that. For example if you call your Powershell script from a batch file which is executed through Windows Explorer or through an external application like in my case CruiseControl.Net where I may provide an external command.

Powershell -PSConfigFile?... no way!

I hoped the solution would be in an extra parameter to the Powershell command-line host: -PSConfigFile. You can specify a Powershell console configuration XML file here that you can generate with the command Export-Console ConsoleConfiguration. This results in the ConsoleConfiguration.psc1 XML file:

<?xml version="1.0" encoding="utf-8"?>
<PSConsoleFile ConsoleSchemaVersion="1.0">
  <PSVersion>1.0</PSVersion>
  <PSSnapIns />
</PSConsoleFile>

I searched and searched for documentation on extra configuration  like console width or something, but documentation on this is really sparse. So I fired up good old Reflector. After some drilling I ended up with the following code for the Export-Console command:

internal static void WriteToFile(MshConsoleInfo consoleInfo, string path)
{
    using (tracer.TraceMethod())
    {
        _mshsnapinTracer.WriteLine("Saving console info to file {0}.", new object[] { path });        
XmlWriterSettings settings = new XmlWriterSettings(); settings.Indent = true; settings.Encoding = Encoding.UTF8; using (XmlWriter writer = XmlWriter.Create(path, settings)) { writer.WriteStartDocument(); writer.WriteStartElement("PSConsoleFile"); writer.WriteAttributeString("ConsoleSchemaVersion", "1.0"); writer.WriteStartElement("PSVersion"); writer.WriteString(consoleInfo.PSVersion.ToString()); writer.WriteEndElement(); writer.WriteStartElement("PSSnapIns"); foreach (PSSnapInInfo info in consoleInfo.ExternalPSSnapIns) { writer.WriteStartElement("PSSnapIn"); writer.WriteAttributeString("Name", info.Name); writer.WriteEndElement(); } writer.WriteEndElement(); writer.WriteEndElement(); writer.WriteEndDocument(); writer.Close(); } _mshsnapinTracer.WriteLine("Saving console info succeeded.", new object[0]); } }

So the XML that we already saw is all there is... a missed chance there.

A custom command-line PowerShell host?

I have written a few Powershell hosts already, for example one hosted into Visual Studio, and there I encountered the same wrapping issue which I solved by the following implementation of the PSHostRawUserInterface:

using System;
using System.Management.Automation.Host;

namespace Macaw.SolutionsFactory.DotNet3.IntegratedUI.Business.Components.Powershell
{
    /// <summary>
    /// Implementation of PSHostRawUserInterface.
    /// </summary>
    public class PowershellHostRawUI : PSHostRawUserInterface
    {
        :
        public override Size BufferSize
        {
            get
            {   
                // The width is the width of the output, make it very wide!
                return new Size(512,1);
            }
            set
            {
                throw new Exception("BufferSize - set: The method or operation is not implemented.");
            }
        }
        :
    }
}

I solved transcripting in my own Powershell hosts by capturing all text output done by Powershell through custom implemented functions for the PSHostUserInterface you need to implement anyway, works like a breeze.

The solution, without custom host...

I was just getting started to solve the transcripting and wrapping issues by implementing again a Powershell host, but now a command-line Powershell host when I did one last digging step. What is you can set the BufferSize at runtime in your Powershell code. And the answer is.... yes you can! Through the $host Powershell variable: $host.UI.RawUI.BufferSize = new-object System.Management.Automation.Host.Size(512,50). This says that the output may be max 512 chars wide, the height may not be 1 (as in my code), bu this height value is not important in this case. It is used for paging in a interactive shell.

And this last statement gives us full control and solves all our transcripting and wrapping issues:

PowerShell -Command  "`$host.UI.RawUI.BufferSize = new-object System.Management.Automation.Host.Size(512,50); `"c:\temp\testoutputandcapture.ps1`" -argument `"A value`"" >c:\temp\out.txt 2>&1

And this concludes a long journey into the transcripting and wrapping caves of Powershell. I hope this will save you the days of searching we had to put into it to tame the great but naughty Powershell beast.

14 Comments

  • Another update:

    Instead of redirection it is also possible to make output directly visible AND transcript to file using Tee-Object. Works only from a Powershell script, so extra wrapping is needed:

    powershell -command "for (`$i=0; `$i -lt 20; `$i++) { Write-Host `$i; start-sleep -s 1 }" | Tee-
    Object -FilePath c:\temp\out.txt

  • Congrats on this lightweight solution Serge, this was a tough nut to crack!

  • BIG regards!!! Ypu are the GENIUS! :)
    Огромное спасибо :)

  • Is there any way to use this same logic for the Exchange Powershell console? I am having wrapping issues when executing a script via asp code, but it works fine when called directly.


    ERROR: Get-MailboxStatistics : The specified mailbox database "NXPRDVSMBX16\Storage Gr
    ERROR: oup 3\Priv03" does not exist.
    ERROR: At line:1 char:22
    ERROR: + get-mailboxstatistics <<<< jtye |out-file e:\web\intake\scripts\mailsize.log

    Thanks! James

  • What is -argument `"A value`"" in your command?

  • I've been working on this for a few days now and found your post. I had problems when passing command arguments to an external exe and the here-string wrappend and messed up passing the parramenters to the external command in the script. Using $host.UI.RawUI.BufferSize = new-object System.Management.Automation.Host.Size(3000,50) in my case solved the problem.

    Thanks a lot.
    Cosmin

  • Already spent a lot of time hunting before I found your post, but yours was the first effective fix I found. Our use case was slightly different (an Invoke-Command with remoting), but the problem was the same. Thanks for sharing the fruits of your labor...

  • I, like one of the other responders, am using Exchange Powershell. I'm attempting to extend the buffer for the Exchange Powershell shortcut so that I can display the AddressSpaces under Get-SendConnector for a client. The problem is that the field has about 150 entries - no use of Format-anything works.

    How would I modify the Exchange Powershell shortcut command to utilize the increased buffer you've defined here? The shortcut command string looks like this:

    C:\WINDOWS\system32\windowspowershell\v1.0\powershell.exe -PSConsoleFile "C:\Program Files\Microsoft\Exchange Server\bin\exshell.psc1" -noexit -command ". 'C:\Program Files\Microsoft\Exchange Server\bin\Exchange.ps1'"

    I've attempted several variations increasing the buffers, but came short of success each time. It would be VERY useful for me if I could display this.

    Conversely, if I could run something from an existing Exchange Powershell, that would also be fine. An example of what I'd run in the "A value" section would look like this:

    Get-SendConnector -Identity "smtp connector name" | ft AddressSpaces

    The output stops after only a few lines with an ellipses, no matter what I try.

    If it's not a terrible trouble, could you email me at john_at_dumb_dot_org? If not, I'll just check back here soon.

    Thanks a ton.

    j.k

  • Hi, Start-TransScript is not a good choice because it does not seem to work with team build.
    Please provide solution for this if u can.

  • This didn't work for me the first time I tried it and it took me a while to figure why :-)

    It isn't obvious from the post but doing this actually changes the buffer size of your console window (e.g. cmd.exe). It also seems that the minimum values for the with & height arguments to Size() are the physical dimensions of the console window (Properties | Layout | Window Size).

    For example my default console window is 128x90 characters (with 10pt Lucida Console). This means that anything less than Size(128, 90) will cause the following error:-

    Exception setting "BufferSize": "Cannot set the buffer size because the size specified is too large or too small.

    What this means is that the script could fail if the console window is already larger than the size you pick (height being to likely candidate). Hence if you're running the script as, say, a scheduled task don't forget to add some error handling in place as this could cause the script to fail.

  • Definitely works...

  • Great line wrap solution! It should be noted though that write-warning will not output to the redirection (but does in start-transcript).

  • Thanks for this, was very annoying get output truncated when run from batch scripts.

    You do not need to run the $host.UI.RawUI.BufferSize = new-object System.Management.Automation.Host.Size(512,50) line from the command that launches the script either.

    If you put that line into the start of your script the output will come out correctly and you can launch the script using whatever method you prefer.

    I find using Size(512,80) is a much safer setting to be within the minimum size constraints as John K. mentioned above.

  • This website is great. I like it.(www.linkspirit.net)N_X_D_S.

Comments have been disabled for this content.