C++ でコマンドを実行し、出力と終了ステータスの両方を取得する (Windows &Linux)

最近、C++ プログラム内のコマンド ライン出力を解析する必要がありました。 std::system を使用すると、コマンドを実行して終了ステータスを簡単に取得できます 、しかし出力を取得するのは少し難しく、OS固有です。 popen を使用して 、POSIX C 関数を使用すると、終了ステータスと特定のコマンドの出力の両方を取得できます。 Windows では _popen を使用しています 、したがって、コードはクロスプラットフォームである必要がありますが、Windows の終了ステータスが常に 0 であることを除いて、その概念は存在しません。この記事では、コマンドの出力だけを取得するスタック オーバーフローの例から始め、その上に、終了ステータスとコマンド出力の両方を返すより安全なバージョン (null バイト処理) を構築します。また、fread に関する多くの詳細も含まれます。 vs fgets およびバイナリ データの処理方法。

使用例を含む完全なコード例は、こちらの github またはこのページの下部にあります。実際の例は、さまざまなプラットフォーム (Windows と Linux) の github アクションでコンパイルされています。

通常、コマンド ライン出力を解析しないことをお勧めします。エラーが発生しやすく、ユーザーが選択した言語に依存しているため、バージョンが異なればフラグが異なる場合があります (OS X vs Linux ) などなど。ネイティブ ライブラリを使用するオプションがある場合は、それを使用する必要があります。例は curl の解析です output を使用して、API からデータを取得します。 http はおそらく 1 トンあります curl を解析する代わりに、お気に入りのプログラミング言語で使用できるライブラリ または wget または fetch 出力。

私の場合、古いプログラムを使用してクローズド ソース ファイルを解析し、バイナリ出力を取得する必要があります。これは一時的な状況であり、ネイティブ解析ライブラリも開発中です。バイナリは、システム設定、言語、その他のツールなどと同様に私の管理下にあるため、この特定のユース ケースでは、コマンド ライン出力を解析するソリューションが当面受け入れられました。

この投稿では、nullbyte、null 文字、null 終端、null 終端という用語を交換することに注意してください。それらはすべて同じ意味で、C 文字列を終了するために使用されるヌルバイト文字 (\0) 、または ^@U+0000 または 0x00 、要点がわかります)。

より多くの機能、クロスプラットフォームまたは非同期実行が必要な場合は、boost.Process が優れた代替手段です。ただし、コンパイラとサイズの制約により、このコードが実行される環境でブーストを使用することはできません.

fgets を使用したスタック オーバーフローの例

stackoverflow では、指定された例は構築するのに適したベースですが、終了コードと出力を取得するには、変更する必要があります。終了コードも取得したいので、std::unique_ptr を使用する例は使用できません。 .それ自体が unique_ptr を使用する良い例です カスタムデリータ (cmd const char* です 実行するコマンド:

std::unique_ptr<FILE, decltype(&pclose)> pipe(popen(cmd, "r"), pclose);

コードは以下にコピーされます:

std::string exec(const char* cmd) {
    char buffer[128];
    std::string result = "";
    FILE* pipe = popen(cmd, "r");
    if (!pipe) throw std::runtime_error("popen() failed!");
    try {
        while (fgets(buffer, sizeof buffer, pipe) != NULL) 
            result += buffer;
        }
    } catch (...) {
        pclose(pipe);
        throw;
    }
    pclose(pipe);
    return result;
}

この例は、記載されていることを実行しますが、いくつかの落とし穴があります。 FILE* を使用しています (ポインター)、char バッファ割り当てと手動で FILE* を閉じる 何か問題が発生した場合 (catch )。 unique_ptr 例は、例外を処理する必要がなく、std::array<char, 128> を使用するため、より現代的です。 C スタイルの char* の代わりに バッファ。例外のスローはまったく別の問題ですが、今日はそれには触れません。今日は、FILE* から読み取る C スタイルのコードについて説明します。 およびバイナリ データの処理方法。

std::string へのテキスト出力のみが必要な場合は、stackoverflow の例で十分でしょう。 .ただし、この記事の残りの部分を読んでいくうちにわかるように、私のユースケースはもう少し複雑でした。

fread vs fgets

コード スタイルはさておき、私の最大の問題は fgets を使用することでした このように const char* を追加して組み合わせる std::stringnullyte に遭遇すると停止します (\0 )。多くの場合問題にならない通常の文字列出力の場合、ほとんどのコマンドは、いくつかの文字列を出力し、それを 1 日呼び出すだけです。私の出力は、ヌルバイトを含む可能性のある abinary blob を返します。 fread std::string に出力を追加するときに使用できるバイト数を読み取り、正常に読み取られた量を返します ヌルバイトを含みます。

上記の例は result += buffer を実行します 、 const char* を追加 std::string に 、この場合、 std::string:Appends the null-terminated character string pointed to by s. の operator+=の cppreference によると

その問題は、私の場合、ヌルバイトの後の文字も追加する必要があることです。 fgets 読み取ったデータ量は返されません。 fgets の使用 バッファが 128 の場合、nullbyte が 10 で、改行が 40 の場合、最初の 10 バイトと 40 バイトより後のものが返されます。事実上、ヌルバイトと改行の間、または間に改行がない場合はバッファの最後 (128) まで、すべてを失っています。

fread 読み取ったバイト数を返します。それを std::string のコンストラクターと組み合わせる const char* を取る そして size_t 文字列内のコンテンツ全体を強制できます。 std::string であるため、これは安全です。 サイズがわかっているため、ヌル終了文字に依存しません。ただし、const char* を使用する他のコード これらのヌルバイトを扱うことはできませんので、注意してください。

このスタックオーバーフローの投稿は、fread を理解するのに非常に役立ちました 、そして C で夢を見る同僚からの助け 、彼は多くの内部の仕組みを説明しました.

そして、このすべての後、なぜ私が std::string 内にバイナリ データを押し込んでいるのか疑問に思っているなら、 、素晴らしい質問です。これについては、この記事全体よりも長い投稿が必要になるため、おそらく別の機会に説明します。

出力と終了コードを含むコマンドの実行

私のコードは、実行されたバイナリの終了ステータスを (エラー処理のために) チェックし、返されたデータをさらに処理するために使用します。これをすべて 1 つの便利な場所に保持するために、そのデータを保持する構造体を定義することから始めましょう。 command の結果を保持します 、だから名前 CommandResult

以下に、等価演算子とストリーム出力演算子を含む構造体コードを示します。

struct CommandResult {
    std::string output;
    int exitstatus;

    friend std::ostream &operator<<(std::ostream &os, const CommandResult &result) {
        os << "command exitstatus: " << result.exitstatus << " output: " << result.output;
        return os;
    }
    bool operator==(const CommandResult &rhs) const {
        return output == rhs.output &&
               exitstatus == rhs.exitstatus;
    }
    bool operator!=(const CommandResult &rhs) const {
        return !(rhs == *this);
    }
};

構造体の本質はもちろん output です および exitstatus . int を考えています 理由による終了ステータス。

次の部分は Command です クラス自体、これがそのコードです:

class Command {

public:
    /**
     * Execute system command and get STDOUT result.
     * Like system() but gives back exit status and stdout.
     * @param command system command to execute
     * @return CommandResult containing STDOUT (not stderr) output & exitstatus
     * of command. Empty if command failed (or has no output). If you want stderr,
     * use shell redirection (2&>1).
     */
    static CommandResult exec(const std::string &command) {
        int exitcode = 255;
        std::array<char, 1048576> buffer {};
        std::string result;
#ifdef _WIN32
#define popen _popen
#define pclose _pclose
#define WEXITSTATUS
#endif
        FILE *pipe = popen(command.c_str(), "r");
        if (pipe == nullptr) {
            throw std::runtime_error("popen() failed!");
        }
        try {
            std::size_t bytesread;
            while ((bytesread = fread(buffer.data(), sizeof(buffer.at(0)), sizeof(buffer), pipe)) != 0) {
                result += std::string(buffer.data(), bytesread);
            }
        } catch (...) {
            pclose(pipe);
            throw;
        }
        exitcode = WEXITSTATUS(pclose(pipe));
        return CommandResult{result, exitcode};
    }
}

fread コマンドは、コマンド出力から返されるバイトがなくなるまで実行されます。使用している出力の種類を知っているので、バッファは 1MiB です。これはおそらくデータには大きすぎます。私の場合、ベンチマークを行ったところ、10KiB から 1 MiB の間がターゲット アーキテクチャで最速でした。 128 または 8192 もおそらく問題ありませんが、自分でベンチマークする必要があります。かなり単純なテストは、cat でいくつかの巨大なファイルを出力することです 実行時間に加えて、CPU とメモリの使用量を計算します。結果を印刷しないでください。これら 3 つのことを見て、許容できる比率を選択してください。

std::string も初期化してみませんか 1 MiB の文字で? std::strings それらを満たすか、後で.reserve()を呼び出す以外は、構築時に特定のサイズに割り当てることはできません 、私のベンチマークでは、どちらを実行しても意味のある速度またはパフォーマンスの向上は見られませんでした.

上記のコードを使用するのは簡単です。これは静的関数であるため、使用するためにクラス インスタンスは必要ありません。以下に例を示します:

std::cout << Command::exec("echo 'Hello you absolute legends!'") << std::endl;

その結果:

command exitstatus: 0 output: Hello you absolute legends!

シェルを経由しているので、リダイレクトも同様に機能します。 stdout をリダイレクトしています stderr へ 結果は何も出力されず、終了ステータスのみが表示されます:

std::cout << Command::exec("echo 'Hello you absolute legends!' 1>&2") << std::endl;

出力は stderr にあります ただし、私のシェルでは、これが予想されます:

stderr をキャプチャする必要がある場合 次に、出力を逆方向にリダイレクトします。

std::cout << Command::exec("/bin/bash --invalid  2>&1") << std::endl;

パイプはシェルと同様に機能しますが、これはすべて sh を使用していることに注意してください 環境変数やデフォルトのシェルを制御することはできません。 popen の POSIX ページで詳細を読む その理由を探ってください。

Windows に関する注意

これは Windows の例で、_popen を使用する必要があります。 および _pclose :

std::cout << "Windows example:" << std::endl;
std::cout << Command::exec("dir * /on /p") << std::endl;

その概念はウィンドウに変換されないため、終了コードは常にゼロになります。 %ErrorLevel% があります ですが、これはコンソール アプリケーションの環境変数に過ぎず、実際の終了ステータスではありません。

マイクロソフトのページには、 _popen も記載されています GUI アプリケーションでは動作せず、コンソール プログラムのみで動作します。必要な場合は、Boost.process または system を使用してください .

出力例のヌルバイト:

github のサンプル コードでは、 execFgets も表示されます。 関数、nullbyte 処理の違いを示すためにそこに残しました。参考までに、ここにも例を示します。 fgets を使用したコマンドの関連部分 :

while (std::fgets(buffer.data(), buffer.size(), pipe) != nullptr)
    result += buffer.data();

fread を使用している部分 :

std::size_t bytesread;
while ((bytesread = fread(buffer.data(), sizeof(buffer.at(0)), sizeof(buffer), pipe)) != 0)         
    result += std::string(buffer.data(), bytesread);

clang-tidy 警告の除外を含むテスト コマンド (// NOLINT ):

int main() {
    using namespace raymii;

    std::string expectedOutput("test\000abc\n", 9); //NOLINT
    commandResult nullbyteCommand = command::exec("/usr/bin/printf 'test\\000abc\\n'"); // NOLINT(bugprone-string-literal-with-embedded-nul)
    commandResult fgetsNullbyteCommand = command::execFgets("/usr/bin/printf 'test\\000abc\\n'"); // NOLINT(bugprone-string-literal-with-embedded-nul)

    std::cout << "Expected output: " << expectedOutput << std::endl;
    std::cout << "Output using fread: " << nullbyteCommand << std::endl;
    std::cout << "Output using fgets: " << fgetsNullbyteCommand << std::endl;
    return 0;
}

出力:

Expected output: test\0abc
A command with nullbytes using fread: exitstatus: 0 output: test\0abc
A command with nullbytes using fgets: exitstatus: 0 output: test

ヌルバイト文字は \0 に置き換えられます 上記の出力で。これは私の端末でどのように見えるかを示すスクリーンショットです:

これは std::strings で安全に使用できることに注意してください。 、string_view を取るメソッド ora const char* おそらくヌルバイトにはうまく反応しないでしょう。私のユースケースでは、これは安全ですが、マイレージは異なる場合があります.

buffer で遊んでみてください サイズを確認してから、出力を確認します。 4 に設定すると、 fgets で出力されます testbc です .おかしいでしょ?

完全なコード

以下に、ヘッダー ファイル command.h があります。 .私のgithubにもあります。使用例が必要な場合は、github プロジェクト main.cpp で見つけることができます。 ファイル。

#command.h
#ifndef COMMAND_H
#define COMMAND_H
// Copyright (C) 2021 Remy van Elst
//
//     This program is free software: you can redistribute it and/or modify
//     it under the terms of the GNU General Public License as published by
//     the Free Software Foundation, either version 3 of the License, or
//     (at your option) any later version.
//
//     This program is distributed in the hope that it will be useful,
//     but WITHOUT ANY WARRANTY; without even the implied warranty of
//     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//     GNU General Public License for more details.
//
//     You should have received a copy of the GNU General Public License
//     along with this program.  If not, see <http://www.gnu.org/licenses/>.
#include <array>
#include <ostream>
#include <string>
#ifdef _WIN32
#include <stdio.h>
#endif

namespace raymii {

    struct CommandResult {
        std::string output;
        int exitstatus;
        friend std::ostream &operator<<(std::ostream &os, const CommandResult &result) {
            os << "command exitstatus: " << result.exitstatus << " output: " << result.output;
            return os;
        }
        bool operator==(const CommandResult &rhs) const {
            return output == rhs.output &&
                   exitstatus == rhs.exitstatus;
        }
        bool operator!=(const CommandResult &rhs) const {
            return !(rhs == *this);
        }
    };

    class Command {
    public:
        /**
             * Execute system command and get STDOUT result.
             * Regular system() only gives back exit status, this gives back output as well.
             * @param command system command to execute
             * @return commandResult containing STDOUT (not stderr) output & exitstatus
             * of command. Empty if command failed (or has no output). If you want stderr,
             * use shell redirection (2&>1).
             */
        static CommandResult exec(const std::string &command) {
            int exitcode = 0;
            std::array<char, 1048576> buffer {};
            std::string result;
#ifdef _WIN32
#define popen _popen
#define pclose _pclose
#define WEXITSTATUS
#endif
            FILE *pipe = popen(command.c_str(), "r");
            if (pipe == nullptr) {
                throw std::runtime_error("popen() failed!");
            }
            try {
                std::size_t bytesread;
                while ((bytesread = std::fread(buffer.data(), sizeof(buffer.at(0)), sizeof(buffer), pipe)) != 0) {
                    result += std::string(buffer.data(), bytesread);
                }
            } catch (...) {
                pclose(pipe);
                throw;
            }
            exitcode = WEXITSTATUS(pclose(pipe));
            return CommandResult{result, exitcode};
        }

    };

}// namespace raymii
#endif//COMMAND_H