mirror of
https://github.com/CLIUtils/CLI11.git
synced 2025-04-29 20:23:55 +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:
parent
2c024401cc
commit
ce6dc0723e
10
README.md
10
README.md
@ -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.
|
||||||
|
@ -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 ¤t) const {
|
detail::Classifier _recognize(const std::string ¤t) 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 {
|
||||||
|
@ -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);
|
||||||
|
@ -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_);
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ inline bool split_short(const std::string ¤t, 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 ¤t, std::string &name, std::string &value) {
|
inline bool split_long(const std::string ¤t, 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 ¤t, 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 ¤t, 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;
|
||||||
|
@ -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) {
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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");
|
||||||
|
@ -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";
|
||||||
|
@ -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"};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user