phpからのコマンド実行(標準入力あり)

Date:

Share post:

前回の記事でphpから外部コマンドを実行する方法の一つであるexec関数に関して触れましたが、コマンドの中には起動後に標準入力から必要な情報を取得しようとするものも多くあります。execではコマンド起動時の引数として情報を与えることはできますが、コマンドの標準入力に対して情報を与える手段がありません。

そのようなコマンドをphpから実行したい場合はproc_open関数を利用します。

proc_openの仕様

proc_openの仕様は以下のようになっています。

proc_open(array|string $command, array $descriptor_spec, array &$pipes): resource|false

実は第四〜第六引数もあるのですが、重要ではない(個人的には使わない)ため省略しています。
その辺含めて詳しく知りたい方はGoogle先生に聞いてください。

第一引数には実行したいコマンドを指定します。
この辺はexecと同じです。
なお、型として配列が指定でき、この場合はコマンドの引数を配列の要素として指定できるようになっている模様ですが、個人的にはexecと同様にコマンドの引数を含む文字列を第一引数として指定する方法を採用しており、配列で指定できるありがたみに関しては今ひとつ分かっていません。

第二引数にはディスクリプタ番号をキーとする二次元配列を指定します。一次元目の配列の各要素は、型と用途の2つの要素を持つ配列になります。
と言っても、この引数の内容を文章のみで説明するのは大変なので、この後提示する例に基づいて詳しく触れたいと思います。

第三引数には第二引数の指定に準じて当該phpプログラムとコマンド間に生成されたパイプのファイルポインタが格納されます。
こちらも文章だけでは意味が伝わり難い内容になるので例に基づいて説明したいと思います。

proc_openの実行結果としてはリソースが返されます。
リソースの説明も面倒なので、詳しくはGoogle先生に。
重要な点は、execと異なりproc_openの完了がコマンドの完了ではないと言う点です。proc_openは第一引数で指定されたコマンドを起動し、同コマンドと起動元のphpプログラムとの間にパイプを生成し情報の入出力を可能にします。つまり、phpプログラムとコマンドが並行して動作します。両者の動作は基本的には非同期ですが、両者の間に生成されたパイプに対する入出力の待ち合わせによって対話的に処理を進めていく形になります。

なお、前述のようにproc_openはコマンドを起動するだけなので、コマンドの終了に関する処理も必要になります。
これはproc_close関数で行いますが、その辺も例を見てもらえればと思います。

proc_openの使用例

そもそもproc_openを使用したくなったのは、php側からAWS CLIのプロファイル設定を行いたいと思ったからです。
AWS CLIのプロファイル設定自体もAWS CLIから行えるのですが、同操作が標準入出力経由で対話的に行われるため、同操作をphpから実行しようと思うとexecでは不可能で、proc_openを使用する必要性が生じたというのが本記事を書くことになったきっかけです。

と言うことで、AWS CLIプロファイルの設定処理に関して以下に示します。

<?php
  
namespace App\Repositories\Aws;

class CreateProfileRepository
{
    public function __invoke($profile, $accessKey, $secretAccessKey, $region, $output)
    {                                                                
        $cmd = '/usr/local/bin/aws configure --profile '.$profile;   
        $descriptorspec = [
            ['pipe', 'r'],
            ['pipe', 'w'],
        ];
        $process = proc_open($cmd, $descriptorspec, $pipes);
        if (is_resource($process)) {
            fread($pipes[1], 4096); // AWS Access Key ID
            fwrite($pipes[0], $accessKey."\n");
            
            fread($pipes[1], 4096); // AWS Secret Access Key
            fwrite($pipes[0], $secretAccessKey."\n");
            
            fread($pipes[1], 4096); // Default region name
            fwrite($pipes[0], $region."\n");
            
            fread($pipes[1], 4096); // Default output format
            fwrite($pipes[0], $output."\n");
            
            // 終了
            fclose($pipes[0]);
            fclose($pipes[1]);
            proc_close($process);
        }
    }
}

AWS CLIのプロファイル設定では4つの情報(アクセスキー、シークレットアクセスキー、リージョン、標準的な出力形式)を対話的に設定します。

proc_openの第二引数である「descriptorspec」ですが、先に示したようにディスクリプタ番号をキーにしていますので、上記では0(標準入力)と1(標準出力)に関する設定を行っていることになります。
標準入力、標準出力いずれに対しても型としては「pipe」を指定しています。これによりコマンド側から見た標準入力、標準出力に対してphpプログラムとの間に生成されたパイプが割り当てられることになります。
なお、型としては「pipe」以外に「file」も指定できるようですが、個人的に使う必要性がなかったので挙動は未確認です。

また、用途(各配列の二番目の要素)としては、標準入力には「r」標準出力には「w」が指定されています。これはコマンド側から見てディスクリプタ番号0(標準入力)からは情報を「r(read)」し、ディスクリプタ番号1(標準出力)に対しては情報を「w(write)」することを意味します。
入出力の方向性に関しては、あくまでコマンド側から見た場合の指定になっている点に要注意です。

「標準入力が入力(read)、標準出力が出力(write)となるのは当たり前では?」と思わなくもないですが、ディスクリプタ番号と用途である標準入出力の関係性はあくまで基本であり、これに準じないコマンドが存在しても良いので、このようなインタフェースになっているものと推測します。
まぁ、そのようなコマンドを見かけることはほとんどありませんが…

第二引数を上記のように指定することで、第三引数には一番目の要素として標準入力用パイプのファイルポインタが、二番目の要素として標準出力用パイプのファイルポインタがそれぞれ設定されます。
以降の入出力処理ではこの2つのパイプを使用します。

また、proc_openの実行結果としては、コマンドが正常に起動できていればリソースが返されます。これはproc_close実行時に必要になりますので、変数「process」に格納しておきます。

コマンドが無事起動できたら、想定される4つの情報入力を行って行きますが、その都度コマンド側から入力を求めるメッセージが標準出力に対して出力されるようになっていますので、それを受けて該当する入力を行っています。と言っても上記例では単に順序性のみで、例えば最初の出力が本当にアクセスキーの入力を求めるものであったかという点は確認していません。この辺は改善の余地ありかもしれません。

必要な入力が終了したらコマンドを終了します。
proc_closeではコマンドの終了を待ち合わせるようなので、最後の入力実行後に同処理が完了する前にコマンドを強制的に終了してしまうような挙動にはならないようです。

なお、proc_closeに際しては、それに先行して生成したパイプを閉じておく必要があるようです。
理由は「デッドロックを防ぐため」とのことですが、前述したようにproc_closeではコマンドの終了を待ち合わせる一方で、コマンド側では引き続きphp側からの入力を待ち続けていたと言うような状況が発生することを防ぐためということかと推測します。

以上のようにproc_open(および一連の処理)を利用することで、標準入出力を使って対話的に処理を進めるコマンドに関してもphp経由での実行が可能になります。

総括

phpからコマンドを実行すると言うこと自体があまり頻繁に必要となる操作ではなく、必要な場合でもコマンド(および引数)を指定し、結果を受け取る程度の単純が操作が多いため、今まではexecで十分でした。

今回、対話的処理が必要になったため初めてproc_openを使ってみましたが、第二、第三引数辺りの仕様が直感的には分かり難いので、当初は本関数を使用するかどうか躊躇しました。

慣れてみれば今後も含めて使えそうな印象ですが、おそらくは次に本関数を使用することになるのはしばらく先の話になると思いますし、その際には使い方を忘れている可能性が大であるため、ここに備忘録として本記事を残しておきたいと思います。

Related articles

EC-CUBE 4系のプラグイン開発について その...

前回、プラグインを一旦有効化させて管理...

EC-CUBE 4系のプラグイン開発について その...

以前から作成したいと考えていたのですが...

Laravel Filamentを使用した管理画面...

前回Breezeをインストールしたこと...

Laravel Filamentを使用した管理画面...

前回、filamentでのリソース作成...