diff --git a/.gitignore b/.gitignore index 259148f..3a02d2c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,32 +1,23 @@ -# Prerequisites -*.d +# Ignore cmake generated directories and files. +build/ +out/ +/CMakeSettings.json -# Compiled Object files -*.slo -*.lo -*.o -*.obj +# Visual Studio files +.vs +*.suo +*.user -# Precompiled Headers -*.gch -*.pch +# VSCode files +.vscode/ +.cache/ +cmake-variants.yaml -# Compiled Dynamic libraries -*.so -*.dylib -*.dll +# macOS files +.DS_Store -# Fortran module files -*.mod -*.smod +# Idea files +.idea/ -# Compiled Static libraries -*.lai -*.la -*.a -*.lib - -# Executables -*.exe -*.out -*.app +# python +*.pyc diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..4e8f70a --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,23 @@ +cmake_minimum_required(VERSION 3.19) +project(cmdout.sln VERSION 0.0) + +#set(CMAKE_CXX_STANDARD 11) +#set(CMAKE_CXX_STANDARD 14) +set(CMAKE_CXX_STANDARD 17) +#set(CMAKE_CXX_STANDARD 20) +#set(CMAKE_CXX_STANDARD 23) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +if(NOT CYGWIN AND NOT MSYS AND NOT ${CMAKE_SYSTEM_NAME} STREQUAL QNX) + set(CMAKE_CXX_EXTENSIONS OFF) +endif() + +include(CMakeDependentOption) +include(GNUInstallDirs) + +option(BUILD_DEMO "Builds the demo subproject" ON) +option(INSTALL_CMDOUT "Enable installation of cmdout. (Projects embedding cmdout may want to turn this OFF.)" ON) + +add_subdirectory(cmdout) +if(BUILD_DEMO) + add_subdirectory(demo) +endif() diff --git a/LICENSE b/LICENSE index 54bea55..3a19aea 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2023 Myvas +Copyright (c) 2023 Myvas Foundation Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index e620556..cc810fb 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,47 @@ # cmdout -To execute a command and get its output: stdout, stderr, status (exit code) +To execute a shell command and get its output, including stdout, stderr, status (exit code). + +There are 3 overloads: +``` +cmdout system(const char *cmd); +cmdout system(const char *cmd, unsigned timeout_ms); +cmdout system(const char *cmd, std::chrono::milliseconds timeout_ms); +``` +- All of them will return a result or a timeout error, both are wrapped in a `cmdout` object. +- Each of them will wait for the result to become available until specified timeout has elapsed. +- The default value of timeout is **10 seconds**, if the `timeout_ms` argument is 0 or unspecified. + +### Todo list +- By now, it works well on Linux; but it should work on more platforms. + +## Getting Started +### CMake file +``` +FetchContent_Declare( + cmdout + GIT_REPOSITORY https://github.com/myvas/cmdout.git + GIT_TAG 0.0.1 # release-0.0.1 +) +FetchContent_MakeAvailable(cmdout) + +#target_link_libraries( PRIVATE cmdout) +``` +### Include the header file +``` +#include +``` + +## Demo +### C++17 on GNU/Linux Debian 11 (bullseye) +``` +#include +#include + +int main() { + auto result22 = myvas::system("ls not-exist 2>&1"); + std::cout << result22 << std::endl; + + auto result42 = myvas::system("ls / -l", std::chrono::milliseconds(4)); + std::cout << result42 << std::endl; +} +``` diff --git a/cmdout/CMakeLists.txt b/cmdout/CMakeLists.txt new file mode 100644 index 0000000..8f3d55d --- /dev/null +++ b/cmdout/CMakeLists.txt @@ -0,0 +1,11 @@ +find_package(Threads) + +add_library(cmdout) +target_include_directories(cmdout PUBLIC include) +target_sources(cmdout PRIVATE src/cmdout.cpp "src/system.cpp") +target_link_libraries(cmdout PRIVATE ${CMAKE_THREAD_LIBS_INIT}) +#set_target_properties(cmdout PROPERTIES COMPILE_FLAGS "-pthread" LINK_FLAGS "-pthread") + +if(INSTALL_CMDOUT) + install(TARGETS cmdout) +endif() \ No newline at end of file diff --git a/cmdout/include/cmdout.hpp b/cmdout/include/cmdout.hpp new file mode 100644 index 0000000..9d421ec --- /dev/null +++ b/cmdout/include/cmdout.hpp @@ -0,0 +1,125 @@ +/** + * Copyright (c) 2023 Myvas Foundation + * SPDX-License-Identifier: MIT + * + * @file cmdout.hpp + * @brief `myvas::system()` is used to invoke a shell command and get its output, within timeout feature. + * + * @see {std::system()} + */ +#pragma once + +#include +#include +#include +#include +#include + +namespace myvas { + +/** + * @brief Executor to execute a shell command and get its output. + */ +class cmdout +{ + int status_; + std::string out_; + std::string cmd_; + +public: + /** + * @brief Gets the exit code after `exec` returned. + * + * The value is one of the following: + * - If `cmd` is empty, then a nonzero value if a shell is available, or 0 if + * no shell is available. + * - If a child process in `cmd` could not be created, or its status could not + * be retrieved, the return value is -1 and `errno` is set to indicate the + * error. + * - If a shell could not be executed, then the return value is 127. + * - If all system call succeed, then the return value is the status of the + * last command in `cmd`. + */ + int status() const; + + void status(int value); + + /** + * @brief Gets the content of `stdout` (file descriptor 1). + */ + std::string_view out() const; + + void out(std::string_view value); + + /** + * @brief Gets the shell command line. + */ + std::string_view cmd() const; + + void cmd(std::string_view value); + + /** + * @brief Default to cmdout(ENOTSUP, "Not supported"). + */ + cmdout(); + + cmdout(std::string_view cmd); + + /** + * @brief Constructs an output. + * + * @param [in] status EXIT_SUCCESS, EXIT_FAILURE or errno + * @see `cerrno` and `cstdlib`. + */ + explicit cmdout(int status, std::string_view out, std::string_view cmd); + + cmdout(const cmdout&); + cmdout& operator=(const cmdout&); + cmdout(cmdout&&); + cmdout& operator=(cmdout&&); + + virtual ~cmdout(); +}; + +/** + * @brief Execute a shell command specified in `cmd`, waiting for the result until specified timeout (10 seconds) has elapsed. + * NOTE: A shell command with a tail of `2>&1` will catch both stdout and stderr, otherwise stdout only. + * + * @param [in] cmd The `cmd` argument is a pointer to a null-terminated string + * containing a shell command. + * + * @return The return value is a `cmdout` object, in which wraps the status code + * and output string of the shell command. + */ +cmdout system(const char* cmd); + +/** + * @brief Execute a shell command specified in `cmd`, waiting for the result until specified timeout has elapsed. + * NOTE: A shell command with a tail of `2>&1` will catch both stdout and + * stderr, otherwise stdout only. + * + * @param [in] cmd The `cmd` argument is a pointer to a null-terminated string + * containing a shell command. + * + * @param [in] timeout_ms timeout in milliseconds. (If zero, defaults to 10 seconds) + * + * @return The return value is a `cmdout` object, in which wraps the status code + * and output string of the shell command. + */ +cmdout system(const char* cmd, unsigned timeout_ms); + +/** + * @brief Execute a shell command specified in `cmd`, waiting for the result until specified timeout has elapsed. + * NOTE: A shell command with a tail of `2>&1` will catch both stdout and stderr, otherwise stdout only. + * + * @param [in] cmd The `cmd` argument is a pointer to a null-terminated string + * containing a shell command. + * + * @param [in] timeout_ms timeout in milliseconds. (If zero, defaults to 10 seconds) + * + * @return The return value is a `cmdout` object, in which wraps the status code + * and output string of the shell command. + */ +cmdout system(const char* cmd, std::chrono::milliseconds timeout_ms); + +} // namespace myvas \ No newline at end of file diff --git a/cmdout/include/cmdout_ext.hpp b/cmdout/include/cmdout_ext.hpp new file mode 100644 index 0000000..2bda67b --- /dev/null +++ b/cmdout/include/cmdout_ext.hpp @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2023 Myvas Foundation + * SPDX-License-Identifier: MIT + * + * @file cmdout_ext.hpp + * @brief Helpers for `cmdout`. + */ +#pragma once + +#include "cmdout.hpp" + +#include + +/** + * @brief Helper for printing a cmdout object. + */ +std::ostream& operator<<(std::ostream& os, const myvas::cmdout& value) +{ + os << "cmdout {" << value.cmd() << "} return " << value.status() + << " length " << value.out().length() << "\n" << value.out(); + return os; +} \ No newline at end of file diff --git a/cmdout/src/cmdout.cpp b/cmdout/src/cmdout.cpp new file mode 100644 index 0000000..b40a178 --- /dev/null +++ b/cmdout/src/cmdout.cpp @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2023 Myvas Foundation + * SPDX-License-Identifier: MIT + * + * @file cmdout.cpp + * @brief Implementation of class `cmdout`. + */ +#include "cmdout.hpp" + +#include +#include +#include +#include + +namespace myvas { + +int cmdout::status() const { return status_; } + +void cmdout::status(int value) { status_ = value; } + +std::string_view cmdout::out() const { return out_; } + +void cmdout::out(std::string_view value) { out_ = value; } + +std::string_view cmdout::cmd() const { return cmd_; } + +void cmdout::cmd(std::string_view value) { cmd_ = value; } + +cmdout::cmdout() + : status_(ENOTSUP), out_("Not supported"), cmd_("") +{ +} + +cmdout::cmdout(std::string_view cmd) + : status_(ENOTSUP), out_("Not supported"), cmd_(cmd) +{ +} + +cmdout::cmdout(int status, std::string_view out, std::string_view cmd) + : status_(status), out_(out), cmd_(cmd) +{ +} + +cmdout::cmdout(const cmdout&) = default; +cmdout& cmdout::operator=(const cmdout&) = default; +cmdout::cmdout(cmdout&&) = default; +cmdout& cmdout::operator=(cmdout&&) = default; +cmdout::~cmdout() = default; + +} // namespace myvas \ No newline at end of file diff --git a/cmdout/src/system.cpp b/cmdout/src/system.cpp new file mode 100644 index 0000000..a4adae1 --- /dev/null +++ b/cmdout/src/system.cpp @@ -0,0 +1,111 @@ +/** + * Copyright (c) 2023 Myvas Foundation + * SPDX-License-Identifier: MIT + * + * @file system.cpp + * @brief Implementation of function `myvas::system()`. + */ +#include "cmdout.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace myvas { + +constexpr std::chrono::milliseconds CMDOUT_TIMEOUT = std::chrono::seconds(10); + +/** + * @brief Execute a shell command specified in `cmd`. + */ +cmdout do_system(const char* cmd) +{ + assert(cmd != nullptr); + + cmdout result(cmd); + std::string out; + std::array buff{}; + + FILE* fp = popen(cmd, "r"); + if (fp == NULL) + { + result.status(EXIT_FAILURE); + result.out("popen() failed!"); + return result; + } + + try + { + size_t n; + while (n = fread(buff.data(), sizeof(buff.at(0)), sizeof(buff), fp), n != 0) + { + out += std::string(buff.data(), n); + } + } + catch (const std::exception& ex) + { + out += ex.what(); + } + + auto res = pclose(fp); + auto status = WEXITSTATUS(res); + + result.out(out); + result.status(status); + return result; +} + +cmdout do_system_timeout(const char* cmd, std::chrono::milliseconds timeout_ms) +{ + assert(cmd != nullptr); + + if (timeout_ms.count() < 1) + timeout_ms = CMDOUT_TIMEOUT; + + std::future future = std::async(std::launch::async, [&cmd]() { + return myvas::do_system(cmd); + }); + + std::future_status status; + do + { + switch (status = future.wait_for(timeout_ms); status) + { + case std::future_status::timeout: + { + auto s = "Timeout: The command did not return before specified timeout duration (" + + std::to_string(timeout_ms.count()) + " ms) has passed."; + return cmdout(ETIME, s, cmd); // #include , ECANCELED or ETIME + } break; + case std::future_status::ready: + { + return future.get(); + } break; + default: + break; + } + } while (status != std::future_status::ready && + status != std::future_status::timeout); + + return cmdout(cmd); +} + +cmdout system(const char* cmd, std::chrono::milliseconds timeout_ms) +{ + if (cmd == nullptr) + { + return do_system("exit 0"); + } + return do_system_timeout(cmd, timeout_ms); +} + +cmdout system(const char* cmd, unsigned timeout_ms) { return system(cmd, std::chrono::milliseconds(timeout_ms)); } + +cmdout system(const char* cmd) { return system(cmd, CMDOUT_TIMEOUT); } + +} // namespace myvas \ No newline at end of file diff --git a/demo/CMakeLists.txt b/demo/CMakeLists.txt new file mode 100644 index 0000000..88cd0bd --- /dev/null +++ b/demo/CMakeLists.txt @@ -0,0 +1,3 @@ +add_executable(demo) +target_sources(demo PRIVATE src/main.cpp) +target_link_libraries(demo PRIVATE cmdout) \ No newline at end of file diff --git a/demo/src/main.cpp b/demo/src/main.cpp new file mode 100644 index 0000000..64ffd27 --- /dev/null +++ b/demo/src/main.cpp @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2023 Myvas Foundation + * SPDX-License-Identifier: MIT + * + * @file main.cpp + * @brief Demonstrate the usage of `myvas::system()`. + */ +#include +#include + +int main() +{ + // std::system(cmd) + auto result10 = std::system(NULL); + std::cout << result10 << std::endl; + + auto result11 = std::system("ls ~/"); + std::cout << result11 << std::endl; + + auto result12 = std::system("ls not-exist 2>&1"); + std::cout << result12 << std::endl; + + // myvas::system(cmd) + auto result20 = myvas::system(NULL); + std::cout << result20 << std::endl; + + auto result21 = myvas::system("ls ~/"); + std::cout << result21 << std::endl; + + auto result22 = myvas::system("ls not-exist 2>&1"); + std::cout << result22 << std::endl; + + auto result23 = myvas::system("ls / -l"); + std::cout << result23 << std::endl; + + // myvas::system(cmd, timeout) + auto result40 = myvas::system("ls / -l", std::chrono::milliseconds(0)); + std::cout << result40 << std::endl; + + auto result41 = myvas::system("ls / -l", std::chrono::milliseconds(1)); + std::cout << result41 << std::endl; + + auto result42 = myvas::system("ls / -l", std::chrono::milliseconds(9)); + std::cout << result42 << std::endl; + + auto result5 = myvas::system("ls not-exist 2>&1", std::chrono::milliseconds(9)); + std::cout << result5 << std::endl; +} \ No newline at end of file