◆ リダイレクトせず画面上に stderr が表示されるときは stderr に送られた文字列だけ
◆ リダイレクトすると書き込んだファイルに実行したコマンドやスタックトレースが含まれてる
◆ リダイレクトは捨てて過去のエラーを保持する $Error から文字列を作ってファイルに書き込み

標準出力と標準エラー出力をするプログラムを用意します
ここでは Node.js を使います

[write.js]
console.log("a")
console.error("b")

PowerShell の ps1 ファイルでこのファイルを実行します

[script.ps1]
node write.js
PS C:\tmp\031> & .\script.ps1
a
b

これだと普通に stdout/stderr が表示できてます
結果をそれぞれファイルに出力したいのでリダイレクトをつけてみます

[script.ps1]
node write.js > out1.txt 2> out2.txt
PS C:\tmp\031> & .\script.ps1
PS C:\tmp\031> cat .\out1.txt
a
PS C:\tmp\031> cat .\out2.txt
node : b
発生場所 C:\tmp\031\script.ps1:1 文字:1
+ node write.js > out1.txt 2> out2.txt
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (b:String) [], RemoteException
+ FullyQualifiedErrorId : NativeCommandError

stdout の方は想定どおりの結果なのですが stderr の方は 「b」 以外にコマンドやスタックトレースなどの余計な例外表示が入ってます
なんでこんな余計なものが入るのでしょうか……

ググってみてもこれだけを消す方法が見当たらず stderr 表示を抑制するという方法を使うと肝心の 「b」 まで出力されなくなりました
もしかするとそういう機能が用意されてないのでしょうか

もう少し調べるとスタックトレースなどが出るのは ErrorRecord 型をオブジェクトとして出力してるからみたいな情報がありました
そこから文字列だけ取り出して出力すれば良いみたいですが どうやって取り出すんでしょう?
探すと $Error 変数に過去のエラーが保持されてるようです
$Error は配列形式で index が 0 のものが最も新しいエラーで配列の後ろになるほど古いエラーでした

node write.js > out1.txt 2> $null
$Error[0].ToString() > out2.txt

これを実行するようにしたら 期待通り out2.txt に 「b」 という出力になりました
本来の stderr は捨てて 最後のエラーの文字列を out2.txt に書き込んでます

ただこの方法 stderr に何も出力されないと $Error にエラーが追加されないので 1 つ前のエラーを出力してしまいます
直前に実行したコマンドにエラーがあり $Error に入ってるかという情報を取得するのは難しそうなので $null に捨てずに一時ファイルに書き込み 中身があるかで判断するようにしました

$tmp = "tmp00"
node write.js > out1.txt 2> $tmp
if (cat $tmp) {
$Error[0].ToString() > out2.txt
}
rm $tmp

これでうまく動きました
$tmp を別ファイルにせず stderr を書き込みたい out2.txt という名前で使い回せばもう少し短くなります
ですが 追記にしたい場合を考えると別ファイルにしないといけないので別名で一時ファイルを作ってます

これで一応完成 と思ってたのですが……別のパターンも試すと問題がありました
$Error[0] には最後の行しか入っていませんでした
1 コマンドの stderr の文字列すべてが 1 つの $Error の要素になると思っていたのですが 行ごとに 1 つの $Error 要素のようです
Node.js 側で console.error の呼び出し回数は関係なく stderr に出力された行です
そうなると $Error のどこからが今回の結果なのかわかりません
事前に $Error[0] の保持しておいて その ErrorRecord が来るまでを取り出す処理が必要です
こうなると $tmp はいらなくなります

$last = $Error[0]
node write.js > out1.txt 2> $null
if ($last -ne $Error[0]) {
$lines = @()
foreach ($err in $Error) {
if ($err -eq $last) {
break
}
$lines += $err.ToString()
}
[array]::Reverse($lines)
$lines -join "`n" > out2.txt
}

[write.js]
console.log("o1")
console.error("e1")
console.log("o2")
console.error("e2")
console.error("e3-1\ne3-2")
console.log("o3-1\no3-2")
PS C:\tmp\031> & .\script.ps1
PS C:\tmp\031> cat .\out1.txt
o1
o2
o3-1
o3-2
PS C:\tmp\031> cat .\out2.txt
e1
e2
e3-1
e3-2

とりあえず 期待通りに動くようになりました

$Error の最大要素数はデフォルトは 256 です
つまり 256 行分しか保存できません
$MaximumErrorCount 変数で最大で 32768 まで増やせるので 増やしておけば大抵のものはすべて保持できると思います
常時起動したままのプログラムだとこの制限も超えるかもしれませんが 今回の取得方法はプログラムが終了してからファイル出力というやり方なのでプログラムを起動したままログファイルからエラーを確認はできず そのタイプのプログラムとは相性が良くないです

それにしてもなぜこれだけのことをするのに こんな面倒なことが必要なんでしょうか
もっと良い方法があるとしか思えないですしあってほしいものです