【PowerShell】他プロセスの実行方法まとめ

2018/11/22 追記

以下の修正を行いました。

  • ちゃんとシンタックスハイライトを適用しました。
  • 呼び出し元のプロセスに標準 / エラー出力をリダイレクトする方法を追記しました。
  • 一部のリンクが 404 を返すようになったので削除しました。

前書き

何だかんだで最近 PowerShell を書く機会が増えまくっています。まぁ中身はほとんど C#なんですが。

で、全部 C#で書いてもいいんだけど、外部ツール使えば一瞬みたいなものもあるので、そう言うものはなるべくそっちでやってしまいたいです。特に zip 関連の処理。

シェルってぐらいなんだからそれぐらい簡単にできるでしょー?と思っていたら大分ハマったので色々とメモしておきます。

実行例

引数で受け取ったフォルダのサブフォルダ内にある zip ファイルをC:\Program Files\7-Zip\7z.exeで解凍する、みたいなパターン。7z.exe にパスは通していない状態。

コマンドはxコマンドを使います。

大体こんなコードになります。Get-ChildItem?何のことかよくわからないですね…。

$nIO = "System.IO"
$cDirectory = "$nIO.Directory" -as [type]

foreach($root in $cDirectory::GetDirectories($args[0])) {
    foreach($7zFile in $cDirectory::GetFiles($root,"*7z")) {
        #ここで7zipを使って解凍したい
    }
}

普通に実行する

何をもって普通と呼ぶのかですが、こんな風にすりゃ呼べるんじゃねーの?と普通は思います。

$nIO = "System.IO"
$cDirectory = "$nIO.Directory" -as [type]

$7zexe = "C:\Program Files\7-Zip\7z.exe"

foreach($root in $cDirectory::GetDirectories($args[0])) {
    foreach($7zFile in $cDirectory::GetFiles($root, "*7z")) {
        # ここで7zipを使って解凍したい
        $7zexe x -o$root $7zFile
    }
}

$root$7zFileにスペースが入っていたりするとやっぱりダメなので、安全性を鑑みてこのようにするのがベストでしょう。

$nIO = "System.IO"
$cDirectory = "$nIO.Directory" -as [type]

$7zexe = "C:\Program Files\7-Zip\7z.exe"

foreach($root in $cDirectory::GetDirectories($args[0])) {
    foreach($7zFile in $cDirectory::GetFiles($root,"*7z")) {
        #ここで7zipを使って解凍したい
        & $7zexe x -o"$root" "$7zFile"
    }
}

ちなみにこの&Invoke-Expressionエイリアスです。まぁわざわざInvoke-Expressionなんて長ったらしいコマンドを書く必要は皆無なので tips として覚えておきましょう。

また、似たようなコマンドレットにInvoke-Itemがありますが、使える範囲が非常に狭いので覚えなくていいです。(既定のプログラムで txt 開きたいとかのケースだと使えるけど…。)

また、$?を使うことで直前のコマンドが成功したかどうかを boolean で取得できるので、こんなこともできます。

$nIO = "System.IO"
$cDirectory = "$nIO.Directory" -as [type]

$7zexe = "C:\Program Files\7-Zip\7z.exe"

foreach($root in $cDirectory::GetDirectories($args[0])) {
    foreach($7zFile in $cDirectory::GetFiles($root,"*7z")) {
        #ここで7zipを使って解凍したい
        & $7zexe x -o"$root" "$7zFile"

        if($?) {
            #解凍後にzipを削除する
            [System.IO.File]::Delete($7zFile)
        }
    }
}

Start-Processを使う

他の方法としてはStart-Processコマンドレットを使用する必要があります。 System.Diagnostics.Process::Startみたいなもんだと思えば OK です。

Start-Processのパラメータ

色々と引数がありますが、頻繁に使うのはこのあたりでしょうか。

パラメータ 用途
-FilePath 実行ファイルのパス。パス内にスペースが入っているとキレられるので、ダブルクオーテーションでくくってやる必要がある。
-ArgumentList 実行ファイルへ渡す引数。一応String[]を受け付けるが、普通に一個のStringを渡す方が安全。
-NoNewWindow プロセス実行時に別ウィンドウを立ち上げない。-WindowStyleとは併用不可。
-PassThru 実行するプロセスのオブジェクトを生成してくれる。デフォルトではオフなので、戻り値が欲しい時は必須のパラメータとなる。後述。
-Wait 指定したプロセスの実行が完了するまで PowerShell が待っていてくれる。
-WindowStyle プロセスを実行する際のウィンドウサイズを指定。規定値はNormal-NoNewWindowとは併用不可。

-WindowStyleに指定可能な値

-WindowStyleには以下の値を設定することができます。内容は読んで字の如く。

  • Normal
  • Hidden
  • Minimized
  • Maximized

Start-Processでのプロセス実行例

と言うわけで、先ほどの処理を行うならこんな感じですね。

$nIO = "System.IO"
$cDirectory = "$nIO.Directory" -as [type]

$7zexe = "C:\Program Files\7-Zip\7z.exe"

foreach($root in$cDirectory::GetDirectories($args[0])) {
  foreach($7zFile in $cDirectory::GetFiles($root,"\*7z")) {
    #ここで 7zip を使って解凍したい
    $arg = "x -o`"$root`" `"$7zFile`""
    Start-Process -FilePath $7zexe -ArgumentList $arg -Wait -NoNewWindow
  }
}

ただこの方法、標準出力 / エラー出力を取得することが出来ません…。

-PassThru を使って更に細かい制御を行う

先述した通り、-PassThruオプションをつけるとプロセスのオブジェクトを作成してくれます。

具体的には、System.Diagnostics.Processが取得できます。

Processに含まれるプロパティExitCodeを見ることで処理結果からその後の処理を分岐させることが可能です。

$nIO = "System.IO" 
$cDirectory = "$nIO.Directory" -as [type]

$7zexe = "C:\Program Files\7-Zip\7z.exe"

foreach($root in $cDirectory::GetDirectories($args[0])) {
    foreach($7zFile in $cDirectory::GetFiles($root,"*7z")) {
        #ここで7zipを使って解凍したい
        $arg = "x -o`"$root`" `"$7zFile`""
        $proc = Start-Process -FilePath $7zexe -ArgumentList $arg -Wait -NoNewWindow -PassThru
        
        if($proc.ExitCode -eq 0) {
            #解凍後にzipを削除する
            [System.IO.File]::Delete($7zFile)
        }
    }
}

他にもWaitForExitメソッドあたりを使ってタイムアウトの制御なんかもできます。(当然-Waitオプションをはずさないと意味ないですが)

System.Diagnostics.Processを使う

え?PowerShell の文法なんか覚えられない?じゃあ C#書いたら?

$nIO = "System.IO"
$cDirectory = "$nIO.Directory" -as [type]
$nDiagnostics = "System.Diagnostics"

$7zexe = "C:\Program Files\7-Zip\7z.exe"

foreach($root in $cDirectory::GetDirectories($args[0])) {
    foreach($7zFile in $cDirectory::GetFiles($root,"*7z")) {
        #ここで7zipを使って解凍したい
        $arg = "x -o`"$root`" `"$7zFile`""
        $proc = New-Object "$nDiagnostics.Process"
        $psi = New-Object "$nDiagnostics.ProcessStartInfo"

        $psi.FileName = $7zexe
        $psi.Arguments = $arg
        $psi.UseShellExecute = $false
        $psi.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Hidden

        $proc.StartInfo = $psi

        $proc.Start()
        $proc.WaitForExit()

        if($proc.ExitCode -eq 0) {
            #解凍後にzipを削除する
            [System.IO.File]::Delete($7zFile)
        }
    }
}

とまぁ、見ていただくとわかるんですがすげー冗長です。シェルとは何だったのか。

ただ、Start-Processとは違ってSystem.Diagnostics.ProcessStartInfoをこちらで用意できるので、標準出力 / エラー出力をごにょごにょし放題です。

呼び出し元のプロセスと同じ標準出力 / エラー出力にリダイレクトさせるだけなら、以下のプロパティをtrueにすれば OK です。

また、この記事Process.OutputDataReceivedの設定方法などが詳細に解説されていますので、興味ある人はどうぞ。私はスクリプトのためにそこまでしないタイプの人間なので紹介だけにとどめておきます。

まとめ

PowerShell 使いにくい。

参考