diff --git a/README.md b/README.md index f98e8c8f..f10248e5 100644 --- a/README.md +++ b/README.md @@ -224,7 +224,7 @@ 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 or that takes an int ๐Ÿ†•, double ๐Ÿ†•, or string in a constructor. Also allowed are tuples ๐Ÿ†•, std::array ๐Ÿ†• or std::pair ๐Ÿ†•. Also supported are complex numbers๐Ÿšง, wrapper types๐Ÿšง, and containers besides vector๐Ÿšง of any other supported type. + variable_to_bind_to, // bool, char(see note)๐Ÿšง, 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. Also allowed are tuples ๐Ÿ†•, std::array ๐Ÿ†• or std::pair ๐Ÿ†•. Also supported are complex numbers๐Ÿšง, wrapper types๐Ÿšง, and containers besides vector๐Ÿšง of any other supported type. help_string="") app.add_option_function(option_name, @@ -233,6 +233,8 @@ app.add_option_function(option_name, app.add_complex(... // Special case: support for complex numbers โš ๏ธ. Complex numbers are now fully supported in the add_option so this function is redundant. +// char as an option type is supported before 2.0 but in 2.0 it defaulted to allowing single non numerical characters in addition to the numeric values. + // ๐Ÿ†• 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, such as any of the types that work in the non-template version. If XC is a std::pair and T is some non pair type. Then a two argument constructor for T is called to assign the value. For tuples or other multi element types, XC must be a single type or a tuple like object of the same size as the assignment type app.add_option(option_name, T &output, // output must be assignable or constructible from a value of type XC diff --git a/book/chapters/options.md b/book/chapters/options.md index 17fcd578..04bfd0bb 100644 --- a/book/chapters/options.md +++ b/book/chapters/options.md @@ -22,6 +22,7 @@ You can use any C++ int-like type, not just `int`. CLI11 understands the followi |-------------|-------| | number like | Integers, floats, bools, or any type that can be constructed from an integer or floating point number | | string-like | std\::string, or anything that can be constructed from or assigned a std\::string | +| char | For a single char, single string values are accepted, otherwise longer strings are treated as integral values and a conversion is attempted | | complex-number | std::complex or any type which has a real(), and imag() operations available, will allow 1 or 2 string definitions like "1+2j" or two arguments "1","2" | | enumeration | any enum or enum class type is supported through conversion from the underlying type(typically int, though it can be specified otherwise) | | container-like | a container(like vector) of any available types including other containers | diff --git a/include/CLI/TypeTools.hpp b/include/CLI/TypeTools.hpp index 4b85f05c..147ca91d 100644 --- a/include/CLI/TypeTools.hpp +++ b/include/CLI/TypeTools.hpp @@ -505,6 +505,7 @@ struct expected_count::value // Enumeration of the different supported categorizations of objects enum class object_category : int { + char_value = 1, integral_value = 2, unsigned_integral = 4, enumeration = 6, @@ -525,27 +526,36 @@ enum class object_category : int { }; +/// Set of overloads to classify an object according to type + /// some type that is not otherwise recognized template struct classify_object { static constexpr object_category value{object_category::other}; }; -/// Set of overloads to classify an object according to type +/// Signed integers template -struct classify_object::value && std::is_signed::value && - !is_bool::value && !std::is_enum::value>::type> { +struct classify_object< + T, + typename std::enable_if::value && !std::is_same::value && std::is_signed::value && + !is_bool::value && !std::is_enum::value>::type> { static constexpr object_category value{object_category::integral_value}; }; /// Unsigned integers template -struct classify_object< - T, - typename std::enable_if::value && std::is_unsigned::value && !is_bool::value>::type> { +struct classify_object::value && std::is_unsigned::value && + !std::is_same::value && !is_bool::value>::type> { static constexpr object_category value{object_category::unsigned_integral}; }; +/// single character values +template +struct classify_object::value && !std::is_enum::value>::type> { + static constexpr object_category value{object_category::char_value}; +}; + /// Boolean values template struct classify_object::value>::type> { static constexpr object_category value{object_category::boolean_value}; @@ -657,6 +667,12 @@ template struct classify_object::value == object_category::char_value, detail::enabler> = detail::dummy> +constexpr const char *type_name() { + return "CHAR"; +} + template ::value == object_category::integral_value || classify_object::value == object_category::integer_constructible, @@ -767,6 +783,30 @@ inline std::string type_name() { // Lexical cast +/// Convert to an unsigned integral +template ::value, detail::enabler> = detail::dummy> +bool integral_conversion(const std::string &input, T &output) noexcept { + if(input.empty()) { + return false; + } + char *val = nullptr; + std::uint64_t output_ll = std::strtoull(input.c_str(), &val, 0); + output = static_cast(output_ll); + return val == (input.c_str() + input.size()) && static_cast(output) == output_ll; +} + +/// Convert to a signed integral +template ::value, detail::enabler> = detail::dummy> +bool integral_conversion(const std::string &input, T &output) noexcept { + if(input.empty()) { + return false; + } + char *val = nullptr; + std::int64_t output_ll = std::strtoll(input.c_str(), &val, 0); + output = static_cast(output_ll); + return val == (input.c_str() + input.size()) && static_cast(output) == output_ll; +} + /// Convert a flag into an integer value typically binary flags inline std::int64_t to_flag_value(std::string val) { static const std::string trueString("true"); @@ -810,39 +850,24 @@ inline std::int64_t to_flag_value(std::string val) { return ret; } -/// Signed integers +/// Integer conversion template ::value == object_category::integral_value, detail::enabler> = detail::dummy> + enable_if_t::value == object_category::integral_value || + classify_object::value == object_category::unsigned_integral, + detail::enabler> = detail::dummy> bool lexical_cast(const std::string &input, T &output) { - try { - std::size_t n = 0; - std::int64_t output_ll = std::stoll(input, &n, 0); - output = static_cast(output_ll); - return n == input.size() && static_cast(output) == output_ll; - } catch(const std::invalid_argument &) { - return false; - } catch(const std::out_of_range &) { - return false; - } + return integral_conversion(input, output); } -/// Unsigned integers +/// char values template ::value == object_category::unsigned_integral, detail::enabler> = detail::dummy> + enable_if_t::value == object_category::char_value, detail::enabler> = detail::dummy> bool lexical_cast(const std::string &input, T &output) { - if(!input.empty() && input.front() == '-') - return false; // std::stoull happily converts negative values to junk without any errors. - - try { - std::size_t n = 0; - std::uint64_t output_ll = std::stoull(input, &n, 0); - output = static_cast(output_ll); - return n == input.size() && static_cast(output) == output_ll; - } catch(const std::invalid_argument &) { - return false; - } catch(const std::out_of_range &) { - return false; + if(input.size() == 1) { + output = static_cast(input[0]); + return true; } + return integral_conversion(input, output); } /// Boolean values @@ -867,15 +892,13 @@ bool lexical_cast(const std::string &input, T &output) { template ::value == object_category::floating_point, detail::enabler> = detail::dummy> bool lexical_cast(const std::string &input, T &output) { - try { - std::size_t n = 0; - output = static_cast(std::stold(input, &n)); - return n == input.size(); - } catch(const std::invalid_argument &) { - return false; - } catch(const std::out_of_range &) { + if(input.empty()) { return false; } + char *val = nullptr; + auto output_ld = std::strtold(input.c_str(), &val); + output = static_cast(output_ld); + return val == (input.c_str() + input.size()); } /// complex @@ -932,8 +955,7 @@ template ::value == object_category::enumeration, detail::enabler> = detail::dummy> bool lexical_cast(const std::string &input, T &output) { typename std::underlying_type::type val; - bool retval = detail::lexical_cast(input, val); - if(!retval) { + if(!integral_conversion(input, val)) { return false; } output = static_cast(val); @@ -958,7 +980,7 @@ template < enable_if_t::value == object_category::number_constructible, detail::enabler> = detail::dummy> bool lexical_cast(const std::string &input, T &output) { int val; - if(lexical_cast(input, val)) { + if(integral_conversion(input, val)) { output = T(val); return true; } else { @@ -977,7 +999,7 @@ template < enable_if_t::value == object_category::integer_constructible, detail::enabler> = detail::dummy> bool lexical_cast(const std::string &input, T &output) { int val; - if(lexical_cast(input, val)) { + if(integral_conversion(input, val)) { output = T(val); return true; } diff --git a/include/CLI/Validators.hpp b/include/CLI/Validators.hpp index 4d90896a..8992f75d 100644 --- a/include/CLI/Validators.hpp +++ b/include/CLI/Validators.hpp @@ -966,14 +966,11 @@ class AsNumberWithUnit : public Validator { if(opts & CASE_INSENSITIVE) { unit = detail::to_lower(unit); } - - bool converted = detail::lexical_cast(input, num); - if(!converted) { - throw ValidationError(std::string("Value ") + input + " could not be converted to " + - detail::type_name()); - } - if(unit.empty()) { + if(!detail::lexical_cast(input, num)) { + throw ValidationError(std::string("Value ") + input + " could not be converted to " + + detail::type_name()); + } // No need to modify input if no unit passed return {}; } @@ -987,12 +984,22 @@ class AsNumberWithUnit : public Validator { detail::generate_map(mapping, true)); } - // perform safe multiplication - bool ok = detail::checked_multiply(num, it->second); - if(!ok) { - throw ValidationError(detail::to_string(num) + " multiplied by " + unit + - " factor would cause number overflow. Use smaller value."); + if(!input.empty()) { + bool converted = detail::lexical_cast(input, num); + if(!converted) { + throw ValidationError(std::string("Value ") + input + " could not be converted to " + + detail::type_name()); + } + // perform safe multiplication + bool ok = detail::checked_multiply(num, it->second); + if(!ok) { + throw ValidationError(detail::to_string(num) + " multiplied by " + unit + + " factor would cause number overflow. Use smaller value."); + } + } else { + num = static_cast(it->second); } + input = detail::to_string(num); return {}; diff --git a/tests/HelpersTest.cpp b/tests/HelpersTest.cpp index a6007626..fb5bd7b8 100644 --- a/tests/HelpersTest.cpp +++ b/tests/HelpersTest.cpp @@ -911,6 +911,9 @@ TEST(Types, TypeName) { std::string float_name = CLI::detail::type_name(); EXPECT_EQ("FLOAT", float_name); + std::string char_name = CLI::detail::type_name(); + EXPECT_EQ("CHAR", char_name); + std::string vector_name = CLI::detail::type_name>(); EXPECT_EQ("INT", vector_name); @@ -1025,6 +1028,11 @@ TEST(Types, LexicalCastInt) { std::string extra_input = "912i"; EXPECT_FALSE(CLI::detail::lexical_cast(extra_input, y)); + + std::string empty_input{}; + EXPECT_FALSE(CLI::detail::lexical_cast(empty_input, x_signed)); + EXPECT_FALSE(CLI::detail::lexical_cast(empty_input, x_unsigned)); + EXPECT_FALSE(CLI::detail::lexical_cast(empty_input, y_signed)); } TEST(Types, LexicalCastDouble) { @@ -1037,10 +1045,14 @@ TEST(Types, LexicalCastDouble) { EXPECT_FALSE(CLI::detail::lexical_cast(bad_input, x)); std::string overflow_input = "1" + std::to_string(LDBL_MAX); - EXPECT_FALSE(CLI::detail::lexical_cast(overflow_input, x)); + EXPECT_TRUE(CLI::detail::lexical_cast(overflow_input, x)); + EXPECT_FALSE(std::isfinite(x)); std::string extra_input = "9.12i"; EXPECT_FALSE(CLI::detail::lexical_cast(extra_input, x)); + + std::string empty_input{}; + EXPECT_FALSE(CLI::detail::lexical_cast(empty_input, x)); } TEST(Types, LexicalCastBool) { diff --git a/tests/OptionTypeTest.cpp b/tests/OptionTypeTest.cpp index e78d10a7..7ef54dd2 100644 --- a/tests/OptionTypeTest.cpp +++ b/tests/OptionTypeTest.cpp @@ -167,6 +167,31 @@ TEST_F(TApp, BoolOption) { EXPECT_FALSE(bflag); } +TEST_F(TApp, CharOption) { + char c1{'t'}; + app.add_option("-c", c1); + + args = {"-c", "g"}; + run(); + EXPECT_EQ(c1, 'g'); + + args = {"-c", "1"}; + run(); + EXPECT_EQ(c1, '1'); + + args = {"-c", "77"}; + run(); + EXPECT_EQ(c1, 77); + + // convert hex for digit + args = {"-c", "0x44"}; + run(); + EXPECT_EQ(c1, 0x44); + + args = {"-c", "751615654161688126132138844896646748852"}; + EXPECT_THROW(run(), CLI::ConversionError); +} + TEST_F(TApp, vectorDefaults) { std::vector vals{4, 5}; auto opt = app.add_option("--long", vals, "", true); diff --git a/tests/OptionalTest.cpp b/tests/OptionalTest.cpp index a52c3890..527751a6 100644 --- a/tests/OptionalTest.cpp +++ b/tests/OptionalTest.cpp @@ -218,6 +218,7 @@ TEST_F(TApp, BoostOptionalEnumTest) { enum class eval : char { val0 = 0, val1 = 1, val2 = 2, val3 = 3, val4 = 4 }; boost::optional opt, opt2; + auto optptr = app.add_option("-v,--val", opt); app.add_option_no_stream("-e,--eval", opt2); optptr->capture_default_str(); diff --git a/tests/TransformTest.cpp b/tests/TransformTest.cpp index 523472f0..53df504a 100644 --- a/tests/TransformTest.cpp +++ b/tests/TransformTest.cpp @@ -692,7 +692,8 @@ TEST_F(TApp, NumberWithUnitBadInput) { args = {"-n", "13 c"}; EXPECT_THROW(run(), CLI::ValidationError); args = {"-n", "a"}; - EXPECT_THROW(run(), CLI::ValidationError); + // Assume 1.0 unit + EXPECT_NO_THROW(run()); args = {"-n", "12.0a"}; EXPECT_THROW(run(), CLI::ValidationError); args = {"-n", "a5"};