1
0
mirror of https://github.com/CLIUtils/CLI11.git synced 2025-04-29 12:13:52 +00:00

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
This commit is contained in:
Philip Top 2019-01-06 01:30:49 -08:00 committed by Henry Schreiner
parent 3a2c5112a3
commit 30c2e327d1
7 changed files with 307 additions and 31 deletions

2
extern/json vendored

@ -1 +1 @@
Subproject commit db53bdac1926d1baebcb459b685dcd2e4608c355
Subproject commit f1768a540a7b7c5cc30cdcd6be9e9ef91083719b

View File

@ -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<std::string> &)
/// 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<std::string> &args) {

View File

@ -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<std::string> split_up(std::string str) {
std::vector<char> delims = {'\'', '\"'};
const std::string delims("\'\"`");
auto find_ws = [](char ch) { return std::isspace<char>(ch, std::locale()); };
trim(str);
std::vector<std::string> 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<std::string> 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<std::string> 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

View File

@ -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();

View File

@ -33,6 +33,7 @@ set(CLI11_TESTS
NewParseTest
OptionalTest
DeprecatedTest
StringParseTest
)
if(WIN32)

View File

@ -356,6 +356,20 @@ TEST(SplitUp, Simple) {
EXPECT_EQ(oput, result);
}
TEST(SplitUp, SimpleDifferentQuotes) {
std::vector<std::string> oput = {"one", "two three"};
std::string orig{R"(one `two three`)"};
std::vector<std::string> result = CLI::detail::split_up(orig);
EXPECT_EQ(oput, result);
}
TEST(SplitUp, SimpleDifferentQuotes2) {
std::vector<std::string> oput = {"one", "two three"};
std::string orig{R"(one 'two three')"};
std::vector<std::string> result = CLI::detail::split_up(orig);
EXPECT_EQ(oput, result);
}
TEST(SplitUp, Layered) {
std::vector<std::string> output = {R"(one 'two three')"};
std::string orig{R"("one 'two three'")"};

75
tests/StringParseTest.cpp Normal file
View File

@ -0,0 +1,75 @@
#include "app_helper.hpp"
#include "gmock/gmock.h"
#include <cstdio>
#include <sstream>
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));
}