powershell で外部プロセスを起動して標準出力・標準エラー出力を得る

## 概要
PowerShell で 外部コマンドを実行して、エラーになったりするので、標準出力・標準エラー出力を取得できるようにします。

## 前提

PowerShell: 5.1

補足)
`$PSVersionTable` コマンドを PowerShellのターミナルで叩くとバージョンが分ります

## 解決方法

こだわりがなければ、解決方法2をおすすめします。

### 解決方法1: コードが長くなっても標準出力/エラー出力をファイルに残さず出す

#### 問題点

* `Start-Process` でコマンドを実行するがウインドウが一瞬で閉じてエラーが分らん  
  → ファイル出力をする方法もあるみたいだが `System.Diagnostics.Process` でやればうまくいきそうに見える
* `System.Diagnostics.Process` で表示がでるようになったが、日本語ファイル名の部分が文字化けしてしまっている  
  → Unicodeでコンパイルしたので utf-8 で合わせる必要があったが…動かん
* `5.1` からエンコーディング指定が変わっているらしい
* stdout と stderr を取得するとコマンドがハングアップする  
  → `ReadToEnd()` ばデッドロックを引き起こすようだ

#### コード例
ちゃんと理解したい人は参照文献を見てもらうとして、おおまかに言うと `ReadToEnd()` をやめて、
非同期の書き込みをしています。
`Register-ObjectEvent` でstdoutやstderrに文字が書き込まれたら教えてねって根回しをしておいて、実際にstdout/stderrに書き込まれたらStringBufferに追記していく。イベントはタイミングを見計らって呼ばれるのでデッドロックとかはない。プロセスの終了で書き込まれなくなるだけ。

```powershell
$PSDefaultParameterValues['Out-File:Encoding'] = 'utf8'  # (1) PSv5.1 以降の記法
$strCmd = "...略..."

function Invoke-ExternalCommand([string]$commandPath, [string]$arguments) {
  try {
    # Creating process object.
    $pinfo = New-Object System.Diagnostics.Process
    # Setting process invocation parameters.
    $pinfo.StartInfo.FileName = $commandPath
    $pinfo.StartInfo.Arguments = $arguments
    $pinfo.StartInfo.UseShellExecute = $false
    $pinfo.StartInfo.CreateNoWindow = $true
    $pinfo.StartInfo.UseShellExecute = $false
    $pinfo.StartInfo.RedirectStandardOutput = $true
    $pinfo.StartInfo.RedirectStandardError = $true

    # ↓↓↓ (3) 非同期書き込み
    # Creating string builders to store stdout and stderr.
    $oStdOutBuilder = New-Object -TypeName System.Text.StringBuilder
    $oStdErrBuilder = New-Object -TypeName System.Text.StringBuilder

    # Adding event handers for stdout and stderr.
    $sScripBlock = {
      if (! [String]::IsNullOrEmpty($EventArgs.Data)) {
        $Event.MessageData.AppendLine($EventArgs.Data)
      }
    }
    $oStdOutEvent = Register-ObjectEvent -InputObject $pinfo `
      -Action $sScripBlock -EventName 'OutputDataReceived' `
      -MessageData $oStdOutBuilder
    $oStdErrEvent = Register-ObjectEvent -InputObject $pinfo `
      -Action $sScripBlock -EventName 'ErrorDataReceived' `
      -MessageData $oStdErrBuilder

    # Starting process.
    [Void]$pinfo.Start()
    $pinfo.BeginOutputReadLine()
    $pinfo.BeginErrorReadLine()
    [Void]$pinfo.WaitForExit()
    # ↑↑↑

    # Unregistering events to retrieve process output.
    Unregister-Event -SourceIdentifier $oStdOutEvent.Name
    Unregister-Event -SourceIdentifier $oStdErrEvent.Name

    $oResult = New-Object -TypeName PSObject -Property ([Ordered]@{
      "ExitCode" = $pinfo.ExitCode;
      "stdout"   = $oStdOutBuilder.ToString().Trim();
      "stderr"   = $oStdErrBuilder.ToString().Trim()
    })
    return $oResult
  } finally {
    $pinfo.Dispose()
  }
}
```

上の関数の実行部分は次のようになる。

```powershell
$strCmd = 'foo.exe'
$arguments = '... 省略 ...'  # (2) 日本語をコマンドに渡す場合は、エンコーディングをちゃんと選ぶ
Write-Host("+ $strCmd $arguments")
$oResult = Invoke-ExternalCommand $strCmd $arguments
Write-Host $oResult.stdout
Write-Host $oResult.stderr -BackgroundColor DarkRed
```

**補足**

* コマンドの出力中の日本語の文字化けをしないようにするには、PowerShell の端末側を Unicode端末として起動する必要があります  
* 上の `Out-File:Encoding` の指定は、Unicodeの引数を受け取るコマンドにShift_JISでなくちゃんと Unicode を渡してやる

### 解決方法2: ファイル出力は容認する

#### 問題点

* 必要のないファイルが作られる (実行の度にハードディスクの寿命が少し短くなる)

#### コード例

```powershell
$PSDefaultParameterValues['Out-File:Encoding'] = 'utf8' # なくても動くが日本語を含む出力が文字化けする
Set-Location "C:\path_of_workdirectory"
$commandPath = 'C:\path_of_command\foo.exe'
$stdErrLogTmp = ".\stderr.log"
$stdOutLogTmp = ".\stdout.log"

$inFileName="スペース入り 日本語ファイル.dat"

Start-Process -FilePath $commandPath `
-ArgumentList "-y","-i","`"$($inFileName)`"","以下略..." `
-NoNewWindow `
-RedirectStandardOutput $stdOutLogTmp -RedirectStandardError $stdErrLogTmp -wait
Get-Content $stdErrLogTmp, $stdOutLogTmp
foreach($strLogPath in @($stdErrLogTmp, $stdOutLogTmp)){
  if (Test-Path $strLogPath) {
    Remove-Item $strLogPath;
  }
}
```

**補足**

* コマンドを並列実行する場合は、ログファイルが被るので `New-TemporaryFile` などで出力先を変えてください
  * 一時ファイルを作る場合は消さないと増え続けるので、このコードのようにファイル削除が重要です

## あとがき

ただ、期待通り動かせたのだけど、どうしてここまで書かせますか。

とはいえ、UNIXのTerminalでStdoutとStderrを同じバッファーに垂れ流すのも、エラーがすぐ確認出来て便利ではあるのですが、出力が混ざったりするので、PowerShellの入出力をオブジェクトとして扱うという考えと相容れない一面もあると思います。

正直、端末制御まわりは進行状況だとか色付けとか、カーソル制御だとか、色んな機能がGUI以上に魔窟ではあります。
魔窟だけあって、色付きの状態でのコピペができんだとか、構造がない分、情報のやり取りに色々と情報が欠落する。
あんまり、`Start-Process` の惨状はこんなレガシーもののサポートは知れば知るほど、やりたくねーな……という気持ちの表れが現状のような気もする。

追記(2020-10-11)

* Windows 10 Version 2004 では今のところ標準では入っていないのですが、 pwsh 7.x 以降で今後改善されるかもしれません
  * 記事に取り込もうとしましたが、PowerShell: 5.1 から環境的にも外れるので、書くとしても別記事かな (それに簡単にできるなら、誰かがもう記事を書いているだろう)

### 履歴

* 2020-10-11
  * 新規作成
  * あとがきと参考を更新、ファイル指定の例を追記

## 参考

### 解決方法1に関連したもの

* エンコーディング指定関連
    * [windows - コマンドプロンプトでのUTF-8エンコーディングの使用(CHCP 65001)/ Windows Powershell(Windows 10)](https://python5.com/q/dmrhnzec) 

        > PowerShellセッション内からchcp 65001を実行しても、.NETは起動時にコンソールの出力エンコーディングをキャッシュし、chcpで行われたその後の変更を認識しないため、効果がないことに注意してください。

   * [PowerShell起動時、文字コードをUTF-8に変える方法 - Qiita](https://qiita.com/s4i/items/75c19c9feb10b54c1ce9)

   * [utf 8 - Changing PowerShell's default output encoding to UTF-8 - Stack Overflow](https://stackoverflow.com/questions/40098771/changing-powershells-default-output-encoding-to-utf-8)  
    PSv5.1の記法について言及している
  * [Powershell上で、パイプで渡すと文字化けする。 - Qiita](https://qiita.com/EmEpsilon/items/7e8f72b9c58576b4c5a5)
  * [各種処理の入出力文字コード設定方法 - Lazy Diary @ Hatena Blog](https://satob.hatenablog.com/entry/20140729/p1)

* [非同期外部プロセス起動で標準出力を受け取る際の注意点 - Qiita](https://qiita.com/skitoy4321/items/10c47eea93e5c6145d48)  
  少し参考にした。
* [events - プロセス出力をPowerShellで非同期に取り込む方法は？](https://stackoverrun.com/ja/q/6679167)  
  大いに参考にした
* [Process.BeginOutputReadLine メソッド (System.Diagnostics) | Microsoft Docs](https://docs.microsoft.com/ja-jp/dotnet/api/system.diagnostics.process.beginoutputreadline?redirectedfrom=MSDN&view=netcore-3.1#System_Diagnostics_Process_BeginOutputReadLine)  
  **注釈** にある内容が、おそらく一次情報。重要。
* MSのドキュメント(一次情報源)
  * [Process.StandardOutput プロパティ (System.Diagnostics) | Microsoft Docs](https://docs.microsoft.com/ja-jp/dotnet/api/system.diagnostics.process.standardoutput?redirectedfrom=MSDN&view=netcore-3.1#System_Diagnostics_Process_StandardOutput)

    > 同期読み取り操作では、ストリームから読み取った呼び出し元 StandardOutput と、そのストリームに書き込む子プロセスとの間に依存関係が生じます。 これらの依存関係によって、デッドロック状態が発生する可能性があります。 呼び出し元が子プロセスのリダイレクトされたストリームから読み取る場合は、子に依存します。 呼び出し元は、読み取り操作で、子がストリームに書き込むか、ストリームを閉じるまで待機します。 子プロセスは、リダイレクトされたストリームを埋めるために十分なデータを書き込むときに、親に依存します。 子プロセスは、親が完全なストリームから読み取るか、ストリームを閉じるまで、次の書き込み操作で待機します。 デッドロック状態は、呼び出し元と子プロセスが操作を完了するために互いに待機し、どちらも続行できない場合に発生します。 呼び出し元と子プロセスの間の依存関係を評価することによって、デッドロックを回避できます。

* トラブル事例
  * [powershellで実行した外部アプリケーションが固まってしまう。](https://social.msdn.microsoft.com/Forums/ja-JP/e810dbca-db94-4794-8a88-bbc326c92dad/powershell1239123455348921237512383228063709612450125031252212465?forum=powershellja)  
    上の MSのドキュメントに言及している
  * [powershell - Start-Processで標準出力とエラーをキャプチャする](https://stackoverrun.com/ja/q/2276010)

### 解決方法2に関連したもの

* [powershell - Redirection of standard and error output appending to the same log file - Stack Overflow](https://stackoverflow.com/questions/8925323/redirection-of-standard-and-error-output-appending-to-the-same-log-file)
* [PowerShell 7.0 の新機能 - PowerShell | Microsoft Docs](https://docs.microsoft.com/ja-jp/powershell/scripting/whats-new/what-s-new-in-powershell-70?view=powershell-7)

### pwsh関連

* [Windows への PowerShell のインストールに関する情報](https://docs.microsoft.com/ja-jp/powershell/scripting/install/installing-powershell-core-on-windows?view=powershell-7)