Commit ce6dc0723e90f4ffccdaab30e32045cc199730df
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
Showing
11 changed files
with
295 additions
and
104 deletions
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 ¤t) const { | |
| 1587 | + detail::Classifier _recognize(const std::string ¤t) 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<Option> { |
| 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<Option> { |
| 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<Option> { |
| 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 &current, std::string &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 ¤t, 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 &current, std::string &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 ¤t, 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"}; | ... | ... |