diff --git a/extern/json b/extern/json index db53bdac..f1768a54 160000 --- a/extern/json +++ b/extern/json @@ -1 +1 @@ -Subproject commit db53bdac1926d1baebcb459b685dcd2e4608c355 +Subproject commit f1768a540a7b7c5cc30cdcd6be9e9ef91083719b diff --git a/include/CLI/App.hpp b/include/CLI/App.hpp index 42d3bf88..237530a7 100644 --- a/include/CLI/App.hpp +++ b/include/CLI/App.hpp @@ -1167,6 +1167,58 @@ class App { parse(args); } + /// parse a single string as if it contained command line arguments + /// this function splits the string into arguments then calls parse(std::vector &) + /// the function takes an optional boolean argument specifying if the programName is included in the string to + /// process + void parse(std::string commandline, bool ProgramNameIncluded = false) { + detail::trim(commandline); + if(ProgramNameIncluded) { + // try to determine the programName + auto esp = commandline.find_first_of(' ', 1); + while(!ExistingFile(commandline.substr(0, esp)).empty()) { + esp = commandline.find_first_of(' ', esp + 1); + if(esp == std::string::npos) { + // if we have reached the end and haven't found a valid file just assume the first argument is the + // program name + esp = commandline.find_first_of(' ', 1); + break; + } + } + if(name_.empty()) { + name_ = commandline.substr(0, esp); + detail::rtrim(name_); + } + // strip the program name + commandline = commandline.substr(esp + 1); + } + // the first section of code is to deal with quoted arguments after and '=' + if(!commandline.empty()) { + size_t offset = commandline.length() - 1; + auto qeq = commandline.find_last_of('=', offset); + while(qeq != std::string::npos) { + if((commandline[qeq + 1] == '\"') || (commandline[qeq + 1] == '\'') || (commandline[qeq + 1] == '`')) { + auto astart = commandline.find_last_of("- \"\'`", qeq - 1); + if(astart != std::string::npos) { + if(commandline[astart] == '-') { + commandline[qeq] = ' '; // interpret this a space so the split_up works properly + offset = (astart == 0) ? 0 : (astart - 1); + } + } + } + offset = qeq - 1; + qeq = commandline.find_last_of('=', offset); + } + } + + auto args = detail::split_up(std::move(commandline)); + // remove all empty strings + args.erase(std::remove(args.begin(), args.end(), std::string()), args.end()); + std::reverse(args.begin(), args.end()); + + parse(args); + } + /// The real work is done here. Expects a reversed vector. /// Changes the vector to the remaining options. void parse(std::vector &args) { diff --git a/include/CLI/StringTools.hpp b/include/CLI/StringTools.hpp index 55068f95..42e4ba10 100644 --- a/include/CLI/StringTools.hpp +++ b/include/CLI/StringTools.hpp @@ -148,18 +148,38 @@ inline std::string remove_underscore(std::string str) { return str; } +/// Find and replace a substring with another substring +inline std::string find_and_replace(std::string str, std::string from, std::string to) { + + size_t start_pos = 0; + + while((start_pos = str.find(from, start_pos)) != std::string::npos) { + str.replace(start_pos, from.length(), to); + start_pos += to.length(); + } + + return str; +} + /// Split a string '"one two" "three"' into 'one two', 'three' +/// Quote characters can be ` ' or " inline std::vector split_up(std::string str) { - std::vector delims = {'\'', '\"'}; + const std::string delims("\'\"`"); auto find_ws = [](char ch) { return std::isspace(ch, std::locale()); }; trim(str); std::vector output; - + bool embeddedQuote = false; + char keyChar = ' '; while(!str.empty()) { - if(str[0] == '\'') { - auto end = str.find('\'', 1); + if(delims.find_first_of(str[0]) != std::string::npos) { + keyChar = str[0]; + auto end = str.find_first_of(keyChar, 1); + while((end != std::string::npos) && (str[end - 1] == '\\')) { // deal with escaped quotes + end = str.find_first_of(keyChar, end + 1); + embeddedQuote = true; + } if(end != std::string::npos) { output.push_back(str.substr(1, end - 1)); str = str.substr(end + 1); @@ -167,16 +187,6 @@ inline std::vector split_up(std::string str) { output.push_back(str.substr(1)); str = ""; } - } else if(str[0] == '\"') { - auto end = str.find('\"', 1); - if(end != std::string::npos) { - output.push_back(str.substr(1, end - 1)); - str = str.substr(end + 1); - } else { - output.push_back(str.substr(1)); - str = ""; - } - } else { auto it = std::find_if(std::begin(str), std::end(str), find_ws); if(it != std::end(str)) { @@ -188,9 +198,13 @@ inline std::vector split_up(std::string str) { str = ""; } } + // transform any embedded quotes into the regular character + if(embeddedQuote) { + output.back() = find_and_replace(output.back(), std::string("\\") + keyChar, std::string(1, keyChar)); + embeddedQuote = false; + } trim(str); } - return output; } @@ -210,18 +224,5 @@ inline std::string fix_newlines(std::string leader, std::string input) { return input; } -/// Find and replace a subtring with another substring -inline std::string find_and_replace(std::string str, std::string from, std::string to) { - - size_t start_pos = 0; - - while((start_pos = str.find(from, start_pos)) != std::string::npos) { - str.replace(start_pos, from.length(), to); - start_pos += to.length(); - } - - return str; -} - } // namespace detail } // namespace CLI diff --git a/tests/AppTest.cpp b/tests/AppTest.cpp index f8699a7b..af3a551a 100644 --- a/tests/AppTest.cpp +++ b/tests/AppTest.cpp @@ -38,6 +38,18 @@ TEST_F(TApp, DashedOptions) { EXPECT_EQ((size_t)2, app.count("--that")); } +TEST_F(TApp, DashedOptionsSingleString) { + app.add_flag("-c"); + app.add_flag("--q"); + app.add_flag("--this,--that"); + + app.parse("-c --q --this --that"); + EXPECT_EQ((size_t)1, app.count("-c")); + EXPECT_EQ((size_t)1, app.count("--q")); + EXPECT_EQ((size_t)2, app.count("--this")); + EXPECT_EQ((size_t)2, app.count("--that")); +} + TEST_F(TApp, OneFlagRef) { int ref; app.add_flag("-c,--count", ref); @@ -58,6 +70,16 @@ TEST_F(TApp, OneString) { EXPECT_EQ(str, "mystring"); } +TEST_F(TApp, OneStringSingleStringInput) { + std::string str; + app.add_option("-s,--string", str); + + app.parse("--string mystring"); + EXPECT_EQ((size_t)1, app.count("-s")); + EXPECT_EQ((size_t)1, app.count("--string")); + EXPECT_EQ(str, "mystring"); +} + TEST_F(TApp, OneStringEqualVersion) { std::string str; app.add_option("-s,--string", str); @@ -68,6 +90,84 @@ TEST_F(TApp, OneStringEqualVersion) { EXPECT_EQ(str, "mystring"); } +TEST_F(TApp, OneStringEqualVersionSingleString) { + std::string str; + app.add_option("-s,--string", str); + app.parse("--string=mystring"); + EXPECT_EQ((size_t)1, app.count("-s")); + EXPECT_EQ((size_t)1, app.count("--string")); + EXPECT_EQ(str, "mystring"); +} + +TEST_F(TApp, OneStringEqualVersionSingleStringQuoted) { + std::string str; + app.add_option("-s,--string", str); + app.parse("--string=\"this is my quoted string\""); + EXPECT_EQ((size_t)1, app.count("-s")); + EXPECT_EQ((size_t)1, app.count("--string")); + EXPECT_EQ(str, "this is my quoted string"); +} + +TEST_F(TApp, OneStringEqualVersionSingleStringQuotedMultiple) { + std::string str, str2, str3; + app.add_option("-s,--string", str); + app.add_option("-t,--tstr", str2); + app.add_option("-m,--mstr", str3); + app.parse("--string=\"this is my quoted string\" -t 'qstring 2' -m=`\"quoted string\"`"); + EXPECT_EQ(str, "this is my quoted string"); + EXPECT_EQ(str2, "qstring 2"); + EXPECT_EQ(str3, "\"quoted string\""); +} + +TEST_F(TApp, OneStringEqualVersionSingleStringQuotedMultipleInMiddle) { + std::string str, str2, str3; + app.add_option("-s,--string", str); + app.add_option("-t,--tstr", str2); + app.add_option("-m,--mstr", str3); + app.parse(R"raw(--string="this is my quoted string" -t "qst\"ring 2" -m=`"quoted string"`")raw"); + EXPECT_EQ(str, "this is my quoted string"); + EXPECT_EQ(str2, "qst\"ring 2"); + EXPECT_EQ(str3, "\"quoted string\""); +} + +TEST_F(TApp, OneStringEqualVersionSingleStringQuotedEscapedCharacters) { + std::string str, str2, str3; + app.add_option("-s,--string", str); + app.add_option("-t,--tstr", str2); + app.add_option("-m,--mstr", str3); + app.parse(R"raw(--string="this is my \"quoted\" string" -t 'qst\'ring 2' -m=`"quoted\` string"`")raw"); + EXPECT_EQ(str, "this is my \"quoted\" string"); + EXPECT_EQ(str2, "qst\'ring 2"); + EXPECT_EQ(str3, "\"quoted` string\""); +} + +TEST_F(TApp, OneStringEqualVersionSingleStringQuotedMultipleWithEqual) { + std::string str, str2, str3, str4; + app.add_option("-s,--string", str); + app.add_option("-t,--tstr", str2); + app.add_option("-m,--mstr", str3); + app.add_option("-j,--jstr", str4); + app.parse("--string=\"this is my quoted string\" -t 'qstring 2' -m=`\"quoted string\"` --jstr=Unquoted"); + EXPECT_EQ(str, "this is my quoted string"); + EXPECT_EQ(str2, "qstring 2"); + EXPECT_EQ(str3, "\"quoted string\""); + EXPECT_EQ(str4, "Unquoted"); +} + +TEST_F(TApp, OneStringEqualVersionSingleStringQuotedMultipleWithEqualAndProgram) { + std::string str, str2, str3, str4; + app.add_option("-s,--string", str); + app.add_option("-t,--tstr", str2); + app.add_option("-m,--mstr", str3); + app.add_option("-j,--jstr", str4); + app.parse("program --string=\"this is my quoted string\" -t 'qstring 2' -m=`\"quoted string\"` --jstr=Unquoted", + true); + EXPECT_EQ(str, "this is my quoted string"); + EXPECT_EQ(str2, "qstring 2"); + EXPECT_EQ(str3, "\"quoted string\""); + EXPECT_EQ(str4, "Unquoted"); +} + TEST_F(TApp, TogetherInt) { int i; app.add_option("-i,--int", i); @@ -107,6 +207,15 @@ TEST_F(TApp, DefaultStringAgain) { EXPECT_EQ(str, "previous"); } +TEST_F(TApp, DefaultStringAgainEmpty) { + std::string str = "previous"; + app.add_option("-s,--string", str); + app.parse(" "); + EXPECT_EQ((size_t)0, app.count("-s")); + EXPECT_EQ((size_t)0, app.count("--string")); + EXPECT_EQ(str, "previous"); +} + TEST_F(TApp, DualOptions) { std::string str = "previous"; @@ -136,6 +245,30 @@ TEST_F(TApp, LotsOfFlags) { EXPECT_EQ((size_t)1, app.count("-A")); } +TEST_F(TApp, LotsOfFlagsSingleString) { + + app.add_flag("-a"); + app.add_flag("-A"); + app.add_flag("-b"); + + app.parse("-a -b -aA"); + EXPECT_EQ((size_t)2, app.count("-a")); + EXPECT_EQ((size_t)1, app.count("-b")); + EXPECT_EQ((size_t)1, app.count("-A")); +} + +TEST_F(TApp, LotsOfFlagsSingleStringExtraSpace) { + + app.add_flag("-a"); + app.add_flag("-A"); + app.add_flag("-b"); + + app.parse(" -a -b -aA "); + EXPECT_EQ((size_t)2, app.count("-a")); + EXPECT_EQ((size_t)1, app.count("-b")); + EXPECT_EQ((size_t)1, app.count("-A")); +} + TEST_F(TApp, BoolAndIntFlags) { bool bflag; @@ -686,7 +819,7 @@ TEST_F(TApp, CallbackFlags) { EXPECT_THROW(app.add_flag_function("hi", func), CLI::IncorrectConstruction); } -#if __cplusplus >= 201402L +#if __cplusplus >= 201402L || _MSC_VER >= 1900 TEST_F(TApp, CallbackFlagsAuto) { size_t value = 0; @@ -1381,7 +1514,7 @@ TEST_F(TApp, RangeDouble) { run(); } -// Check to make sure progromatic access to left over is available +// Check to make sure programmatic access to left over is available TEST_F(TApp, AllowExtras) { app.allow_extras(); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 848f1bae..ffbdf43e 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -33,6 +33,7 @@ set(CLI11_TESTS NewParseTest OptionalTest DeprecatedTest + StringParseTest ) if(WIN32) diff --git a/tests/HelpersTest.cpp b/tests/HelpersTest.cpp index d79354e7..2b6cb976 100644 --- a/tests/HelpersTest.cpp +++ b/tests/HelpersTest.cpp @@ -356,6 +356,20 @@ TEST(SplitUp, Simple) { EXPECT_EQ(oput, result); } +TEST(SplitUp, SimpleDifferentQuotes) { + std::vector oput = {"one", "two three"}; + std::string orig{R"(one `two three`)"}; + std::vector result = CLI::detail::split_up(orig); + EXPECT_EQ(oput, result); +} + +TEST(SplitUp, SimpleDifferentQuotes2) { + std::vector oput = {"one", "two three"}; + std::string orig{R"(one 'two three')"}; + std::vector result = CLI::detail::split_up(orig); + EXPECT_EQ(oput, result); +} + TEST(SplitUp, Layered) { std::vector output = {R"(one 'two three')"}; std::string orig{R"("one 'two three'")"}; diff --git a/tests/StringParseTest.cpp b/tests/StringParseTest.cpp new file mode 100644 index 00000000..a457403c --- /dev/null +++ b/tests/StringParseTest.cpp @@ -0,0 +1,75 @@ +#include "app_helper.hpp" + +#include "gmock/gmock.h" +#include +#include + +TEST_F(TApp, ExistingExeCheck) { + + TempFile tmpexe{"existingExe.out"}; + + std::string str, str2, str3; + app.add_option("-s,--string", str); + app.add_option("-t,--tstr", str2); + app.add_option("-m,--mstr", str3); + + { + std::ofstream out{tmpexe}; + out << "useless string doesn't matter" << std::endl; + } + + app.parse(std::string("./") + std::string(tmpexe) + + " --string=\"this is my quoted string\" -t 'qstring 2' -m=`\"quoted string\"`", + true); + EXPECT_EQ(str, "this is my quoted string"); + EXPECT_EQ(str2, "qstring 2"); + EXPECT_EQ(str3, "\"quoted string\""); +} + +TEST_F(TApp, ExistingExeCheckWithSpace) { + + TempFile tmpexe{"Space File.out"}; + + std::string str, str2, str3; + app.add_option("-s,--string", str); + app.add_option("-t,--tstr", str2); + app.add_option("-m,--mstr", str3); + + { + std::ofstream out{tmpexe}; + out << "useless string doesn't matter" << std::endl; + } + + app.parse(std::string("./") + std::string(tmpexe) + + " --string=\"this is my quoted string\" -t 'qstring 2' -m=`\"quoted string\"`", + true); + EXPECT_EQ(str, "this is my quoted string"); + EXPECT_EQ(str2, "qstring 2"); + EXPECT_EQ(str3, "\"quoted string\""); + + EXPECT_EQ(app.get_name(), std::string("./") + std::string(tmpexe)); +} + +TEST_F(TApp, ExistingExeCheckWithLotsOfSpace) { + + TempFile tmpexe{"this is a weird file.exe"}; + + std::string str, str2, str3; + app.add_option("-s,--string", str); + app.add_option("-t,--tstr", str2); + app.add_option("-m,--mstr", str3); + + { + std::ofstream out{tmpexe}; + out << "useless string doesn't matter" << std::endl; + } + + app.parse(std::string("./") + std::string(tmpexe) + + " --string=\"this is my quoted string\" -t 'qstring 2' -m=`\"quoted string\"`", + true); + EXPECT_EQ(str, "this is my quoted string"); + EXPECT_EQ(str2, "qstring 2"); + EXPECT_EQ(str3, "\"quoted string\""); + + EXPECT_EQ(app.get_name(), std::string("./") + std::string(tmpexe)); +}