Commit 75f2d7747fe3a49367565fe2c69ef64257125f01

Authored by Philip Top
Committed by GitHub
1 parent f27822de

fix: allow quotes to specify a program name with spaces on the single string parse operation. (#605)

README.md
@@ -787,8 +787,8 @@ The App class was designed allow toolkits to subclass it, to provide preset defa @@ -787,8 +787,8 @@ The App class was designed allow toolkits to subclass it, to provide preset defa
787 but before run behavior, while 787 but before run behavior, while
788 still giving the user freedom to `callback` on the main app. 788 still giving the user freedom to `callback` on the main app.
789 789
790 -The most important parse function is `parse(std::vector<std::string>)`, which takes a reversed list of arguments (so that `pop_back` processes the args in the correct order). `get_help_ptr` and `get_config_ptr` give you access to the help/config option pointers. The standard `parse` manually sets the name from the first argument, so it should not be in this vector. You can also use `parse(string, bool)` to split up and parse a string; the optional boolean should be set to true if you are  
791 -including the program name in the string, and false otherwise. 790 +The most important parse function is `parse(std::vector<std::string>)`, which takes a reversed list of arguments (so that `pop_back` processes the args in the correct order). `get_help_ptr` and `get_config_ptr` give you access to the help/config option pointers. The standard `parse` manually sets the name from the first argument, so it should not be in this vector. You can also use `parse(string, bool)` to split up and parse a single string; the optional boolean should be set to true if you are
  791 +including the program name in the string, and false otherwise. The program name can contain spaces if it is an existing file, otherwise can be enclosed in quotes(single quote, double quote or backtick). Embedded quote characters can be escaped with `\`.
792 792
793 Also, in a related note, the `App` you get a pointer to is stored in the parent `App` in a `shared_ptr`s (similar to `Option`s) and are deleted when the main `App` goes out of scope unless the object has another owner. 793 Also, in a related note, the `App` you get a pointer to is stored in the parent `App` in a `shared_ptr`s (similar to `Option`s) and are deleted when the main `App` goes out of scope unless the object has another owner.
794 794
include/CLI/Validators.hpp
@@ -1100,12 +1100,36 @@ inline std::pair&lt;std::string, std::string&gt; split_program_name(std::string comman @@ -1100,12 +1100,36 @@ inline std::pair&lt;std::string, std::string&gt; split_program_name(std::string comman
1100 if(esp == std::string::npos) { 1100 if(esp == std::string::npos) {
1101 // if we have reached the end and haven't found a valid file just assume the first argument is the 1101 // if we have reached the end and haven't found a valid file just assume the first argument is the
1102 // program name 1102 // program name
1103 - esp = commandline.find_first_of(' ', 1); 1103 + if(commandline[0] == '"' || commandline[0] == '\'' || commandline[0] == '`') {
  1104 + bool embeddedQuote = false;
  1105 + auto keyChar = commandline[0];
  1106 + auto end = commandline.find_first_of(keyChar, 1);
  1107 + while((end != std::string::npos) && (commandline[end - 1] == '\\')) { // deal with escaped quotes
  1108 + end = commandline.find_first_of(keyChar, end + 1);
  1109 + embeddedQuote = true;
  1110 + }
  1111 + if(end != std::string::npos) {
  1112 + vals.first = commandline.substr(1, end - 1);
  1113 + esp = end + 1;
  1114 + if(embeddedQuote) {
  1115 + vals.first = find_and_replace(vals.first, std::string("\\") + keyChar, std::string(1, keyChar));
  1116 + embeddedQuote = false;
  1117 + }
  1118 + } else {
  1119 + esp = commandline.find_first_of(' ', 1);
  1120 + }
  1121 + } else {
  1122 + esp = commandline.find_first_of(' ', 1);
  1123 + }
  1124 +
1104 break; 1125 break;
1105 } 1126 }
1106 } 1127 }
1107 - vals.first = commandline.substr(0, esp);  
1108 - rtrim(vals.first); 1128 + if(vals.first.empty()) {
  1129 + vals.first = commandline.substr(0, esp);
  1130 + rtrim(vals.first);
  1131 + }
  1132 +
1109 // strip the program name 1133 // strip the program name
1110 vals.second = (esp != std::string::npos) ? commandline.substr(esp + 1) : std::string{}; 1134 vals.second = (esp != std::string::npos) ? commandline.substr(esp + 1) : std::string{};
1111 ltrim(vals.second); 1135 ltrim(vals.second);
tests/StringParseTest.cpp
@@ -78,3 +78,31 @@ TEST_CASE_METHOD(TApp, &quot;ExistingExeCheckWithLotsOfSpace&quot;, &quot;[stringparse]&quot;) { @@ -78,3 +78,31 @@ TEST_CASE_METHOD(TApp, &quot;ExistingExeCheckWithLotsOfSpace&quot;, &quot;[stringparse]&quot;) {
78 78
79 CHECK(std::string("./") + std::string(tmpexe) == app.get_name()); 79 CHECK(std::string("./") + std::string(tmpexe) == app.get_name());
80 } 80 }
  81 +
  82 +// From Github issue #591 https://github.com/CLIUtils/CLI11/issues/591
  83 +TEST_CASE_METHOD(TApp, "ProgNameWithSpace", "[stringparse]") {
  84 +
  85 + app.add_flag("--foo");
  86 + CHECK_NOTHROW(app.parse("\"Foo Bar\" --foo", true));
  87 +
  88 + CHECK(app["--foo"]->as<bool>());
  89 + CHECK(app.get_name() == "Foo Bar");
  90 +}
  91 +
  92 +TEST_CASE_METHOD(TApp, "ProgNameWithSpaceEmbeddedQuote", "[stringparse]") {
  93 +
  94 + app.add_flag("--foo");
  95 + CHECK_NOTHROW(app.parse("\"Foo\\\" Bar\" --foo", true));
  96 +
  97 + CHECK(app["--foo"]->as<bool>());
  98 + CHECK(app.get_name() == "Foo\" Bar");
  99 +}
  100 +
  101 +TEST_CASE_METHOD(TApp, "ProgNameWithSpaceSingleQuote", "[stringparse]") {
  102 +
  103 + app.add_flag("--foo");
  104 + CHECK_NOTHROW(app.parse(R"('Foo\' Bar' --foo)", true));
  105 +
  106 + CHECK(app["--foo"]->as<bool>());
  107 + CHECK(app.get_name() == "Foo' Bar");
  108 +}