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:
parent
3a2c5112a3
commit
30c2e327d1
2
extern/json
vendored
2
extern/json
vendored
@ -1 +1 @@
|
||||
Subproject commit db53bdac1926d1baebcb459b685dcd2e4608c355
|
||||
Subproject commit f1768a540a7b7c5cc30cdcd6be9e9ef91083719b
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
|
@ -33,6 +33,7 @@ set(CLI11_TESTS
|
||||
NewParseTest
|
||||
OptionalTest
|
||||
DeprecatedTest
|
||||
StringParseTest
|
||||
)
|
||||
|
||||
if(WIN32)
|
||||
|
@ -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
75
tests/StringParseTest.cpp
Normal 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));
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user