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 787 but before run behavior, while
788 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 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 1100 if(esp == std::string::npos) {
1101 1101 // if we have reached the end and haven't found a valid file just assume the first argument is the
1102 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 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 1133 // strip the program name
1110 1134 vals.second = (esp != std::string::npos) ? commandline.substr(esp + 1) : std::string{};
1111 1135 ltrim(vals.second);
... ...
tests/StringParseTest.cpp
... ... @@ -78,3 +78,31 @@ TEST_CASE_METHOD(TApp, &quot;ExistingExeCheckWithLotsOfSpace&quot;, &quot;[stringparse]&quot;) {
78 78  
79 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 +}
... ...