前回の
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"; # => 0
system関数を利用してデプロイスクリプトを作る
さて、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"; #=> 0
IPC::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