最近、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::string
に nullyte
に遭遇すると停止します (\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