Commit 30c2e327d1b2ba67eefd3019d5f9bc660685a316

Authored by Philip Top
Committed by Henry Schreiner
1 parent 3a2c5112

Add single string parsing (#186)

* add Tests and ability to handle program file inclusion in the single string.

add the ability to deal with a single string in the parse command and handle quoted string appropriately

* Add extra test cases for full coverage, clear up escape quote sequencing and handling of extra spaces
1   -Subproject commit db53bdac1926d1baebcb459b685dcd2e4608c355
  1 +Subproject commit f1768a540a7b7c5cc30cdcd6be9e9ef91083719b
... ...
include/CLI/App.hpp
... ... @@ -1167,6 +1167,58 @@ class App {
1167 1167 parse(args);
1168 1168 }
1169 1169  
  1170 + /// parse a single string as if it contained command line arguments
  1171 + /// this function splits the string into arguments then calls parse(std::vector<std::string> &)
  1172 + /// the function takes an optional boolean argument specifying if the programName is included in the string to
  1173 + /// process
  1174 + void parse(std::string commandline, bool ProgramNameIncluded = false) {
  1175 + detail::trim(commandline);
  1176 + if(ProgramNameIncluded) {
  1177 + // try to determine the programName
  1178 + auto esp = commandline.find_first_of(' ', 1);
  1179 + while(!ExistingFile(commandline.substr(0, esp)).empty()) {
  1180 + esp = commandline.find_first_of(' ', esp + 1);
  1181 + if(esp == std::string::npos) {
  1182 + // if we have reached the end and haven't found a valid file just assume the first argument is the
  1183 + // program name
  1184 + esp = commandline.find_first_of(' ', 1);
  1185 + break;
  1186 + }
  1187 + }
  1188 + if(name_.empty()) {
  1189 + name_ = commandline.substr(0, esp);
  1190 + detail::rtrim(name_);
  1191 + }
  1192 + // strip the program name
  1193 + commandline = commandline.substr(esp + 1);
  1194 + }
  1195 + // the first section of code is to deal with quoted arguments after and '='
  1196 + if(!commandline.empty()) {
  1197 + size_t offset = commandline.length() - 1;
  1198 + auto qeq = commandline.find_last_of('=', offset);
  1199 + while(qeq != std::string::npos) {
  1200 + if((commandline[qeq + 1] == '\"') || (commandline[qeq + 1] == '\'') || (commandline[qeq + 1] == '`')) {
  1201 + auto astart = commandline.find_last_of("- \"\'`", qeq - 1);
  1202 + if(astart != std::string::npos) {
  1203 + if(commandline[astart] == '-') {
  1204 + commandline[qeq] = ' '; // interpret this a space so the split_up works properly
  1205 + offset = (astart == 0) ? 0 : (astart - 1);
  1206 + }
  1207 + }
  1208 + }
  1209 + offset = qeq - 1;
  1210 + qeq = commandline.find_last_of('=', offset);
  1211 + }
  1212 + }
  1213 +
  1214 + auto args = detail::split_up(std::move(commandline));
  1215 + // remove all empty strings
  1216 + args.erase(std::remove(args.begin(), args.end(), std::string()), args.end());
  1217 + std::reverse(args.begin(), args.end());
  1218 +
  1219 + parse(args);
  1220 + }
  1221 +
1170 1222 /// The real work is done here. Expects a reversed vector.
1171 1223 /// Changes the vector to the remaining options.
1172 1224 void parse(std::vector<std::string> &args) {
... ...
include/CLI/StringTools.hpp
... ... @@ -148,27 +148,38 @@ inline std::string remove_underscore(std::string str) {
148 148 return str;
149 149 }
150 150  
  151 +/// Find and replace a substring with another substring
  152 +inline std::string find_and_replace(std::string str, std::string from, std::string to) {
  153 +
  154 + size_t start_pos = 0;
  155 +
  156 + while((start_pos = str.find(from, start_pos)) != std::string::npos) {
  157 + str.replace(start_pos, from.length(), to);
  158 + start_pos += to.length();
  159 + }
  160 +
  161 + return str;
  162 +}
  163 +
151 164 /// Split a string '"one two" "three"' into 'one two', 'three'
  165 +/// Quote characters can be ` ' or "
152 166 inline std::vector<std::string> split_up(std::string str) {
153 167  
154   - std::vector<char> delims = {'\'', '\"'};
  168 + const std::string delims("\'\"`");
155 169 auto find_ws = [](char ch) { return std::isspace<char>(ch, std::locale()); };
156 170 trim(str);
157 171  
158 172 std::vector<std::string> output;
159   -
  173 + bool embeddedQuote = false;
  174 + char keyChar = ' ';
160 175 while(!str.empty()) {
161   - if(str[0] == '\'') {
162   - auto end = str.find('\'', 1);
163   - if(end != std::string::npos) {
164   - output.push_back(str.substr(1, end - 1));
165   - str = str.substr(end + 1);
166   - } else {
167   - output.push_back(str.substr(1));
168   - str = "";
  176 + if(delims.find_first_of(str[0]) != std::string::npos) {
  177 + keyChar = str[0];
  178 + auto end = str.find_first_of(keyChar, 1);
  179 + while((end != std::string::npos) && (str[end - 1] == '\\')) { // deal with escaped quotes
  180 + end = str.find_first_of(keyChar, end + 1);
  181 + embeddedQuote = true;
169 182 }
170   - } else if(str[0] == '\"') {
171   - auto end = str.find('\"', 1);
172 183 if(end != std::string::npos) {
173 184 output.push_back(str.substr(1, end - 1));
174 185 str = str.substr(end + 1);
... ... @@ -176,7 +187,6 @@ inline std::vector&lt;std::string&gt; split_up(std::string str) {
176 187 output.push_back(str.substr(1));
177 188 str = "";
178 189 }
179   -
180 190 } else {
181 191 auto it = std::find_if(std::begin(str), std::end(str), find_ws);
182 192 if(it != std::end(str)) {
... ... @@ -188,9 +198,13 @@ inline std::vector&lt;std::string&gt; split_up(std::string str) {
188 198 str = "";
189 199 }
190 200 }
  201 + // transform any embedded quotes into the regular character
  202 + if(embeddedQuote) {
  203 + output.back() = find_and_replace(output.back(), std::string("\\") + keyChar, std::string(1, keyChar));
  204 + embeddedQuote = false;
  205 + }
191 206 trim(str);
192 207 }
193   -
194 208 return output;
195 209 }
196 210  
... ... @@ -210,18 +224,5 @@ inline std::string fix_newlines(std::string leader, std::string input) {
210 224 return input;
211 225 }
212 226  
213   -/// Find and replace a subtring with another substring
214   -inline std::string find_and_replace(std::string str, std::string from, std::string to) {
215   -
216   - size_t start_pos = 0;
217   -
218   - while((start_pos = str.find(from, start_pos)) != std::string::npos) {
219   - str.replace(start_pos, from.length(), to);
220   - start_pos += to.length();
221   - }
222   -
223   - return str;
224   -}
225   -
226 227 } // namespace detail
227 228 } // namespace CLI
... ...
tests/AppTest.cpp
... ... @@ -38,6 +38,18 @@ TEST_F(TApp, DashedOptions) {
38 38 EXPECT_EQ((size_t)2, app.count("--that"));
39 39 }
40 40  
  41 +TEST_F(TApp, DashedOptionsSingleString) {
  42 + app.add_flag("-c");
  43 + app.add_flag("--q");
  44 + app.add_flag("--this,--that");
  45 +
  46 + app.parse("-c --q --this --that");
  47 + EXPECT_EQ((size_t)1, app.count("-c"));
  48 + EXPECT_EQ((size_t)1, app.count("--q"));
  49 + EXPECT_EQ((size_t)2, app.count("--this"));
  50 + EXPECT_EQ((size_t)2, app.count("--that"));
  51 +}
  52 +
41 53 TEST_F(TApp, OneFlagRef) {
42 54 int ref;
43 55 app.add_flag("-c,--count", ref);
... ... @@ -58,6 +70,16 @@ TEST_F(TApp, OneString) {
58 70 EXPECT_EQ(str, "mystring");
59 71 }
60 72  
  73 +TEST_F(TApp, OneStringSingleStringInput) {
  74 + std::string str;
  75 + app.add_option("-s,--string", str);
  76 +
  77 + app.parse("--string mystring");
  78 + EXPECT_EQ((size_t)1, app.count("-s"));
  79 + EXPECT_EQ((size_t)1, app.count("--string"));
  80 + EXPECT_EQ(str, "mystring");
  81 +}
  82 +
61 83 TEST_F(TApp, OneStringEqualVersion) {
62 84 std::string str;
63 85 app.add_option("-s,--string", str);
... ... @@ -68,6 +90,84 @@ TEST_F(TApp, OneStringEqualVersion) {
68 90 EXPECT_EQ(str, "mystring");
69 91 }
70 92  
  93 +TEST_F(TApp, OneStringEqualVersionSingleString) {
  94 + std::string str;
  95 + app.add_option("-s,--string", str);
  96 + app.parse("--string=mystring");
  97 + EXPECT_EQ((size_t)1, app.count("-s"));
  98 + EXPECT_EQ((size_t)1, app.count("--string"));
  99 + EXPECT_EQ(str, "mystring");
  100 +}
  101 +
  102 +TEST_F(TApp, OneStringEqualVersionSingleStringQuoted) {
  103 + std::string str;
  104 + app.add_option("-s,--string", str);
  105 + app.parse("--string=\"this is my quoted string\"");
  106 + EXPECT_EQ((size_t)1, app.count("-s"));
  107 + EXPECT_EQ((size_t)1, app.count("--string"));
  108 + EXPECT_EQ(str, "this is my quoted string");
  109 +}
  110 +
  111 +TEST_F(TApp, OneStringEqualVersionSingleStringQuotedMultiple) {
  112 + std::string str, str2, str3;
  113 + app.add_option("-s,--string", str);
  114 + app.add_option("-t,--tstr", str2);
  115 + app.add_option("-m,--mstr", str3);
  116 + app.parse("--string=\"this is my quoted string\" -t 'qstring 2' -m=`\"quoted string\"`");
  117 + EXPECT_EQ(str, "this is my quoted string");
  118 + EXPECT_EQ(str2, "qstring 2");
  119 + EXPECT_EQ(str3, "\"quoted string\"");
  120 +}
  121 +
  122 +TEST_F(TApp, OneStringEqualVersionSingleStringQuotedMultipleInMiddle) {
  123 + std::string str, str2, str3;
  124 + app.add_option("-s,--string", str);
  125 + app.add_option("-t,--tstr", str2);
  126 + app.add_option("-m,--mstr", str3);
  127 + app.parse(R"raw(--string="this is my quoted string" -t "qst\"ring 2" -m=`"quoted string"`")raw");
  128 + EXPECT_EQ(str, "this is my quoted string");
  129 + EXPECT_EQ(str2, "qst\"ring 2");
  130 + EXPECT_EQ(str3, "\"quoted string\"");
  131 +}
  132 +
  133 +TEST_F(TApp, OneStringEqualVersionSingleStringQuotedEscapedCharacters) {
  134 + std::string str, str2, str3;
  135 + app.add_option("-s,--string", str);
  136 + app.add_option("-t,--tstr", str2);
  137 + app.add_option("-m,--mstr", str3);
  138 + app.parse(R"raw(--string="this is my \"quoted\" string" -t 'qst\'ring 2' -m=`"quoted\` string"`")raw");
  139 + EXPECT_EQ(str, "this is my \"quoted\" string");
  140 + EXPECT_EQ(str2, "qst\'ring 2");
  141 + EXPECT_EQ(str3, "\"quoted` string\"");
  142 +}
  143 +
  144 +TEST_F(TApp, OneStringEqualVersionSingleStringQuotedMultipleWithEqual) {
  145 + std::string str, str2, str3, str4;
  146 + app.add_option("-s,--string", str);
  147 + app.add_option("-t,--tstr", str2);
  148 + app.add_option("-m,--mstr", str3);
  149 + app.add_option("-j,--jstr", str4);
  150 + app.parse("--string=\"this is my quoted string\" -t 'qstring 2' -m=`\"quoted string\"` --jstr=Unquoted");
  151 + EXPECT_EQ(str, "this is my quoted string");
  152 + EXPECT_EQ(str2, "qstring 2");
  153 + EXPECT_EQ(str3, "\"quoted string\"");
  154 + EXPECT_EQ(str4, "Unquoted");
  155 +}
  156 +
  157 +TEST_F(TApp, OneStringEqualVersionSingleStringQuotedMultipleWithEqualAndProgram) {
  158 + std::string str, str2, str3, str4;
  159 + app.add_option("-s,--string", str);
  160 + app.add_option("-t,--tstr", str2);
  161 + app.add_option("-m,--mstr", str3);
  162 + app.add_option("-j,--jstr", str4);
  163 + app.parse("program --string=\"this is my quoted string\" -t 'qstring 2' -m=`\"quoted string\"` --jstr=Unquoted",
  164 + true);
  165 + EXPECT_EQ(str, "this is my quoted string");
  166 + EXPECT_EQ(str2, "qstring 2");
  167 + EXPECT_EQ(str3, "\"quoted string\"");
  168 + EXPECT_EQ(str4, "Unquoted");
  169 +}
  170 +
71 171 TEST_F(TApp, TogetherInt) {
72 172 int i;
73 173 app.add_option("-i,--int", i);
... ... @@ -107,6 +207,15 @@ TEST_F(TApp, DefaultStringAgain) {
107 207 EXPECT_EQ(str, "previous");
108 208 }
109 209  
  210 +TEST_F(TApp, DefaultStringAgainEmpty) {
  211 + std::string str = "previous";
  212 + app.add_option("-s,--string", str);
  213 + app.parse(" ");
  214 + EXPECT_EQ((size_t)0, app.count("-s"));
  215 + EXPECT_EQ((size_t)0, app.count("--string"));
  216 + EXPECT_EQ(str, "previous");
  217 +}
  218 +
110 219 TEST_F(TApp, DualOptions) {
111 220  
112 221 std::string str = "previous";
... ... @@ -136,6 +245,30 @@ TEST_F(TApp, LotsOfFlags) {
136 245 EXPECT_EQ((size_t)1, app.count("-A"));
137 246 }
138 247  
  248 +TEST_F(TApp, LotsOfFlagsSingleString) {
  249 +
  250 + app.add_flag("-a");
  251 + app.add_flag("-A");
  252 + app.add_flag("-b");
  253 +
  254 + app.parse("-a -b -aA");
  255 + EXPECT_EQ((size_t)2, app.count("-a"));
  256 + EXPECT_EQ((size_t)1, app.count("-b"));
  257 + EXPECT_EQ((size_t)1, app.count("-A"));
  258 +}
  259 +
  260 +TEST_F(TApp, LotsOfFlagsSingleStringExtraSpace) {
  261 +
  262 + app.add_flag("-a");
  263 + app.add_flag("-A");
  264 + app.add_flag("-b");
  265 +
  266 + app.parse(" -a -b -aA ");
  267 + EXPECT_EQ((size_t)2, app.count("-a"));
  268 + EXPECT_EQ((size_t)1, app.count("-b"));
  269 + EXPECT_EQ((size_t)1, app.count("-A"));
  270 +}
  271 +
139 272 TEST_F(TApp, BoolAndIntFlags) {
140 273  
141 274 bool bflag;
... ... @@ -686,7 +819,7 @@ TEST_F(TApp, CallbackFlags) {
686 819 EXPECT_THROW(app.add_flag_function("hi", func), CLI::IncorrectConstruction);
687 820 }
688 821  
689   -#if __cplusplus >= 201402L
  822 +#if __cplusplus >= 201402L || _MSC_VER >= 1900
690 823 TEST_F(TApp, CallbackFlagsAuto) {
691 824  
692 825 size_t value = 0;
... ... @@ -1381,7 +1514,7 @@ TEST_F(TApp, RangeDouble) {
1381 1514 run();
1382 1515 }
1383 1516  
1384   -// Check to make sure progromatic access to left over is available
  1517 +// Check to make sure programmatic access to left over is available
1385 1518 TEST_F(TApp, AllowExtras) {
1386 1519  
1387 1520 app.allow_extras();
... ...
tests/CMakeLists.txt
... ... @@ -33,6 +33,7 @@ set(CLI11_TESTS
33 33 NewParseTest
34 34 OptionalTest
35 35 DeprecatedTest
  36 + StringParseTest
36 37 )
37 38  
38 39 if(WIN32)
... ...
tests/HelpersTest.cpp
... ... @@ -356,6 +356,20 @@ TEST(SplitUp, Simple) {
356 356 EXPECT_EQ(oput, result);
357 357 }
358 358  
  359 +TEST(SplitUp, SimpleDifferentQuotes) {
  360 + std::vector<std::string> oput = {"one", "two three"};
  361 + std::string orig{R"(one `two three`)"};
  362 + std::vector<std::string> result = CLI::detail::split_up(orig);
  363 + EXPECT_EQ(oput, result);
  364 +}
  365 +
  366 +TEST(SplitUp, SimpleDifferentQuotes2) {
  367 + std::vector<std::string> oput = {"one", "two three"};
  368 + std::string orig{R"(one 'two three')"};
  369 + std::vector<std::string> result = CLI::detail::split_up(orig);
  370 + EXPECT_EQ(oput, result);
  371 +}
  372 +
359 373 TEST(SplitUp, Layered) {
360 374 std::vector<std::string> output = {R"(one 'two three')"};
361 375 std::string orig{R"("one 'two three'")"};
... ...
tests/StringParseTest.cpp 0 → 100644
  1 +#include "app_helper.hpp"
  2 +
  3 +#include "gmock/gmock.h"
  4 +#include <cstdio>
  5 +#include <sstream>
  6 +
  7 +TEST_F(TApp, ExistingExeCheck) {
  8 +
  9 + TempFile tmpexe{"existingExe.out"};
  10 +
  11 + std::string str, str2, str3;
  12 + app.add_option("-s,--string", str);
  13 + app.add_option("-t,--tstr", str2);
  14 + app.add_option("-m,--mstr", str3);
  15 +
  16 + {
  17 + std::ofstream out{tmpexe};
  18 + out << "useless string doesn't matter" << std::endl;
  19 + }
  20 +
  21 + app.parse(std::string("./") + std::string(tmpexe) +
  22 + " --string=\"this is my quoted string\" -t 'qstring 2' -m=`\"quoted string\"`",
  23 + true);
  24 + EXPECT_EQ(str, "this is my quoted string");
  25 + EXPECT_EQ(str2, "qstring 2");
  26 + EXPECT_EQ(str3, "\"quoted string\"");
  27 +}
  28 +
  29 +TEST_F(TApp, ExistingExeCheckWithSpace) {
  30 +
  31 + TempFile tmpexe{"Space File.out"};
  32 +
  33 + std::string str, str2, str3;
  34 + app.add_option("-s,--string", str);
  35 + app.add_option("-t,--tstr", str2);
  36 + app.add_option("-m,--mstr", str3);
  37 +
  38 + {
  39 + std::ofstream out{tmpexe};
  40 + out << "useless string doesn't matter" << std::endl;
  41 + }
  42 +
  43 + app.parse(std::string("./") + std::string(tmpexe) +
  44 + " --string=\"this is my quoted string\" -t 'qstring 2' -m=`\"quoted string\"`",
  45 + true);
  46 + EXPECT_EQ(str, "this is my quoted string");
  47 + EXPECT_EQ(str2, "qstring 2");
  48 + EXPECT_EQ(str3, "\"quoted string\"");
  49 +
  50 + EXPECT_EQ(app.get_name(), std::string("./") + std::string(tmpexe));
  51 +}
  52 +
  53 +TEST_F(TApp, ExistingExeCheckWithLotsOfSpace) {
  54 +
  55 + TempFile tmpexe{"this is a weird file.exe"};
  56 +
  57 + std::string str, str2, str3;
  58 + app.add_option("-s,--string", str);
  59 + app.add_option("-t,--tstr", str2);
  60 + app.add_option("-m,--mstr", str3);
  61 +
  62 + {
  63 + std::ofstream out{tmpexe};
  64 + out << "useless string doesn't matter" << std::endl;
  65 + }
  66 +
  67 + app.parse(std::string("./") + std::string(tmpexe) +
  68 + " --string=\"this is my quoted string\" -t 'qstring 2' -m=`\"quoted string\"`",
  69 + true);
  70 + EXPECT_EQ(str, "this is my quoted string");
  71 + EXPECT_EQ(str2, "qstring 2");
  72 + EXPECT_EQ(str3, "\"quoted string\"");
  73 +
  74 + EXPECT_EQ(app.get_name(), std::string("./") + std::string(tmpexe));
  75 +}
... ...