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文化圏では下記のようなフローでそれを行う。

本当にどうしても外部コマンドを呼び出すだけのためにこんな面倒なことをしなければならないのか。こと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日 嶋田大貴

記事一覧へ戻る