diff --git a/README.md b/README.md index 01f0f195..1c46686b 100644 --- a/README.md +++ b/README.md @@ -255,6 +255,14 @@ On the command line, options can be given as: - `--file filename` (space) - `--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. 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_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. - `.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. diff --git a/include/CLI/App.hpp b/include/CLI/App.hpp index d773e01a..5f420edc 100644 --- a/include/CLI/App.hpp +++ b/include/CLI/App.hpp @@ -38,7 +38,7 @@ namespace CLI { #endif namespace detail { -enum class Classifer { NONE, POSITIONAL_MARK, SHORT, LONG, SUBCOMMAND }; +enum class Classifier { NONE, POSITIONAL_MARK, SHORT, LONG, WINDOWS, SUBCOMMAND }; struct AppFriend; } // namespace detail @@ -116,7 +116,7 @@ class App { /// @name Parsing ///@{ - using missing_t = std::vector>; + using missing_t = std::vector>; /// 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 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 App *parent_{nullptr}; @@ -200,6 +203,7 @@ class App { ignore_case_ = parent_->ignore_case_; ignore_underscore_ = parent_->ignore_underscore_; fallthrough_ = parent_->fallthrough_; + allow_windows_style_options_ = parent_->allow_windows_style_options_; group_ = parent_->group_; footer_ = parent_->footer_; formatter_ = parent_->formatter_; @@ -251,7 +255,7 @@ class App { 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) { prefix_command_ = allow; return this; @@ -269,6 +273,12 @@ class App { 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. App *ignore_underscore(bool value = true) { 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 /// process void parse(std::string commandline, bool program_name_included = false) { - detail::trim(commandline); + if(program_name_included) { - // try to determine the programName - auto esp = commandline.find_first_of(' ', 1); - while(!ExistingFile(commandline.substr(0, esp)).empty()) { - esp = commandline.find_first_of(' ', esp + 1); - if(esp == std::string::npos) { - // if we have reached the end and haven't found a valid file just assume the first argument is the - // program name - esp = commandline.find_first_of(' ', 1); - break; - } - } + auto nstr = detail::split_program_name(commandline); if(name_.empty()) { - name_ = commandline.substr(0, esp); - detail::rtrim(name_); + name_ = nstr.first; } - // strip the program name - commandline = commandline.substr(esp + 1); - } - // the first section of code is to deal with quoted arguments after and '=' + commandline = std::move(nstr.second); + } else + detail::trim(commandline); + // the next section of code is to deal with quoted arguments after an '=' or ':' for windows like operations if(!commandline.empty()) { - size_t offset = commandline.length() - 1; - auto qeq = commandline.find_last_of('=', offset); - while(qeq != std::string::npos) { - if((commandline[qeq + 1] == '\"') || (commandline[qeq + 1] == '\'') || (commandline[qeq + 1] == '`')) { - auto astart = commandline.find_last_of("- \"\'`", qeq - 1); + auto escape_detect = [](std::string &str, size_t offset) { + auto next = str[offset + 1]; + if((next == '\"') || (next == '\'') || (next == '`')) { + auto astart = str.find_last_of("-/ \"\'`", offset - 1); if(astart != std::string::npos) { - if(commandline[astart] == '-') { - commandline[qeq] = ' '; // interpret this a space so the split_up works properly - offset = (astart == 0) ? 0 : (astart - 1); - } + if(str[astart] == (str[offset] == '=') ? '-' : '/') + str[offset] = ' '; // interpret this as a space so the split_up works properly } } - offset = qeq - 1; - qeq = commandline.find_last_of('=', offset); - } + return (offset + 1); + }; + + 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)); @@ -1339,8 +1338,8 @@ class App { 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 - /// default arguments. Prefix will add a string to the beginning of each option. + /// Produce a string that could be read in as a config of the current values of the App. Set default_also to + /// 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 { return config_formatter_->to_config(this, default_also, write_description, ""); } @@ -1432,6 +1431,9 @@ class App { /// Check the status of 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 const std::string &get_group() const { return group_; } @@ -1512,7 +1514,7 @@ class App { /// This returns the missing options from the current subcommand std::vector remaining(bool recurse = false) const { std::vector miss_list; - for(const std::pair &miss : missing_) { + for(const std::pair &miss : missing_) { miss_list.push_back(std::get<1>(miss)); } @@ -1526,11 +1528,11 @@ class App { 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 { auto count = static_cast(std::count_if( - std::begin(missing_), std::end(missing_), [](const std::pair &val) { - return val.first != detail::Classifer::POSITIONAL_MARK; + std::begin(missing_), std::end(missing_), [](const std::pair &val) { + return val.first != detail::Classifier::POSITIONAL_MARK; })); if(recurse) { for(const App_p &sub : subcommands_) { @@ -1582,18 +1584,20 @@ class App { } /// 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; if(current == "--") - return detail::Classifer::POSITIONAL_MARK; + return detail::Classifier::POSITIONAL_MARK; if(_valid_subcommand(current)) - return detail::Classifer::SUBCOMMAND; + return detail::Classifier::SUBCOMMAND; if(detail::split_long(current, dummy1, dummy2)) - return detail::Classifer::LONG; + return detail::Classifier::LONG; if(detail::split_short(current, dummy1, dummy2)) - return detail::Classifer::SHORT; - return detail::Classifer::NONE; + return detail::Classifier::SHORT; + 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 @@ -1800,7 +1804,7 @@ class App { // If the option was not present if(get_allow_config_extras()) // 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; } @@ -1820,29 +1824,27 @@ class App { return true; } - /// Parse "one" argument (some may eat more than one), delegate to parent if fails, add to missing if missing from - /// master + /// Parse "one" argument (some may eat more than one), delegate to parent if fails, add to missing if missing + /// from master void _parse_single(std::vector &args, bool &positional_only) { - detail::Classifer classifer = positional_only ? detail::Classifer::NONE : _recognize(args.back()); - switch(classifer) { - case detail::Classifer::POSITIONAL_MARK: - missing_.emplace_back(classifer, args.back()); + detail::Classifier classifier = positional_only ? detail::Classifier::NONE : _recognize(args.back()); + switch(classifier) { + case detail::Classifier::POSITIONAL_MARK: + missing_.emplace_back(classifier, args.back()); args.pop_back(); positional_only = true; break; - case detail::Classifer::SUBCOMMAND: + case detail::Classifier::SUBCOMMAND: _parse_subcommand(args); 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_ - _parse_arg(args, true); + _parse_arg(args, classifier); break; - case detail::Classifer::SHORT: - // If already parsed a subcommand, don't accept options_ - _parse_arg(args, false); - break; - case detail::Classifer::NONE: + case detail::Classifier::NONE: // Probably a positional or something for a parent (sub)command _parse_positional(args); } @@ -1879,11 +1881,11 @@ class App { return parent_->_parse_positional(args); else { args.pop_back(); - missing_.emplace_back(detail::Classifer::NONE, positional); + missing_.emplace_back(detail::Classifier::NONE, positional); if(prefix_command_) { while(!args.empty()) { - missing_.emplace_back(detail::Classifer::NONE, args.back()); + missing_.emplace_back(detail::Classifier::NONE, args.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 - void _parse_arg(std::vector &args, bool second_dash) { - - detail::Classifer current_type = second_dash ? detail::Classifer::LONG : detail::Classifer::SHORT; + void _parse_arg(std::vector &args, detail::Classifier current_type) { std::string current = args.back(); @@ -1923,23 +1923,37 @@ class App { std::string value; std::string rest; - if(second_dash) { + switch(current_type) { + case detail::Classifier::LONG: if(!detail::split_long(current, name, value)) 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)) 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) { - return second_dash ? opt->check_lname(name) : opt->check_sname(name); + auto op_ptr = std::find_if(std::begin(options_), std::end(options_), [name, current_type](const Option_p &opt) { + 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 if(op_ptr == std::end(options_)) { // If a subcommand, try the master command if(parent_ != nullptr && fallthrough_) - return parent_->_parse_arg(args, second_dash); + return parent_->_parse_arg(args, current_type); // Otherwise, add to missing else { args.pop_back(); @@ -1981,7 +1995,7 @@ class App { // Unlimited vector parser if(num < 0) { - while(!args.empty() && _recognize(args.back()) == detail::Classifer::NONE) { + while(!args.empty() && _recognize(args.back()) == detail::Classifier::NONE) { if(collected >= -num) { // 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 - if(!args.empty() && _recognize(args.back()) == detail::Classifer::POSITIONAL_MARK) + if(!args.empty() && _recognize(args.back()) == detail::Classifier::POSITIONAL_MARK) args.pop_back(); } else { diff --git a/include/CLI/Formatter.hpp b/include/CLI/Formatter.hpp index 7f1885ec..df37279f 100644 --- a/include/CLI/Formatter.hpp +++ b/include/CLI/Formatter.hpp @@ -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 { - // 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 if(mode == AppFormatMode::Sub) return make_expanded(app); diff --git a/include/CLI/Option.hpp b/include/CLI/Option.hpp index f858ed6c..9da3450a 100644 --- a/include/CLI/Option.hpp +++ b/include/CLI/Option.hpp @@ -479,7 +479,7 @@ class Option : public OptionBase