Skip to content

Commit 438eabe

Browse files
authored
feat: add char type (#449)
add a test for char options add support for char types to the lexical cast, to allow single character types that make sense, add a integral_conversion operations to simplify the conversions from string to integers and allow discrimination in a few cases with enumerations.
1 parent a1dd4d7 commit 438eabe

File tree

8 files changed

+130
-59
lines changed

8 files changed

+130
-59
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ While all options internally are the same type, there are several ways to add an
224224
app.add_option(option_name, help_str="")
225225

226226
app.add_option(option_name,
227-
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.
227+
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.
228228
help_string="")
229229

230230
app.add_option_function<type>(option_name,
@@ -233,6 +233,8 @@ app.add_option_function<type>(option_name,
233233

234234
app.add_complex(... // Special case: support for complex numbers ⚠️. Complex numbers are now fully supported in the add_option so this function is redundant.
235235

236+
// 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.
237+
236238
// 🆕 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
237239
app.add_option<typename T, typename XC>(option_name,
238240
T &output, // output must be assignable or constructible from a value of type XC

book/chapters/options.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ You can use any C++ int-like type, not just `int`. CLI11 understands the followi
2222
|-------------|-------|
2323
| number like | Integers, floats, bools, or any type that can be constructed from an integer or floating point number |
2424
| string-like | std\::string, or anything that can be constructed from or assigned a std\::string |
25+
| char | For a single char, single string values are accepted, otherwise longer strings are treated as integral values and a conversion is attempted |
2526
| 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" |
2627
| enumeration | any enum or enum class type is supported through conversion from the underlying type(typically int, though it can be specified otherwise) |
2728
| container-like | a container(like vector) of any available types including other containers |

include/CLI/TypeTools.hpp

Lines changed: 66 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -505,6 +505,7 @@ struct expected_count<T, typename std::enable_if<!is_mutable_container<T>::value
505505

506506
// Enumeration of the different supported categorizations of objects
507507
enum class object_category : int {
508+
char_value = 1,
508509
integral_value = 2,
509510
unsigned_integral = 4,
510511
enumeration = 6,
@@ -525,27 +526,36 @@ enum class object_category : int {
525526

526527
};
527528

529+
/// Set of overloads to classify an object according to type
530+
528531
/// some type that is not otherwise recognized
529532
template <typename T, typename Enable = void> struct classify_object {
530533
static constexpr object_category value{object_category::other};
531534
};
532535

533-
/// Set of overloads to classify an object according to type
536+
/// Signed integers
534537
template <typename T>
535-
struct classify_object<T,
536-
typename std::enable_if<std::is_integral<T>::value && std::is_signed<T>::value &&
537-
!is_bool<T>::value && !std::is_enum<T>::value>::type> {
538+
struct classify_object<
539+
T,
540+
typename std::enable_if<std::is_integral<T>::value && !std::is_same<T, char>::value && std::is_signed<T>::value &&
541+
!is_bool<T>::value && !std::is_enum<T>::value>::type> {
538542
static constexpr object_category value{object_category::integral_value};
539543
};
540544

541545
/// Unsigned integers
542546
template <typename T>
543-
struct classify_object<
544-
T,
545-
typename std::enable_if<std::is_integral<T>::value && std::is_unsigned<T>::value && !is_bool<T>::value>::type> {
547+
struct classify_object<T,
548+
typename std::enable_if<std::is_integral<T>::value && std::is_unsigned<T>::value &&
549+
!std::is_same<T, char>::value && !is_bool<T>::value>::type> {
546550
static constexpr object_category value{object_category::unsigned_integral};
547551
};
548552

553+
/// single character values
554+
template <typename T>
555+
struct classify_object<T, typename std::enable_if<std::is_same<T, char>::value && !std::is_enum<T>::value>::type> {
556+
static constexpr object_category value{object_category::char_value};
557+
};
558+
549559
/// Boolean values
550560
template <typename T> struct classify_object<T, typename std::enable_if<is_bool<T>::value>::type> {
551561
static constexpr object_category value{object_category::boolean_value};
@@ -657,6 +667,12 @@ template <typename T> struct classify_object<T, typename std::enable_if<is_mutab
657667
/// http://stackoverflow.com/questions/1055452/c-get-name-of-type-in-template
658668
/// But this is cleaner and works better in this case
659669

670+
template <typename T,
671+
enable_if_t<classify_object<T>::value == object_category::char_value, detail::enabler> = detail::dummy>
672+
constexpr const char *type_name() {
673+
return "CHAR";
674+
}
675+
660676
template <typename T,
661677
enable_if_t<classify_object<T>::value == object_category::integral_value ||
662678
classify_object<T>::value == object_category::integer_constructible,
@@ -767,6 +783,30 @@ inline std::string type_name() {
767783

768784
// Lexical cast
769785

786+
/// Convert to an unsigned integral
787+
template <typename T, enable_if_t<std::is_unsigned<T>::value, detail::enabler> = detail::dummy>
788+
bool integral_conversion(const std::string &input, T &output) noexcept {
789+
if(input.empty()) {
790+
return false;
791+
}
792+
char *val = nullptr;
793+
std::uint64_t output_ll = std::strtoull(input.c_str(), &val, 0);
794+
output = static_cast<T>(output_ll);
795+
return val == (input.c_str() + input.size()) && static_cast<std::uint64_t>(output) == output_ll;
796+
}
797+
798+
/// Convert to a signed integral
799+
template <typename T, enable_if_t<std::is_signed<T>::value, detail::enabler> = detail::dummy>
800+
bool integral_conversion(const std::string &input, T &output) noexcept {
801+
if(input.empty()) {
802+
return false;
803+
}
804+
char *val = nullptr;
805+
std::int64_t output_ll = std::strtoll(input.c_str(), &val, 0);
806+
output = static_cast<T>(output_ll);
807+
return val == (input.c_str() + input.size()) && static_cast<std::int64_t>(output) == output_ll;
808+
}
809+
770810
/// Convert a flag into an integer value typically binary flags
771811
inline std::int64_t to_flag_value(std::string val) {
772812
static const std::string trueString("true");
@@ -810,39 +850,24 @@ inline std::int64_t to_flag_value(std::string val) {
810850
return ret;
811851
}
812852

813-
/// Signed integers
853+
/// Integer conversion
814854
template <typename T,
815-
enable_if_t<classify_object<T>::value == object_category::integral_value, detail::enabler> = detail::dummy>
855+
enable_if_t<classify_object<T>::value == object_category::integral_value ||
856+
classify_object<T>::value == object_category::unsigned_integral,
857+
detail::enabler> = detail::dummy>
816858
bool lexical_cast(const std::string &input, T &output) {
817-
try {
818-
std::size_t n = 0;
819-
std::int64_t output_ll = std::stoll(input, &n, 0);
820-
output = static_cast<T>(output_ll);
821-
return n == input.size() && static_cast<std::int64_t>(output) == output_ll;
822-
} catch(const std::invalid_argument &) {
823-
return false;
824-
} catch(const std::out_of_range &) {
825-
return false;
826-
}
859+
return integral_conversion(input, output);
827860
}
828861

829-
/// Unsigned integers
862+
/// char values
830863
template <typename T,
831-
enable_if_t<classify_object<T>::value == object_category::unsigned_integral, detail::enabler> = detail::dummy>
864+
enable_if_t<classify_object<T>::value == object_category::char_value, detail::enabler> = detail::dummy>
832865
bool lexical_cast(const std::string &input, T &output) {
833-
if(!input.empty() && input.front() == '-')
834-
return false; // std::stoull happily converts negative values to junk without any errors.
835-
836-
try {
837-
std::size_t n = 0;
838-
std::uint64_t output_ll = std::stoull(input, &n, 0);
839-
output = static_cast<T>(output_ll);
840-
return n == input.size() && static_cast<std::uint64_t>(output) == output_ll;
841-
} catch(const std::invalid_argument &) {
842-
return false;
843-
} catch(const std::out_of_range &) {
844-
return false;
866+
if(input.size() == 1) {
867+
output = static_cast<T>(input[0]);
868+
return true;
845869
}
870+
return integral_conversion(input, output);
846871
}
847872

848873
/// Boolean values
@@ -867,15 +892,13 @@ bool lexical_cast(const std::string &input, T &output) {
867892
template <typename T,
868893
enable_if_t<classify_object<T>::value == object_category::floating_point, detail::enabler> = detail::dummy>
869894
bool lexical_cast(const std::string &input, T &output) {
870-
try {
871-
std::size_t n = 0;
872-
output = static_cast<T>(std::stold(input, &n));
873-
return n == input.size();
874-
} catch(const std::invalid_argument &) {
875-
return false;
876-
} catch(const std::out_of_range &) {
895+
if(input.empty()) {
877896
return false;
878897
}
898+
char *val = nullptr;
899+
auto output_ld = std::strtold(input.c_str(), &val);
900+
output = static_cast<T>(output_ld);
901+
return val == (input.c_str() + input.size());
879902
}
880903

881904
/// complex
@@ -932,8 +955,7 @@ template <typename T,
932955
enable_if_t<classify_object<T>::value == object_category::enumeration, detail::enabler> = detail::dummy>
933956
bool lexical_cast(const std::string &input, T &output) {
934957
typename std::underlying_type<T>::type val;
935-
bool retval = detail::lexical_cast(input, val);
936-
if(!retval) {
958+
if(!integral_conversion(input, val)) {
937959
return false;
938960
}
939961
output = static_cast<T>(val);
@@ -958,7 +980,7 @@ template <
958980
enable_if_t<classify_object<T>::value == object_category::number_constructible, detail::enabler> = detail::dummy>
959981
bool lexical_cast(const std::string &input, T &output) {
960982
int val;
961-
if(lexical_cast(input, val)) {
983+
if(integral_conversion(input, val)) {
962984
output = T(val);
963985
return true;
964986
} else {
@@ -977,7 +999,7 @@ template <
977999
enable_if_t<classify_object<T>::value == object_category::integer_constructible, detail::enabler> = detail::dummy>
9781000
bool lexical_cast(const std::string &input, T &output) {
9791001
int val;
980-
if(lexical_cast(input, val)) {
1002+
if(integral_conversion(input, val)) {
9811003
output = T(val);
9821004
return true;
9831005
}

include/CLI/Validators.hpp

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -966,14 +966,11 @@ class AsNumberWithUnit : public Validator {
966966
if(opts & CASE_INSENSITIVE) {
967967
unit = detail::to_lower(unit);
968968
}
969-
970-
bool converted = detail::lexical_cast(input, num);
971-
if(!converted) {
972-
throw ValidationError(std::string("Value ") + input + " could not be converted to " +
973-
detail::type_name<Number>());
974-
}
975-
976969
if(unit.empty()) {
970+
if(!detail::lexical_cast(input, num)) {
971+
throw ValidationError(std::string("Value ") + input + " could not be converted to " +
972+
detail::type_name<Number>());
973+
}
977974
// No need to modify input if no unit passed
978975
return {};
979976
}
@@ -987,12 +984,22 @@ class AsNumberWithUnit : public Validator {
987984
detail::generate_map(mapping, true));
988985
}
989986

990-
// perform safe multiplication
991-
bool ok = detail::checked_multiply(num, it->second);
992-
if(!ok) {
993-
throw ValidationError(detail::to_string(num) + " multiplied by " + unit +
994-
" factor would cause number overflow. Use smaller value.");
987+
if(!input.empty()) {
988+
bool converted = detail::lexical_cast(input, num);
989+
if(!converted) {
990+
throw ValidationError(std::string("Value ") + input + " could not be converted to " +
991+
detail::type_name<Number>());
992+
}
993+
// perform safe multiplication
994+
bool ok = detail::checked_multiply(num, it->second);
995+
if(!ok) {
996+
throw ValidationError(detail::to_string(num) + " multiplied by " + unit +
997+
" factor would cause number overflow. Use smaller value.");
998+
}
999+
} else {
1000+
num = static_cast<Number>(it->second);
9951001
}
1002+
9961003
input = detail::to_string(num);
9971004

9981005
return {};

tests/HelpersTest.cpp

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -911,6 +911,9 @@ TEST(Types, TypeName) {
911911
std::string float_name = CLI::detail::type_name<double>();
912912
EXPECT_EQ("FLOAT", float_name);
913913

914+
std::string char_name = CLI::detail::type_name<char>();
915+
EXPECT_EQ("CHAR", char_name);
916+
914917
std::string vector_name = CLI::detail::type_name<std::vector<int>>();
915918
EXPECT_EQ("INT", vector_name);
916919

@@ -1025,6 +1028,11 @@ TEST(Types, LexicalCastInt) {
10251028

10261029
std::string extra_input = "912i";
10271030
EXPECT_FALSE(CLI::detail::lexical_cast(extra_input, y));
1031+
1032+
std::string empty_input{};
1033+
EXPECT_FALSE(CLI::detail::lexical_cast(empty_input, x_signed));
1034+
EXPECT_FALSE(CLI::detail::lexical_cast(empty_input, x_unsigned));
1035+
EXPECT_FALSE(CLI::detail::lexical_cast(empty_input, y_signed));
10281036
}
10291037

10301038
TEST(Types, LexicalCastDouble) {
@@ -1037,10 +1045,14 @@ TEST(Types, LexicalCastDouble) {
10371045
EXPECT_FALSE(CLI::detail::lexical_cast(bad_input, x));
10381046

10391047
std::string overflow_input = "1" + std::to_string(LDBL_MAX);
1040-
EXPECT_FALSE(CLI::detail::lexical_cast(overflow_input, x));
1048+
EXPECT_TRUE(CLI::detail::lexical_cast(overflow_input, x));
1049+
EXPECT_FALSE(std::isfinite(x));
10411050

10421051
std::string extra_input = "9.12i";
10431052
EXPECT_FALSE(CLI::detail::lexical_cast(extra_input, x));
1053+
1054+
std::string empty_input{};
1055+
EXPECT_FALSE(CLI::detail::lexical_cast(empty_input, x));
10441056
}
10451057

10461058
TEST(Types, LexicalCastBool) {

tests/OptionTypeTest.cpp

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,31 @@ TEST_F(TApp, BoolOption) {
167167
EXPECT_FALSE(bflag);
168168
}
169169

170+
TEST_F(TApp, CharOption) {
171+
char c1{'t'};
172+
app.add_option("-c", c1);
173+
174+
args = {"-c", "g"};
175+
run();
176+
EXPECT_EQ(c1, 'g');
177+
178+
args = {"-c", "1"};
179+
run();
180+
EXPECT_EQ(c1, '1');
181+
182+
args = {"-c", "77"};
183+
run();
184+
EXPECT_EQ(c1, 77);
185+
186+
// convert hex for digit
187+
args = {"-c", "0x44"};
188+
run();
189+
EXPECT_EQ(c1, 0x44);
190+
191+
args = {"-c", "751615654161688126132138844896646748852"};
192+
EXPECT_THROW(run(), CLI::ConversionError);
193+
}
194+
170195
TEST_F(TApp, vectorDefaults) {
171196
std::vector<int> vals{4, 5};
172197
auto opt = app.add_option("--long", vals, "", true);

tests/OptionalTest.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,7 @@ TEST_F(TApp, BoostOptionalEnumTest) {
218218

219219
enum class eval : char { val0 = 0, val1 = 1, val2 = 2, val3 = 3, val4 = 4 };
220220
boost::optional<eval> opt, opt2;
221+
221222
auto optptr = app.add_option<decltype(opt), eval>("-v,--val", opt);
222223
app.add_option_no_stream("-e,--eval", opt2);
223224
optptr->capture_default_str();

tests/TransformTest.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -692,7 +692,8 @@ TEST_F(TApp, NumberWithUnitBadInput) {
692692
args = {"-n", "13 c"};
693693
EXPECT_THROW(run(), CLI::ValidationError);
694694
args = {"-n", "a"};
695-
EXPECT_THROW(run(), CLI::ValidationError);
695+
// Assume 1.0 unit
696+
EXPECT_NO_THROW(run());
696697
args = {"-n", "12.0a"};
697698
EXPECT_THROW(run(), CLI::ValidationError);
698699
args = {"-n", "a5"};

0 commit comments

Comments
 (0)