2

I have the following files:

test.ps1

& e:\test.bat > stdout.txt 2> stderr.txt

test.bat

@echo off
echo write to stdout
echo write to stderr >&2

When I call test.ps1 like this:

powershell -ExecutionPolicy bypass e:\test.ps1

The output files look like this:

stdout.txt

write argument to stdout

stderr.txt

test.bat : write to stderr 
At E:\test.ps1:5 char:1
+ & "$application" "$argument" > $stdout 2> $stderr
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (write to stderr :String) [], RemoteException
    + FullyQualifiedErrorId : NativeCommandError

There is an answer how to prevent the NativeCommandError output from being written to file when redirecting both stdout and stderr to the same file, but how can I achieve that when writing to different files?

Thomas W
  • 14,757
  • 6
  • 48
  • 67
  • If you can, upgrade to [PowerShell (Core) 7+](https://github.com/PowerShell/PowerShell), which fixes the NativeCommandError issue. – zett42 Sep 03 '22 at 18:17
  • @zett42 Oh, that's good to know! Alas I'm not sure what version of PowerShell runs on the users' PCs that this piece of PowerShell should work on. – Thomas W Sep 03 '22 at 18:22
  • Unfortunately, PS 7+ doesn't come with the OS by default, it has to be installed explicitly. So if they are on Windows 10, they will have PowerShell 5.1 only. – zett42 Sep 03 '22 at 18:24
  • So the expected here would be to `stderr.txt` not be created, since it's a `NativeCommandError` ? If so, why not redirect both outputs to success and then filter? – Santiago Squarzon Sep 03 '22 at 18:25

2 Answers2

3

Complementing mklement0's helpful answer, here is a function Tee-StdErr for PowerShell 5 and older versions that you can chain in-between to redirect stderr to a file without NativeCommandError, while forwarding regular output to the next pipeline command (or redirect it to a file if > is used):

Function Tee-StdErr {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)] [string] $Path,
        [Parameter(Mandatory, ValueFromPipeline)] $InputObject
    )

    begin {
        $file = if( $item = New-Item -Path $Path -Force ) { 
            [IO.StreamWriter]::new( $item.FullName ) 
        }
    }
    process {
        if( $InputObject -is [Management.Automation.ErrorRecord] ) {
            # stringify error message and write to file
            if( $file ) { $file.WriteLine( "$InputObject" ) }   
        }
        else { 
            # pass stdout through
            $InputObject   
        }  
    }
    end {
        $file | ForEach-Object Dispose
    }
}

Usage:

& .\test.bat 2>&1 | Tee-StdErr stderr.txt > stdout.txt

# Alternatively pipe stdout to other commands:
& .\test.bat 2>&1 | Tee-StdErr stderr.txt | Set-Content stdout.txt
  • 2>&1 merges the error (#2) stream with the success (#1) stream, so both can be processed by the next pipeline command
  • Tee-StdErr tests whether the current pipeline object ($InputObject) is an ErrorRecord and if so, it stringifies ("$_") it and writes it to the error file. Otherwise the current object is a string from the success stream which is passed through by using PowerShell's implicit output feature (just naming the variable outputs it).
zett42
  • 25,437
  • 3
  • 35
  • 72
2

To build on the helpful comments:

  • In PowerShell (Core) 7+, your command would work as expected.

  • In Windows PowerShell, unfortunately, stderr lines are implicitly formatted as if they were PowerShell errors when a 2> redirection is involved, resulting in the noisy output you saw.

The solution is to merge stderr into stdout with 2>&1, and separate the collected lines into stdout and stderr by their type, allowing you to stringify the stderr lines, which PowerShell wraps in [System.Management.Automation.ErrorRecord] instances, via their .ToString() method, which results in their text content only.

Apart from being cumbersome, this approach requires collecting all output in memory first.

$stdout, $stderr = (& e:\test.bat 2>&1).Where({ $_ -is [string] }, 'Split')

$stdout > stdout.txt
$stderr.ForEach('ToString') > stderr.txt
mklement0
  • 382,024
  • 64
  • 607
  • 775
  • `$stderr.ForEachObject('ToString') > stderr.txt` gives me the error `Method invocation failed because [System.Management.Automation.ErrorRecord] does not contain a method named 'ForEachObject'`. But based on this, I found a working solution: `foreach ($err in $stderr) { "$err" >> stderr.txt }`. I did not expect that it would write only the actual message without `.ToString()`, but it does. – Thomas W Sep 03 '22 at 19:32
  • Sorry, @ThomasW , that was a typo. Please see my update. – mklement0 Sep 03 '22 at 20:16
  • @ThomasW, enclosing a variable reference in “….” Is essentially the same as calling.ToString() on it. – mklement0 Sep 03 '22 at 20:20
  • There are some differences though, e. g. arrays just print the type when using `.ToString()`, but are joined when doing `"$array"`. Do you know if and where these differences are documented? – zett42 Sep 03 '22 at 21:07
  • 1
    @zett42, yea, there are differences, not only with respect to arrays but also in that implicit stringification, such as in expandable strings and with `[string]` casts uses culture-_invariant_ representations. In short, it is the equivalent of calling the stringification PowerShell performs is the equivalent of calling `.psobject.ToString([NullString]::Value, [cultureinfo]::InvariantCulture)` on a value, from what I can tell. I don't know of any official documentation, but I have summarized my own findings in [this answer](https://stackoverflow.com/a/37603732/45375). – mklement0 Sep 04 '22 at 00:55