前回の
Perlとシェルとの連携
ここからはデプロイスクリプトを例に、
UNIX系システムの日々の運用では、system関数、``
system関数でコマンドを呼ぶ
systemはPerl組込みの関数で、
use strict;
use warnings;
# カレントディレクトリのファイル名一覧を表示する
system "ls";この関数に渡されたコマンドは内部的にはforkで作られた子プロセス上で実行され、
system関数は、
引数が1つの場合
引数が1つの場合で、
use strict;
use warnings;
# カレントディレクトリのファイル名一覧を表示する
system "echo ./*";シェルのメタ文字が含まれていない場合は、execvp(3)に直接渡されます。この場合はシェルを起動するコストがないため、
execvp(3)とは、execシステムコールの本体です。プログラムの中でexecシステムコールを呼び出すと、execの引数に渡されたプログラムで置き換えられ、execで実行されたプログラムが終了しても、
前述の通り、system関数では子プロセス上でexecが実行されます。これにより、systemで実行されたプログラムが終了したあとに、
引数が2つ以上の場合
引数が2つ以上ある場合も、execvp(3)に渡され、./*がシェルで展開されず、./*という文字列が表示されます。
use strict;
use warnings;
# "./*"という文字列を表示する
system "echo", "./*";引数が1つでシェルのメタ文字を含まない場合と、execvp(3)に渡されるわけですが、system関数で実行してみましょう
#!/usr/bin/env perl
use strict;
use warnings;
print scalar @ARGV, "\n";use strict;
use warnings;
my @command = ("./print_argc.pl", "a", "hello world");
my $command_string = join(" ", @command);
system @command; # => 2
system $command_string; # => 3リスト5を見るとわかるとおり、"hello world"という文字列は"hello"と"world"に分解されてしまいます。一方で、
常にシェルを経由しない安全な呼び出しを行う
シェルを経由してコマンドを呼び出す場合、execvp(3)に渡される方法はないのでしょうか。実は、
間接オブジェクト記法とは、func a b, c;のように、systemに対してリスト6のように間接オブジェクト記法を利用することで、
- シェルを経由せず最初の引数を実行する
- そのプログラムの引数として、3つ目以降の引数が渡される 
- プログラムの「名前」 ( psに表示されるもの)のCMDは2つ目以降の引数を合わせたものとなる 
という挙動を実現できます。この場合は、systemの最初の引数に"yes"を渡しているため、yesコマンドが実行されます。yesコマンドの引数には@commandsの2つ目以降、"hello world"が渡されますので、hello worldが出力され続けます。そして、psなどで確認すると、CMDのところには!!!yes!!! hello worldと表示されます。
use strict;
use warnings;
my @commands = ("!!!yes!!!", "hello world");
system {"yes"} @commands;コマンドの終了コードを取得する
system関数は、forkした子プロセスでコマンドを実行するというのは前述のとおりですが、
注意点として、
use strict;
use warnings;
my $failed = system "false";
print $failed . "\n"; # => 256
my $succeeded = system "true";
print $succeeded . "\n"; # => 0system関数を利用してデプロイスクリプトを作る
さて、rsyncで配ってもよいのですが、
- どこかのサーバへのデプロイに失敗したらロールバックする
- 複数のサーバのファイルをなるべく同じタイミングで更新する
という2点を重視し、
まずは特定のディレクトリをtarでアーカイブするスクリプトを書いてみましょう
use strict;
use warnings;
use File::Basename qw/dirname/;
my $static_file_dir = dirname(__FILE__) . "/static";
my $tar_command =
  ["tar", "zcvf", "static.tgz", $static_file_dir];
my $ret = system @$tar_command;
if ( $ret != 0 ) {
  die "archive failed";
}次に、system関数以外にもコマンドを呼ぶ方法が存在します。そこで、
バッククオートでコマンドを呼ぶ
前述のとおり、system関数はforkしてできた子プロセスでコマンドを実行します。そのため、
単純にコマンドの標準出力の結果を得たい場合に一番お手軽なのは、
標準出力をキャプチャする
実際にコマンドの標準出力を取得してみるコードがリスト9です。バッククオートによるコマンド実行は、system関数と違い必ずシェルによって解釈されることに気を付けてください。
use strict;
use warnings;
my $out = `echo hello world`;
print $out . "\n"; # => hello worldコマンドの終了コードを取得する
バッククオートを使ったコマンド実行の終了コードを取りたいときは、$?という特殊変数を利用しますsystemのときと同様に、
use strict;
use warnings;
`false`;
print $?. "\n"; #=> 256
`true`;
print $?. "\n"; #=> 0IPC::Open3でコマンドを呼ぶ
バッククオートを利用して取得できるのは標準出力だけです。標準エラー出力も取得したい場合は、
IPC::Open3のopen3関数も子プロセスを生成しそこでコマンドを実行しますが、
標準出力、標準エラー出力の両方をキャプチャする 
では、
stdin
stdout
stderr
0という文字列がコンソールに表示されます。
use strict;
use warnings;
use IPC::Open3;
my ($wtr, $rdr, $err);
$err = Symbol::gensym;  ━(1)
my $script = 'print <>; print "stdout\n"; warn "stderr\n";';  ━(2)
my $command = ['perl', '-e', $script];
my $pid = open3($wtr, $rdr, $err, @$command);  ━(3)
print $wtr "stdin\n";  ┓
close($wtr);           ┛(4)
print <$rdr>;    ┓
print <$err>;    ┛(5)
waitpid($pid, 0);
print $?;リスト11$script変数にはPerlのワンライナーが格納されています。その内容は、"stdout"という文字列を標準出力に出力し、"stderr"という文字列を標準エラー出力に出力するというものです。
リスト11は、open3で実行し、
少し複雑なプログラムなので、
リスト11open3関数に@$commandを渡すことで、$wtr、$rdr、$errという変数も同時に渡していますが、$wtrは子プロセスの標準出力に書き込むためのファイルハンドル、$rdrには子プロセスの標準出力を受け取るためのファイルハンドル、$errには子プロセスの標準エラー出力を受け取るためのファイルハンドルが代入されます。
一点注意したいのが、open3に渡す前に、$errにSymbol::gensymを渡しているところです。open3関数は、dup(3)します。もっとありていに言えば、
これを防ぐために、$errはundefであってはいけません。Perlではスカラコンテキストにおけるundefは偽値ですので、undefを渡すと、$errにあらかじめSymbol::gensymを代入することで、$errがundefと評価されないようにしています。逆に言うと、0やundefを渡してあげるとよい、
さて、open3関数で子プロセスで実行されたPerlワンライナーは、print $wtr "stdin\n";とすることで、"stdin\n"という文字列を書き込みます。そして、close($wtr);することで子プロセスの標準入力へのファイルハンドルをcloseし、"stdin"を出力します。
リスト11"stdout\n"という文字列を書き込みます。子プロセスの標準出力は親プロセスの$rdrとつながっているので、<$rdr>で読み込み、"stderr\n"を標準エラー出力に書き込み、<$err>として$errを読み込んでいます。
このように、open3関数を利用することで、open3関数を利用するとよいでしょう。
標準出力、標準エラー出力の両方を扱う場合の注意点 
注意点として、
それを理解したうえでリスト11を見てみると、
そのような場合は、
コマンドの終了コードを取得する
コマンドの終了ステータスを知るためには、waitpidでそのプロセスの終了を待ち、$?特殊変数を調べます。ここでも実際の終了コードを知るためには右に8ビットシフトする必要がありますが、
<続きの
本誌最新号をチェック!
WEB+DB PRESS Vol.130
2022年8月24日発売
B5判/
定価1,628円
ISBN978-4-297-13000-8
- 特集1
 イミュータブルデータモデルで始める
 実践データモデリング
 業務の複雑さをシンプルに表現!
- 特集2
 いまはじめるFlutter
 iOS/Android両対応アプリを開発してみよう 
- 特集3
 作って学ぶWeb3
 ブロックチェーン、スマートコントラクト、 NFT 
