From 8bad2811afeab04976755350f0f257c47aea5f1e Mon Sep 17 00:00:00 2001 From: Philip Top Date: Mon, 29 Apr 2019 15:39:44 -0700 Subject: [PATCH 01/21] add some tests with default capture on the two parameter template and some notes about it in the README.md remove the test from visual studio 2015 vs2015 doesn't seem to properly deal with is_assignable in the cases we care about so make a standalone version that is more direct in what we are doing add version to appveyor and add some notes to the readme fix a few test cases to make sure code is covered and test a few other paths remove unneeded enum streaming operator add some diagnostic escapes around trait code to eliminate gcc Wnarrowing warnings work specification of the template operations remove optional add some templates for options conversions add the two parameter template for add_option --- .appveyor.yml | 2 + README.md | 37 +++++-- include/CLI/App.hpp | 15 +-- include/CLI/CLI.hpp | 2 - include/CLI/Optional.hpp | 97 ------------------ include/CLI/StringTools.hpp | 19 ---- include/CLI/TypeTools.hpp | 193 +++++++++++++++++++++++++++++++++--- include/CLI/Validators.hpp | 24 ++--- tests/AppTest.cpp | 21 ++++ tests/CreationTest.cpp | 16 ++- tests/HelpersTest.cpp | 4 + tests/NewParseTest.cpp | 184 ++++++++++++++++++++++++++++++++++ tests/OptionalTest.cpp | 98 ++++++++++++++++++ 13 files changed, 552 insertions(+), 160 deletions(-) delete mode 100644 include/CLI/Optional.hpp diff --git a/.appveyor.yml b/.appveyor.yml index 5a8df0c29..9d9394713 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -1,3 +1,5 @@ +version: 1.8.0.{build} + branches: only: - master diff --git a/README.md b/README.md index cfa48b318..fa02aba7f 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ CLI11 is a command line parser for C++11 and beyond that provides a rich feature - [Contribute](#contribute) - [License](#license) -Features that were added in the last released major version are marked with "πŸ†•". Features only available in master are marked with "πŸ†•". +Features that were added in the last released major version are marked with "πŸ†•". Features only available in master are marked with "🚧". ## Background @@ -194,21 +194,25 @@ While all options internally are the same type, there are several ways to add an app.add_option(option_name, help_str="") // πŸ†• app.add_option(option_name, - variable_to_bind_to, // bool, int, float, vector, πŸ†• enum, or string-like, or anything with a defined conversion from a string + variable_to_bind_to, // bool, int, float, vector, πŸ†• enum, or string-like, or anything with a defined conversion from a string or that takes an int🚧, double🚧, or string in a constructor. help_string="") app.add_option_function(option_name, - function , // πŸ†• int, bool, float, enum, vector, or string-like, or anything with a defined conversion from a string + function , // πŸ†• int, bool, float, enum, or string-like, or anything with a defined conversion from a string, or a vector of any of the previous objects. help_string="") app.add_complex(... // Special case: support for complex numbers +//🚧There is a template overload which takes two template parameters the first is the type of object to assign the value to, the second is the conversion type. The conversion type should have a known way to convert from a string. +app.add_option(option_name, + T &output, // output must be assignable or constructible from a value of type XC + help_string="") // Add flags app.add_flag(option_name, help_string="") app.add_flag(option_name, - variable_to_bind_to, // bool, int, πŸ†• float, πŸ†• vector, πŸ†• enum, or πŸ†• string-like, or πŸ†• anything with a defined conversion from a string + variable_to_bind_to, // bool, int, πŸ†• float, πŸ†• vector, πŸ†• enum, or πŸ†• string-like, or πŸ†• anything with a defined conversion from a string like add_option help_string="") app.add_flag_function(option_name, // πŸ†• @@ -240,6 +244,25 @@ An option name must start with a alphabetic character, underscore, a number πŸ†• The `add_option_function(...` function will typically require the template parameter be given unless a `std::function` object with an exact match is passed. The type can be any type supported by the `add_option` function. The function should throw an error (`CLI::ConversionError` or `CLI::ValidationError` possibly) if the value is not valid. +🚧 The two parameter template overload can be used in cases where you want to restrict the input such as +``` +double val +app.add_option("-v",val); +``` +which would first verify the input is convertible to an int before assigning it. Or using some variant type +``` +using vtype=std::variant; + vtype v1; +app.add_option("--vs",v1); +app.add_option("--vi",v1); +app.add_option("--vf",v1); +``` +otherwise the output would default to a string. The add_option can be used with any integral or floating point types, enumerations, or strings. Or any type that takes an int, double, or std::string in an assignment operator or constructor. If an object can take multiple varieties of those, std::string takes precedence, then double then int. To better control which one is used or to use another type for the underlying conversions use the two parameter template to directly specify the conversion type. + +Type such as optional, optional, and optional are supported directly, other optional types can be added using the two parameter template. See [CLI11 Internals][] for information on how this could done and how you can add your own converters for additional types. + +Automatic direct capture of the default string is disabled when using the two parameter template. Use `set_default_str(...)` or `->default_function(std::string())` to set the default string or capture function directly for these cases. + πŸ†• Flag options specified through the `add_flag*` functions allow a syntax for the option names to default particular options to a false value or any other value if some flags are passed. For example: ```cpp @@ -263,8 +286,6 @@ using any of those flags on the command line will result in the specified number On a `C++14` compiler, you can pass a callback function directly to `.add_flag`, while in C++11 mode you'll need to use `.add_flag_function` if you want a callback function. The function will be given the number of times the flag was passed. You can throw a relevant `CLI::ParseError` to signal a failure. -On a compiler that supports C++17's `__has_include`, you can also use `std::optional`, `std::experimental::optional`, and `boost::optional` directly in an `add_option` call. If you don't have `__has_include`, you can define `CLI11_BOOST_OPTIONAL 1` before including CLI11 to manually add support (or 0 to remove) for `boost::optional`. See [CLI11 Internals][] for information on how this was done and how you can add your own converters. Optional values are only supported for types that support the `>>` operator. - #### Example - `"one,-o,--one"`: Valid as long as not a flag, would create an option that can be specified positionally, or with `-o` or `--one` @@ -300,8 +321,8 @@ Before parsing, you can set the following options: - `->each(void(const std::string &)>`: Run this function on each value received, as it is received. It should throw a `ValidationError` if an error is encountered. - `->configurable(false)`: Disable this option from being in a configuration file. `->capture_default_str()`: πŸ†• Store the current value attached and display it in the help string. - `->default_function(std::string())`: πŸ†• Advanced: Change the function that `capture_default_str()` uses. - `->always_capture_default()`: πŸ†• Always run `capture_default_str()` when creating new options. Only useful on an App's `option_defaults`. +- `->default_function(std::string())`: πŸ†• Advanced: Change the function that `capture_default_str()` uses. +- `->always_capture_default()`: πŸ†• Always run `capture_default_str()` when creating new options. Only useful on an App's `option_defaults`. These options return the `Option` pointer, so you can chain them together, and even skip storing the pointer entirely. The `each` function takes any function that has the signature `void(const std::string&)`; it should throw a `ValidationError` when validation fails. The help message will have the name of the parent option prepended. Since `each`, `check` and `transform` use the same underlying mechanism, you can chain as many as you want, and they will be executed in order. Operations added through `transform` are executed first in reverse order of addition, and `check` and `each` are run following the transform functions in order of addition. If you just want to see the unconverted values, use `.results()` to get the `std::vector` of results. diff --git a/include/CLI/App.hpp b/include/CLI/App.hpp index 0994c5411..a423e87da 100644 --- a/include/CLI/App.hpp +++ b/include/CLI/App.hpp @@ -4,13 +4,11 @@ // file LICENSE or https://github.com/CLIUtils/CLI11 for details. #include -#include #include #include #include #include #include -#include #include #include #include @@ -469,18 +467,23 @@ class App { } /// Add option for non-vectors (duplicate copy needed without defaulted to avoid `iostream << value`) - template ::value & !std::is_const::value, detail::enabler> = detail::dummy> + + template ::value && !std::is_const::value, detail::enabler> = detail::dummy> Option *add_option(std::string option_name, T &variable, ///< The variable to set std::string option_description = "", bool defaulted = false) { - auto fun = [&variable](CLI::results_t res) { return detail::lexical_cast(res[0], variable); }; + auto fun = [&variable](CLI::results_t res) { // comment for spacing + return detail::lexical_assign(res[0], variable); + }; Option *opt = add_option(option_name, fun, option_description, defaulted, [&variable]() { - return std::string(CLI::detail::to_string(variable)); + return std::string(CLI::detail::checked_to_string(variable)); }); - opt->type_name(detail::type_name()); + opt->type_name(detail::type_name()); return opt; } diff --git a/include/CLI/CLI.hpp b/include/CLI/CLI.hpp index 2feb19247..c2dbe7c5f 100644 --- a/include/CLI/CLI.hpp +++ b/include/CLI/CLI.hpp @@ -10,8 +10,6 @@ #include "CLI/Macros.hpp" -#include "CLI/Optional.hpp" - #include "CLI/StringTools.hpp" #include "CLI/Error.hpp" diff --git a/include/CLI/Optional.hpp b/include/CLI/Optional.hpp deleted file mode 100644 index 510af3b3f..000000000 --- a/include/CLI/Optional.hpp +++ /dev/null @@ -1,97 +0,0 @@ -#pragma once - -// Distributed under the 3-Clause BSD License. See accompanying -// file LICENSE or https://github.com/CLIUtils/CLI11 for details. - -#include - -#include "CLI/Macros.hpp" - -// [CLI11:verbatim] - -// You can explicitly enable or disable support -// by defining to 1 or 0. Extra check here to ensure it's in the stdlib too. -// We nest the check for __has_include and it's usage -#ifndef CLI11_STD_OPTIONAL -#ifdef __has_include -#if defined(CLI11_CPP17) && __has_include() -#define CLI11_STD_OPTIONAL 1 -#else -#define CLI11_STD_OPTIONAL 0 -#endif -#else -#define CLI11_STD_OPTIONAL 0 -#endif -#endif - -#ifndef CLI11_EXPERIMENTAL_OPTIONAL -#define CLI11_EXPERIMENTAL_OPTIONAL 0 -#endif - -#ifndef CLI11_BOOST_OPTIONAL -#define CLI11_BOOST_OPTIONAL 0 -#endif - -#if CLI11_BOOST_OPTIONAL -#include -#if BOOST_VERSION < 106100 -#error "This boost::optional version is not supported, use 1.61 or better" -#endif -#endif - -#if CLI11_STD_OPTIONAL -#include -#endif -#if CLI11_EXPERIMENTAL_OPTIONAL -#include -#endif -#if CLI11_BOOST_OPTIONAL -#include -#include -#endif -// [CLI11:verbatim] - -namespace CLI { - -#if CLI11_STD_OPTIONAL -template std::istream &operator>>(std::istream &in, std::optional &val) { - T v; - in >> v; - val = v; - return in; -} -#endif - -#if CLI11_EXPERIMENTAL_OPTIONAL -template std::istream &operator>>(std::istream &in, std::experimental::optional &val) { - T v; - in >> v; - val = v; - return in; -} -#endif - -#if CLI11_BOOST_OPTIONAL -template std::istream &operator>>(std::istream &in, boost::optional &val) { - T v; - in >> v; - val = v; - return in; -} -#endif - -// Export the best optional to the CLI namespace -#if CLI11_STD_OPTIONAL -using std::optional; -#elif CLI11_EXPERIMENTAL_OPTIONAL -using std::experimental::optional; -#elif CLI11_BOOST_OPTIONAL -using boost::optional; -#endif - -// This is true if any optional is found -#if CLI11_STD_OPTIONAL || CLI11_EXPERIMENTAL_OPTIONAL || CLI11_BOOST_OPTIONAL -#define CLI11_OPTIONAL 1 -#endif - -} // namespace CLI diff --git a/include/CLI/StringTools.hpp b/include/CLI/StringTools.hpp index c5961c1d5..4ed8ea057 100644 --- a/include/CLI/StringTools.hpp +++ b/include/CLI/StringTools.hpp @@ -25,14 +25,6 @@ std::ostream &operator<<(std::ostream &in, const T &item) { return in << static_cast::type>(item); } -/// input streaming for enumerations -template ::value>::type> -std::istream &operator>>(std::istream &in, T &item) { - typename std::underlying_type::type i; - in >> i; - item = static_cast(i); - return in; -} } // namespace enums /// Export to CLI namespace @@ -57,17 +49,6 @@ inline std::vector split(const std::string &s, char delim) { } return elems; } -/// simple utility to convert various types to a string -template inline std::string as_string(const T &v) { - std::ostringstream s; - s << v; - return s.str(); -} -// if the data type is already a string just forward it -template ::value>::type> -inline auto as_string(T &&v) -> decltype(std::forward(v)) { - return std::forward(v); -} /// Simple function to join a string template std::string join(const T &v, std::string delim = ",") { diff --git a/include/CLI/TypeTools.hpp b/include/CLI/TypeTools.hpp index e2a0e87bb..a6569212d 100644 --- a/include/CLI/TypeTools.hpp +++ b/include/CLI/TypeTools.hpp @@ -124,19 +124,62 @@ struct pair_adaptor< } }; +#ifdef __GNUC__ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wnarrowing" +#endif +// check for constructibility from a specific type and copy assignable used in the parse detection +template class is_direct_constructible { + template + static auto test(int) -> decltype(TT{std::declval()}, std::is_move_assignable()); + + template static auto test(...) -> std::false_type; + + public: + static const bool value = decltype(test(0))::value; +}; +#ifdef __GNUC__ +#pragma GCC diagnostic pop +#endif + // Check for streamability // Based on https://stackoverflow.com/questions/22758291/how-can-i-detect-if-a-type-can-be-streamed-to-an-stdostream -template class is_streamable { - template +template class is_streamable { + template static auto test(int) -> decltype(std::declval() << std::declval(), std::true_type()); template static auto test(...) -> std::false_type; public: - static const bool value = decltype(test(0))::value; + static const bool value = decltype(test(0))::value; +}; + +// check for input streamability +template class is_istreamable { + template + static auto test(int) -> decltype(std::declval() >> std::declval(), std::true_type()); + + template static auto test(...) -> std::false_type; + + public: + static const bool value = decltype(test(0))::value; }; +// templated operation to get a value from a stream +template ::value, detail::enabler> = detail::dummy> +bool from_stream(const std::string &istring, T &obj) { + std::istringstream is; + is.str(istring); + is >> obj; + return !is.fail() && !is.rdbuf()->in_avail(); +} + +template ::value, detail::enabler> = detail::dummy> +bool from_stream(const std::string & /*istring*/, T & /*obj*/) { + return false; +} + /// Convert an object to a string (directly forward if this can become a string) template ::value, detail::enabler> = detail::dummy> auto to_string(T &&value) -> decltype(std::forward(value)) { @@ -145,8 +188,8 @@ auto to_string(T &&value) -> decltype(std::forward(value)) { /// Convert an object to a string (streaming must be supported for that type) template ::value && is_streamable::value, - detail::enabler> = detail::dummy> + enable_if_t::value && is_streamable::value, detail::enabler> = + detail::dummy> std::string to_string(T &&value) { std::stringstream stream; stream << value; @@ -155,12 +198,30 @@ std::string to_string(T &&value) { /// If conversion is not supported, return an empty string (streaming is not supported for that type) template ::value && !is_streamable::value, - detail::enabler> = detail::dummy> + enable_if_t::value && !is_streamable::value, detail::enabler> = + detail::dummy> std::string to_string(T &&) { return std::string{}; } +/// special template overload +template ::value, detail::enabler> = detail::dummy> +auto checked_to_string(T &&value) -> decltype(to_string(std::forward(value))) { + return to_string(std::forward(value)); +} + +/// special template overload +template ::value, detail::enabler> = detail::dummy> +std::string checked_to_string(T &&) { + return std::string{}; +} + // Type name print /// Was going to be based on @@ -331,6 +392,16 @@ bool lexical_cast(std::string input, T &output) { return true; } +/// String and similar +template ::value && !std::is_integral::value && + !std::is_assignable::value && std::is_constructible::value, + detail::enabler> = detail::dummy> +bool lexical_cast(std::string input, T &output) { + output = T(input); + return true; +} + /// Enumerations template ::value, detail::enabler> = detail::dummy> bool lexical_cast(std::string input, T &output) { @@ -343,21 +414,112 @@ bool lexical_cast(std::string input, T &output) { return true; } -/// Non-string parsable +/// assignable from double or int template ::value && !std::is_integral::value && - !std::is_assignable::value && !std::is_enum::value, + !std::is_assignable::value && + !std::is_constructible::value && !std::is_enum::value && + is_direct_constructible::value && is_direct_constructible::value, detail::enabler> = detail::dummy> bool lexical_cast(std::string input, T &output) { - std::istringstream is; + int val; + if(lexical_cast(input, val)) { + output = T(val); + return true; + } else { + double dval; + if(lexical_cast(input, dval)) { + output = T{dval}; + return true; + } + } + return from_stream(input, output); +} - is.str(input); - is >> output; - return !is.fail() && !is.rdbuf()->in_avail(); +/// assignable from int64 +template ::value && !std::is_integral::value && + !std::is_assignable::value && + !std::is_constructible::value && !std::is_enum::value && + !is_direct_constructible::value && is_direct_constructible::value, + detail::enabler> = detail::dummy> +bool lexical_cast(std::string input, T &output) { + int val; + if(lexical_cast(input, val)) { + output = T(val); + return true; + } + return from_stream(input, output); +} + +/// assignable from double +template ::value && !std::is_integral::value && + !std::is_assignable::value && + !std::is_constructible::value && !std::is_enum::value && + is_direct_constructible::value && !is_direct_constructible::value, + detail::enabler> = detail::dummy> +bool lexical_cast(std::string input, T &output) { + double val; + if(lexical_cast(input, val)) { + output = T{val}; + return true; + } + return from_stream(input, output); +} + +/// Non-string parsable by a stream +template ::value && !std::is_integral::value && + !std::is_assignable::value && + !std::is_constructible::value && !std::is_enum::value && + !is_direct_constructible::value && !is_direct_constructible::value, + detail::enabler> = detail::dummy> +bool lexical_cast(std::string input, T &output) { + static_assert(is_istreamable::value, + "option object type must have a lexical cast overload or streaming input operator(>>) defined if it " + "is convertible from another type use the add_option(...) with XC being the known type"); + return from_stream(input, output); +} + +/// if they are the same just do the regular lexical cast +template ::value, detail::enabler> = detail::dummy> +bool lexical_assign(const std::string &input, T &output) { + return lexical_cast(input, output); +} + +/// if there is a way to directly assign a specific type +template < + class T, + class XC, + enable_if_t::value && std::is_assignable::value, detail::enabler> = detail::dummy> +bool lexical_assign(const std::string &input, T &output) { + XC val; + auto parse_result = lexical_cast(input, val); + if(parse_result) { + output = val; + } + return parse_result; +} + +/// if there is a way to construct the value and do copy assignment +template ::value && !std::is_assignable::value && + std::is_move_assignable::value, + detail::enabler> = detail::dummy> +bool lexical_assign(const std::string &input, T &output) { + XC val; + bool parse_result = lexical_cast(input, val); + if(parse_result) { + output = T{val}; + } + return parse_result; } /// Sum a vector of flag representations -/// The flag vector produces a series of strings in a vector, simple true is represented by a "1", simple false is by +/// The flag vector produces a series of strings in a vector, simple true is represented by a "1", simple false is +/// by /// "-1" an if numbers are passed by some fashion they are captured as well so the function just checks for the most /// common true and false strings then uses stoll to convert the rest for summing template &flags, T &output) { } /// Sum a vector of flag representations -/// The flag vector produces a series of strings in a vector, simple true is represented by a "1", simple false is by +/// The flag vector produces a series of strings in a vector, simple true is represented by a "1", simple false is +/// by /// "-1" an if numbers are passed by some fashion they are captured as well so the function just checks for the most /// common true and false strings then uses stoll to convert the rest for summing template max) - input = detail::as_string(max); + input = detail::to_string(max); - return std::string(); + return std::string{}; }; } @@ -451,9 +451,9 @@ template std::string generate_map(const T &map, bool key_only = fal std::string out(1, '{'); out.append(detail::join(detail::smart_deref(map), [key_only](const iteration_type_t &v) { - auto res = detail::as_string(detail::pair_adaptor::first(v)); + auto res = detail::to_string(detail::pair_adaptor::first(v)); if(!key_only) { - res += "->" + detail::as_string(detail::pair_adaptor::second(v)); + res += "->" + detail::to_string(detail::pair_adaptor::second(v)); } return res; }, @@ -581,7 +581,7 @@ class IsMember : public Validator { if(res.first) { // Make sure the version in the input string is identical to the one in the set if(filter_fn) { - input = detail::as_string(detail::pair_adaptor::first(*(res.second))); + input = detail::to_string(detail::pair_adaptor::first(*(res.second))); } // Return empty error string (success) @@ -649,7 +649,7 @@ class Transformer : public Validator { } auto res = detail::search(mapping, b, filter_fn); if(res.first) { - input = detail::as_string(detail::pair_adaptor::second(*res.second)); + input = detail::to_string(detail::pair_adaptor::second(*res.second)); } return std::string{}; }; @@ -699,7 +699,7 @@ class CheckedTransformer : public Validator { out += detail::generate_map(detail::smart_deref(mapping)) + " OR {"; out += detail::join( detail::smart_deref(mapping), - [](const iteration_type_t &v) { return detail::as_string(detail::pair_adaptor::second(v)); }, + [](const iteration_type_t &v) { return detail::to_string(detail::pair_adaptor::second(v)); }, ","); out.push_back('}'); return out; @@ -716,12 +716,12 @@ class CheckedTransformer : public Validator { } auto res = detail::search(mapping, b, filter_fn); if(res.first) { - input = detail::as_string(detail::pair_adaptor::second(*res.second)); + input = detail::to_string(detail::pair_adaptor::second(*res.second)); return std::string{}; } } for(const auto &v : detail::smart_deref(mapping)) { - auto output_string = detail::as_string(detail::pair_adaptor::second(v)); + auto output_string = detail::to_string(detail::pair_adaptor::second(v)); if(output_string == input) { return std::string(); } @@ -832,10 +832,10 @@ class AsNumberWithUnit : public Validator { // perform safe multiplication bool ok = detail::checked_multiply(num, it->second); if(!ok) { - throw ValidationError(detail::as_string(num) + " multiplied by " + unit + + throw ValidationError(detail::to_string(num) + " multiplied by " + unit + " factor would cause number overflow. Use smaller value."); } - input = detail::as_string(num); + input = detail::to_string(num); return {}; }; diff --git a/tests/AppTest.cpp b/tests/AppTest.cpp index bb6bd864e..4742dfe1a 100644 --- a/tests/AppTest.cpp +++ b/tests/AppTest.cpp @@ -669,6 +669,27 @@ TEST_F(TApp, ShortOpts) { EXPECT_EQ(app.count_all(), 3u); } +TEST_F(TApp, TwoParamTemplateOpts) { + + double funnyint; + auto opt = app.add_option("-y", funnyint); + + args = {"-y", "32"}; + + run(); + + EXPECT_EQ(32.0, funnyint); + + args = {"-y", "32.3"}; + EXPECT_THROW(run(), CLI::ConversionError); + + args = {"-y", "-19"}; + EXPECT_THROW(run(), CLI::ConversionError); + + opt->capture_default_str(); + EXPECT_TRUE(opt->get_default_str().empty()); +} + TEST_F(TApp, DefaultOpts) { int i = 3; diff --git a/tests/CreationTest.cpp b/tests/CreationTest.cpp index a1cf95fb1..682a0cd57 100644 --- a/tests/CreationTest.cpp +++ b/tests/CreationTest.cpp @@ -741,14 +741,19 @@ class Unstreamable { void set_x(int x) { x_ = x; } }; +// this needs to be a different check then the one after the function definition otherwise they conflict +static_assert(!CLI::detail::is_istreamable::value, "Unstreamable type is streamable"); + std::istream &operator>>(std::istream &in, Unstreamable &value) { int x; in >> x; value.set_x(x); return in; } +// these need to be different classes otherwise the definitions conflict +static_assert(CLI::detail::is_istreamable::value, "Unstreamable type is still unstreamable"); -TEST_F(TApp, MakeUnstreamableOptiions) { +TEST_F(TApp, MakeUnstreamableOptions) { Unstreamable value; app.add_option("--value", value); @@ -760,4 +765,13 @@ TEST_F(TApp, MakeUnstreamableOptiions) { // This used to fail to build, since it tries to stream from Unstreamable app.add_option("--values2", values, "", false); + + args = {"--value", "45"}; + run(); + EXPECT_EQ(value.get_x(), 45); + + args = {"--values", "45", "27", "34"}; + run(); + EXPECT_EQ(values.size(), 3u); + EXPECT_EQ(values[2].get_x(), 34); } diff --git a/tests/HelpersTest.cpp b/tests/HelpersTest.cpp index c7fa8eb18..bf1b44a09 100644 --- a/tests/HelpersTest.cpp +++ b/tests/HelpersTest.cpp @@ -855,6 +855,10 @@ TEST(Types, LexicalCastParsable) { EXPECT_DOUBLE_EQ(output.real(), 4.2); // Doing this in one go sometimes has trouble EXPECT_DOUBLE_EQ(output.imag(), 7.3); // on clang + c++4.8 due to missing const + EXPECT_TRUE(CLI::detail::lexical_cast("2.456", output)); + EXPECT_DOUBLE_EQ(output.real(), 2.456); // Doing this in one go sometimes has trouble + EXPECT_DOUBLE_EQ(output.imag(), 0.0); // on clang + c++4.8 due to missing const + EXPECT_FALSE(CLI::detail::lexical_cast(fail_input, output)); EXPECT_FALSE(CLI::detail::lexical_cast(extra_input, output)); } diff --git a/tests/NewParseTest.cpp b/tests/NewParseTest.cpp index 89e7fbeea..de110d54a 100644 --- a/tests/NewParseTest.cpp +++ b/tests/NewParseTest.cpp @@ -259,3 +259,187 @@ TEST_F(TApp, AddingComplexParserDetail) { } } #endif + +/// simple class to wrap another with a very specific type constructor and assignment operators to test out some of the +/// option assignments +template class objWrapper { + public: + objWrapper() = default; + explicit objWrapper(X obj) : val_{obj} {}; + objWrapper(const objWrapper &ow) = default; + template objWrapper(const TT &obj) = delete; + objWrapper &operator=(const objWrapper &) = default; + objWrapper &operator=(objWrapper &&) = default; + // delete all other assignment operators + template void operator=(TT &&obj) = delete; + + const X &value() const { return val_; } + + private: + X val_; +}; + +// I think there is a bug with the is_assignable in visual studio 2015 it is fixed in later versions +// so this test will not compile in that compiler +#if !defined(_MSC_VER) || _MSC_VER >= 1910 + +static_assert(CLI::detail::is_direct_constructible, std::string>::value, + "string wrapper isn't properly constructible"); + +static_assert(!std::is_assignable, std::string>::value, + "string wrapper is improperly assignable"); +TEST_F(TApp, stringWrapper) { + objWrapper sWrapper; + app.add_option("-v", sWrapper); + args = {"-v", "string test"}; + + run(); + + EXPECT_EQ(sWrapper.value(), "string test"); +} + +static_assert(CLI::detail::is_direct_constructible, double>::value, + "double wrapper isn't properly assignable"); + +static_assert(!CLI::detail::is_direct_constructible, int>::value, + "double wrapper can be assigned from int"); + +static_assert(!CLI::detail::is_istreamable>::value, + "double wrapper is input streamable and it shouldn't be"); + +TEST_F(TApp, doubleWrapper) { + objWrapper dWrapper; + app.add_option("-v", dWrapper); + args = {"-v", "2.36"}; + + run(); + + EXPECT_EQ(dWrapper.value(), 2.36); + + args = {"-v", "thing"}; + + EXPECT_THROW(run(), CLI::ConversionError); +} + +static_assert(CLI::detail::is_direct_constructible, int>::value, + "int wrapper is not constructible from int64"); + +static_assert(!CLI::detail::is_direct_constructible, double>::value, + "int wrapper is constructible from double"); + +static_assert(!CLI::detail::is_istreamable>::value, + "int wrapper is input streamable and it shouldn't be"); + +TEST_F(TApp, intWrapper) { + objWrapper iWrapper; + app.add_option("-v", iWrapper); + args = {"-v", "45"}; + + run(); + + EXPECT_EQ(iWrapper.value(), 45); + args = {"-v", "thing"}; + + EXPECT_THROW(run(), CLI::ConversionError); +} + +static_assert(!CLI::detail::is_direct_constructible, int>::value, + "float wrapper is constructible from int"); +static_assert(!CLI::detail::is_direct_constructible, double>::value, + "float wrapper is constructible from double"); + +static_assert(!CLI::detail::is_istreamable>::value, + "float wrapper is input streamable and it shouldn't be"); + +TEST_F(TApp, floatWrapper) { + objWrapper iWrapper; + app.add_option, float>("-v", iWrapper); + args = {"-v", "45.3"}; + + run(); + + EXPECT_EQ(iWrapper.value(), 45.3f); + args = {"-v", "thing"}; + + EXPECT_THROW(run(), CLI::ConversionError); +} + +#endif +/// simple class to wrap another with a very specific type constructor to test out some of the option assignments +class dobjWrapper { + public: + dobjWrapper() = default; + explicit dobjWrapper(double obj) : dval_{obj} {}; + explicit dobjWrapper(int obj) : ival_{obj} {}; + + double dvalue() const { return dval_; } + int ivalue() const { return ival_; } + + private: + double dval_{0.0}; + int ival_{0}; +}; + +TEST_F(TApp, dobjWrapper) { + dobjWrapper iWrapper; + app.add_option("-v", iWrapper); + args = {"-v", "45"}; + + run(); + + EXPECT_EQ(iWrapper.ivalue(), 45); + EXPECT_EQ(iWrapper.dvalue(), 0.0); + + args = {"-v", "thing"}; + + EXPECT_THROW(run(), CLI::ConversionError); + iWrapper = dobjWrapper{}; + + args = {"-v", "45.1"}; + + run(); + EXPECT_EQ(iWrapper.ivalue(), 0); + EXPECT_EQ(iWrapper.dvalue(), 45.1); +} + +/// simple class to wrap another with a very specific type constructor and assignment operators to test out some of the +/// option assignments +template class AobjWrapper { + public: + AobjWrapper() = default; + // delete all other constructors + template AobjWrapper(TT &&obj) = delete; + // single assignment operator + void operator=(X val) { val_ = val; } + // delete all other assignment operators + template void operator=(TT &&obj) = delete; + + const X &value() const { return val_; } + + private: + X val_; +}; + +static_assert(std::is_assignable &, uint16_t>::value, + "AobjWrapper not assignable like it should be "); + +TEST_F(TApp, uint16Wrapper) { + AobjWrapper sWrapper; + app.add_option, uint16_t>("-v", sWrapper); + args = {"-v", "9"}; + + run(); + + EXPECT_EQ(sWrapper.value(), 9u); + args = {"-v", "thing"}; + + EXPECT_THROW(run(), CLI::ConversionError); + + args = {"-v", "72456245754"}; + + EXPECT_THROW(run(), CLI::ConversionError); + + args = {"-v", "-3"}; + + EXPECT_THROW(run(), CLI::ConversionError); +} diff --git a/tests/OptionalTest.cpp b/tests/OptionalTest.cpp index 720e73b7a..32c29d303 100644 --- a/tests/OptionalTest.cpp +++ b/tests/OptionalTest.cpp @@ -3,6 +3,48 @@ #include "app_helper.hpp" +// You can explicitly enable or disable support +// by defining to 1 or 0. Extra check here to ensure it's in the stdlib too. +// We nest the check for __has_include and it's usage +#ifndef CLI11_STD_OPTIONAL +#ifdef __has_include +#if defined(CLI11_CPP17) && __has_include() +#define CLI11_STD_OPTIONAL 1 +#else +#define CLI11_STD_OPTIONAL 0 +#endif +#else +#define CLI11_STD_OPTIONAL 0 +#endif +#endif + +#ifndef CLI11_EXPERIMENTAL_OPTIONAL +#define CLI11_EXPERIMENTAL_OPTIONAL 0 +#endif + +#ifndef CLI11_BOOST_OPTIONAL +#define CLI11_BOOST_OPTIONAL 0 +#endif + +#if CLI11_BOOST_OPTIONAL +#include +#if BOOST_VERSION < 106100 +#error "This boost::optional version is not supported, use 1.61 or better" +#endif +#endif + +#if CLI11_STD_OPTIONAL +#include +#endif +#if CLI11_EXPERIMENTAL_OPTIONAL +#include +#endif +#if CLI11_BOOST_OPTIONAL +#include +#include +#endif +// [CLI11:verbatim] + #if CLI11_STD_OPTIONAL TEST_F(TApp, StdOptionalTest) { @@ -55,13 +97,69 @@ TEST_F(TApp, BoostOptionalTest) { run(); EXPECT_TRUE(opt); EXPECT_EQ(*opt, 1); + opt = {}; + args = {"--count", "3"}; + run(); + EXPECT_TRUE(opt); + EXPECT_EQ(*opt, 3); +} +TEST_F(TApp, BoostOptionalint64Test) { + boost::optional opt; + app.add_option("-c,--count", opt); + run(); + EXPECT_FALSE(opt); + + args = {"-c", "1"}; + run(); + EXPECT_TRUE(opt); + EXPECT_EQ(*opt, 1); + opt = {}; args = {"--count", "3"}; run(); EXPECT_TRUE(opt); EXPECT_EQ(*opt, 3); } +TEST_F(TApp, BoostOptionalStringTest) { + boost::optional opt; + app.add_option("-s,--string", opt); + run(); + EXPECT_FALSE(opt); + + args = {"-s", "strval"}; + run(); + EXPECT_TRUE(opt); + EXPECT_EQ(*opt, "strval"); + opt = {}; + args = {"--string", "strv"}; + run(); + EXPECT_TRUE(opt); + EXPECT_EQ(*opt, "strv"); +} + +TEST_F(TApp, BoostOptionalEnumTest) { + enum class eval : char { val0 = 0, val1 = 1, val2 = 2, val3 = 3, val4 = 4 }; + boost::optional opt; + auto optptr = app.add_option("-v,--val", opt); + optptr->capture_default_str(); + + auto dstring = optptr->get_default_str(); + EXPECT_TRUE(dstring.empty()); + run(); + EXPECT_FALSE(opt); + + args = {"-v", "3"}; + run(); + EXPECT_TRUE(opt); + EXPECT_TRUE(*opt == eval::val3); + opt = {}; + args = {"--val", "1"}; + run(); + EXPECT_TRUE(opt); + EXPECT_TRUE(*opt == eval::val1); +} + TEST_F(TApp, BoostOptionalVector) { boost::optional> opt; app.add_option_function>("-v,--vec", [&opt](const std::vector &v) { opt = v; }, "some vector") From 0c024df06f81d60b7fdabc8729a81f557556e692 Mon Sep 17 00:00:00 2001 From: Philip Top Date: Wed, 29 May 2019 06:15:28 -0700 Subject: [PATCH 02/21] Fix some comments from Code review and add more description --- include/CLI/TypeTools.hpp | 36 +++++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/include/CLI/TypeTools.hpp b/include/CLI/TypeTools.hpp index a6569212d..030852c7f 100644 --- a/include/CLI/TypeTools.hpp +++ b/include/CLI/TypeTools.hpp @@ -124,6 +124,12 @@ struct pair_adaptor< } }; +// Warning is suppressed due to "bug" in gcc<5.0 and gcc 7.0 with c++17 enabled that generates a Wnarrowing warning +// in the unevaluated context even if the function that was using this wasn't used. The standard says narrowing in +// brace initialization shouldn't be allowed but for backwards compatibility gcc allows it in some contexts. It is a +// little fuzzy what happens in template constructs and I think that was something GCC took a little while to work out. +// But regardless some versions of gcc generate a warning when they shouldn't from the following code so that should be +// suppressed #ifdef __GNUC__ #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wnarrowing" @@ -142,10 +148,10 @@ template class is_direct_constructible { #pragma GCC diagnostic pop #endif -// Check for streamability +// Check for output streamability // Based on https://stackoverflow.com/questions/22758291/how-can-i-detect-if-a-type-can-be-streamed-to-an-stdostream -template class is_streamable { +template class is_ostreamable { template static auto test(int) -> decltype(std::declval() << std::declval(), std::true_type()); @@ -155,7 +161,7 @@ template class is_streamable { static const bool value = decltype(test(0))::value; }; -// check for input streamability +/// Check for input streamability template class is_istreamable { template static auto test(int) -> decltype(std::declval() >> std::declval(), std::true_type()); @@ -166,7 +172,7 @@ template class is_istreamable { static const bool value = decltype(test(0))::value; }; -// templated operation to get a value from a stream +/// Templated operation to get a value from a stream template ::value, detail::enabler> = detail::dummy> bool from_stream(const std::string &istring, T &obj) { std::istringstream is; @@ -188,7 +194,7 @@ auto to_string(T &&value) -> decltype(std::forward(value)) { /// Convert an object to a string (streaming must be supported for that type) template ::value && is_streamable::value, detail::enabler> = + enable_if_t::value && is_ostreamable::value, detail::enabler> = detail::dummy> std::string to_string(T &&value) { std::stringstream stream; @@ -198,7 +204,7 @@ std::string to_string(T &&value) { /// If conversion is not supported, return an empty string (streaming is not supported for that type) template ::value && !is_streamable::value, detail::enabler> = + enable_if_t::value && !is_ostreamable::value, detail::enabler> = detail::dummy> std::string to_string(T &&) { return std::string{}; @@ -382,7 +388,7 @@ bool lexical_cast(std::string input, T &output) { } } -/// String and similar +/// String and similar direct assignment template ::value && !std::is_integral::value && std::is_assignable::value, @@ -392,7 +398,7 @@ bool lexical_cast(std::string input, T &output) { return true; } -/// String and similar +/// String and similar constructible and copy assignment template ::value && !std::is_integral::value && !std::is_assignable::value && std::is_constructible::value, @@ -414,7 +420,7 @@ bool lexical_cast(std::string input, T &output) { return true; } -/// assignable from double or int +/// Assignable from double or int template ::value && !std::is_integral::value && !std::is_assignable::value && @@ -436,7 +442,7 @@ bool lexical_cast(std::string input, T &output) { return from_stream(input, output); } -/// assignable from int64 +/// Assignable from int64 template ::value && !std::is_integral::value && !std::is_assignable::value && @@ -452,7 +458,7 @@ bool lexical_cast(std::string input, T &output) { return from_stream(input, output); } -/// assignable from double +/// Assignable from double template ::value && !std::is_integral::value && !std::is_assignable::value && @@ -482,13 +488,13 @@ bool lexical_cast(std::string input, T &output) { return from_stream(input, output); } -/// if they are the same just do the regular lexical cast +/// Assign a value through lexical cast operations template ::value, detail::enabler> = detail::dummy> bool lexical_assign(const std::string &input, T &output) { return lexical_cast(input, output); } -/// if there is a way to directly assign a specific type +/// Assign a value converted from a string in lexical cast to the output value directly template < class T, class XC, @@ -502,7 +508,7 @@ bool lexical_assign(const std::string &input, T &output) { return parse_result; } -/// if there is a way to construct the value and do copy assignment +/// Assign a value from a lexical cast through constructing a value and move assigning it template ::value && !std::is_assignable::value && @@ -512,7 +518,7 @@ bool lexical_assign(const std::string &input, T &output) { XC val; bool parse_result = lexical_cast(input, val); if(parse_result) { - output = T{val}; + output = T(val); // use () form of constructor to allow some implicit conversions } return parse_result; } From ae41f363df1980f2f864453f24501211a303c119 Mon Sep 17 00:00:00 2001 From: Philip Top Date: Wed, 29 May 2019 09:02:40 -0700 Subject: [PATCH 03/21] start work on trying to clean up the type traits for which lexical cast overload to use --- include/CLI/TypeTools.hpp | 195 +++++++++++++++++++++++++------------- 1 file changed, 131 insertions(+), 64 deletions(-) diff --git a/include/CLI/TypeTools.hpp b/include/CLI/TypeTools.hpp index 030852c7f..c8de3abbf 100644 --- a/include/CLI/TypeTools.hpp +++ b/include/CLI/TypeTools.hpp @@ -228,6 +228,105 @@ std::string checked_to_string(T &&) { return std::string{}; } +// Enumeration of the different supported categorizations of objects +enum objCategory { + integral_value, + unsigned_integral, + enumeration, + boolean_value, + floating_point, + number_constructible, + double_constructible, + integer_constructible, + string_assignable, + string_constructible, + other, +}; + +/// some type that is not otherwise recognized +template class classify_object { static constexpr objCategory value = other; }; + +/// Set of overloads to classify an object according to type +template +class classify_object::value && std::is_signed::value && + !is_bool::value && !std::is_enum::value>::type> { + static constexpr objCategory value{integral_value}; +}; + +/// Unsigned integers +template +class classify_object< + T, + typename std::enable_if::value && std::is_unsigned::value && !is_bool::value>::type> { + static constexpr objCategory value{unsigned_integral}; +}; + +/// Boolean values +template class classify_object::value>::type> { + static constexpr objCategory value = boolean_value; +}; + +/// Floats +template class classify_object::value>::type> { + static constexpr objCategory value = floating_point; +}; + +/// String and similar direct assignment +template +class classify_object::value && !std::is_integral::value && + std::is_assignable::value>::type> { + static constexpr objCategory value = string_assignable; +}; + +/// String and similar constructible and copy assignment +template +class classify_object::value && !std::is_integral::value && + !std::is_assignable::value && + std::is_constructible::value>::type> { + static constexpr objCategory value = string_constructible; +}; + +/// Enumerations +template class classify_object::value>::type> { + static constexpr objCategory value = enumeration; +}; + +/// Assignable from double or int +template +class classify_object::value && !std::is_integral::value && + !std::is_assignable::value && + !std::is_constructible::value && + !std::is_enum::value && is_direct_constructible::value && + is_direct_constructible::value>::type> { + static constexpr objCategory value = number_constructible; +}; + +/// Assignable from int +template +class classify_object::value && !std::is_integral::value && + !std::is_assignable::value && + !std::is_constructible::value && + !std::is_enum::value && !is_direct_constructible::value && + is_direct_constructible::value>::type> { + static const objCategory value = integer_constructible; +}; + +/// Assignable from double +template +class classify_object::value && !std::is_integral::value && + !std::is_assignable::value && + !std::is_constructible::value && + !std::is_enum::value && is_direct_constructible::value && + !is_direct_constructible::value>::type> { + static const objCategory value = double_constructible; +}; + // Type name print /// Was going to be based on @@ -235,18 +334,22 @@ std::string checked_to_string(T &&) { /// But this is cleaner and works better in this case template ::value && std::is_signed::value, detail::enabler> = detail::dummy> + enable_if_t::value == integral_value || classify_object::value == integer_constructible, + detail::enabler> = detail::dummy> constexpr const char *type_name() { return "INT"; } -template ::value && std::is_unsigned::value, detail::enabler> = detail::dummy> +template ::value == unsigned_integral, detail::enabler> = detail::dummy> constexpr const char *type_name() { return "UINT"; } -template ::value, detail::enabler> = detail::dummy> +template < + typename T, + enable_if_t::value == floating_point || classify_object::value == number_constructible || + classify_object::value == double_constructible, + detail::enabler> = detail::dummy> constexpr const char *type_name() { return "FLOAT"; } @@ -257,16 +360,19 @@ constexpr const char *type_name() { return "VECTOR"; } /// Print name for enumeration types -template ::value, detail::enabler> = detail::dummy> +template ::value == enumeration, detail::enabler> = detail::dummy> constexpr const char *type_name() { return "ENUM"; } +/// Print name for enumeration types +template ::value == boolean_value, detail::enabler> = detail::dummy> +constexpr const char *type_name() { + return "BOOLEAN"; +} + /// Print for all other types -template ::value && !std::is_integral::value && !is_vector::value && - !std::is_enum::value, - detail::enabler> = detail::dummy> +template ::value >= string_assignable, detail::enabler> = detail::dummy> constexpr const char *type_name() { return "TEXT"; } @@ -286,6 +392,9 @@ inline int64_t to_flag_value(std::string val) { val = detail::to_lower(val); int64_t ret; if(val.size() == 1) { + if(val[0] >= '1' && val[0] <= '9') { + return (val[0] - '0'); + } switch(val[0]) { case '0': case 'f': @@ -293,22 +402,11 @@ inline int64_t to_flag_value(std::string val) { case '-': ret = -1; break; - case '1': case 't': case 'y': case '+': ret = 1; break; - case '2': - case '3': - case '4': - case '5': - case '6': - case '7': - case '8': - case '9': - ret = val[0] - '0'; - break; default: throw std::invalid_argument("unrecognized character"); } @@ -325,10 +423,7 @@ inline int64_t to_flag_value(std::string val) { } /// Signed integers -template < - typename T, - enable_if_t::value && std::is_signed::value && !is_bool::value && !std::is_enum::value, - detail::enabler> = detail::dummy> +template ::value == integral_value, detail::enabler> = detail::dummy> bool lexical_cast(std::string input, T &output) { try { size_t n = 0; @@ -343,9 +438,7 @@ bool lexical_cast(std::string input, T &output) { } /// Unsigned integers -template ::value && std::is_unsigned::value && !is_bool::value, detail::enabler> = - detail::dummy> +template ::value == unsigned_integral, detail::enabler> = detail::dummy> bool lexical_cast(std::string input, T &output) { if(!input.empty() && input.front() == '-') return false; // std::stoull happily converts negative values to junk without any errors. @@ -363,7 +456,7 @@ bool lexical_cast(std::string input, T &output) { } /// Boolean values -template ::value, detail::enabler> = detail::dummy> +template ::value == boolean_value, detail::enabler> = detail::dummy> bool lexical_cast(std::string input, T &output) { try { auto out = to_flag_value(input); @@ -375,7 +468,7 @@ bool lexical_cast(std::string input, T &output) { } /// Floats -template ::value, detail::enabler> = detail::dummy> +template ::value == floating_point, detail::enabler> = detail::dummy> bool lexical_cast(std::string input, T &output) { try { size_t n = 0; @@ -389,27 +482,21 @@ bool lexical_cast(std::string input, T &output) { } /// String and similar direct assignment -template ::value && !std::is_integral::value && - std::is_assignable::value, - detail::enabler> = detail::dummy> +template ::value == string_assignable, detail::enabler> = detail::dummy> bool lexical_cast(std::string input, T &output) { output = input; return true; } /// String and similar constructible and copy assignment -template ::value && !std::is_integral::value && - !std::is_assignable::value && std::is_constructible::value, - detail::enabler> = detail::dummy> +template ::value == string_constructible, detail::enabler> = detail::dummy> bool lexical_cast(std::string input, T &output) { output = T(input); return true; } /// Enumerations -template ::value, detail::enabler> = detail::dummy> +template ::value == enumeration, detail::enabler> = detail::dummy> bool lexical_cast(std::string input, T &output) { typename std::underlying_type::type val; bool retval = detail::lexical_cast(input, val); @@ -421,12 +508,7 @@ bool lexical_cast(std::string input, T &output) { } /// Assignable from double or int -template ::value && !std::is_integral::value && - !std::is_assignable::value && - !std::is_constructible::value && !std::is_enum::value && - is_direct_constructible::value && is_direct_constructible::value, - detail::enabler> = detail::dummy> +template ::value == number_constructible, detail::enabler> = detail::dummy> bool lexical_cast(std::string input, T &output) { int val; if(lexical_cast(input, val)) { @@ -442,13 +524,8 @@ bool lexical_cast(std::string input, T &output) { return from_stream(input, output); } -/// Assignable from int64 -template ::value && !std::is_integral::value && - !std::is_assignable::value && - !std::is_constructible::value && !std::is_enum::value && - !is_direct_constructible::value && is_direct_constructible::value, - detail::enabler> = detail::dummy> +/// Assignable from int +template ::value == integer_constructible, detail::enabler> = detail::dummy> bool lexical_cast(std::string input, T &output) { int val; if(lexical_cast(input, val)) { @@ -459,12 +536,7 @@ bool lexical_cast(std::string input, T &output) { } /// Assignable from double -template ::value && !std::is_integral::value && - !std::is_assignable::value && - !std::is_constructible::value && !std::is_enum::value && - is_direct_constructible::value && !is_direct_constructible::value, - detail::enabler> = detail::dummy> +template ::value == double_constructible, detail::enabler> = detail::dummy> bool lexical_cast(std::string input, T &output) { double val; if(lexical_cast(input, val)) { @@ -475,15 +547,10 @@ bool lexical_cast(std::string input, T &output) { } /// Non-string parsable by a stream -template ::value && !std::is_integral::value && - !std::is_assignable::value && - !std::is_constructible::value && !std::is_enum::value && - !is_direct_constructible::value && !is_direct_constructible::value, - detail::enabler> = detail::dummy> +template ::value == other, detail::enabler> = detail::dummy> bool lexical_cast(std::string input, T &output) { static_assert(is_istreamable::value, - "option object type must have a lexical cast overload or streaming input operator(>>) defined if it " + "option object type must have a lexical cast overload or streaming input operator(>>) defined, if it " "is convertible from another type use the add_option(...) with XC being the known type"); return from_stream(input, output); } From b3c9b0882f9ca9a10df0492f4b660b473ef5a1a4 Mon Sep 17 00:00:00 2001 From: Philip Top Date: Thu, 30 May 2019 08:06:18 -0700 Subject: [PATCH 04/21] fix readme issue and make the condition tests a little clearer --- README.md | 2 +- include/CLI/TypeTools.hpp | 121 +++++++++++++++++++++----------------- 2 files changed, 68 insertions(+), 55 deletions(-) diff --git a/README.md b/README.md index fa02aba7f..265a02f31 100644 --- a/README.md +++ b/README.md @@ -604,7 +604,7 @@ The subcommand method .add_option_group(name,description) ``` -Will create an option group, and return a pointer to it. An option group allows creation of a collection of options, similar to the groups function on options, but with additional controls and requirements. They allow specific sets of options to be composed and controlled as a collective. For an example see [range test](./tests/ranges.cpp). Option groups are a specialization of an App so all [functions](#subcommand-options) that work with an App or subcommand also work on option groups. Options can be created as part of an option group using the add functions just like a subcommand, or previously created options can be added through +Will create an option group, and return a pointer to it. An option group allows creation of a collection of options, similar to the groups function on options, but with additional controls and requirements. They allow specific sets of options to be composed and controlled as a collective. For an example see [range example](https://github.com/CLIUtils/CLI11/blob/master/examples/ranges.cpp). Option groups are a specialization of an App so all [functions](#subcommand-options) that work with an App or subcommand also work on option groups. Options can be created as part of an option group using the add functions just like a subcommand, or previously created options can be added through ```cpp ogroup->add_option(option_pointer); diff --git a/include/CLI/TypeTools.hpp b/include/CLI/TypeTools.hpp index c8de3abbf..5a07307fd 100644 --- a/include/CLI/TypeTools.hpp +++ b/include/CLI/TypeTools.hpp @@ -229,102 +229,115 @@ std::string checked_to_string(T &&) { } // Enumeration of the different supported categorizations of objects -enum objCategory { - integral_value, - unsigned_integral, - enumeration, - boolean_value, - floating_point, - number_constructible, - double_constructible, - integer_constructible, - string_assignable, - string_constructible, - other, +enum objCategory : int { + integral_value = 2, + unsigned_integral = 4, + enumeration = 6, + boolean_value = 8, + floating_point = 10, + number_constructible = 12, + double_constructible = 14, + integer_constructible = 16, + vector_value = 30, + // string assignable or greater used in a condition so anything string like must come last + string_assignable = 50, + string_constructible = 60, + other = 200, + }; /// some type that is not otherwise recognized -template class classify_object { static constexpr objCategory value = other; }; +template struct classify_object { static constexpr objCategory value{other}; }; /// Set of overloads to classify an object according to type template -class classify_object::value && std::is_signed::value && - !is_bool::value && !std::is_enum::value>::type> { +struct classify_object::value && std::is_signed::value && + !is_bool::value && !std::is_enum::value>::type> { static constexpr objCategory value{integral_value}; }; /// Unsigned integers template -class classify_object< +struct classify_object< T, typename std::enable_if::value && std::is_unsigned::value && !is_bool::value>::type> { static constexpr objCategory value{unsigned_integral}; }; /// Boolean values -template class classify_object::value>::type> { - static constexpr objCategory value = boolean_value; +template struct classify_object::value>::type> { + static constexpr objCategory value{boolean_value}; }; /// Floats -template class classify_object::value>::type> { - static constexpr objCategory value = floating_point; +template struct classify_object::value>::type> { + static constexpr objCategory value{floating_point}; }; /// String and similar direct assignment template -class classify_object::value && !std::is_integral::value && - std::is_assignable::value>::type> { - static constexpr objCategory value = string_assignable; +struct classify_object< + T, + typename std::enable_if::value && !std::is_integral::value && + std::is_assignable::value && !is_vector::value>::type> { + static constexpr objCategory value{string_assignable}; }; /// String and similar constructible and copy assignment template -class classify_object::value && !std::is_integral::value && - !std::is_assignable::value && - std::is_constructible::value>::type> { - static constexpr objCategory value = string_constructible; +struct classify_object< + T, + typename std::enable_if::value && !std::is_integral::value && + !std::is_assignable::value && + std::is_constructible::value && !is_vector::value>::type> { + static constexpr objCategory value{string_constructible}; }; /// Enumerations -template class classify_object::value>::type> { - static constexpr objCategory value = enumeration; +template struct classify_object::value>::type> { + static constexpr objCategory value{enumeration}; +}; + +/// Handy helper to contain a bunch of checks that rule out many common types (integers, string like, floating point, +/// vectors, and enumerations +template struct uncommon_type { + using type = typename std::conditional::value && !std::is_integral::value && + !std::is_assignable::value && + !std::is_constructible::value && !is_vector::value && + !std::is_enum::value, + std::true_type, + std::false_type>::type; + static const bool value = type::value; }; /// Assignable from double or int template -class classify_object::value && !std::is_integral::value && - !std::is_assignable::value && - !std::is_constructible::value && - !std::is_enum::value && is_direct_constructible::value && - is_direct_constructible::value>::type> { - static constexpr objCategory value = number_constructible; +struct classify_object::value && is_direct_constructible::value && + is_direct_constructible::value>::type> { + static constexpr objCategory value{number_constructible}; }; /// Assignable from int template -class classify_object::value && !std::is_integral::value && - !std::is_assignable::value && - !std::is_constructible::value && - !std::is_enum::value && !is_direct_constructible::value && - is_direct_constructible::value>::type> { - static const objCategory value = integer_constructible; +struct classify_object::value && !is_direct_constructible::value && + is_direct_constructible::value>::type> { + static const objCategory value{integer_constructible}; }; /// Assignable from double template -class classify_object::value && !std::is_integral::value && - !std::is_assignable::value && - !std::is_constructible::value && - !std::is_enum::value && is_direct_constructible::value && - !is_direct_constructible::value>::type> { - static const objCategory value = double_constructible; +struct classify_object::value && is_direct_constructible::value && + !is_direct_constructible::value>::type> { + static const objCategory value{double_constructible}; +}; + +/// vector type +template struct classify_object::value>::type> { + static const objCategory value{vector_value}; }; // Type name print @@ -355,7 +368,7 @@ constexpr const char *type_name() { } /// This one should not be used, since vector types print the internal type -template ::value, detail::enabler> = detail::dummy> +template ::value == vector_value, detail::enabler> = detail::dummy> constexpr const char *type_name() { return "VECTOR"; } From 397ce1eb25a4f390cdbebd5ba8fe7bc316199d73 Mon Sep 17 00:00:00 2001 From: Philip Top Date: Fri, 31 May 2019 05:30:44 -0700 Subject: [PATCH 05/21] add a check for out of range errors on boolean conversions --- include/CLI/TypeTools.hpp | 5 +++++ tests/AppTest.cpp | 9 +++++++++ 2 files changed, 14 insertions(+) diff --git a/include/CLI/TypeTools.hpp b/include/CLI/TypeTools.hpp index 5a07307fd..9b6395006 100644 --- a/include/CLI/TypeTools.hpp +++ b/include/CLI/TypeTools.hpp @@ -477,6 +477,11 @@ bool lexical_cast(std::string input, T &output) { return true; } catch(const std::invalid_argument &) { return false; + } catch(const std::out_of_range &) { + // if the number is out of the range of a 64 bit value then it is still a number and for this purpose is still + // valid all we care about the sign + output = (input[0] != '-'); + return true; } } diff --git a/tests/AppTest.cpp b/tests/AppTest.cpp index 4742dfe1a..4a829dff9 100644 --- a/tests/AppTest.cpp +++ b/tests/AppTest.cpp @@ -647,6 +647,15 @@ TEST_F(TApp, BoolOption) { args = {"-b", "-7"}; run(); EXPECT_FALSE(bflag); + + // cause an out of bounds error internally + args = {"-b", "751615654161688126132138844896646748852"}; + run(); + EXPECT_TRUE(bflag); + + args = {"-b", "-751615654161688126132138844896646748852"}; + run(); + EXPECT_FALSE(bflag); } TEST_F(TApp, ShortOpts) { From ebbeecadba2d14aeabdcc6a33c9056c878e50549 Mon Sep 17 00:00:00 2001 From: Philip Top Date: Mon, 3 Jun 2019 06:31:25 -0700 Subject: [PATCH 06/21] Fix capitalization and some comments on option functions --- include/CLI/Option.hpp | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/include/CLI/Option.hpp b/include/CLI/Option.hpp index 3d90ab874..024e2fb7d 100644 --- a/include/CLI/Option.hpp +++ b/include/CLI/Option.hpp @@ -527,7 +527,7 @@ class Option : public OptionBase