8
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2020-10-10

概要

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に追記していく。イベントはタイミングを見計らって呼ばれるのでデッドロックとかはない。プロセスの終了で書き込まれなくなるだけ。

$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()
  }
}

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

$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: ファイル出力は容認する

問題点

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

コード例

$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に関連したもの

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

pwsh関連

8
7
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?