
C++で Pythonの subprocess.check_callのようにコマンドを呼び出す
外部プログラムを同期的に実行して終了ステータスをチェックし、正常終了でなければ例外を送出するには
2021年8月25日 嶋田大貴
Cのプログラムから外部のプログラムを同期的に実行するには system(3)を使用するのが一番簡単だが、この関数に渡したコマンド文字列は一旦シェル(/bin/sh
)に渡り解釈される。つまり、シェルが特別な解釈をする可能性のある文字は全てエスケープしなければならない。単純な文字列連結でSQLを生成して実行するとあっという間に SQLインジェクションの脆弱性につながるのと同じで、sysyem(3) を何も考えずに使用すると簡単にOSコマンドインジェクションを許してしまう。したがって、
- 固定の文字列からなるコマンドを実行するだけのケース
- パターンや変数の展開、マルチステートメントなどといったシェルの機能を必要とするケース
以外では system(3) 関数を用いてシェル経由で外部のプログラムを実行するのは良いアイディアではない。
UNIX由来のOSでシェルを介さずに直接外部のプログラムを実行するには exec(3)シリーズの関数を使用する。しかしながら、UNIX文化圏にあまり馴染みのない諸氏は不思議に思えるかもしれないが exec(3)は外部プログラムを自分の子プロセスとして実行する機能ではなく、いわばプロセスが自分自身を上書きする形で指定のプログラムを実行する。
とはいえ外部コマンドを実行したいケースの多くでは指定したプログラムを(自分自身とは別の)子プロセスとして実行し、子プロセスの実行が終わったら成否をチェックして次の処理に移るという同期的な処理が求められるはずだ。UNIX文化圏では下記のようなフローでそれを行う。
- fork(2) でプロセスが自分自身の複製を生成する(実行中のプロセスがなんとふたつに分岐する)
- 複製された側のプロセス(子プロセス)は exec(3) を使用して目的のコマンドで自分自身を上書きする
- 複製元のプログラムはwait(2)や waitpid(2) を呼び出して上記の自分から分岐していった子プロセスが終了するのを待つ
- 終了ステータスを WIFEXITED, WEXITSTATUS などのマクロで取り出してチェックし、次の処理へ移る
本当にどうしても外部コマンドを呼び出すだけのためにこんな面倒なことをしなければならないのか。ことCの標準ライブラリだけで記述する限りは残念ながらそのとおりである。
いっぽう Pythonの標準ライブラリではではこのあたりの面倒を肩代わりしてくれる subprocess モジュールが充実している。subprocess.check_callという関数では、文字列のリストで渡したコマンドラインに基づいて同期的に外部プロセスを実行し、終了ステータスが0(=正常)でない場合は例外を送出するところまでやってくれるため、forkしてexecしてwaitするというまどろっこしい手間が要らない上に一連の外部コマンドを順に実行するような処理ではエラー発生時の中断処理をまとめて例外処理で記述することができるため重宝する。
そういうものを知っていると、Python同様に言語機能として例外処理を持つ C++でもそのようなものが欲しくなるので、最低限の機能に絞って実装したものが下記となる。(check_call
関数が本題)
#include <vector>
#include <string>
#include <functional>
#include <stdexcept>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
void exec(const std::vector<std::string>& cmdline)
{
if (cmdline.size() < 1) throw std::logic_error("cmdline too short");
char ** argv = new char *[cmdline.size() + 1];
for (int i = 0; i < cmdline.size(); i++) {
argv[i] = strdup(cmdline[i].c_str());
}
argv[cmdline.size()] = NULL;
if (execvp(cmdline[0].c_str(), argv) < 0) exit(-1);
}
pid_t fork(std::function<void(void)> func)
{
auto pid = fork();
if (pid < 0) throw std::runtime_error("fork() failed");
if (pid > 0) return pid;
//else(child process)
try {
func();
}
catch (...) {
// jumping across scope border in forked process may not be a good idea.
}
_exit(-1);
}
int call(const std::vector<std::string>& cmdline)
{
auto pid = fork([&cmdline](){exec(cmdline);});
int wstatus;
if (waitpid(pid, &wstatus, 0) < 0) throw std::runtime_error("waitpid() failed");
if (!WIFEXITED(wstatus)) {
if (WIFSIGNALED(wstatus)) {
throw std::runtime_error(std::string("Command(") + cmdline[0] + ") execution terminated by signal " + std::to_string(WTERMSIG(wstatus)) + ".");
}
//else
throw std::runtime_error(std::string("Command(") + cmdline[0] + ") execution terminated.");
}
return WEXITSTATUS(wstatus);
}
void check_call(const std::vector<std::string>& cmdline)
{
auto status = call(cmdline);
if (status != 0) throw std::runtime_error(std::string("Command(") + cmdline[0] + ") execution failed. Exit status: " + std::to_string(status));
}
#ifdef __MAIN__
// g++ -std=c++20 -D__MAIN__ check_call.cpp
#include <iostream>
int main()
{
try {
check_call({"ls", "-l"});
check_call({"false"}); // throws exception
}
catch (const std::exception& e) {
std::cout << e.what() << std::endl;
return 1;
}
return 0;
}
#endif // __MAIN__
2021年8月25日 嶋田大貴