webアプリにおいても時折、シェルスクリプトやLinuxコマンドなどを実行したい場合があります。そんな時、従来はexec()system()を使用して実行していましたが、Laravel10からはProcessファサードが導入されLaravel的なインタフェースが用意された事でより直感的且つ読み易いコードが書けるようになりました。今回はProcessファサードの基本的な使い方を解説したいと思います。

従来の方法

Processの解説に入る前に、従来の手法についておさらいしておきましょう。冒頭で述べたようにexec()system()を使用するか、あるいはProcessファサードの核であるSymphonyのProcessコンポーネントを使用するなど、色々やり方があります。私の場合は主にexec()を使用していました。

exec()は以下のように使います。

exec('実行するコマンド', $output, $resultCode);

第二引数の$outputと第三引数の$resultCodeは参照渡しです。従って関数の実行後に$outputには実行したコマンドの出力、$resultCodeには終了ステータスが格納されます。試しにディレクトリ内のファイル一覧を取得するlsコマンドをtinkerで実行してみましょう。

exec('ls -1', $output, $resultCode);    // オプション-1はファイル名を1行ずつ表示するオプション

$output
>> [
    "README.md",
    "app",
    "artisan",
    "bootstrap",
    "composer.json",
    "composer.lock",
    "config",
    "database",
    "lang",
    "package.json",
    "phpunit.xml",
    "public",
    "resources",
    "routes",
    "storage",
    "tests",
    "vendor",
    "vite.config.js",
  ]

$resultCode
>> 0

$outputは配列で各出力行が格納されます。$resultCodeには整数が格納されます、一般的なLinuxコマンドなら0で正常終了、0以外なら異常終了です。

次にProcessファサードを使って実行する場合はどうなるのか?見てみましょう。

Processでコマンドを実行する

Processでコマンドを実行するにはrun()start()を使用します。前者のrun()同期実行、つまり、実行したコマンドが終了してから次の処理に移ります。一方、後者のstart()非同期実行で実行したコマンドの完了を待たずにバックグラウンドで実行させたまますぐ次の処理に進むことができます。今回は前者のrun()についてのみ取り上げます。早速、Process::run()を使用して前項と同じコマンドを実行してみましょう。

// tinkerでの実行
$result = Process::run('ls -1')

Process::run()を実行するとProcessResultクラスのインスタンスが返却され、そのインスタンスメソッドを通してコマンドの実行結果について色々確認できます。

// 実行したコマンドを確認
$result->command();
>> "ls -1"

// コマンド実行時の標準出力を取得
$result->output();
>> """
  README.md\n
  app\n
  artisan\n
  bootstrap\n
  composer.json\n
  composer.lock\n
  ...
"""

// 出力に特定の文字列が含まれているかチェック
$result->seeInOutput('app');
>> true

// コマンドの終了ステータス取得
$result->exitCode();
>> 0

// コマンド実行成功? ※終了ステータスが0ならtrue
$result->successful();
>> true

// コマンド失敗? ※終了ステータスが0以外ならtrue
$result->failed();
>> false

exec()の場合は$outputで取得した出力が配列でしたが、Processではoutput()で取得した出力は文字列なので注意です。

エラー出力

コマンドが異常終了したか否かは終了ステータスを確認すれば分かりますが、その原因を把握する為にはエラー出力を確認する必要があります。エラー出力を取得する場合、exec()では実行するコマンドの末尾に2>&1を追加してエラー出力を標準出力にリダイレクトさせる必要がありました。

// lsコマンドに存在しないディレクトリaaaを引数で指定
exec('ls aaa', $output, $resultCode);

// 2>&1無しだとエラー出力が$outputに格納されない
$output
>> []

// 2>&1付で実行
exec('ls aaa 2>&1', $output, $resultCode);

// 今度は$outputに格納された
$output
>> [
"ls: aaa: No such file or directory",
]

Processの場合はコマンドに変更を加えなくてもerrorOutput()でエラー出力を取得できます。

Process::run('ls aaa')->errorOutput();
>"ls: aaa: No such file or directory\n"

カレントディレクトリを指定して実行

特定のディレクトリにてコマンドを実行したい場合、exec()では先頭にcd {指定のディレクトリ};を付与して実行します。

// 例. 親階層に移動してからpwdを実行
exec('cd ../;pwd', $output, $resultCode);

Processの場合はpath()でコマンドを実行したいディレクトリを指定できます。

Process::path('../')->run('pwd');

パイプ処理

Linuxコマンドの場合、パイプ(|)でコマンド同士を繋げて実行すると、前のコマンドの出力を次のコマンドの入力として実行できます。例えば、アクセスログに特定のIPが記載されている行が何行あるか調べたい時、以下の様に実行します。

grep '192.168.101.1' access.log | wc -l
>>      5

上記の実行ではgrepコマンドの出力をwcコマンドの入力として受け渡しています。上記の実行では5行ヒットしました。

こちらをProcessで実行する場合はpipe()というメソッドを使います。

$result = Process::pipe([
    "grep '192.168.101.1' access.log",
    'wc -l',
]);

// wc -l の実行結果には余白が含まれている
$result->output()
>> "       5\n"

// trimすれば数値のみ取得できる(またはsedコマンドをpipeして整形しても良いかも)
trim($result->output())
>> "5"

もちろん、run()の引数にパイプで結合したコマンドを指定しても同じ結果が得られます。

$result = Process::run("grep '192.168.101.1' access.log | wc -l");
trim($result->output());
>> "5"

しかし、プログラムから実行するコマンドは長文になりがち、かつ、要所要所で変数が埋め込まれがち(ファイルのパスなど)なので、pipe()を使った方がスッキリ書けると思います。

まとめ

Processの基本的な操作について従来のexec()を使った方法と比較しながら解説しました。まとめていて気が付いたのは、exec()を使用していた時はコマンドの末尾にエラー出力用に2>&1を付与したり、カレントディレクトリを指定する為に先頭にcdを付与したり、パイプ処理を実行する際はコマンドの途中にパイプ(|)を織り交ぜたり、とコマンド文がゴチャゴチャになりがちでした。一方でProcessではコマンドを汚さずに素の状態のまま保つ事ができるのでコードが読みやすく理解しやすいと思いました。

By hikaru