diff options
-rw-r--r-- | include/winapi/cmd_line.hpp | 8 | ||||
-rw-r--r-- | src/cmd_line.cpp | 59 | ||||
-rw-r--r-- | test/unit_tests/cmd_line.cpp | 81 |
3 files changed, 119 insertions, 29 deletions
diff --git a/include/winapi/cmd_line.hpp b/include/winapi/cmd_line.hpp index 176cf3d..c6e8c65 100644 --- a/include/winapi/cmd_line.hpp +++ b/include/winapi/cmd_line.hpp @@ -29,6 +29,10 @@ public: CommandLine(std::string&& argv0, std::vector<std::string>&& args = {}) : argv0(std::move(argv0)), args(std::move(args)) {} + CommandLine(const std::vector<std::string>& argv); + + CommandLine(std::vector<std::string>&& argv); + static std::string escape(const std::string&); static std::string escape_cmd(const std::string&); @@ -43,6 +47,8 @@ public: const std::vector<std::string>& get_args() const { return args; } + std::vector<std::string> get_argv() const; + private: static BOOST_CONSTEXPR char token_sep() { return ' '; } @@ -50,6 +56,8 @@ private: std::vector<std::string> escape_args() const; + std::vector<std::string> escape_argv() const; + std::string argv0; std::vector<std::string> args; }; diff --git a/src/cmd_line.cpp b/src/cmd_line.cpp index 468bb96..2f9be41 100644 --- a/src/cmd_line.cpp +++ b/src/cmd_line.cpp @@ -57,6 +57,23 @@ CommandLine do_parse(std::wstring src) { return {std::move(argv0), std::move(args)}; } +std::string split_argv0(std::vector<std::string>& argv) { + if (argv.empty()) { + throw std::range_error{"argv must contain at least one element"}; + } + const auto argv0 = argv[0]; + argv.erase(argv.begin()); + return argv0; +} + +std::vector<std::string> escape_all(const std::vector<std::string>& xs) { + std::vector<std::string> escaped; + escaped.reserve(xs.size()); + for (const auto& x : xs) + escaped.emplace_back(CommandLine::escape(x)); + return escaped; +} + } // namespace CommandLine CommandLine::query() { @@ -71,17 +88,21 @@ CommandLine CommandLine::from_main(int argc, wchar_t* argv[]) { if (argc < 1) throw std::range_error{"argc must be a positive number"}; - std::string argv0{narrow(argv[0])}; - --argc; - ++argv; - - std::vector<std::string> args; - args.reserve(argc); + std::vector<std::string> utf8_argv; + utf8_argv.reserve(argc); for (int i = 0; i < argc; ++i) - args.emplace_back(narrow(argv[i])); + utf8_argv.emplace_back(narrow(argv[i])); - return {std::move(argv0), std::move(args)}; + return {std::move(utf8_argv)}; +} + +CommandLine::CommandLine(const std::vector<std::string>& argv) : args(argv) { + argv0 = split_argv0(args); +} + +CommandLine::CommandLine(std::vector<std::string>&& argv) : args(std::move(argv)) { + argv0 = split_argv0(args); } std::string CommandLine::escape(const std::string& arg) { @@ -135,23 +156,25 @@ std::string CommandLine::escape_cmd(const std::string& arg) { } std::string CommandLine::to_string() const { - std::ostringstream oss; - oss << escape(get_argv0()); - if (has_args()) - oss << token_sep() << args_to_string(); - return oss.str(); + return boost::algorithm::join(escape_argv(), std::string{token_sep()}); } std::string CommandLine::args_to_string() const { return boost::algorithm::join(escape_args(), std::string{token_sep()}); } +std::vector<std::string> CommandLine::get_argv() const { + auto argv = get_args(); + argv.emplace(argv.begin(), get_argv0()); + return argv; +} + std::vector<std::string> CommandLine::escape_args() const { - std::vector<std::string> safe; - safe.reserve(args.size()); - for (const auto& arg : args) - safe.emplace_back(escape(arg)); - return safe; + return escape_all(get_args()); +} + +std::vector<std::string> CommandLine::escape_argv() const { + return escape_all(get_argv()); } } // namespace winapi diff --git a/test/unit_tests/cmd_line.cpp b/test/unit_tests/cmd_line.cpp index 99646ff..42f4a93 100644 --- a/test/unit_tests/cmd_line.cpp +++ b/test/unit_tests/cmd_line.cpp @@ -5,30 +5,89 @@ #include <winapi/cmd_line.hpp> +// clang-format off +// The order matters for older Boost versions. #include <boost/test/unit_test.hpp> +#include <boost/test/data/test_case.hpp> +#include <boost/test/data/monomorphic.hpp> +// clang-format on +#include <ostream> #include <string> +#include <vector> + +using namespace winapi; + +namespace std { + +ostream& operator<<(ostream& os, const vector<string>& xs) { + os << "[\n"; + for (const auto& x : xs) { + os << x << '\n'; + } + os << "]"; + return os; +} + +} // namespace std + +BOOST_TEST_SPECIALIZED_COLLECTION_COMPARE(std::vector<std::string>); + +namespace { + +// MSDN examples +// https://docs.microsoft.com/en-us/cpp/c-language/parsing-c-command-line-arguments?view=vs-2019 + +const std::vector<std::string> msdn_string{ + R"(test.exe "abc" d e)", + R"(test.exe a\\\b d"e f"g h)", + R"(test.exe a\\\"b c d)", + R"(test.exe a\\\\"b c" d e)", +}; + +const std::vector<std::vector<std::string>> msdn_argv{ + {"test.exe", "abc", "d", "e"}, + {"test.exe", R"(a\\\b)", "de fg", "h"}, + {"test.exe", R"(a\"b)", "c", "d"}, + {"test.exe", R"(a\\b c)", "d", "e"}, +}; + +} // namespace BOOST_AUTO_TEST_SUITE(cmd_line_tests) BOOST_AUTO_TEST_CASE(query) { - const auto cmd_line = winapi::CommandLine::query(); - BOOST_TEST(!cmd_line.get_argv0().empty()); - BOOST_TEST_MESSAGE(cmd_line.get_argv0()); + const auto cmd_line = CommandLine::query(); + BOOST_TEST(!cmd_line.get_argv0().empty(), "argv[0]: " << cmd_line.get_argv0()); } -BOOST_AUTO_TEST_CASE(escape) { - wchar_t* argv[] = { - L"test.exe", - L"arg1 arg2", - LR"(path\to\file)", - LR"(path\to\dir\)", - LR"(weird\\argument)", +BOOST_AUTO_TEST_CASE(to_string) { + const std::vector<std::string> argv{ + "test.exe", + "arg1 arg2", + R"(path\to\file)", + R"(path\to\dir\)", + R"(weird\\argument)", }; - const auto cmd_line = winapi::CommandLine::from_main(5, argv); + const CommandLine cmd_line{argv}; const auto expected = R"("test.exe" "arg1 arg2" "path\to\file" "path\to\dir\\" "weird\\argument")"; BOOST_TEST(cmd_line.to_string() == expected); } +BOOST_DATA_TEST_CASE(msdn_parse, + boost::unit_test::data::make(msdn_string) ^ msdn_argv, + input, + expected) { + const auto cmd_line = CommandLine::parse(input); + const auto actual = cmd_line.get_argv(); + BOOST_TEST(actual == expected, "actual: " << actual); +} + +BOOST_DATA_TEST_CASE(msdn_to_string, msdn_argv, argv) { + const CommandLine cmd_line{argv}; + const auto actual = CommandLine::parse(cmd_line.to_string()).get_argv(); + BOOST_TEST(actual == argv, "actual: " << actual); +} + BOOST_AUTO_TEST_SUITE_END() |