From 196012a9f796eb76b5de8adfd89e359c9e8be1b3 Mon Sep 17 00:00:00 2001 From: Henry Fredrick Schreiner Date: Mon, 20 Feb 2017 13:22:42 -0500 Subject: [PATCH] Adding (untested) ignore case abilities --- CHANGELOG.md | 5 +++-- README.md | 19 +++++++++-------- include/CLI/App.hpp | 41 +++++++++++++++++++++++++++++++++++-- include/CLI/Option.hpp | 37 +++++++++++++++++++++++++++------ include/CLI/StringTools.hpp | 2 +- 5 files changed, 85 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c8e4c55..776a952e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,8 @@ ## Version 0.5 (in progress) -* Subcommands now can be "chained", that is, left over arguments can now include subcommands that then get parsed. Subcommands are now a list (`get_subcommands`). Added `got_subcommand(App_or_name)` to check for subcommands. -* Added `.allow_extras()` to disable error on failure. Parse returns a vector of leftover options. Renamed error to `ExtrasError`, and now triggers on extra options too. +* `->ignore_case()` added to subcommands, options, and `add_set_ignore_case` (untested). Subcommand inherit setting from parent App on creation. +* Subcommands now can be "chained", that is, left over arguments can now include subcommands that then get parsed. Subcommands are now a list (`get_subcommands`). Added `got_subcommand(App_or_name)` to check for subcommands. (untested) +* Added `.allow_extras()` to disable error on failure. Parse returns a vector of leftover options. Renamed error to `ExtrasError`, and now triggers on extra options too. (untested) * Added `require_subcommand` to `App`, to simplify forcing subcommands. Do not "chain" with `add_subcommand`, since that is the subcommand, not the master `App`. * Added printout of ini file text given parsed options, skips flags. * Support for quotes and spaces in ini files diff --git a/README.md b/README.md index 7a9b3b85..5c9f0e43 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,8 @@ app.add_set(option_name, help_string="", default=false) +app.add_set_ignore_case(... // String only + app.add_config(option_name, default_file_name="", help_string="Read an ini file", @@ -125,14 +127,15 @@ Adding a configuration option is special. If it is present, it will be read alon The add commands return a pointer to an internally stored `Option`. If you set the final argument to true, the default value is captured and printed on the command line with the help flag. This option can be used directly to check for the count (`->count()`) after parsing to avoid a string based lookup. Before parsing, you can set the following options: * `->required()`: The program will quit if this option is not present. This is `mandatory` in Plumbum, but required options seems to be a more standard term. For compatibility, `->mandatory()` also works. -* `->expected(N)`: Take `N` values instead of as many as possible, only for vector args -* `->requires(opt)`: This option requires another option to also be present, opt is an `Option` pointer -* `->excludes(opt)`: This option cannot be given with `opt` present, opt is an `Option` pointer -* `->envname(name)`: Gets the value from the environment if present and not passed on the command line +* `->expected(N)`: Take `N` values instead of as many as possible, only for vector args. +* `->requires(opt)`: This option requires another option to also be present, opt is an `Option` pointer. +* `->excludes(opt)`: This option cannot be given with `opt` present, opt is an `Option` pointer. +* `->envname(name)`: Gets the value from the environment if present and not passed on the command line. * `->group(name)`: The help group to put the option in. No effect for positional options. Defaults to `"Options"`. -* `->check(CLI::ExistingFile)`: Requires that the file exists if given -* `->check(CLI::ExistingDirectory)`: Requires that the directory exists -* `->check(CLI::NonexistentPath)`: Requires that the path does not exist +* `->ignore_case()`: Ignore the case on the command line (also works on subcommands, does not affect arguments). +* `->check(CLI::ExistingFile)`: Requires that the file exists if given. +* `->check(CLI::ExistingDirectory)`: Requires that the directory exists. +* `->check(CLI::NonexistentPath)`: Requires that the path does not exist. * `->check(CLI::Range(min,max))`: Requires that the option be between min and max (make sure to use floating point if needed). Min defaults to 0. These options return the `Option` pointer, so you can chain them together, and even skip storing the pointer entirely. Check takes any function that has the signature `bool(std::string)`. If you want to change the default help option, it is available through `get_help_ptr`. @@ -157,7 +160,7 @@ everything after that is positional only. ## Subcommands -Subcommands are naturally supported, with an infinite depth. To add a subcommand, call the `add_subcommand` method with a name and an optional description. This gives a pointer to an `App` that behaves just like the main app, and can take options or further subcommands. +Subcommands are naturally supported, with an infinite depth. To add a subcommand, call the `add_subcommand` method with a name and an optional description. This gives a pointer to an `App` that behaves just like the main app, and can take options or further subcommands. Add `->ignore_case()` to a subcommand to allow any variation of caps to also be accepted. Children inherit the current setting from the parent. All `App`s have a `get_subcommands()` method, which returns a list of pointers to the subcommand passed on the command line. A simple compare of these pointers to each subcommand allows choosing based on subcommand, facilitated by a `got_subcommand(App_or_name) method that will check the list for you. For many cases, however, using an app's callback may be easier. Every app executes a callback function after it parses; just use a lambda function (with capture to get parsed values) to `.add_callback`. If you throw `CLI::Success`, you can even exit the program through the callback. The main `App` has a callback slot, as well, but it is generally not as useful. diff --git a/include/CLI/App.hpp b/include/CLI/App.hpp index 5b103e2c..21c0dcce 100644 --- a/include/CLI/App.hpp +++ b/include/CLI/App.hpp @@ -123,6 +123,7 @@ public: subcommands.emplace_back(new App(description, help)); subcommands.back()->name = name_; subcommands.back()->allow_extras(); + subcommands.back()->case_insensitive = case_insensitive; return subcommands.back().get(); } @@ -312,6 +313,42 @@ public: return retval; } + /// Add set of options, string only, ignore case + Option* add_set_ignore_case( + std::string name_, + std::string &member, ///< The selected member of the set + std::set _options, ///< The set of posibilities + std::string description="", + bool defaulted=false + ) { + + CLI::callback_t fun = [&member, _options](CLI::results_t res){ + if(res.size()!=1) { + return false; + } + if(res[0].size()!=1) { + return false; + } + member = detail::to_lower(res.at(0).at(0)); + auto iter = std::find_if(std::begin(_options), std::end(_options), + [&member](std::string val){return detail::to_lower(val) == member;}); + if(iter == std::end(_options)) + return false; + else { + member = *iter; + return true; + } + }; + + Option* retval = add_option(name_, fun, description, defaulted); + retval->typeval = detail::type_name(); + retval->typeval += " in {" + detail::join(_options) + "}"; + if(defaulted) { + retval->defaultval = detail::to_lower(member); + } + return retval; + } + /// Add a configuration ini file option Option* add_config(std::string name_="--config", @@ -504,8 +541,8 @@ public: return local_name == name_to_check; } - /// Accept any case - App* anycase(bool value = true) { + /// Ignore case + App* ignore_case(bool value = true) { case_insensitive = value; return this; } diff --git a/include/CLI/Option.hpp b/include/CLI/Option.hpp index ea90bd73..0251c0e6 100644 --- a/include/CLI/Option.hpp +++ b/include/CLI/Option.hpp @@ -41,6 +41,7 @@ protected: bool _required {false}; int _expected {1}; bool allow_vector {false}; + bool case_insensitive {false}; std::vector> _validators; std::set _requires; @@ -232,6 +233,12 @@ public: return detail::join(name_list); } + /// Ignore case + Option* ignore_case(bool value = true) { + case_insensitive = value; + return this; + } + /// Check a name. Requires "-" or "--" for short / long, supports positional name bool check_name(std::string name) const { @@ -239,18 +246,36 @@ public: return check_lname(name.substr(2)); else if (name.length()>1 && name.substr(0,1) == "-") return check_sname(name.substr(1)); - else - return name == pname; + else { + std::string local_pname = pname; + if(case_insensitive) { + local_pname = detail::to_lower(local_pname); + name = detail::to_lower(name); + } + return name == local_pname; + } } /// Requires "-" to be removed from string - bool check_sname(const std::string& name) const { - return std::find(std::begin(snames), std::end(snames), name) != std::end(snames); + bool check_sname(std::string name) const { + if(case_insensitive) { + name = detail::to_lower(name); + return std::find_if(std::begin(snames), std::end(snames), + [&name](std::string local_sname){return detail::to_lower(local_sname) == name;}) + != std::end(snames); + } else + return std::find(std::begin(snames), std::end(snames), name) != std::end(snames); } /// Requires "--" to be removed from string - bool check_lname(const std::string& name) const { - return std::find(std::begin(lnames), std::end(lnames), name) != std::end(lnames); + bool check_lname(std::string name) const { + if(case_insensitive) { + name = detail::to_lower(name); + return std::find_if(std::begin(lnames), std::end(lnames), + [&name](std::string local_sname){return detail::to_lower(local_sname) == name;}) + != std::end(lnames); + } else + return std::find(std::begin(lnames), std::end(lnames), name) != std::end(lnames); } diff --git a/include/CLI/StringTools.hpp b/include/CLI/StringTools.hpp index c2d138be..3ce6b2c2 100644 --- a/include/CLI/StringTools.hpp +++ b/include/CLI/StringTools.hpp @@ -116,7 +116,7 @@ inline bool valid_name_string(const std::string &str) { /// Return a lower case version of a string std::string inline to_lower(std::string str) { std::transform(std::begin(str), std::end(str), std::begin(str), - [](const std::string::value_type x){return std::tolower(x,std::locale());}); + [](const std::string::value_type &x){return std::tolower(x,std::locale());}); return str; }