Führen Sie einen Befehl aus und erhalten Sie sowohl den Ausgabe- als auch den Exit-Status in C++ (Windows &Linux)

Führen Sie einen Befehl aus und erhalten Sie sowohl den Ausgabe- als auch den Exit-Status in C++ (Windows &Linux)

Kürzlich musste ich einige Befehlszeilenausgaben in einem C++-Programm analysieren. Mit std::system ist es einfach, einen Befehl auszuführen und nur den Exit-Status zu erhalten , aber auch die Ausgabe ist etwas schwieriger und betriebssystemspezifisch. Durch die Verwendung von popen , ein POSIX C -Funktion können wir sowohl den Exit-Status als auch die Ausgabe eines bestimmten Befehls erhalten. Unter Windows verwende ich _popen , also sollte der Code plattformübergreifend sein, außer dass der Exit-Status unter Windows immer 0 ist, dieses Konzept existiert dort nicht. Dieser Artikel beginnt mit einem Stapelüberlaufbeispiel, um nur die Ausgabe eines Befehls zu erhalten, und baut darauf auf eine sicherere Version (Null-Byte-Behandlung) auf, die sowohl den Exit-Status als auch die Befehlsausgabe zurückgibt. Es beinhaltet auch viele Details zu fread gegenüber fgets und wie man mit binären Daten umgeht.

Das vollständige Codebeispiel mit Verwendungsbeispielen finden Sie auf github hier oder unten auf dieser Seite. Ein funktionierendes Beispiel ist auf Github-Aktionen für verschiedene Plattformen (Windows &Linux) kompiliert.

Normalerweise würde ich davon abraten, die Befehlszeilenausgabe zu analysieren. Es ist fehleranfällig, man ist abhängig von der vom Benutzer gewählten Sprache, verschiedene Versionen können unterschiedliche Flags haben (OS X gegenüber Linux ) und vieles mehr. Wenn Sie die Möglichkeit haben, eine native Bibliothek zu verwenden, sollten Sie diese verwenden. Ein Beispiel könnte das Parsen von curl sein Ausgabe, um einige Daten von einer API zu erhalten. Es gibt wahrscheinlich eine Tonne http Bibliotheken, die für Ihre bevorzugte Programmiersprache verfügbar sind, anstatt den curl zu parsen oder wget oder fetch Ausgang.

In meinem Fall muss ich ein altes Programm verwenden, um eine Closed-Source-Datei zu analysieren, um eine binäre Ausgabe zu erhalten. Dies ist eine vorübergehende Situation, eine native Parsing-Bibliothek ist ebenfalls in der Entwicklung. Die Binärdatei befindet sich unter meiner Kontrolle, ebenso wie die Systemeinstellungen, die Sprache, andere Tools und dergleichen, daher war für diesen speziellen Anwendungsfall die Lösung zum Analysieren der Befehlszeilenausgabe vorerst akzeptabel.

Beachten Sie, dass ich in diesem Beitrag die Begriffe Nullbyte, Nullzeichen, Nullterminierung und Nullterminierung vertausche. Sie bedeuten alle dasselbe, das Null-Byte-Zeichen, das zum Beenden eines C-Strings verwendet wird (\0 , oder ^@ , U+0000 oder 0x00 , du verstehst das Wesentliche).

Wenn Sie mehr Funktionen, mehr plattformübergreifende oder asynchrone Ausführung benötigen, ist boost.Process eine großartige Alternative. Aufgrund von Compiler- und Größenbeschränkungen kann ich jedoch boost nicht in der Umgebung verwenden, in der dieser Code ausgeführt wird.

Das Stackoverflow-Beispiel mit fgets

Bei Stackoverflow ist das angegebene Beispiel eine gute Basis, um darauf aufzubauen, aber um den Exit-Code und die Ausgabe zu erhalten, muss es geändert werden. Da wir auch den Exit-Code erfassen wollen, können wir das Beispiel, das den std::unique_ptr verwendet, nicht verwenden . Was an sich schon ein großartiges Beispiel für die Verwendung eines unique_ptr ist mit einem benutzerdefinierten Löschprogramm (cmd ist ein const char* mit dem auszuführenden Befehl:

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

Der Code wird unten kopiert:

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;
}

Dieses Beispiel tut, was es sagt, aber mit ein paar Fallstricken. Es verwendet einen FILE* (Zeiger), char Pufferzuordnung und manuelles Schließen des FILE* wenn etwas schief geht (catch ). Der unique_ptr Beispiel ist moderner, weil es die Ausnahme nicht behandeln muss und einen std::array<char, 128> verwendet anstelle eines char* im C-Stil Puffer. Das Auslösen von Ausnahmen ist ein ganz anderes Thema, aber darauf gehen wir heute nicht ein. Worauf wir heute eingehen werden, ist Code im C-Stil, der von FILE* gelesen werden kann und wie binäre Daten gehandhabt werden.

Das Stackoverflow-Beispiel ist wahrscheinlich in Ordnung, wenn Sie nur eine Textausgabe in ein std::string benötigen . Mein Anwendungsfall war jedoch etwas komplexer, wie Sie beim Lesen des restlichen Artikels herausfinden werden.

Fread gegen Fgets

Abgesehen vom Codestil war mein größtes Problem die Verwendung von fgets auf diese Weise kombiniert mit dem Hinzufügen eines const char* zu einem std::string stoppt, wenn es auf nullyte trifft (\0 ). Für die reguläre Zeichenfolgenausgabe, die oft kein Problem darstellt, geben die meisten Befehle nur ein paar Zeichenfolgen aus und machen Schluss mit dem Tag. Meine Ausgabe gibt ein binäres Blob zurück, das Nullbytes enthalten kann. fread liest eine Anzahl von Bytes und gibt zurück, wie viele erfolgreich gelesen wurden, was wir verwenden können, wenn wir die Ausgabe zu unserem std::string hinzufügen einschließlich der Nullbytes.

Das obige Beispiel macht result += buffer , indem Sie einen const char* hinzufügen zu einem std::string , in diesem Fall gemäß cpreference on operator+=on std::string:Appends the null-terminated character string pointed to by s.

Das Problem dabei liegt darin, dass in meinem Fall auch die Zeichen nach dem Nullbyte hinzugefügt werden sollten. fgets gibt die gelesene Datenmenge nicht zurück. Mit fgets und einen Puffer von 128, wenn ich ein Nullbyte bei 10 und eine neue Zeile bei 40 habe, dann werden die ersten 10 Bytes plus das, was nach 40 Bytes ist, zurückgegeben. Tatsächlich verlieren wir alles zwischen dem Nullbyte und dem Zeilenumbruch oder bis zum Ende des Puffers (128), wenn dazwischen kein Zeilenumbruch steht.

fread gibt die Anzahl der gelesenen Bytes zurück. Kombinieren Sie das mit einem Konstruktor von std::string das dauert ein const char* und ein size_t wir können den gesamten Inhalt innerhalb der Zeichenfolge erzwingen. Dies ist sicher, da astd::string seine Größe kennt, ist es nicht auf ein Nullterminierungszeichen angewiesen. Anderer Code jedoch, der const char* verwendet nicht in der Lage sein, mit diesen Nullbytes zu arbeiten, denken Sie daran.

Dieser Stackoverflow-Beitrag war sehr hilfreich für mich, fread zu verstehen , sowie die Hilfe eines Kollegen, der in C träumt , er hat viel über das Innenleben erklärt.

Und falls Sie sich nach all dem fragen, warum ich binäre Daten in einen std::string stecke , tolle Frage. Ich werde wahrscheinlich ein anderes Mal darauf eingehen, da dies einen längeren Post als diesen gesamten Artikel erfordern würde.

Befehlsausführung inklusive Ausgabe und Exit-Code

Mein Code überprüft den Exit-Status der ausgeführten Binärdatei (zur Fehlerbehandlung) und verwendet die zurückgegebenen Daten zur weiteren Verarbeitung. Um dies alles an einem praktischen Ort zu halten, beginnen wir mit der Definition einer Struktur, die diese Daten enthält. Es enthält das Ergebnis von command , also der Name CommandResult klingt beschreibend genug.

Unten finden Sie den Strukturcode, einschließlich eines Gleichheitsoperators sowie eines Stream-Ausgabeoperators.

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);
    }
};

Das Fleisch und die Kartoffeln des Structs sind natürlich output und exitstatus . Ich denke an int für den Austrittsstatus aus Gründen.

Der nächste Teil ist der Command Klasse selbst, hier ist dieser Code:

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};
    }
}

Der fread Der Befehl wird ausgeführt, bis keine Bytes mehr von der Befehlsausgabe zurückgegeben werden. Ich kenne die Art der Ausgabe, mit der ich arbeite, daher ist mein Puffer 1 MB groß, was wahrscheinlich zu groß für Ihre Daten ist. In meinem Fall habe ich es bewertet und zwischen 10 KiB und 1 MiB war das schnellste auf der Zielarchitektur. 128 oder 8192 ist wahrscheinlich auch in Ordnung, aber Sie sollten das für sich selbst vergleichen. Ein ziemlich einfacher Test besteht darin, eine riesige Datei mit cat auszugeben und nehmen Sie die Ausführungszeit plus CPU- und Speicherauslastung. Drucken Sie das Ergebnis nicht aus, sehen Sie sich nur diese drei Dinge an und wählen Sie, welches Verhältnis für Sie akzeptabel ist.

Warum nicht auch den std::string initialisieren mit 1 MiB Zeichen? std::strings können bei der Konstruktion nicht für eine bestimmte Größe zugewiesen werden, außer durch Ausfüllen oder anschließendes Aufrufen von .reserve() , meine Benchmarks zeigten auch keine nennenswerte Geschwindigkeits- oder Leistungssteigerung.

Die Verwendung des obigen Codes ist einfach. Da es sich um eine statische Funktion handelt, benötigen Sie keine Klasseninstanz, um sie zu verwenden. Hier ist ein Beispiel:

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

Was ergibt:

command exitstatus: 0 output: Hello you absolute legends!

Da wir eine Shell durchlaufen, funktioniert auch die Umleitung. Umleitung stdout bis stderr ergibt keine Ausgabe, nur einen Exit-Status:

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

Die Ausgabe erfolgt auf stderr in meiner Shell jedoch, was erwartet wird:

Wenn Sie stderr erfassen müssen dann leiten Sie die Ausgabe andersherum um, likeso:

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

Pipes funktionieren genauso gut in Ihrer Shell, aber beachten Sie, dass dies alles sh verwendet und Sie haben keine Kontrolle über Umgebungsvariablen oder die Standard-Shell. Lesen Sie mehr auf der POSIX-Seite zu popen um herauszufinden, warum das so ist.

Ein Hinweis zu Windows

Hier ist ein Beispiel für Windows, wo wir _popen verwenden müssen und _pclose :

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

Der Exit-Code ist immer Null, da dieses Konzept nicht auf Windows übertragbar ist. Es gibt %ErrorLevel% , aber das ist nur eine Umgebungsvariable für Konsolenanwendungen, nicht der tatsächliche Beendigungsstatus.

Auf der Microsoft-Seite wird auch darauf hingewiesen, dass _popen funktioniert nicht mit GUI-Anwendungen, sondern nur mit Konsolenprogrammen. Wenn Sie das brauchen, verwenden Sie Boost.process oder system .

Nullbytes im Ausgabebeispiel:

Im Beispielcode auf github sehen Sie auch einen execFgets Funktion habe ich das drin gelassen, um den Unterschied in der Nullbyte-Behandlung zu zeigen. Als Referenz zeige ich auch hier ein Beispiel. Der relevante Teil des Befehls mit fgets :

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

Der Teil mit fread :

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

Der Testbefehl, einschließlich eines Clang-Tidy-Warnungsausschlusses (// 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;
}

Ausgabe:

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

Das Nullbyte-Zeichen wird durch \0 ersetzt in der obigen Ausgabe. Hier ist ein Screenshot, der zeigt, wie es in meinem Terminal aussieht:

Beachten Sie noch einmal, dass dies sicher mit std::strings verwendet werden kann , Methoden, die einen string_view annehmen oder const char* wird wahrscheinlich nicht sehr gut auf Nullbytes reagieren. Für meinen Anwendungsfall ist dies sicher, Ihr Kilometerstand kann variieren.

Versuchen Sie, mit buffer zu spielen Größe und schauen Sie sich dann die Ausgabe an. Wenn Sie es auf 4 setzen, wird die Ausgabe mit fgets ausgegeben ist testbc . Lustig, richtig? Ich mag solche Dinge.

Vollständiger Code

Unten finden Sie die Header-Datei command.h . Es ist auch auf meinem github. Wenn Sie Anwendungsbeispiele wünschen, finden Sie diese im Github-Projekt main.cpp Datei.

#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