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

add options to handle windows style command line options (#187)

* add some fields and functions for windows like options
add test cases for windows options and refactor for additional string functions

* try to fix code coverage to 100% again.  add some additional documentation and a few additional test cases to verify documentation

* remove some extra brackets
This commit is contained in:
Philip Top 2019-01-10 14:07:17 -08:00 committed by Henry Schreiner
parent 2c024401cc
commit ce6dc0723e
11 changed files with 295 additions and 104 deletions

View File

@ -255,6 +255,14 @@ On the command line, options can be given as:
- `--file filename` (space) - `--file filename` (space)
- `--file=filename` (equals) - `--file=filename` (equals)
If allow_windows_style_options() is specified in the application or subcommand options can also be given as:
- `/a` (flag)
- `/f filename` (option)
- `/long` (long flag)
- `/file filename` (space)
- `/file:filename` (colon)
= Windows style options do not allow combining short options or values not separated from the short option like with `-` options
Extra positional arguments will cause the program to exit, so at least one positional option with a vector is recommended if you want to allow extraneous arguments. Extra positional arguments will cause the program to exit, so at least one positional option with a vector is recommended if you want to allow extraneous arguments.
If you set `.allow_extras()` on the main `App`, you will not get an error. You can access the missing options using `remaining` (if you have subcommands, `app.remaining(true)` will get all remaining options, subcommands included). If you set `.allow_extras()` on the main `App`, you will not get an error. You can access the missing options using `remaining` (if you have subcommands, `app.remaining(true)` will get all remaining options, subcommands included).
@ -285,7 +293,7 @@ There are several options that are supported on the main app and subcommands. Th
- `.ignore_case()`: Ignore the case of this subcommand. Inherited by added subcommands, so is usually used on the main `App`. - `.ignore_case()`: Ignore the case of this subcommand. Inherited by added subcommands, so is usually used on the main `App`.
- `.ignore_underscore()`: Ignore any underscores in the subcommand name. Inherited by added subcommands, so is usually used on the main `App`. - `.ignore_underscore()`: Ignore any underscores in the subcommand name. Inherited by added subcommands, so is usually used on the main `App`.
- `.allow_windows_style_options()`: Allow command line options to be parsed in the form of `/s /long /file:file_name.ext` This option does not change how options are specified in the `add_option` calls or the ability to process options in the form of `-s --long --file=file_name.ext`
- `.fallthrough()`: Allow extra unmatched options and positionals to "fall through" and be matched on a parent command. Subcommands always are allowed to fall through. - `.fallthrough()`: Allow extra unmatched options and positionals to "fall through" and be matched on a parent command. Subcommands always are allowed to fall through.
- `.require_subcommand()`: Require 1 or more subcommands. - `.require_subcommand()`: Require 1 or more subcommands.
- `.require_subcommand(N)`: Require `N` subcommands if `N>0`, or up to `N` if `N<0`. `N=0` resets to the default 0 or more. - `.require_subcommand(N)`: Require `N` subcommands if `N>0`, or up to `N` if `N<0`. `N=0` resets to the default 0 or more.

View File

@ -38,7 +38,7 @@ namespace CLI {
#endif #endif
namespace detail { namespace detail {
enum class Classifer { NONE, POSITIONAL_MARK, SHORT, LONG, SUBCOMMAND }; enum class Classifier { NONE, POSITIONAL_MARK, SHORT, LONG, WINDOWS, SUBCOMMAND };
struct AppFriend; struct AppFriend;
} // namespace detail } // namespace detail
@ -116,7 +116,7 @@ class App {
/// @name Parsing /// @name Parsing
///@{ ///@{
using missing_t = std::vector<std::pair<detail::Classifer, std::string>>; using missing_t = std::vector<std::pair<detail::Classifier, std::string>>;
/// Pair of classifier, string for missing options. (extra detail is removed on returning from parse) /// Pair of classifier, string for missing options. (extra detail is removed on returning from parse)
/// ///
@ -145,6 +145,9 @@ class App {
/// Allow subcommand fallthrough, so that parent commands can collect commands after subcommand. INHERITABLE /// Allow subcommand fallthrough, so that parent commands can collect commands after subcommand. INHERITABLE
bool fallthrough_{false}; bool fallthrough_{false};
/// Allow '/' for options for windows like options INHERITABLE
bool allow_windows_style_options_{false};
/// A pointer to the parent if this is a subcommand /// A pointer to the parent if this is a subcommand
App *parent_{nullptr}; App *parent_{nullptr};
@ -200,6 +203,7 @@ class App {
ignore_case_ = parent_->ignore_case_; ignore_case_ = parent_->ignore_case_;
ignore_underscore_ = parent_->ignore_underscore_; ignore_underscore_ = parent_->ignore_underscore_;
fallthrough_ = parent_->fallthrough_; fallthrough_ = parent_->fallthrough_;
allow_windows_style_options_ = parent_->allow_windows_style_options_;
group_ = parent_->group_; group_ = parent_->group_;
footer_ = parent_->footer_; footer_ = parent_->footer_;
formatter_ = parent_->formatter_; formatter_ = parent_->formatter_;
@ -251,7 +255,7 @@ class App {
return this; return this;
} }
/// Do not parse anything after the first unrecognised option and return /// Do not parse anything after the first unrecognized option and return
App *prefix_command(bool allow = true) { App *prefix_command(bool allow = true) {
prefix_command_ = allow; prefix_command_ = allow;
return this; return this;
@ -269,6 +273,12 @@ class App {
return this; return this;
} }
/// Ignore case. Subcommand inherit value.
App *allow_windows_style_options(bool value = true) {
allow_windows_style_options_ = value;
return this;
}
/// Ignore underscore. Subcommand inherit value. /// Ignore underscore. Subcommand inherit value.
App *ignore_underscore(bool value = true) { App *ignore_underscore(bool value = true) {
ignore_underscore_ = value; ignore_underscore_ = value;
@ -1172,43 +1182,32 @@ class App {
/// the function takes an optional boolean argument specifying if the programName is included in the string to /// the function takes an optional boolean argument specifying if the programName is included in the string to
/// process /// process
void parse(std::string commandline, bool program_name_included = false) { void parse(std::string commandline, bool program_name_included = false) {
detail::trim(commandline);
if(program_name_included) { if(program_name_included) {
// try to determine the programName auto nstr = detail::split_program_name(commandline);
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()) { if(name_.empty()) {
name_ = commandline.substr(0, esp); name_ = nstr.first;
detail::rtrim(name_);
} }
// strip the program name commandline = std::move(nstr.second);
commandline = commandline.substr(esp + 1); } else
} detail::trim(commandline);
// the first section of code is to deal with quoted arguments after and '=' // the next section of code is to deal with quoted arguments after an '=' or ':' for windows like operations
if(!commandline.empty()) { if(!commandline.empty()) {
size_t offset = commandline.length() - 1; auto escape_detect = [](std::string &str, size_t offset) {
auto qeq = commandline.find_last_of('=', offset); auto next = str[offset + 1];
while(qeq != std::string::npos) { if((next == '\"') || (next == '\'') || (next == '`')) {
if((commandline[qeq + 1] == '\"') || (commandline[qeq + 1] == '\'') || (commandline[qeq + 1] == '`')) { auto astart = str.find_last_of("-/ \"\'`", offset - 1);
auto astart = commandline.find_last_of("- \"\'`", qeq - 1);
if(astart != std::string::npos) { if(astart != std::string::npos) {
if(commandline[astart] == '-') { if(str[astart] == (str[offset] == '=') ? '-' : '/')
commandline[qeq] = ' '; // interpret this a space so the split_up works properly str[offset] = ' '; // interpret this as a space so the split_up works properly
offset = (astart == 0) ? 0 : (astart - 1);
}
} }
} }
offset = qeq - 1; return (offset + 1);
qeq = commandline.find_last_of('=', offset); };
}
commandline = detail::find_and_modify(commandline, "=", escape_detect);
if(allow_windows_style_options_)
commandline = detail::find_and_modify(commandline, ":", escape_detect);
} }
auto args = detail::split_up(std::move(commandline)); auto args = detail::split_up(std::move(commandline));
@ -1339,8 +1338,8 @@ class App {
return this; return this;
} }
/// Produce a string that could be read in as a config of the current values of the App. Set default_also to include /// Produce a string that could be read in as a config of the current values of the App. Set default_also to
/// default arguments. Prefix will add a string to the beginning of each option. /// include default arguments. Prefix will add a string to the beginning of each option.
std::string config_to_str(bool default_also = false, bool write_description = false) const { std::string config_to_str(bool default_also = false, bool write_description = false) const {
return config_formatter_->to_config(this, default_also, write_description, ""); return config_formatter_->to_config(this, default_also, write_description, "");
} }
@ -1432,6 +1431,9 @@ class App {
/// Check the status of fallthrough /// Check the status of fallthrough
bool get_fallthrough() const { return fallthrough_; } bool get_fallthrough() const { return fallthrough_; }
/// Check the status of the allow windows style options
bool get_allow_windows_style_options() const { return allow_windows_style_options_; }
/// Get the group of this subcommand /// Get the group of this subcommand
const std::string &get_group() const { return group_; } const std::string &get_group() const { return group_; }
@ -1512,7 +1514,7 @@ class App {
/// This returns the missing options from the current subcommand /// This returns the missing options from the current subcommand
std::vector<std::string> remaining(bool recurse = false) const { std::vector<std::string> remaining(bool recurse = false) const {
std::vector<std::string> miss_list; std::vector<std::string> miss_list;
for(const std::pair<detail::Classifer, std::string> &miss : missing_) { for(const std::pair<detail::Classifier, std::string> &miss : missing_) {
miss_list.push_back(std::get<1>(miss)); miss_list.push_back(std::get<1>(miss));
} }
@ -1526,11 +1528,11 @@ class App {
return miss_list; return miss_list;
} }
/// This returns the number of remaining options, minus the -- seperator /// This returns the number of remaining options, minus the -- separator
size_t remaining_size(bool recurse = false) const { size_t remaining_size(bool recurse = false) const {
auto count = static_cast<size_t>(std::count_if( auto count = static_cast<size_t>(std::count_if(
std::begin(missing_), std::end(missing_), [](const std::pair<detail::Classifer, std::string> &val) { std::begin(missing_), std::end(missing_), [](const std::pair<detail::Classifier, std::string> &val) {
return val.first != detail::Classifer::POSITIONAL_MARK; return val.first != detail::Classifier::POSITIONAL_MARK;
})); }));
if(recurse) { if(recurse) {
for(const App_p &sub : subcommands_) { for(const App_p &sub : subcommands_) {
@ -1582,18 +1584,20 @@ class App {
} }
/// Selects a Classifier enum based on the type of the current argument /// Selects a Classifier enum based on the type of the current argument
detail::Classifer _recognize(const std::string &current) const { detail::Classifier _recognize(const std::string &current) const {
std::string dummy1, dummy2; std::string dummy1, dummy2;
if(current == "--") if(current == "--")
return detail::Classifer::POSITIONAL_MARK; return detail::Classifier::POSITIONAL_MARK;
if(_valid_subcommand(current)) if(_valid_subcommand(current))
return detail::Classifer::SUBCOMMAND; return detail::Classifier::SUBCOMMAND;
if(detail::split_long(current, dummy1, dummy2)) if(detail::split_long(current, dummy1, dummy2))
return detail::Classifer::LONG; return detail::Classifier::LONG;
if(detail::split_short(current, dummy1, dummy2)) if(detail::split_short(current, dummy1, dummy2))
return detail::Classifer::SHORT; return detail::Classifier::SHORT;
return detail::Classifer::NONE; if((allow_windows_style_options_) && (detail::split_windows(current, dummy1, dummy2)))
return detail::Classifier::WINDOWS;
return detail::Classifier::NONE;
} }
// The parse function is now broken into several parts, and part of process // The parse function is now broken into several parts, and part of process
@ -1800,7 +1804,7 @@ class App {
// If the option was not present // If the option was not present
if(get_allow_config_extras()) if(get_allow_config_extras())
// Should we worry about classifying the extras properly? // Should we worry about classifying the extras properly?
missing_.emplace_back(detail::Classifer::NONE, item.fullname()); missing_.emplace_back(detail::Classifier::NONE, item.fullname());
return false; return false;
} }
@ -1820,29 +1824,27 @@ class App {
return true; return true;
} }
/// Parse "one" argument (some may eat more than one), delegate to parent if fails, add to missing if missing from /// Parse "one" argument (some may eat more than one), delegate to parent if fails, add to missing if missing
/// master /// from master
void _parse_single(std::vector<std::string> &args, bool &positional_only) { void _parse_single(std::vector<std::string> &args, bool &positional_only) {
detail::Classifer classifer = positional_only ? detail::Classifer::NONE : _recognize(args.back()); detail::Classifier classifier = positional_only ? detail::Classifier::NONE : _recognize(args.back());
switch(classifer) { switch(classifier) {
case detail::Classifer::POSITIONAL_MARK: case detail::Classifier::POSITIONAL_MARK:
missing_.emplace_back(classifer, args.back()); missing_.emplace_back(classifier, args.back());
args.pop_back(); args.pop_back();
positional_only = true; positional_only = true;
break; break;
case detail::Classifer::SUBCOMMAND: case detail::Classifier::SUBCOMMAND:
_parse_subcommand(args); _parse_subcommand(args);
break; break;
case detail::Classifer::LONG: case detail::Classifier::LONG:
case detail::Classifier::SHORT:
case detail::Classifier::WINDOWS:
// If already parsed a subcommand, don't accept options_ // If already parsed a subcommand, don't accept options_
_parse_arg(args, true); _parse_arg(args, classifier);
break; break;
case detail::Classifer::SHORT: case detail::Classifier::NONE:
// If already parsed a subcommand, don't accept options_
_parse_arg(args, false);
break;
case detail::Classifer::NONE:
// Probably a positional or something for a parent (sub)command // Probably a positional or something for a parent (sub)command
_parse_positional(args); _parse_positional(args);
} }
@ -1879,11 +1881,11 @@ class App {
return parent_->_parse_positional(args); return parent_->_parse_positional(args);
else { else {
args.pop_back(); args.pop_back();
missing_.emplace_back(detail::Classifer::NONE, positional); missing_.emplace_back(detail::Classifier::NONE, positional);
if(prefix_command_) { if(prefix_command_) {
while(!args.empty()) { while(!args.empty()) {
missing_.emplace_back(detail::Classifer::NONE, args.back()); missing_.emplace_back(detail::Classifier::NONE, args.back());
args.pop_back(); args.pop_back();
} }
} }
@ -1913,9 +1915,7 @@ class App {
} }
/// Parse a short (false) or long (true) argument, must be at the top of the list /// Parse a short (false) or long (true) argument, must be at the top of the list
void _parse_arg(std::vector<std::string> &args, bool second_dash) { void _parse_arg(std::vector<std::string> &args, detail::Classifier current_type) {
detail::Classifer current_type = second_dash ? detail::Classifer::LONG : detail::Classifer::SHORT;
std::string current = args.back(); std::string current = args.back();
@ -1923,23 +1923,37 @@ class App {
std::string value; std::string value;
std::string rest; std::string rest;
if(second_dash) { switch(current_type) {
case detail::Classifier::LONG:
if(!detail::split_long(current, name, value)) if(!detail::split_long(current, name, value))
throw HorribleError("Long parsed but missing (you should not see this):" + args.back()); throw HorribleError("Long parsed but missing (you should not see this):" + args.back());
} else { break;
case detail::Classifier::SHORT:
if(!detail::split_short(current, name, rest)) if(!detail::split_short(current, name, rest))
throw HorribleError("Short parsed but missing! You should not see this"); throw HorribleError("Short parsed but missing! You should not see this");
break;
case detail::Classifier::WINDOWS:
if(!detail::split_windows(current, name, value))
throw HorribleError("windows option parsed but missing! You should not see this");
break;
default:
throw HorribleError("parsing got called with invalid option! You should not see this");
} }
auto op_ptr = std::find_if(std::begin(options_), std::end(options_), [name, second_dash](const Option_p &opt) { auto op_ptr = std::find_if(std::begin(options_), std::end(options_), [name, current_type](const Option_p &opt) {
return second_dash ? opt->check_lname(name) : opt->check_sname(name); if(current_type == detail::Classifier::LONG)
return opt->check_lname(name);
if(current_type == detail::Classifier::SHORT)
return opt->check_sname(name);
// this will only get called for detail::Classifier::WINDOWS
return opt->check_lname(name) || opt->check_sname(name);
}); });
// Option not found // Option not found
if(op_ptr == std::end(options_)) { if(op_ptr == std::end(options_)) {
// If a subcommand, try the master command // If a subcommand, try the master command
if(parent_ != nullptr && fallthrough_) if(parent_ != nullptr && fallthrough_)
return parent_->_parse_arg(args, second_dash); return parent_->_parse_arg(args, current_type);
// Otherwise, add to missing // Otherwise, add to missing
else { else {
args.pop_back(); args.pop_back();
@ -1981,7 +1995,7 @@ class App {
// Unlimited vector parser // Unlimited vector parser
if(num < 0) { if(num < 0) {
while(!args.empty() && _recognize(args.back()) == detail::Classifer::NONE) { while(!args.empty() && _recognize(args.back()) == detail::Classifier::NONE) {
if(collected >= -num) { if(collected >= -num) {
// We could break here for allow extras, but we don't // We could break here for allow extras, but we don't
@ -1996,7 +2010,7 @@ class App {
} }
// Allow -- to end an unlimited list and "eat" it // Allow -- to end an unlimited list and "eat" it
if(!args.empty() && _recognize(args.back()) == detail::Classifer::POSITIONAL_MARK) if(!args.empty() && _recognize(args.back()) == detail::Classifier::POSITIONAL_MARK)
args.pop_back(); args.pop_back();
} else { } else {

View File

@ -115,7 +115,7 @@ inline std::string Formatter::make_footer(const App *app) const {
inline std::string Formatter::make_help(const App *app, std::string name, AppFormatMode mode) const { inline std::string Formatter::make_help(const App *app, std::string name, AppFormatMode mode) const {
// This immediatly forwards to the make_expanded method. This is done this way so that subcommands can // This immediately forwards to the make_expanded method. This is done this way so that subcommands can
// have overridden formatters // have overridden formatters
if(mode == AppFormatMode::Sub) if(mode == AppFormatMode::Sub)
return make_expanded(app); return make_expanded(app);

View File

@ -479,7 +479,7 @@ class Option : public OptionBase<Option> {
int get_expected() const { return expected_; } int get_expected() const { return expected_; }
/// \brief The total number of expected values (including the type) /// \brief The total number of expected values (including the type)
/// This is positive if exactly this number is expected, and negitive for at least N values /// This is positive if exactly this number is expected, and negative for at least N values
/// ///
/// v = fabs(size_type*expected) /// v = fabs(size_type*expected)
/// !MultiOptionPolicy::Throw /// !MultiOptionPolicy::Throw
@ -518,7 +518,7 @@ class Option : public OptionBase<Option> {
/// @name Help tools /// @name Help tools
///@{ ///@{
/// \brief Gets a comma seperated list of names. /// \brief Gets a comma separated list of names.
/// Will include / prefer the positional name if positional is true. /// Will include / prefer the positional name if positional is true.
/// If all_options is false, pick just the most descriptive name to show. /// If all_options is false, pick just the most descriptive name to show.
/// Use `get_name(true)` to get the positional name (replaces `get_pname`) /// Use `get_name(true)` to get the positional name (replaces `get_pname`)
@ -530,7 +530,7 @@ class Option : public OptionBase<Option> {
std::vector<std::string> name_list; std::vector<std::string> name_list;
/// The all list wil never include a positional unless asked or that's the only name. /// The all list will never include a positional unless asked or that's the only name.
if((positional && pname_.length()) || (snames_.empty() && lnames_.empty())) if((positional && pname_.length()) || (snames_.empty() && lnames_.empty()))
name_list.push_back(pname_); name_list.push_back(pname_);

View File

@ -26,7 +26,7 @@ inline bool split_short(const std::string &current, std::string &name, std::stri
// Returns false if not a long option. Otherwise, sets opt name and other side of = and returns true // Returns false if not a long option. Otherwise, sets opt name and other side of = and returns true
inline bool split_long(const std::string &current, std::string &name, std::string &value) { inline bool split_long(const std::string &current, std::string &name, std::string &value) {
if(current.size() > 2 && current.substr(0, 2) == "--" && valid_first_char(current[2])) { if(current.size() > 2 && current.substr(0, 2) == "--" && valid_first_char(current[2])) {
auto loc = current.find("="); auto loc = current.find_first_of('=');
if(loc != std::string::npos) { if(loc != std::string::npos) {
name = current.substr(2, loc - 2); name = current.substr(2, loc - 2);
value = current.substr(loc + 1); value = current.substr(loc + 1);
@ -39,6 +39,22 @@ inline bool split_long(const std::string &current, std::string &name, std::strin
return false; return false;
} }
// Returns false if not a windows style option. Otherwise, sets opt name and value and returns true
inline bool split_windows(const std::string &current, std::string &name, std::string &value) {
if(current.size() > 1 && current[0] == '/' && valid_first_char(current[1])) {
auto loc = current.find_first_of(':');
if(loc != std::string::npos) {
name = current.substr(1, loc - 1);
value = current.substr(loc + 1);
} else {
name = current.substr(1);
value = "";
}
return true;
} else
return false;
}
// Splits a string into multiple long and short names // Splits a string into multiple long and short names
inline std::vector<std::string> split_names(std::string current) { inline std::vector<std::string> split_names(std::string current) {
std::vector<std::string> output; std::vector<std::string> output;

View File

@ -161,6 +161,16 @@ inline std::string find_and_replace(std::string str, std::string from, std::stri
return str; return str;
} }
/// Find a trigger string and call a modify callable function that takes the current string and starting position of the
/// trigger and returns the position in the string to search for the next trigger string
template <typename Callable> inline std::string find_and_modify(std::string str, std::string trigger, Callable modify) {
size_t start_pos = 0;
while((start_pos = str.find(trigger, start_pos)) != std::string::npos) {
start_pos = modify(str, start_pos);
}
return str;
}
/// Split a string '"one two" "three"' into 'one two', 'three' /// Split a string '"one two" "three"' into 'one two', 'three'
/// Quote characters can be ` ' or " /// Quote characters can be ` ' or "
inline std::vector<std::string> split_up(std::string str) { inline std::vector<std::string> split_up(std::string str) {

View File

@ -191,6 +191,33 @@ struct Range : public Validator {
template <typename T> explicit Range(T max) : Range(static_cast<T>(0), max) {} template <typename T> explicit Range(T max) : Range(static_cast<T>(0), max) {}
}; };
namespace detail {
/// split a string into a program name and command line arguments
/// the string is assumed to contain a file name followed by other arguments
/// the return value contains is a pair with the first argument containing the program name and the second everything
/// else
inline std::pair<std::string, std::string> split_program_name(std::string commandline) {
// try to determine the programName
std::pair<std::string, std::string> vals;
trim(commandline);
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;
}
}
vals.first = commandline.substr(0, esp);
rtrim(vals.first);
// strip the program name
vals.second = (esp != std::string::npos) ? commandline.substr(esp + 1) : std::string{};
ltrim(vals.second);
return vals;
}
} // namespace detail
/// @} /// @}
} // namespace CLI } // namespace CLI

View File

@ -10,6 +10,15 @@ TEST_F(TApp, OneFlagShort) {
EXPECT_EQ((size_t)1, app.count("--count")); EXPECT_EQ((size_t)1, app.count("--count"));
} }
TEST_F(TApp, OneFlagShortWindows) {
app.add_flag("-c,--count");
args = {"/c"};
app.allow_windows_style_options();
run();
EXPECT_EQ((size_t)1, app.count("-c"));
EXPECT_EQ((size_t)1, app.count("--count"));
}
TEST_F(TApp, CountNonExist) { TEST_F(TApp, CountNonExist) {
app.add_flag("-c,--count"); app.add_flag("-c,--count");
args = {"-c"}; args = {"-c"};
@ -70,6 +79,17 @@ TEST_F(TApp, OneString) {
EXPECT_EQ(str, "mystring"); EXPECT_EQ(str, "mystring");
} }
TEST_F(TApp, OneStringWindowsStyle) {
std::string str;
app.add_option("-s,--string", str);
args = {"/string", "mystring"};
app.allow_windows_style_options();
run();
EXPECT_EQ((size_t)1, app.count("-s"));
EXPECT_EQ((size_t)1, app.count("--string"));
EXPECT_EQ(str, "mystring");
}
TEST_F(TApp, OneStringSingleStringInput) { TEST_F(TApp, OneStringSingleStringInput) {
std::string str; std::string str;
app.add_option("-s,--string", str); app.add_option("-s,--string", str);
@ -90,6 +110,17 @@ TEST_F(TApp, OneStringEqualVersion) {
EXPECT_EQ(str, "mystring"); EXPECT_EQ(str, "mystring");
} }
TEST_F(TApp, OneStringEqualVersionWindowsStyle) {
std::string str;
app.add_option("-s,--string", str);
args = {"/string:mystring"};
app.allow_windows_style_options();
run();
EXPECT_EQ((size_t)1, app.count("-s"));
EXPECT_EQ((size_t)1, app.count("--string"));
EXPECT_EQ(str, "mystring");
}
TEST_F(TApp, OneStringEqualVersionSingleString) { TEST_F(TApp, OneStringEqualVersionSingleString) {
std::string str; std::string str;
app.add_option("-s,--string", str); app.add_option("-s,--string", str);
@ -119,6 +150,18 @@ TEST_F(TApp, OneStringEqualVersionSingleStringQuotedMultiple) {
EXPECT_EQ(str3, "\"quoted string\""); EXPECT_EQ(str3, "\"quoted string\"");
} }
TEST_F(TApp, OneStringEqualVersionSingleStringQuotedMultipleMixedStyle) {
std::string str, str2, str3;
app.add_option("-s,--string", str);
app.add_option("-t,--tstr", str2);
app.add_option("-m,--mstr", str3);
app.allow_windows_style_options();
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) { TEST_F(TApp, OneStringEqualVersionSingleStringQuotedMultipleInMiddle) {
std::string str, str2, str3; std::string str, str2, str3;
app.add_option("-s,--string", str); app.add_option("-s,--string", str);
@ -1077,6 +1120,20 @@ TEST_F(TApp, InIntSet) {
EXPECT_THROW(run(), CLI::ConversionError); EXPECT_THROW(run(), CLI::ConversionError);
} }
TEST_F(TApp, InIntSetWindows) {
int choice;
app.add_set("-q,--quick", choice, {1, 2, 3});
app.allow_windows_style_options();
args = {"/q", "2"};
run();
EXPECT_EQ(2, choice);
args = {"/q4"};
EXPECT_THROW(run(), CLI::ExtrasError);
}
TEST_F(TApp, FailSet) { TEST_F(TApp, FailSet) {
int choice; int choice;
@ -1547,14 +1604,28 @@ TEST_F(TApp, AllowExtrasOrder) {
TEST_F(TApp, CheckShortFail) { TEST_F(TApp, CheckShortFail) {
args = {"--two"}; args = {"--two"};
EXPECT_THROW(CLI::detail::AppFriend::parse_arg(&app, args, false), CLI::HorribleError); EXPECT_THROW(CLI::detail::AppFriend::parse_arg(&app, args, CLI::detail::Classifier::SHORT), CLI::HorribleError);
} }
// Test horrible error // Test horrible error
TEST_F(TApp, CheckLongFail) { TEST_F(TApp, CheckLongFail) {
args = {"-t"}; args = {"-t"};
EXPECT_THROW(CLI::detail::AppFriend::parse_arg(&app, args, true), CLI::HorribleError); EXPECT_THROW(CLI::detail::AppFriend::parse_arg(&app, args, CLI::detail::Classifier::LONG), CLI::HorribleError);
}
// Test horrible error
TEST_F(TApp, CheckWindowsFail) {
args = {"-t"};
EXPECT_THROW(CLI::detail::AppFriend::parse_arg(&app, args, CLI::detail::Classifier::WINDOWS), CLI::HorribleError);
}
// Test horrible error
TEST_F(TApp, CheckOtherFail) {
args = {"-t"};
EXPECT_THROW(CLI::detail::AppFriend::parse_arg(&app, args, CLI::detail::Classifier::NONE), CLI::HorribleError);
} }
// Test horrible error // Test horrible error

View File

@ -471,6 +471,7 @@ TEST_F(TApp, SubcommandDefaults) {
EXPECT_FALSE(app.get_prefix_command()); EXPECT_FALSE(app.get_prefix_command());
EXPECT_FALSE(app.get_ignore_case()); EXPECT_FALSE(app.get_ignore_case());
EXPECT_FALSE(app.get_ignore_underscore()); EXPECT_FALSE(app.get_ignore_underscore());
EXPECT_FALSE(app.get_allow_windows_style_options());
EXPECT_FALSE(app.get_fallthrough()); EXPECT_FALSE(app.get_fallthrough());
EXPECT_EQ(app.get_footer(), ""); EXPECT_EQ(app.get_footer(), "");
EXPECT_EQ(app.get_group(), "Subcommands"); EXPECT_EQ(app.get_group(), "Subcommands");
@ -481,6 +482,7 @@ TEST_F(TApp, SubcommandDefaults) {
app.prefix_command(); app.prefix_command();
app.ignore_case(); app.ignore_case();
app.ignore_underscore(); app.ignore_underscore();
app.allow_windows_style_options();
app.fallthrough(); app.fallthrough();
app.footer("footy"); app.footer("footy");
app.group("Stuff"); app.group("Stuff");
@ -493,6 +495,7 @@ TEST_F(TApp, SubcommandDefaults) {
EXPECT_TRUE(app2->get_prefix_command()); EXPECT_TRUE(app2->get_prefix_command());
EXPECT_TRUE(app2->get_ignore_case()); EXPECT_TRUE(app2->get_ignore_case());
EXPECT_TRUE(app2->get_ignore_underscore()); EXPECT_TRUE(app2->get_ignore_underscore());
EXPECT_TRUE(app2->get_allow_windows_style_options());
EXPECT_TRUE(app2->get_fallthrough()); EXPECT_TRUE(app2->get_fallthrough());
EXPECT_EQ(app2->get_footer(), "footy"); EXPECT_EQ(app2->get_footer(), "footy");
EXPECT_EQ(app2->get_group(), "Stuff"); EXPECT_EQ(app2->get_group(), "Stuff");

View File

@ -32,6 +32,42 @@ TEST(String, InvalidName) {
EXPECT_TRUE(CLI::detail::valid_name_string("va-li-d")); EXPECT_TRUE(CLI::detail::valid_name_string("va-li-d"));
EXPECT_FALSE(CLI::detail::valid_name_string("vali&d")); EXPECT_FALSE(CLI::detail::valid_name_string("vali&d"));
EXPECT_TRUE(CLI::detail::valid_name_string("_valid")); EXPECT_TRUE(CLI::detail::valid_name_string("_valid"));
EXPECT_FALSE(CLI::detail::valid_name_string("/valid"));
}
TEST(StringTools, Modify) {
int cnt = 0;
std::string newString = CLI::detail::find_and_modify("======", "=", [&cnt](std::string &str, size_t index) {
if((++cnt) % 2 == 0) {
str[index] = ':';
}
return index + 1;
});
EXPECT_EQ(newString, "=:=:=:");
}
TEST(StringTools, Modify2) {
int cnt = 0;
std::string newString =
CLI::detail::find_and_modify("this is a string test", "is", [&cnt](std::string &str, size_t index) {
if((index > 1) && (str[index - 1] != ' ')) {
str[index] = 'a';
str[index + 1] = 't';
}
return index + 1;
});
EXPECT_EQ(newString, "that is a string test");
}
TEST(StringTools, Modify3) {
int cnt = 0;
// this picks up 3 sets of 3 after the 'b' then collapses the new first set
std::string newString = CLI::detail::find_and_modify("baaaaaaaaaa", "aaa", [&cnt](std::string &str, size_t index) {
str.erase(index, 3);
str.insert(str.begin(), 'a');
return 0;
});
EXPECT_EQ(newString, "aba");
} }
TEST(Trim, Various) { TEST(Trim, Various) {
@ -212,6 +248,36 @@ TEST(Validators, CombinedPaths) {
EXPECT_FALSE(CLI::ExistingFile(myfile).empty()); EXPECT_FALSE(CLI::ExistingFile(myfile).empty());
} }
TEST(Validators, ProgramNameSplit) {
TempFile myfile{"program_name1.exe"};
{
std::ofstream out{myfile};
out << "useless string doesn't matter" << std::endl;
}
auto res =
CLI::detail::split_program_name(std::string("./") + std::string(myfile) + " this is a bunch of extra stuff ");
EXPECT_EQ(res.first, std::string("./") + std::string(myfile));
EXPECT_EQ(res.second, "this is a bunch of extra stuff");
TempFile myfile2{"program name1.exe"};
{
std::ofstream out{myfile2};
out << "useless string doesn't matter" << std::endl;
}
res = CLI::detail::split_program_name(std::string(" ") + std::string("./") + std::string(myfile2) +
" this is a bunch of extra stuff ");
EXPECT_EQ(res.first, std::string("./") + std::string(myfile2));
EXPECT_EQ(res.second, "this is a bunch of extra stuff");
res = CLI::detail::split_program_name("./program_name this is a bunch of extra stuff ");
EXPECT_EQ(res.first, "./program_name"); // test sectioning of first argument even if it can't detect the file
EXPECT_EQ(res.second, "this is a bunch of extra stuff");
res = CLI::detail::split_program_name(std::string(" ./") + std::string(myfile) + " ");
EXPECT_EQ(res.first, std::string("./") + std::string(myfile));
EXPECT_TRUE(res.second.empty());
}
// Yes, this is testing an app_helper :) // Yes, this is testing an app_helper :)
TEST(AppHelper, TempfileCreated) { TEST(AppHelper, TempfileCreated) {
std::string name = "TestFileNotUsed.txt"; std::string name = "TestFileNotUsed.txt";

View File

@ -26,30 +26,6 @@ TEST_F(TApp, ExistingExeCheck) {
EXPECT_EQ(str3, "\"quoted string\""); 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) { TEST_F(TApp, ExistingExeCheckWithLotsOfSpace) {
TempFile tmpexe{"this is a weird file.exe"}; TempFile tmpexe{"this is a weird file.exe"};