読者です 読者をやめる 読者になる 読者になる

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

前書き

何だかんだで最近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
    }
}

で、やってみるとわかるんですが、できません。理由は簡単で、Program Filesのように実行ファイルのパスにスペースが入っているとこの方法じゃ呼べません。(逆に言えばスペースが入っていなければこれで呼べるんですが。)

じゃあどうするのかと言うと、&を使います。

$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です。

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

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

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

$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をこちらで用意できるので、標準出力 / エラー出力をごにょごにょし放題です。し放題ですが、超絶面倒です。

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

まとめ

PowerShell使いにくい。

参考