Commit ce6dc0723e90f4ffccdaab30e32045cc199730df

Authored by Philip Top
Committed by Henry Schreiner
1 parent 2c024401

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
README.md
... ... @@ -255,6 +255,14 @@ On the command line, options can be given as:
255 255 - `--file filename` (space)
256 256 - `--file=filename` (equals)
257 257  
  258 +If allow_windows_style_options() is specified in the application or subcommand options can also be given as:
  259 +- `/a` (flag)
  260 +- `/f filename` (option)
  261 +- `/long` (long flag)
  262 +- `/file filename` (space)
  263 +- `/file:filename` (colon)
  264 += Windows style options do not allow combining short options or values not separated from the short option like with `-` options
  265 +
258 266 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.
259 267 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).
260 268  
... ... @@ -285,7 +293,7 @@ There are several options that are supported on the main app and subcommands. Th
285 293  
286 294 - `.ignore_case()`: Ignore the case of this subcommand. Inherited by added subcommands, so is usually used on the main `App`.
287 295 - `.ignore_underscore()`: Ignore any underscores in the subcommand name. Inherited by added subcommands, so is usually used on the main `App`.
288   -
  296 +- `.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`
289 297 - `.fallthrough()`: Allow extra unmatched options and positionals to "fall through" and be matched on a parent command. Subcommands always are allowed to fall through.
290 298 - `.require_subcommand()`: Require 1 or more subcommands.
291 299 - `.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.
... ...
include/CLI/App.hpp
... ... @@ -38,7 +38,7 @@ namespace CLI {
38 38 #endif
39 39  
40 40 namespace detail {
41   -enum class Classifer { NONE, POSITIONAL_MARK, SHORT, LONG, SUBCOMMAND };
  41 +enum class Classifier { NONE, POSITIONAL_MARK, SHORT, LONG, WINDOWS, SUBCOMMAND };
42 42 struct AppFriend;
43 43 } // namespace detail
44 44  
... ... @@ -116,7 +116,7 @@ class App {
116 116 /// @name Parsing
117 117 ///@{
118 118  
119   - using missing_t = std::vector<std::pair<detail::Classifer, std::string>>;
  119 + using missing_t = std::vector<std::pair<detail::Classifier, std::string>>;
120 120  
121 121 /// Pair of classifier, string for missing options. (extra detail is removed on returning from parse)
122 122 ///
... ... @@ -145,6 +145,9 @@ class App {
145 145 /// Allow subcommand fallthrough, so that parent commands can collect commands after subcommand. INHERITABLE
146 146 bool fallthrough_{false};
147 147  
  148 + /// Allow '/' for options for windows like options INHERITABLE
  149 + bool allow_windows_style_options_{false};
  150 +
148 151 /// A pointer to the parent if this is a subcommand
149 152 App *parent_{nullptr};
150 153  
... ... @@ -200,6 +203,7 @@ class App {
200 203 ignore_case_ = parent_->ignore_case_;
201 204 ignore_underscore_ = parent_->ignore_underscore_;
202 205 fallthrough_ = parent_->fallthrough_;
  206 + allow_windows_style_options_ = parent_->allow_windows_style_options_;
203 207 group_ = parent_->group_;
204 208 footer_ = parent_->footer_;
205 209 formatter_ = parent_->formatter_;
... ... @@ -251,7 +255,7 @@ class App {
251 255 return this;
252 256 }
253 257  
254   - /// Do not parse anything after the first unrecognised option and return
  258 + /// Do not parse anything after the first unrecognized option and return
255 259 App *prefix_command(bool allow = true) {
256 260 prefix_command_ = allow;
257 261 return this;
... ... @@ -269,6 +273,12 @@ class App {
269 273 return this;
270 274 }
271 275  
  276 + /// Ignore case. Subcommand inherit value.
  277 + App *allow_windows_style_options(bool value = true) {
  278 + allow_windows_style_options_ = value;
  279 + return this;
  280 + }
  281 +
272 282 /// Ignore underscore. Subcommand inherit value.
273 283 App *ignore_underscore(bool value = true) {
274 284 ignore_underscore_ = value;
... ... @@ -1172,43 +1182,32 @@ class App {
1172 1182 /// the function takes an optional boolean argument specifying if the programName is included in the string to
1173 1183 /// process
1174 1184 void parse(std::string commandline, bool program_name_included = false) {
1175   - detail::trim(commandline);
  1185 +
1176 1186 if(program_name_included) {
1177   - // try to determine the programName
1178   - auto esp = commandline.find_first_of(' ', 1);
1179   - while(!ExistingFile(commandline.substr(0, esp)).empty()) {
1180   - esp = commandline.find_first_of(' ', esp + 1);
1181   - if(esp == std::string::npos) {
1182   - // if we have reached the end and haven't found a valid file just assume the first argument is the
1183   - // program name
1184   - esp = commandline.find_first_of(' ', 1);
1185   - break;
1186   - }
1187   - }
  1187 + auto nstr = detail::split_program_name(commandline);
1188 1188 if(name_.empty()) {
1189   - name_ = commandline.substr(0, esp);
1190   - detail::rtrim(name_);
  1189 + name_ = nstr.first;
1191 1190 }
1192   - // strip the program name
1193   - commandline = commandline.substr(esp + 1);
1194   - }
1195   - // the first section of code is to deal with quoted arguments after and '='
  1191 + commandline = std::move(nstr.second);
  1192 + } else
  1193 + detail::trim(commandline);
  1194 + // the next section of code is to deal with quoted arguments after an '=' or ':' for windows like operations
1196 1195 if(!commandline.empty()) {
1197   - size_t offset = commandline.length() - 1;
1198   - auto qeq = commandline.find_last_of('=', offset);
1199   - while(qeq != std::string::npos) {
1200   - if((commandline[qeq + 1] == '\"') || (commandline[qeq + 1] == '\'') || (commandline[qeq + 1] == '`')) {
1201   - auto astart = commandline.find_last_of("- \"\'`", qeq - 1);
  1196 + auto escape_detect = [](std::string &str, size_t offset) {
  1197 + auto next = str[offset + 1];
  1198 + if((next == '\"') || (next == '\'') || (next == '`')) {
  1199 + auto astart = str.find_last_of("-/ \"\'`", offset - 1);
1202 1200 if(astart != std::string::npos) {
1203   - if(commandline[astart] == '-') {
1204   - commandline[qeq] = ' '; // interpret this a space so the split_up works properly
1205   - offset = (astart == 0) ? 0 : (astart - 1);
1206   - }
  1201 + if(str[astart] == (str[offset] == '=') ? '-' : '/')
  1202 + str[offset] = ' '; // interpret this as a space so the split_up works properly
1207 1203 }
1208 1204 }
1209   - offset = qeq - 1;
1210   - qeq = commandline.find_last_of('=', offset);
1211   - }
  1205 + return (offset + 1);
  1206 + };
  1207 +
  1208 + commandline = detail::find_and_modify(commandline, "=", escape_detect);
  1209 + if(allow_windows_style_options_)
  1210 + commandline = detail::find_and_modify(commandline, ":", escape_detect);
1212 1211 }
1213 1212  
1214 1213 auto args = detail::split_up(std::move(commandline));
... ... @@ -1339,8 +1338,8 @@ class App {
1339 1338 return this;
1340 1339 }
1341 1340  
1342   - /// Produce a string that could be read in as a config of the current values of the App. Set default_also to include
1343   - /// default arguments. Prefix will add a string to the beginning of each option.
  1341 + /// Produce a string that could be read in as a config of the current values of the App. Set default_also to
  1342 + /// include default arguments. Prefix will add a string to the beginning of each option.
1344 1343 std::string config_to_str(bool default_also = false, bool write_description = false) const {
1345 1344 return config_formatter_->to_config(this, default_also, write_description, "");
1346 1345 }
... ... @@ -1432,6 +1431,9 @@ class App {
1432 1431 /// Check the status of fallthrough
1433 1432 bool get_fallthrough() const { return fallthrough_; }
1434 1433  
  1434 + /// Check the status of the allow windows style options
  1435 + bool get_allow_windows_style_options() const { return allow_windows_style_options_; }
  1436 +
1435 1437 /// Get the group of this subcommand
1436 1438 const std::string &get_group() const { return group_; }
1437 1439  
... ... @@ -1512,7 +1514,7 @@ class App {
1512 1514 /// This returns the missing options from the current subcommand
1513 1515 std::vector<std::string> remaining(bool recurse = false) const {
1514 1516 std::vector<std::string> miss_list;
1515   - for(const std::pair<detail::Classifer, std::string> &miss : missing_) {
  1517 + for(const std::pair<detail::Classifier, std::string> &miss : missing_) {
1516 1518 miss_list.push_back(std::get<1>(miss));
1517 1519 }
1518 1520  
... ... @@ -1526,11 +1528,11 @@ class App {
1526 1528 return miss_list;
1527 1529 }
1528 1530  
1529   - /// This returns the number of remaining options, minus the -- seperator
  1531 + /// This returns the number of remaining options, minus the -- separator
1530 1532 size_t remaining_size(bool recurse = false) const {
1531 1533 auto count = static_cast<size_t>(std::count_if(
1532   - std::begin(missing_), std::end(missing_), [](const std::pair<detail::Classifer, std::string> &val) {
1533   - return val.first != detail::Classifer::POSITIONAL_MARK;
  1534 + std::begin(missing_), std::end(missing_), [](const std::pair<detail::Classifier, std::string> &val) {
  1535 + return val.first != detail::Classifier::POSITIONAL_MARK;
1534 1536 }));
1535 1537 if(recurse) {
1536 1538 for(const App_p &sub : subcommands_) {
... ... @@ -1582,18 +1584,20 @@ class App {
1582 1584 }
1583 1585  
1584 1586 /// Selects a Classifier enum based on the type of the current argument
1585   - detail::Classifer _recognize(const std::string &current) const {
  1587 + detail::Classifier _recognize(const std::string &current) const {
1586 1588 std::string dummy1, dummy2;
1587 1589  
1588 1590 if(current == "--")
1589   - return detail::Classifer::POSITIONAL_MARK;
  1591 + return detail::Classifier::POSITIONAL_MARK;
1590 1592 if(_valid_subcommand(current))
1591   - return detail::Classifer::SUBCOMMAND;
  1593 + return detail::Classifier::SUBCOMMAND;
1592 1594 if(detail::split_long(current, dummy1, dummy2))
1593   - return detail::Classifer::LONG;
  1595 + return detail::Classifier::LONG;
1594 1596 if(detail::split_short(current, dummy1, dummy2))
1595   - return detail::Classifer::SHORT;
1596   - return detail::Classifer::NONE;
  1597 + return detail::Classifier::SHORT;
  1598 + if((allow_windows_style_options_) && (detail::split_windows(current, dummy1, dummy2)))
  1599 + return detail::Classifier::WINDOWS;
  1600 + return detail::Classifier::NONE;
1597 1601 }
1598 1602  
1599 1603 // The parse function is now broken into several parts, and part of process
... ... @@ -1800,7 +1804,7 @@ class App {
1800 1804 // If the option was not present
1801 1805 if(get_allow_config_extras())
1802 1806 // Should we worry about classifying the extras properly?
1803   - missing_.emplace_back(detail::Classifer::NONE, item.fullname());
  1807 + missing_.emplace_back(detail::Classifier::NONE, item.fullname());
1804 1808 return false;
1805 1809 }
1806 1810  
... ... @@ -1820,29 +1824,27 @@ class App {
1820 1824 return true;
1821 1825 }
1822 1826  
1823   - /// Parse "one" argument (some may eat more than one), delegate to parent if fails, add to missing if missing from
1824   - /// master
  1827 + /// Parse "one" argument (some may eat more than one), delegate to parent if fails, add to missing if missing
  1828 + /// from master
1825 1829 void _parse_single(std::vector<std::string> &args, bool &positional_only) {
1826 1830  
1827   - detail::Classifer classifer = positional_only ? detail::Classifer::NONE : _recognize(args.back());
1828   - switch(classifer) {
1829   - case detail::Classifer::POSITIONAL_MARK:
1830   - missing_.emplace_back(classifer, args.back());
  1831 + detail::Classifier classifier = positional_only ? detail::Classifier::NONE : _recognize(args.back());
  1832 + switch(classifier) {
  1833 + case detail::Classifier::POSITIONAL_MARK:
  1834 + missing_.emplace_back(classifier, args.back());
1831 1835 args.pop_back();
1832 1836 positional_only = true;
1833 1837 break;
1834   - case detail::Classifer::SUBCOMMAND:
  1838 + case detail::Classifier::SUBCOMMAND:
1835 1839 _parse_subcommand(args);
1836 1840 break;
1837   - case detail::Classifer::LONG:
1838   - // If already parsed a subcommand, don't accept options_
1839   - _parse_arg(args, true);
1840   - break;
1841   - case detail::Classifer::SHORT:
  1841 + case detail::Classifier::LONG:
  1842 + case detail::Classifier::SHORT:
  1843 + case detail::Classifier::WINDOWS:
1842 1844 // If already parsed a subcommand, don't accept options_
1843   - _parse_arg(args, false);
  1845 + _parse_arg(args, classifier);
1844 1846 break;
1845   - case detail::Classifer::NONE:
  1847 + case detail::Classifier::NONE:
1846 1848 // Probably a positional or something for a parent (sub)command
1847 1849 _parse_positional(args);
1848 1850 }
... ... @@ -1879,11 +1881,11 @@ class App {
1879 1881 return parent_->_parse_positional(args);
1880 1882 else {
1881 1883 args.pop_back();
1882   - missing_.emplace_back(detail::Classifer::NONE, positional);
  1884 + missing_.emplace_back(detail::Classifier::NONE, positional);
1883 1885  
1884 1886 if(prefix_command_) {
1885 1887 while(!args.empty()) {
1886   - missing_.emplace_back(detail::Classifer::NONE, args.back());
  1888 + missing_.emplace_back(detail::Classifier::NONE, args.back());
1887 1889 args.pop_back();
1888 1890 }
1889 1891 }
... ... @@ -1913,9 +1915,7 @@ class App {
1913 1915 }
1914 1916  
1915 1917 /// Parse a short (false) or long (true) argument, must be at the top of the list
1916   - void _parse_arg(std::vector<std::string> &args, bool second_dash) {
1917   -
1918   - detail::Classifer current_type = second_dash ? detail::Classifer::LONG : detail::Classifer::SHORT;
  1918 + void _parse_arg(std::vector<std::string> &args, detail::Classifier current_type) {
1919 1919  
1920 1920 std::string current = args.back();
1921 1921  
... ... @@ -1923,23 +1923,37 @@ class App {
1923 1923 std::string value;
1924 1924 std::string rest;
1925 1925  
1926   - if(second_dash) {
  1926 + switch(current_type) {
  1927 + case detail::Classifier::LONG:
1927 1928 if(!detail::split_long(current, name, value))
1928 1929 throw HorribleError("Long parsed but missing (you should not see this):" + args.back());
1929   - } else {
  1930 + break;
  1931 + case detail::Classifier::SHORT:
1930 1932 if(!detail::split_short(current, name, rest))
1931 1933 throw HorribleError("Short parsed but missing! You should not see this");
  1934 + break;
  1935 + case detail::Classifier::WINDOWS:
  1936 + if(!detail::split_windows(current, name, value))
  1937 + throw HorribleError("windows option parsed but missing! You should not see this");
  1938 + break;
  1939 + default:
  1940 + throw HorribleError("parsing got called with invalid option! You should not see this");
1932 1941 }
1933 1942  
1934   - auto op_ptr = std::find_if(std::begin(options_), std::end(options_), [name, second_dash](const Option_p &opt) {
1935   - return second_dash ? opt->check_lname(name) : opt->check_sname(name);
  1943 + auto op_ptr = std::find_if(std::begin(options_), std::end(options_), [name, current_type](const Option_p &opt) {
  1944 + if(current_type == detail::Classifier::LONG)
  1945 + return opt->check_lname(name);
  1946 + if(current_type == detail::Classifier::SHORT)
  1947 + return opt->check_sname(name);
  1948 + // this will only get called for detail::Classifier::WINDOWS
  1949 + return opt->check_lname(name) || opt->check_sname(name);
1936 1950 });
1937 1951  
1938 1952 // Option not found
1939 1953 if(op_ptr == std::end(options_)) {
1940 1954 // If a subcommand, try the master command
1941 1955 if(parent_ != nullptr && fallthrough_)
1942   - return parent_->_parse_arg(args, second_dash);
  1956 + return parent_->_parse_arg(args, current_type);
1943 1957 // Otherwise, add to missing
1944 1958 else {
1945 1959 args.pop_back();
... ... @@ -1981,7 +1995,7 @@ class App {
1981 1995  
1982 1996 // Unlimited vector parser
1983 1997 if(num < 0) {
1984   - while(!args.empty() && _recognize(args.back()) == detail::Classifer::NONE) {
  1998 + while(!args.empty() && _recognize(args.back()) == detail::Classifier::NONE) {
1985 1999 if(collected >= -num) {
1986 2000 // We could break here for allow extras, but we don't
1987 2001  
... ... @@ -1996,7 +2010,7 @@ class App {
1996 2010 }
1997 2011  
1998 2012 // Allow -- to end an unlimited list and "eat" it
1999   - if(!args.empty() && _recognize(args.back()) == detail::Classifer::POSITIONAL_MARK)
  2013 + if(!args.empty() && _recognize(args.back()) == detail::Classifier::POSITIONAL_MARK)
2000 2014 args.pop_back();
2001 2015  
2002 2016 } else {
... ...
include/CLI/Formatter.hpp
... ... @@ -115,7 +115,7 @@ inline std::string Formatter::make_footer(const App *app) const {
115 115  
116 116 inline std::string Formatter::make_help(const App *app, std::string name, AppFormatMode mode) const {
117 117  
118   - // This immediatly forwards to the make_expanded method. This is done this way so that subcommands can
  118 + // This immediately forwards to the make_expanded method. This is done this way so that subcommands can
119 119 // have overridden formatters
120 120 if(mode == AppFormatMode::Sub)
121 121 return make_expanded(app);
... ...
include/CLI/Option.hpp
... ... @@ -479,7 +479,7 @@ class Option : public OptionBase&lt;Option&gt; {
479 479 int get_expected() const { return expected_; }
480 480  
481 481 /// \brief The total number of expected values (including the type)
482   - /// This is positive if exactly this number is expected, and negitive for at least N values
  482 + /// This is positive if exactly this number is expected, and negative for at least N values
483 483 ///
484 484 /// v = fabs(size_type*expected)
485 485 /// !MultiOptionPolicy::Throw
... ... @@ -518,7 +518,7 @@ class Option : public OptionBase&lt;Option&gt; {
518 518 /// @name Help tools
519 519 ///@{
520 520  
521   - /// \brief Gets a comma seperated list of names.
  521 + /// \brief Gets a comma separated list of names.
522 522 /// Will include / prefer the positional name if positional is true.
523 523 /// If all_options is false, pick just the most descriptive name to show.
524 524 /// Use `get_name(true)` to get the positional name (replaces `get_pname`)
... ... @@ -530,7 +530,7 @@ class Option : public OptionBase&lt;Option&gt; {
530 530  
531 531 std::vector<std::string> name_list;
532 532  
533   - /// The all list wil never include a positional unless asked or that's the only name.
  533 + /// The all list will never include a positional unless asked or that's the only name.
534 534 if((positional && pname_.length()) || (snames_.empty() && lnames_.empty()))
535 535 name_list.push_back(pname_);
536 536  
... ...
include/CLI/Split.hpp
... ... @@ -26,7 +26,7 @@ inline bool split_short(const std::string &amp;current, std::string &amp;name, std::stri
26 26 // Returns false if not a long option. Otherwise, sets opt name and other side of = and returns true
27 27 inline bool split_long(const std::string &current, std::string &name, std::string &value) {
28 28 if(current.size() > 2 && current.substr(0, 2) == "--" && valid_first_char(current[2])) {
29   - auto loc = current.find("=");
  29 + auto loc = current.find_first_of('=');
30 30 if(loc != std::string::npos) {
31 31 name = current.substr(2, loc - 2);
32 32 value = current.substr(loc + 1);
... ... @@ -39,6 +39,22 @@ inline bool split_long(const std::string &amp;current, std::string &amp;name, std::strin
39 39 return false;
40 40 }
41 41  
  42 +// Returns false if not a windows style option. Otherwise, sets opt name and value and returns true
  43 +inline bool split_windows(const std::string &current, std::string &name, std::string &value) {
  44 + if(current.size() > 1 && current[0] == '/' && valid_first_char(current[1])) {
  45 + auto loc = current.find_first_of(':');
  46 + if(loc != std::string::npos) {
  47 + name = current.substr(1, loc - 1);
  48 + value = current.substr(loc + 1);
  49 + } else {
  50 + name = current.substr(1);
  51 + value = "";
  52 + }
  53 + return true;
  54 + } else
  55 + return false;
  56 +}
  57 +
42 58 // Splits a string into multiple long and short names
43 59 inline std::vector<std::string> split_names(std::string current) {
44 60 std::vector<std::string> output;
... ...
include/CLI/StringTools.hpp
... ... @@ -161,6 +161,16 @@ inline std::string find_and_replace(std::string str, std::string from, std::stri
161 161 return str;
162 162 }
163 163  
  164 +/// Find a trigger string and call a modify callable function that takes the current string and starting position of the
  165 +/// trigger and returns the position in the string to search for the next trigger string
  166 +template <typename Callable> inline std::string find_and_modify(std::string str, std::string trigger, Callable modify) {
  167 + size_t start_pos = 0;
  168 + while((start_pos = str.find(trigger, start_pos)) != std::string::npos) {
  169 + start_pos = modify(str, start_pos);
  170 + }
  171 + return str;
  172 +}
  173 +
164 174 /// Split a string '"one two" "three"' into 'one two', 'three'
165 175 /// Quote characters can be ` ' or "
166 176 inline std::vector<std::string> split_up(std::string str) {
... ...
include/CLI/Validators.hpp
... ... @@ -191,6 +191,33 @@ struct Range : public Validator {
191 191 template <typename T> explicit Range(T max) : Range(static_cast<T>(0), max) {}
192 192 };
193 193  
  194 +namespace detail {
  195 +/// split a string into a program name and command line arguments
  196 +/// the string is assumed to contain a file name followed by other arguments
  197 +/// the return value contains is a pair with the first argument containing the program name and the second everything
  198 +/// else
  199 +inline std::pair<std::string, std::string> split_program_name(std::string commandline) {
  200 + // try to determine the programName
  201 + std::pair<std::string, std::string> vals;
  202 + trim(commandline);
  203 + auto esp = commandline.find_first_of(' ', 1);
  204 + while(!ExistingFile(commandline.substr(0, esp)).empty()) {
  205 + esp = commandline.find_first_of(' ', esp + 1);
  206 + if(esp == std::string::npos) {
  207 + // if we have reached the end and haven't found a valid file just assume the first argument is the
  208 + // program name
  209 + esp = commandline.find_first_of(' ', 1);
  210 + break;
  211 + }
  212 + }
  213 + vals.first = commandline.substr(0, esp);
  214 + rtrim(vals.first);
  215 + // strip the program name
  216 + vals.second = (esp != std::string::npos) ? commandline.substr(esp + 1) : std::string{};
  217 + ltrim(vals.second);
  218 + return vals;
  219 +}
  220 +} // namespace detail
194 221 /// @}
195 222  
196 223 } // namespace CLI
... ...
tests/AppTest.cpp
... ... @@ -10,6 +10,15 @@ TEST_F(TApp, OneFlagShort) {
10 10 EXPECT_EQ((size_t)1, app.count("--count"));
11 11 }
12 12  
  13 +TEST_F(TApp, OneFlagShortWindows) {
  14 + app.add_flag("-c,--count");
  15 + args = {"/c"};
  16 + app.allow_windows_style_options();
  17 + run();
  18 + EXPECT_EQ((size_t)1, app.count("-c"));
  19 + EXPECT_EQ((size_t)1, app.count("--count"));
  20 +}
  21 +
13 22 TEST_F(TApp, CountNonExist) {
14 23 app.add_flag("-c,--count");
15 24 args = {"-c"};
... ... @@ -70,6 +79,17 @@ TEST_F(TApp, OneString) {
70 79 EXPECT_EQ(str, "mystring");
71 80 }
72 81  
  82 +TEST_F(TApp, OneStringWindowsStyle) {
  83 + std::string str;
  84 + app.add_option("-s,--string", str);
  85 + args = {"/string", "mystring"};
  86 + app.allow_windows_style_options();
  87 + run();
  88 + EXPECT_EQ((size_t)1, app.count("-s"));
  89 + EXPECT_EQ((size_t)1, app.count("--string"));
  90 + EXPECT_EQ(str, "mystring");
  91 +}
  92 +
73 93 TEST_F(TApp, OneStringSingleStringInput) {
74 94 std::string str;
75 95 app.add_option("-s,--string", str);
... ... @@ -90,6 +110,17 @@ TEST_F(TApp, OneStringEqualVersion) {
90 110 EXPECT_EQ(str, "mystring");
91 111 }
92 112  
  113 +TEST_F(TApp, OneStringEqualVersionWindowsStyle) {
  114 + std::string str;
  115 + app.add_option("-s,--string", str);
  116 + args = {"/string:mystring"};
  117 + app.allow_windows_style_options();
  118 + run();
  119 + EXPECT_EQ((size_t)1, app.count("-s"));
  120 + EXPECT_EQ((size_t)1, app.count("--string"));
  121 + EXPECT_EQ(str, "mystring");
  122 +}
  123 +
93 124 TEST_F(TApp, OneStringEqualVersionSingleString) {
94 125 std::string str;
95 126 app.add_option("-s,--string", str);
... ... @@ -119,6 +150,18 @@ TEST_F(TApp, OneStringEqualVersionSingleStringQuotedMultiple) {
119 150 EXPECT_EQ(str3, "\"quoted string\"");
120 151 }
121 152  
  153 +TEST_F(TApp, OneStringEqualVersionSingleStringQuotedMultipleMixedStyle) {
  154 + std::string str, str2, str3;
  155 + app.add_option("-s,--string", str);
  156 + app.add_option("-t,--tstr", str2);
  157 + app.add_option("-m,--mstr", str3);
  158 + app.allow_windows_style_options();
  159 + app.parse("/string:\"this is my quoted string\" /t 'qstring 2' -m=`\"quoted string\"`");
  160 + EXPECT_EQ(str, "this is my quoted string");
  161 + EXPECT_EQ(str2, "qstring 2");
  162 + EXPECT_EQ(str3, "\"quoted string\"");
  163 +}
  164 +
122 165 TEST_F(TApp, OneStringEqualVersionSingleStringQuotedMultipleInMiddle) {
123 166 std::string str, str2, str3;
124 167 app.add_option("-s,--string", str);
... ... @@ -1077,6 +1120,20 @@ TEST_F(TApp, InIntSet) {
1077 1120 EXPECT_THROW(run(), CLI::ConversionError);
1078 1121 }
1079 1122  
  1123 +TEST_F(TApp, InIntSetWindows) {
  1124 +
  1125 + int choice;
  1126 + app.add_set("-q,--quick", choice, {1, 2, 3});
  1127 + app.allow_windows_style_options();
  1128 + args = {"/q", "2"};
  1129 +
  1130 + run();
  1131 + EXPECT_EQ(2, choice);
  1132 +
  1133 + args = {"/q4"};
  1134 + EXPECT_THROW(run(), CLI::ExtrasError);
  1135 +}
  1136 +
1080 1137 TEST_F(TApp, FailSet) {
1081 1138  
1082 1139 int choice;
... ... @@ -1547,14 +1604,28 @@ TEST_F(TApp, AllowExtrasOrder) {
1547 1604 TEST_F(TApp, CheckShortFail) {
1548 1605 args = {"--two"};
1549 1606  
1550   - EXPECT_THROW(CLI::detail::AppFriend::parse_arg(&app, args, false), CLI::HorribleError);
  1607 + EXPECT_THROW(CLI::detail::AppFriend::parse_arg(&app, args, CLI::detail::Classifier::SHORT), CLI::HorribleError);
1551 1608 }
1552 1609  
1553 1610 // Test horrible error
1554 1611 TEST_F(TApp, CheckLongFail) {
1555 1612 args = {"-t"};
1556 1613  
1557   - EXPECT_THROW(CLI::detail::AppFriend::parse_arg(&app, args, true), CLI::HorribleError);
  1614 + EXPECT_THROW(CLI::detail::AppFriend::parse_arg(&app, args, CLI::detail::Classifier::LONG), CLI::HorribleError);
  1615 +}
  1616 +
  1617 +// Test horrible error
  1618 +TEST_F(TApp, CheckWindowsFail) {
  1619 + args = {"-t"};
  1620 +
  1621 + EXPECT_THROW(CLI::detail::AppFriend::parse_arg(&app, args, CLI::detail::Classifier::WINDOWS), CLI::HorribleError);
  1622 +}
  1623 +
  1624 +// Test horrible error
  1625 +TEST_F(TApp, CheckOtherFail) {
  1626 + args = {"-t"};
  1627 +
  1628 + EXPECT_THROW(CLI::detail::AppFriend::parse_arg(&app, args, CLI::detail::Classifier::NONE), CLI::HorribleError);
1558 1629 }
1559 1630  
1560 1631 // Test horrible error
... ...
tests/CreationTest.cpp
... ... @@ -471,6 +471,7 @@ TEST_F(TApp, SubcommandDefaults) {
471 471 EXPECT_FALSE(app.get_prefix_command());
472 472 EXPECT_FALSE(app.get_ignore_case());
473 473 EXPECT_FALSE(app.get_ignore_underscore());
  474 + EXPECT_FALSE(app.get_allow_windows_style_options());
474 475 EXPECT_FALSE(app.get_fallthrough());
475 476 EXPECT_EQ(app.get_footer(), "");
476 477 EXPECT_EQ(app.get_group(), "Subcommands");
... ... @@ -481,6 +482,7 @@ TEST_F(TApp, SubcommandDefaults) {
481 482 app.prefix_command();
482 483 app.ignore_case();
483 484 app.ignore_underscore();
  485 + app.allow_windows_style_options();
484 486 app.fallthrough();
485 487 app.footer("footy");
486 488 app.group("Stuff");
... ... @@ -493,6 +495,7 @@ TEST_F(TApp, SubcommandDefaults) {
493 495 EXPECT_TRUE(app2->get_prefix_command());
494 496 EXPECT_TRUE(app2->get_ignore_case());
495 497 EXPECT_TRUE(app2->get_ignore_underscore());
  498 + EXPECT_TRUE(app2->get_allow_windows_style_options());
496 499 EXPECT_TRUE(app2->get_fallthrough());
497 500 EXPECT_EQ(app2->get_footer(), "footy");
498 501 EXPECT_EQ(app2->get_group(), "Stuff");
... ...
tests/HelpersTest.cpp
... ... @@ -32,6 +32,42 @@ TEST(String, InvalidName) {
32 32 EXPECT_TRUE(CLI::detail::valid_name_string("va-li-d"));
33 33 EXPECT_FALSE(CLI::detail::valid_name_string("vali&d"));
34 34 EXPECT_TRUE(CLI::detail::valid_name_string("_valid"));
  35 + EXPECT_FALSE(CLI::detail::valid_name_string("/valid"));
  36 +}
  37 +
  38 +TEST(StringTools, Modify) {
  39 + int cnt = 0;
  40 + std::string newString = CLI::detail::find_and_modify("======", "=", [&cnt](std::string &str, size_t index) {
  41 + if((++cnt) % 2 == 0) {
  42 + str[index] = ':';
  43 + }
  44 + return index + 1;
  45 + });
  46 + EXPECT_EQ(newString, "=:=:=:");
  47 +}
  48 +
  49 +TEST(StringTools, Modify2) {
  50 + int cnt = 0;
  51 + std::string newString =
  52 + CLI::detail::find_and_modify("this is a string test", "is", [&cnt](std::string &str, size_t index) {
  53 + if((index > 1) && (str[index - 1] != ' ')) {
  54 + str[index] = 'a';
  55 + str[index + 1] = 't';
  56 + }
  57 + return index + 1;
  58 + });
  59 + EXPECT_EQ(newString, "that is a string test");
  60 +}
  61 +
  62 +TEST(StringTools, Modify3) {
  63 + int cnt = 0;
  64 + // this picks up 3 sets of 3 after the 'b' then collapses the new first set
  65 + std::string newString = CLI::detail::find_and_modify("baaaaaaaaaa", "aaa", [&cnt](std::string &str, size_t index) {
  66 + str.erase(index, 3);
  67 + str.insert(str.begin(), 'a');
  68 + return 0;
  69 + });
  70 + EXPECT_EQ(newString, "aba");
35 71 }
36 72  
37 73 TEST(Trim, Various) {
... ... @@ -212,6 +248,36 @@ TEST(Validators, CombinedPaths) {
212 248 EXPECT_FALSE(CLI::ExistingFile(myfile).empty());
213 249 }
214 250  
  251 +TEST(Validators, ProgramNameSplit) {
  252 + TempFile myfile{"program_name1.exe"};
  253 + {
  254 + std::ofstream out{myfile};
  255 + out << "useless string doesn't matter" << std::endl;
  256 + }
  257 + auto res =
  258 + CLI::detail::split_program_name(std::string("./") + std::string(myfile) + " this is a bunch of extra stuff ");
  259 + EXPECT_EQ(res.first, std::string("./") + std::string(myfile));
  260 + EXPECT_EQ(res.second, "this is a bunch of extra stuff");
  261 +
  262 + TempFile myfile2{"program name1.exe"};
  263 + {
  264 + std::ofstream out{myfile2};
  265 + out << "useless string doesn't matter" << std::endl;
  266 + }
  267 + res = CLI::detail::split_program_name(std::string(" ") + std::string("./") + std::string(myfile2) +
  268 + " this is a bunch of extra stuff ");
  269 + EXPECT_EQ(res.first, std::string("./") + std::string(myfile2));
  270 + EXPECT_EQ(res.second, "this is a bunch of extra stuff");
  271 +
  272 + res = CLI::detail::split_program_name("./program_name this is a bunch of extra stuff ");
  273 + EXPECT_EQ(res.first, "./program_name"); // test sectioning of first argument even if it can't detect the file
  274 + EXPECT_EQ(res.second, "this is a bunch of extra stuff");
  275 +
  276 + res = CLI::detail::split_program_name(std::string(" ./") + std::string(myfile) + " ");
  277 + EXPECT_EQ(res.first, std::string("./") + std::string(myfile));
  278 + EXPECT_TRUE(res.second.empty());
  279 +}
  280 +
215 281 // Yes, this is testing an app_helper :)
216 282 TEST(AppHelper, TempfileCreated) {
217 283 std::string name = "TestFileNotUsed.txt";
... ...
tests/StringParseTest.cpp
... ... @@ -26,30 +26,6 @@ TEST_F(TApp, ExistingExeCheck) {
26 26 EXPECT_EQ(str3, "\"quoted string\"");
27 27 }
28 28  
29   -TEST_F(TApp, ExistingExeCheckWithSpace) {
30   -
31   - TempFile tmpexe{"Space File.out"};
32   -
33   - std::string str, str2, str3;
34   - app.add_option("-s,--string", str);
35   - app.add_option("-t,--tstr", str2);
36   - app.add_option("-m,--mstr", str3);
37   -
38   - {
39   - std::ofstream out{tmpexe};
40   - out << "useless string doesn't matter" << std::endl;
41   - }
42   -
43   - app.parse(std::string("./") + std::string(tmpexe) +
44   - " --string=\"this is my quoted string\" -t 'qstring 2' -m=`\"quoted string\"`",
45   - true);
46   - EXPECT_EQ(str, "this is my quoted string");
47   - EXPECT_EQ(str2, "qstring 2");
48   - EXPECT_EQ(str3, "\"quoted string\"");
49   -
50   - EXPECT_EQ(app.get_name(), std::string("./") + std::string(tmpexe));
51   -}
52   -
53 29 TEST_F(TApp, ExistingExeCheckWithLotsOfSpace) {
54 30  
55 31 TempFile tmpexe{"this is a weird file.exe"};
... ...