diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 78ca0b22..23ee017a 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file. - websocket and http server: server does not close the bound client socket in many cases - improve some websocket error messages - add a utility function with unittest to parse status line and stop using scanf which triggers warnings on Windows +- update ws CLI11 (our command line argument parsing library) to the latest, which fix a compiler bug about optional ## [6.1.0] - 2019-09-08 diff --git a/third_party/cli11/CLI11.hpp b/third_party/cli11/CLI11.hpp index ae58c815..5c880875 100644 --- a/third_party/cli11/CLI11.hpp +++ b/third_party/cli11/CLI11.hpp @@ -1,22 +1,22 @@ #pragma once -// CLI11: Version 1.7.1 +// CLI11: Version 1.8.0 // Originally designed by Henry Schreiner // https://github.com/CLIUtils/CLI11 // // This is a standalone header file generated by MakeSingleHeader.py in CLI11/scripts -// from: v1.7.1 +// from: v1.8.0 // // From LICENSE: // -// CLI11 1.7 Copyright (c) 2017-2019 University of Cincinnati, developed by Henry +// CLI11 1.8 Copyright (c) 2017-2019 University of Cincinnati, developed by Henry // Schreiner under NSF AWARD 1414736. All rights reserved. // // Redistribution and use in source and binary forms of CLI11, with or without // modification, are permitted provided that the following conditions are met: // // 1. Redistributions of source code must retain the above copyright notice, this -// list of conditions and the following disclaimer. +// list of conditions and the following disclaimer. // 2. Redistributions in binary form must reproduce the above copyright notice, // this list of conditions and the following disclaimer in the documentation // and/or other materials provided with the distribution. @@ -39,6 +39,7 @@ // Standard combined includes: #include +#include #include #include #include @@ -67,9 +68,9 @@ #define CLI11_VERSION_MAJOR 1 -#define CLI11_VERSION_MINOR 7 -#define CLI11_VERSION_PATCH 1 -#define CLI11_VERSION "1.7.1" +#define CLI11_VERSION_MINOR 8 +#define CLI11_VERSION_PATCH 0 +#define CLI11_VERSION "1.8.0" @@ -115,34 +116,35 @@ // Verbatim copy from CLI/Optional.hpp: -#ifdef __has_include // You can explicitly enable or disable support -// by defining these to 1 or 0. -#if defined(CLI11_CPP17) && __has_include() && \ - !defined(CLI11_STD_OPTIONAL) +// 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 -#elif !defined(CLI11_STD_OPTIONAL) +#else #define CLI11_STD_OPTIONAL 0 #endif +#else +#define CLI11_STD_OPTIONAL 0 +#endif +#endif -#if defined(CLI11_CPP14) && __has_include() && \ - !defined(CLI11_EXPERIMENTAL_OPTIONAL) \ - && (!defined(CLI11_STD_OPTIONAL) || CLI11_STD_OPTIONAL == 0) -#define CLI11_EXPERIMENTAL_OPTIONAL 1 -#elif !defined(CLI11_EXPERIMENTAL_OPTIONAL) +#ifndef CLI11_EXPERIMENTAL_OPTIONAL #define CLI11_EXPERIMENTAL_OPTIONAL 0 #endif -#if __has_include() && !defined(CLI11_BOOST_OPTIONAL) -#include -#if BOOST_VERSION >= 105800 -#define CLI11_BOOST_OPTIONAL 1 -#endif -#elif !defined(CLI11_BOOST_OPTIONAL) +#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 @@ -153,6 +155,7 @@ #endif #if CLI11_BOOST_OPTIONAL #include +#include #endif @@ -214,6 +217,31 @@ using boost::optional; // From CLI/StringTools.hpp: namespace CLI { + +/// Include the items in this namespace to get free conversion of enums to/from streams. +/// (This is available inside CLI as well, so CLI11 will use this without a using statement). +namespace enums { + +/// output streaming for enumerations +template ::value>::type> +std::ostream &operator<<(std::ostream &in, const T &item) { + // make sure this is out of the detail namespace otherwise it won't be found when needed + 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 +using namespace enums; + namespace detail { // Based on http://stackoverflow.com/questions/236129/split-a-string-in-c @@ -222,7 +250,7 @@ inline std::vector split(const std::string &s, char delim) { std::vector elems; // Check to see if empty string, give consistent result if(s.empty()) - elems.emplace_back(""); + elems.emplace_back(); else { std::stringstream ss; ss.str(s); @@ -233,15 +261,43 @@ 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 = ",") { std::ostringstream s; - size_t start = 0; - for(const auto &i : v) { - if(start++ > 0) - s << delim; - s << i; + auto beg = std::begin(v); + auto end = std::end(v); + if(beg != end) + s << *beg++; + while(beg != end) { + s << delim << *beg++; + } + return s.str(); +} + +/// Simple function to join a string from processed elements +template ::value>::type> +std::string join(const T &v, Callable func, std::string delim = ",") { + std::ostringstream s; + auto beg = std::begin(v); + auto end = std::end(v); + if(beg != end) + s << func(*beg++); + while(beg != end) { + s << delim << func(*beg++); } return s.str(); } @@ -312,19 +368,24 @@ inline std::ostream &format_help(std::ostream &out, std::string name, std::strin if(!description.empty()) { if(name.length() >= wid) out << "\n" << std::setw(static_cast(wid)) << ""; - out << description; + for(const char c : description) { + out.put(c); + if(c == '\n') { + out << std::setw(static_cast(wid)) << ""; + } + } } out << "\n"; return out; } /// Verify the first character of an option -template bool valid_first_char(T c) { return std::isalpha(c, std::locale()) || c == '_'; } +template bool valid_first_char(T c) { + return std::isalnum(c, std::locale()) || c == '_' || c == '?' || c == '@'; +} /// Verify following characters of an option -template bool valid_later_char(T c) { - return std::isalnum(c, std::locale()) || c == '_' || c == '.' || c == '-'; -} +template bool valid_later_char(T c) { return valid_first_char(c) || c == '.' || c == '-'; } /// Verify an option name inline bool valid_name_string(const std::string &str) { @@ -336,6 +397,11 @@ inline bool valid_name_string(const std::string &str) { return true; } +/// Verify that str consists of letters only +inline bool isalpha(const std::string &str) { + return std::all_of(str.begin(), str.end(), [](char c) { return std::isalpha(c, std::locale()); }); +} + /// Return a lower case version of a string inline std::string to_lower(std::string str) { std::transform(std::begin(str), std::end(str), std::begin(str), [](const std::string::value_type &x) { @@ -363,6 +429,54 @@ inline std::string find_and_replace(std::string str, std::string from, std::stri return str; } +/// check if the flag definitions has possible false flags +inline bool has_default_flag_values(const std::string &flags) { + return (flags.find_first_of("{!") != std::string::npos); +} + +inline void remove_default_flag_values(std::string &flags) { + auto loc = flags.find_first_of('{'); + while(loc != std::string::npos) { + auto finish = flags.find_first_of("},", loc + 1); + if((finish != std::string::npos) && (flags[finish] == '}')) { + flags.erase(flags.begin() + static_cast(loc), + flags.begin() + static_cast(finish) + 1); + } + loc = flags.find_first_of('{', loc + 1); + } + flags.erase(std::remove(flags.begin(), flags.end(), '!'), flags.end()); +} + +/// Check if a string is a member of a list of strings and optionally ignore case or ignore underscores +inline std::ptrdiff_t find_member(std::string name, + const std::vector names, + bool ignore_case = false, + bool ignore_underscore = false) { + auto it = std::end(names); + if(ignore_case) { + if(ignore_underscore) { + name = detail::to_lower(detail::remove_underscore(name)); + it = std::find_if(std::begin(names), std::end(names), [&name](std::string local_name) { + return detail::to_lower(detail::remove_underscore(local_name)) == name; + }); + } else { + name = detail::to_lower(name); + it = std::find_if(std::begin(names), std::end(names), [&name](std::string local_name) { + return detail::to_lower(local_name) == name; + }); + } + + } else if(ignore_underscore) { + name = detail::remove_underscore(name); + it = std::find_if(std::begin(names), std::end(names), [&name](std::string local_name) { + return detail::remove_underscore(local_name) == name; + }); + } else + it = std::find(std::begin(names), std::end(names), name); + + return (it != std::end(names)) ? (it - std::begin(names)) : (-1); +} + /// Find a trigger string and call a modify callable function that takes the current string and starting position of the /// trigger and returns the position in the string to search for the next trigger string template inline std::string find_and_modify(std::string str, std::string trigger, Callable modify) { @@ -465,6 +579,7 @@ inline std::string &add_quotes_if_needed(std::string &str) { } } // namespace detail + } // namespace CLI // From CLI/Error.hpp: @@ -670,6 +785,27 @@ class RequiredError : public ParseError { return RequiredError("Requires at least " + std::to_string(min_subcom) + " subcommands", ExitCodes::RequiredError); } + static RequiredError Option(size_t min_option, size_t max_option, size_t used, const std::string &option_list) { + if((min_option == 1) && (max_option == 1) && (used == 0)) + return RequiredError("Exactly 1 option from [" + option_list + "]"); + else if((min_option == 1) && (max_option == 1) && (used > 1)) + return RequiredError("Exactly 1 option from [" + option_list + "] is required and " + std::to_string(used) + + " were given", + ExitCodes::RequiredError); + else if((min_option == 1) && (used == 0)) + return RequiredError("At least 1 option from [" + option_list + "]"); + else if(used < min_option) + return RequiredError("Requires at least " + std::to_string(min_option) + " options used and only " + + std::to_string(used) + "were given from [" + option_list + "]", + ExitCodes::RequiredError); + else if(max_option == 1) + return RequiredError("Requires at most 1 options be given from [" + option_list + "]", + ExitCodes::RequiredError); + else + return RequiredError("Requires at most " + std::to_string(max_option) + " options be used and " + + std::to_string(used) + "were given from [" + option_list + "]", + ExitCodes::RequiredError); + } }; /// Thrown when the wrong number of arguments has been received @@ -689,6 +825,9 @@ class ArgumentMismatch : public ParseError { static ArgumentMismatch TypedAtLeast(std::string name, int num, std::string type) { return ArgumentMismatch(name + ": " + std::to_string(num) + " required " + type + " missing"); } + static ArgumentMismatch FlagOverride(std::string name) { + return ArgumentMismatch(name + " was given a disallowed flag override"); + } }; /// Thrown when a requires option is missing @@ -761,26 +900,7 @@ namespace CLI { // Type tools -/// A copy of enable_if_t from C++14, compatible with C++11. -/// -/// We could check to see if C++14 is being used, but it does not hurt to redefine this -/// (even Google does this: https://github.com/google/skia/blob/master/include/private/SkTLogic.h) -/// It is not in the std namespace anyway, so no harm done. - -template using enable_if_t = typename std::enable_if::type; - -/// Check to see if something is a vector (fail check by default) -template struct is_vector { static const bool value = false; }; - -/// Check to see if something is a vector (true if actually a vector) -template struct is_vector> { static bool const value = true; }; - -/// Check to see if something is bool (fail check by default) -template struct is_bool { static const bool value = false; }; - -/// Check to see if something is bool (true if actually a bool) -template <> struct is_bool { static bool const value = true; }; - +// Utilities for type enabling namespace detail { // Based generally on https://rmf.io/cxx11/almost-static-if /// Simple empty scoped class @@ -788,6 +908,144 @@ enum class enabler {}; /// An instance to use in EnableIf constexpr enabler dummy = {}; +} // namespace detail + +/// A copy of enable_if_t from C++14, compatible with C++11. +/// +/// We could check to see if C++14 is being used, but it does not hurt to redefine this +/// (even Google does this: https://github.com/google/skia/blob/master/include/private/SkTLogic.h) +/// It is not in the std namespace anyway, so no harm done. +template using enable_if_t = typename std::enable_if::type; + +/// A copy of std::void_t from C++17 (helper for C++11 and C++14) +template struct make_void { using type = void; }; + +/// A copy of std::void_t from C++17 - same reasoning as enable_if_t, it does not hurt to redefine +template using void_t = typename make_void::type; + +/// A copy of std::conditional_t from C++14 - same reasoning as enable_if_t, it does not hurt to redefine +template using conditional_t = typename std::conditional::type; + +/// Check to see if something is a vector (fail check by default) +template struct is_vector : std::false_type {}; + +/// Check to see if something is a vector (true if actually a vector) +template struct is_vector> : std::true_type {}; + +/// Check to see if something is bool (fail check by default) +template struct is_bool : std::false_type {}; + +/// Check to see if something is bool (true if actually a bool) +template <> struct is_bool : std::true_type {}; + +/// Check to see if something is a shared pointer +template struct is_shared_ptr : std::false_type {}; + +/// Check to see if something is a shared pointer (True if really a shared pointer) +template struct is_shared_ptr> : std::true_type {}; + +/// Check to see if something is a shared pointer (True if really a shared pointer) +template struct is_shared_ptr> : std::true_type {}; + +/// Check to see if something is copyable pointer +template struct is_copyable_ptr { + static bool const value = is_shared_ptr::value || std::is_pointer::value; +}; + +/// This can be specialized to override the type deduction for IsMember. +template struct IsMemberType { using type = T; }; + +/// The main custom type needed here is const char * should be a string. +template <> struct IsMemberType { using type = std::string; }; + +namespace detail { + +// These are utilities for IsMember + +/// Handy helper to access the element_type generically. This is not part of is_copyable_ptr because it requires that +/// pointer_traits be valid. +template struct element_type { + using type = + typename std::conditional::value, typename std::pointer_traits::element_type, T>::type; +}; + +/// Combination of the element type and value type - remove pointer (including smart pointers) and get the value_type of +/// the container +template struct element_value_type { using type = typename element_type::type::value_type; }; + +/// Adaptor for set-like structure: This just wraps a normal container in a few utilities that do almost nothing. +template struct pair_adaptor : std::false_type { + using value_type = typename T::value_type; + using first_type = typename std::remove_const::type; + using second_type = typename std::remove_const::type; + + /// Get the first value (really just the underlying value) + template static auto first(Q &&pair_value) -> decltype(std::forward(pair_value)) { + return std::forward(pair_value); + } + /// Get the second value (really just the underlying value) + template static auto second(Q &&pair_value) -> decltype(std::forward(pair_value)) { + return std::forward(pair_value); + } +}; + +/// Adaptor for map-like structure (true version, must have key_type and mapped_type). +/// This wraps a mapped container in a few utilities access it in a general way. +template +struct pair_adaptor< + T, + conditional_t, void>> + : std::true_type { + using value_type = typename T::value_type; + using first_type = typename std::remove_const::type; + using second_type = typename std::remove_const::type; + + /// Get the first value (really just the underlying value) + template static auto first(Q &&pair_value) -> decltype(std::get<0>(std::forward(pair_value))) { + return std::get<0>(std::forward(pair_value)); + } + /// Get the second value (really just the underlying value) + template static auto second(Q &&pair_value) -> decltype(std::get<1>(std::forward(pair_value))) { + return std::get<1>(std::forward(pair_value)); + } +}; + +// 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 + 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; +}; + +/// 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)) { + return 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> +std::string to_string(T &&value) { + std::stringstream stream; + stream << value; + return stream.str(); +} + +/// 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> +std::string to_string(T &&) { + return std::string{}; +} // Type name print @@ -817,9 +1075,16 @@ template ::value, detail::enabler> = detail constexpr const char *type_name() { return "VECTOR"; } +/// Print name for enumeration types +template ::value, detail::enabler> = detail::dummy> +constexpr const char *type_name() { + return "ENUM"; +} +/// Print for all other types template ::value && !std::is_integral::value && !is_vector::value, + enable_if_t::value && !std::is_integral::value && !is_vector::value && + !std::is_enum::value, detail::enabler> = detail::dummy> constexpr const char *type_name() { return "TEXT"; @@ -827,9 +1092,62 @@ constexpr const char *type_name() { // Lexical cast -/// Signed integers / enums -template ::value && std::is_signed::value), detail::enabler> = detail::dummy> +/// Convert a flag into an integer value typically binary flags +inline int64_t to_flag_value(std::string val) { + static const std::string trueString("true"); + static const std::string falseString("false"); + if(val == trueString) { + return 1; + } + if(val == falseString) { + return -1; + } + val = detail::to_lower(val); + int64_t ret; + if(val.size() == 1) { + switch(val[0]) { + case '0': + case 'f': + case 'n': + 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"); + } + return ret; + } + if(val == trueString || val == "on" || val == "yes" || val == "enable") { + ret = 1; + } else if(val == falseString || val == "off" || val == "no" || val == "disable") { + ret = -1; + } else { + ret = std::stoll(val); + } + return ret; +} + +/// Signed integers +template < + typename T, + enable_if_t::value && std::is_signed::value && !is_bool::value && !std::is_enum::value, + detail::enabler> = detail::dummy> bool lexical_cast(std::string input, T &output) { try { size_t n = 0; @@ -845,7 +1163,8 @@ bool lexical_cast(std::string input, T &output) { /// Unsigned integers template ::value && std::is_unsigned::value, detail::enabler> = detail::dummy> + enable_if_t::value && std::is_unsigned::value && !is_bool::value, 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. @@ -862,6 +1181,18 @@ bool lexical_cast(std::string input, T &output) { } } +/// Boolean values +template ::value, detail::enabler> = detail::dummy> +bool lexical_cast(std::string input, T &output) { + try { + auto out = to_flag_value(input); + output = (out > 0); + return true; + } catch(const std::invalid_argument &) { + return false; + } +} + /// Floats template ::value, detail::enabler> = detail::dummy> bool lexical_cast(std::string input, T &output) { @@ -886,10 +1217,22 @@ bool lexical_cast(std::string input, T &output) { return true; } +/// Enumerations +template ::value, 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); + if(!retval) { + return false; + } + output = static_cast(val); + return true; +} + /// Non-string parsable template ::value && !std::is_integral::value && - !std::is_assignable::value, + !std::is_assignable::value && !std::is_enum::value, detail::enabler> = detail::dummy> bool lexical_cast(std::string input, T &output) { std::istringstream is; @@ -899,6 +1242,34 @@ bool lexical_cast(std::string input, T &output) { return !is.fail() && !is.rdbuf()->in_avail(); } +/// 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 +/// "-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 ::value && std::is_unsigned::value, detail::enabler> = detail::dummy> +void sum_flag_vector(const std::vector &flags, T &output) { + int64_t count{0}; + for(auto &flag : flags) { + count += detail::to_flag_value(flag); + } + output = (count > 0) ? static_cast(count) : T{0}; +} + +/// 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 +/// "-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 ::value && std::is_signed::value, detail::enabler> = detail::dummy> +void sum_flag_vector(const std::vector &flags, T &output) { + int64_t count{0}; + for(auto &flag : flags) { + count += detail::to_flag_value(flag); + } + output = static_cast(count); +} + } // namespace detail } // namespace CLI @@ -934,7 +1305,7 @@ inline bool split_long(const std::string ¤t, std::string &name, std::strin } // Returns false if not a windows style option. Otherwise, sets opt name and value and returns true -inline bool split_windows(const std::string ¤t, std::string &name, std::string &value) { +inline bool split_windows_style(const std::string ¤t, std::string &name, std::string &value) { if(current.size() > 1 && current[0] == '/' && valid_first_char(current[1])) { auto loc = current.find_first_of(':'); if(loc != std::string::npos) { @@ -961,6 +1332,33 @@ inline std::vector split_names(std::string current) { return output; } +/// extract default flag values either {def} or starting with a ! +inline std::vector> get_default_flag_values(const std::string &str) { + std::vector flags = split_names(str); + flags.erase(std::remove_if(flags.begin(), + flags.end(), + [](const std::string &name) { + return ((name.empty()) || (!(((name.find_first_of('{') != std::string::npos) && + (name.back() == '}')) || + (name[0] == '!')))); + }), + flags.end()); + std::vector> output; + output.reserve(flags.size()); + for(auto &flag : flags) { + auto def_start = flag.find_first_of('{'); + std::string defval = "false"; + if((def_start != std::string::npos) && (flag.back() == '}')) { + defval = flag.substr(def_start + 1); + defval.pop_back(); + flag.erase(def_start, std::string::npos); + } + flag.erase(0, flag.find_first_not_of("-!")); + output.emplace_back(flag, defval); + } + return output; +} + /// Get a vector of short names, one of long names, and a single name inline std::tuple, std::vector, std::string> get_names(const std::vector &input) { @@ -1018,10 +1416,10 @@ inline std::string ini_join(std::vector args) { auto it = std::find_if(arg.begin(), arg.end(), [](char ch) { return std::isspace(ch, std::locale()); }); if(it == arg.end()) s << arg; - else if(arg.find(R"(")") == std::string::npos) - s << R"(")" << arg << R"(")"; + else if(arg.find_first_of('\"') == std::string::npos) + s << '\"' << arg << '\"'; else - s << R"(')" << arg << R"(')"; + s << '\'' << arg << '\''; } return s.str(); @@ -1060,27 +1458,12 @@ class Config { /// Convert a configuration into an app virtual std::vector from_config(std::istream &) const = 0; - /// Convert a flag to a bool - virtual std::vector to_flag(const ConfigItem &item) const { + /// Get a flag value + virtual std::string to_flag(const ConfigItem &item) const { if(item.inputs.size() == 1) { - std::string val = item.inputs.at(0); - val = detail::to_lower(val); - - if(val == "true" || val == "on" || val == "yes") { - return std::vector(1); - } else if(val == "false" || val == "off" || val == "no") { - return std::vector(); - } else { - try { - size_t ui = std::stoul(val); - return std::vector(ui); - } catch(const std::invalid_argument &) { - throw ConversionError::TrueFalse(item.fullname()); - } - } - } else { - throw ConversionError::TooManyInputsFlag(item.fullname()); + return item.inputs.at(0); } + throw ConversionError::TooManyInputsFlag(item.fullname()); } /// Parse a config file, throw an error (ParseError:ConfigParseError or FileError) on failure @@ -1092,7 +1475,7 @@ class Config { return from_config(input); } - /// virtual destructor + /// Virtual destructor virtual ~Config() = default; }; @@ -1153,6 +1536,8 @@ class ConfigINI : public Config { namespace CLI { +class Option; + /// @defgroup validator_group Validators /// @brief Some validators that are provided @@ -1163,69 +1548,193 @@ namespace CLI { /// @{ /// -struct Validator { - /// This is the type name, if empty the type name will not be changed - std::string tname; +class Validator { + protected: + /// This is the description function, if empty the description_ will be used + std::function desc_function_{[]() { return std::string{}; }}; /// This it the base function that is to be called. /// Returns a string error message if validation fails. - std::function func; + std::function func_{[](std::string &) { return std::string{}; }}; + /// The name for search purposes of the Validator + std::string name_; + /// Enable for Validator to allow it to be disabled if need be + bool active_{true}; + /// specify that a validator should not modify the input + bool non_modifying_{false}; - /// This is the required operator for a validator - provided to help + public: + Validator() = default; + /// Construct a Validator with just the description string + explicit Validator(std::string validator_desc) : desc_function_([validator_desc]() { return validator_desc; }) {} + // Construct Validator from basic information + Validator(std::function op, std::string validator_desc, std::string validator_name = "") + : desc_function_([validator_desc]() { return validator_desc; }), func_(std::move(op)), + name_(std::move(validator_name)) {} + /// Set the Validator operation function + Validator &operation(std::function op) { + func_ = std::move(op); + return *this; + } + /// This is the required operator for a Validator - provided to help /// users (CLI11 uses the member `func` directly) - std::string operator()(const std::string &str) const { return func(str); }; + std::string operator()(std::string &str) const { + std::string retstring; + if(active_) { + if(non_modifying_) { + std::string value = str; + retstring = func_(value); + } else { + retstring = func_(str); + } + } + return retstring; + }; - /// Combining validators is a new validator + /// This is the required operator for a Validator - provided to help + /// users (CLI11 uses the member `func` directly) + std::string operator()(const std::string &str) const { + std::string value = str; + return (active_) ? func_(value) : std::string{}; + }; + + /// Specify the type string + Validator &description(std::string validator_desc) { + desc_function_ = [validator_desc]() { return validator_desc; }; + return *this; + } + /// Generate type description information for the Validator + std::string get_description() const { + if(active_) { + return desc_function_(); + } + return std::string{}; + } + /// Specify the type string + Validator &name(std::string validator_name) { + name_ = std::move(validator_name); + return *this; + } + /// Get the name of the Validator + const std::string &get_name() const { return name_; } + /// Specify whether the Validator is active or not + Validator &active(bool active_val = true) { + active_ = active_val; + return *this; + } + + /// Specify whether the Validator can be modifying or not + Validator &non_modifying(bool no_modify = true) { + non_modifying_ = no_modify; + return *this; + } + + /// Get a boolean if the validator is active + bool get_active() const { return active_; } + + /// Get a boolean if the validator is allowed to modify the input returns true if it can modify the input + bool get_modifying() const { return !non_modifying_; } + + /// Combining validators is a new validator. Type comes from left validator if function, otherwise only set if the + /// same. Validator operator&(const Validator &other) const { Validator newval; - newval.tname = (tname == other.tname ? tname : ""); + + newval._merge_description(*this, other, " AND "); // Give references (will make a copy in lambda function) - const std::function &f1 = func; - const std::function &f2 = other.func; + const std::function &f1 = func_; + const std::function &f2 = other.func_; - newval.func = [f1, f2](const std::string &filename) { - std::string s1 = f1(filename); - std::string s2 = f2(filename); + newval.func_ = [f1, f2](std::string &input) { + std::string s1 = f1(input); + std::string s2 = f2(input); if(!s1.empty() && !s2.empty()) - return s1 + " & " + s2; + return std::string("(") + s1 + ") AND (" + s2 + ")"; else return s1 + s2; }; + + newval.active_ = (active_ & other.active_); return newval; } - /// Combining validators is a new validator + /// Combining validators is a new validator. Type comes from left validator if function, otherwise only set if the + /// same. Validator operator|(const Validator &other) const { Validator newval; - newval.tname = (tname == other.tname ? tname : ""); + + newval._merge_description(*this, other, " OR "); // Give references (will make a copy in lambda function) - const std::function &f1 = func; - const std::function &f2 = other.func; + const std::function &f1 = func_; + const std::function &f2 = other.func_; - newval.func = [f1, f2](const std::string &filename) { - std::string s1 = f1(filename); - std::string s2 = f2(filename); + newval.func_ = [f1, f2](std::string &input) { + std::string s1 = f1(input); + std::string s2 = f2(input); if(s1.empty() || s2.empty()) return std::string(); else - return s1 + " & " + s2; + return std::string("(") + s1 + ") OR (" + s2 + ")"; }; + newval.active_ = (active_ & other.active_); return newval; } + + /// Create a validator that fails when a given validator succeeds + Validator operator!() const { + Validator newval; + const std::function &dfunc1 = desc_function_; + newval.desc_function_ = [dfunc1]() { + auto str = dfunc1(); + return (!str.empty()) ? std::string("NOT ") + str : std::string{}; + }; + // Give references (will make a copy in lambda function) + const std::function &f1 = func_; + + newval.func_ = [f1, dfunc1](std::string &test) -> std::string { + std::string s1 = f1(test); + if(s1.empty()) { + return std::string("check ") + dfunc1() + " succeeded improperly"; + } else + return std::string{}; + }; + newval.active_ = active_; + return newval; + } + + private: + void _merge_description(const Validator &val1, const Validator &val2, const std::string &merger) { + + const std::function &dfunc1 = val1.desc_function_; + const std::function &dfunc2 = val2.desc_function_; + + desc_function_ = [=]() { + std::string f1 = dfunc1(); + std::string f2 = dfunc2(); + if((f1.empty()) || (f2.empty())) { + return f1 + f2; + } + return std::string("(") + f1 + ")" + merger + "(" + f2 + ")"; + }; + } }; +/// Class wrapping some of the accessors of Validator +class CustomValidator : public Validator { + public: +}; // The implementation of the built in validators is using the Validator class; // the user is only expected to use the const (static) versions (since there's no setup). // Therefore, this is in detail. namespace detail { /// Check for an existing file (returns error message if check fails) -struct ExistingFileValidator : public Validator { - ExistingFileValidator() { - tname = "FILE"; - func = [](const std::string &filename) { +class ExistingFileValidator : public Validator { + public: + ExistingFileValidator() : Validator("FILE") { + func_ = [](std::string &filename) { struct stat buffer; bool exist = stat(filename.c_str(), &buffer) == 0; bool is_dir = (buffer.st_mode & S_IFDIR) != 0; @@ -1240,10 +1749,10 @@ struct ExistingFileValidator : public Validator { }; /// Check for an existing directory (returns error message if check fails) -struct ExistingDirectoryValidator : public Validator { - ExistingDirectoryValidator() { - tname = "DIR"; - func = [](const std::string &filename) { +class ExistingDirectoryValidator : public Validator { + public: + ExistingDirectoryValidator() : Validator("DIR") { + func_ = [](std::string &filename) { struct stat buffer; bool exist = stat(filename.c_str(), &buffer) == 0; bool is_dir = (buffer.st_mode & S_IFDIR) != 0; @@ -1258,10 +1767,10 @@ struct ExistingDirectoryValidator : public Validator { }; /// Check for an existing path -struct ExistingPathValidator : public Validator { - ExistingPathValidator() { - tname = "PATH"; - func = [](const std::string &filename) { +class ExistingPathValidator : public Validator { + public: + ExistingPathValidator() : Validator("PATH(existing)") { + func_ = [](std::string &filename) { struct stat buffer; bool const exist = stat(filename.c_str(), &buffer) == 0; if(!exist) { @@ -1273,10 +1782,10 @@ struct ExistingPathValidator : public Validator { }; /// Check for an non-existing path -struct NonexistentPathValidator : public Validator { - NonexistentPathValidator() { - tname = "PATH"; - func = [](const std::string &filename) { +class NonexistentPathValidator : public Validator { + public: + NonexistentPathValidator() : Validator("PATH(non-existing)") { + func_ = [](std::string &filename) { struct stat buffer; bool exist = stat(filename.c_str(), &buffer) == 0; if(exist) { @@ -1286,6 +1795,63 @@ struct NonexistentPathValidator : public Validator { }; } }; + +/// Validate the given string is a legal ipv4 address +class IPV4Validator : public Validator { + public: + IPV4Validator() : Validator("IPV4") { + func_ = [](std::string &ip_addr) { + auto result = CLI::detail::split(ip_addr, '.'); + if(result.size() != 4) { + return "Invalid IPV4 address must have four parts " + ip_addr; + } + int num; + bool retval = true; + for(const auto &var : result) { + retval &= detail::lexical_cast(var, num); + if(!retval) { + return "Failed parsing number " + var; + } + if(num < 0 || num > 255) { + return "Each IP number must be between 0 and 255 " + var; + } + } + return std::string(); + }; + } +}; + +/// Validate the argument is a number and greater than or equal to 0 +class PositiveNumber : public Validator { + public: + PositiveNumber() : Validator("POSITIVE") { + func_ = [](std::string &number_str) { + int number; + if(!detail::lexical_cast(number_str, number)) { + return "Failed parsing number " + number_str; + } + if(number < 0) { + return "Number less then 0 " + number_str; + } + return std::string(); + }; + } +}; + +/// Validate the argument is a number and greater than or equal to 0 +class Number : public Validator { + public: + Number() : Validator("NUMBER") { + func_ = [](std::string &number_str) { + double number; + if(!detail::lexical_cast(number_str, number)) { + return "Failed parsing as a number " + number_str; + } + return std::string(); + }; + } +}; + } // namespace detail // Static is not needed here, because global const implies static. @@ -1302,8 +1868,18 @@ const detail::ExistingPathValidator ExistingPath; /// Check for an non-existing path const detail::NonexistentPathValidator NonexistentPath; -/// Produce a range (factory). Min and max are inclusive. -struct Range : public Validator { +/// Check for an IP4 address +const detail::IPV4Validator ValidIPV4; + +/// Check for a positive number +const detail::PositiveNumber PositiveNumber; + +/// Check for a number +const detail::Number Number; + +/// Produce a range (factory). Min and max are inclusive. +class Range : public Validator { + public: /// This produces a range with min and max inclusive. /// /// Note that the constructor is templated, but the struct is not, so C++17 is not @@ -1311,12 +1887,12 @@ struct Range : public Validator { template Range(T min, T max) { std::stringstream out; out << detail::type_name() << " in [" << min << " - " << max << "]"; + description(out.str()); - tname = out.str(); - func = [min, max](std::string input) { + func_ = [min, max](std::string &input) { T val; - detail::lexical_cast(input, val); - if(val < min || val > max) + bool converted = detail::lexical_cast(input, val); + if((!converted) || (val < min || val > max)) return "Value " + input + " not in range " + std::to_string(min) + " to " + std::to_string(max); return std::string(); @@ -1327,11 +1903,566 @@ struct Range : public Validator { template explicit Range(T max) : Range(static_cast(0), max) {} }; +/// Produce a bounded range (factory). Min and max are inclusive. +class Bound : public Validator { + public: + /// This bounds a value with min and max inclusive. + /// + /// Note that the constructor is templated, but the struct is not, so C++17 is not + /// needed to provide nice syntax for Range(a,b). + template Bound(T min, T max) { + std::stringstream out; + out << detail::type_name() << " bounded to [" << min << " - " << max << "]"; + description(out.str()); + + func_ = [min, max](std::string &input) { + T val; + bool converted = detail::lexical_cast(input, val); + if(!converted) { + return "Value " + input + " could not be converted"; + } + if(val < min) + input = detail::as_string(min); + else if(val > max) + input = detail::as_string(max); + + return std::string(); + }; + } + + /// Range of one value is 0 to value + template explicit Bound(T max) : Bound(static_cast(0), max) {} +}; + namespace detail { -/// split a string into a program name and command line arguments +template ::type>::value, detail::enabler> = detail::dummy> +auto smart_deref(T value) -> decltype(*value) { + return *value; +} + +template < + typename T, + enable_if_t::type>::value, detail::enabler> = detail::dummy> +typename std::remove_reference::type &smart_deref(T &value) { + return value; +} +/// Generate a string representation of a set +template std::string generate_set(const T &set) { + using element_t = typename detail::element_type::type; + using iteration_type_t = typename detail::pair_adaptor::value_type; // the type of the object pair + std::string out(1, '{'); + out.append(detail::join(detail::smart_deref(set), + [](const iteration_type_t &v) { return detail::pair_adaptor::first(v); }, + ",")); + out.push_back('}'); + return out; +} + +/// Generate a string representation of a map +template std::string generate_map(const T &map, bool key_only = false) { + using element_t = typename detail::element_type::type; + using iteration_type_t = typename detail::pair_adaptor::value_type; // the type of the object pair + 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)); + if(!key_only) { + res += "->" + detail::as_string(detail::pair_adaptor::second(v)); + } + return res; + }, + ",")); + out.push_back('}'); + return out; +} + +template struct sfinae_true : std::true_type {}; +/// Function to check for the existence of a member find function which presumably is more efficient than looping over +/// everything +template +static auto test_find(int) -> sfinae_true().find(std::declval()))>; +template static auto test_find(long) -> std::false_type; + +template struct has_find : decltype(test_find(0)) {}; + +/// A search function +template ::value, detail::enabler> = detail::dummy> +auto search(const T &set, const V &val) -> std::pair { + using element_t = typename detail::element_type::type; + auto &setref = detail::smart_deref(set); + auto it = std::find_if(std::begin(setref), std::end(setref), [&val](decltype(*std::begin(setref)) v) { + return (detail::pair_adaptor::first(v) == val); + }); + return {(it != std::end(setref)), it}; +} + +/// A search function that uses the built in find function +template ::value, detail::enabler> = detail::dummy> +auto search(const T &set, const V &val) -> std::pair { + auto &setref = detail::smart_deref(set); + auto it = setref.find(val); + return {(it != std::end(setref)), it}; +} + +/// A search function with a filter function +template +auto search(const T &set, const V &val, const std::function &filter_function) + -> std::pair { + using element_t = typename detail::element_type::type; + // do the potentially faster first search + auto res = search(set, val); + if((res.first) || (!(filter_function))) { + return res; + } + // if we haven't found it do the longer linear search with all the element translations + auto &setref = detail::smart_deref(set); + auto it = std::find_if(std::begin(setref), std::end(setref), [&](decltype(*std::begin(setref)) v) { + V a = detail::pair_adaptor::first(v); + a = filter_function(a); + return (a == val); + }); + return {(it != std::end(setref)), it}; +} + +/// Performs a *= b; if it doesn't cause integer overflow. Returns false otherwise. +template typename std::enable_if::value, bool>::type checked_multiply(T &a, T b) { + if(a == 0 || b == 0) { + a *= b; + return true; + } + T c = a * b; + if(c / a != b) { + return false; + } + a = c; + return true; +} + +/// Performs a *= b; if it doesn't equal infinity. Returns false otherwise. +template +typename std::enable_if::value, bool>::type checked_multiply(T &a, T b) { + T c = a * b; + if(std::isinf(c) && !std::isinf(a) && !std::isinf(b)) { + return false; + } + a = c; + return true; +} + +} // namespace detail +/// Verify items are in a set +class IsMember : public Validator { + public: + using filter_fn_t = std::function; + + /// This allows in-place construction using an initializer list + template + explicit IsMember(std::initializer_list values, Args &&... args) + : IsMember(std::vector(values), std::forward(args)...) {} + + /// This checks to see if an item is in a set (empty function) + template explicit IsMember(T &&set) : IsMember(std::forward(set), nullptr) {} + + /// This checks to see if an item is in a set: pointer or copy version. You can pass in a function that will filter + /// both sides of the comparison before computing the comparison. + template explicit IsMember(T set, F filter_function) { + + // Get the type of the contained item - requires a container have ::value_type + // if the type does not have first_type and second_type, these are both value_type + using element_t = typename detail::element_type::type; // Removes (smart) pointers if needed + using item_t = typename detail::pair_adaptor::first_type; // Is value_type if not a map + + using local_item_t = typename IsMemberType::type; // This will convert bad types to good ones + // (const char * to std::string) + + // Make a local copy of the filter function, using a std::function if not one already + std::function filter_fn = filter_function; + + // This is the type name for help, it will take the current version of the set contents + desc_function_ = [set]() { return detail::generate_set(detail::smart_deref(set)); }; + + // This is the function that validates + // It stores a copy of the set pointer-like, so shared_ptr will stay alive + func_ = [set, filter_fn](std::string &input) { + local_item_t b; + if(!detail::lexical_cast(input, b)) { + throw ValidationError(input); // name is added later + } + if(filter_fn) { + b = filter_fn(b); + } + auto res = detail::search(set, b, filter_fn); + 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))); + } + + // Return empty error string (success) + return std::string{}; + } + + // If you reach this point, the result was not found + std::string out(" not in "); + out += detail::generate_set(detail::smart_deref(set)); + return out; + }; + } + + /// You can pass in as many filter functions as you like, they nest (string only currently) + template + IsMember(T &&set, filter_fn_t filter_fn_1, filter_fn_t filter_fn_2, Args &&... other) + : IsMember(std::forward(set), + [filter_fn_1, filter_fn_2](std::string a) { return filter_fn_2(filter_fn_1(a)); }, + other...) {} +}; + +/// definition of the default transformation object +template using TransformPairs = std::vector>; + +/// Translate named items to other or a value set +class Transformer : public Validator { + public: + using filter_fn_t = std::function; + + /// This allows in-place construction + template + explicit Transformer(std::initializer_list> values, Args &&... args) + : Transformer(TransformPairs(values), std::forward(args)...) {} + + /// direct map of std::string to std::string + template explicit Transformer(T &&mapping) : Transformer(std::forward(mapping), nullptr) {} + + /// This checks to see if an item is in a set: pointer or copy version. You can pass in a function that will filter + /// both sides of the comparison before computing the comparison. + template explicit Transformer(T mapping, F filter_function) { + + static_assert(detail::pair_adaptor::type>::value, + "mapping must produce value pairs"); + // Get the type of the contained item - requires a container have ::value_type + // if the type does not have first_type and second_type, these are both value_type + using element_t = typename detail::element_type::type; // Removes (smart) pointers if needed + using item_t = typename detail::pair_adaptor::first_type; // Is value_type if not a map + using local_item_t = typename IsMemberType::type; // This will convert bad types to good ones + // (const char * to std::string) + + // Make a local copy of the filter function, using a std::function if not one already + std::function filter_fn = filter_function; + + // This is the type name for help, it will take the current version of the set contents + desc_function_ = [mapping]() { return detail::generate_map(detail::smart_deref(mapping)); }; + + func_ = [mapping, filter_fn](std::string &input) { + local_item_t b; + if(!detail::lexical_cast(input, b)) { + return std::string(); + // there is no possible way we can match anything in the mapping if we can't convert so just return + } + if(filter_fn) { + b = filter_fn(b); + } + auto res = detail::search(mapping, b, filter_fn); + if(res.first) { + input = detail::as_string(detail::pair_adaptor::second(*res.second)); + } + return std::string{}; + }; + } + + /// You can pass in as many filter functions as you like, they nest + template + Transformer(T &&mapping, filter_fn_t filter_fn_1, filter_fn_t filter_fn_2, Args &&... other) + : Transformer(std::forward(mapping), + [filter_fn_1, filter_fn_2](std::string a) { return filter_fn_2(filter_fn_1(a)); }, + other...) {} +}; + +/// translate named items to other or a value set +class CheckedTransformer : public Validator { + public: + using filter_fn_t = std::function; + + /// This allows in-place construction + template + explicit CheckedTransformer(std::initializer_list> values, Args &&... args) + : CheckedTransformer(TransformPairs(values), std::forward(args)...) {} + + /// direct map of std::string to std::string + template explicit CheckedTransformer(T mapping) : CheckedTransformer(std::move(mapping), nullptr) {} + + /// This checks to see if an item is in a set: pointer or copy version. You can pass in a function that will filter + /// both sides of the comparison before computing the comparison. + template explicit CheckedTransformer(T mapping, F filter_function) { + + static_assert(detail::pair_adaptor::type>::value, + "mapping must produce value pairs"); + // Get the type of the contained item - requires a container have ::value_type + // if the type does not have first_type and second_type, these are both value_type + using element_t = typename detail::element_type::type; // Removes (smart) pointers if needed + using item_t = typename detail::pair_adaptor::first_type; // Is value_type if not a map + using local_item_t = typename IsMemberType::type; // This will convert bad types to good ones + // (const char * to std::string) + using iteration_type_t = typename detail::pair_adaptor::value_type; // the type of the object pair // + // the type of the object pair + + // Make a local copy of the filter function, using a std::function if not one already + std::function filter_fn = filter_function; + + auto tfunc = [mapping]() { + std::string out("value in "); + 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)); }, + ","); + out.push_back('}'); + return out; + }; + + desc_function_ = tfunc; + + func_ = [mapping, tfunc, filter_fn](std::string &input) { + local_item_t b; + bool converted = detail::lexical_cast(input, b); + if(converted) { + if(filter_fn) { + b = filter_fn(b); + } + auto res = detail::search(mapping, b, filter_fn); + if(res.first) { + input = detail::as_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)); + if(output_string == input) { + return std::string(); + } + } + + return "Check " + input + " " + tfunc() + " FAILED"; + }; + } + + /// You can pass in as many filter functions as you like, they nest + template + CheckedTransformer(T &&mapping, filter_fn_t filter_fn_1, filter_fn_t filter_fn_2, Args &&... other) + : CheckedTransformer(std::forward(mapping), + [filter_fn_1, filter_fn_2](std::string a) { return filter_fn_2(filter_fn_1(a)); }, + other...) {} +}; + +/// Helper function to allow ignore_case to be passed to IsMember or Transform +inline std::string ignore_case(std::string item) { return detail::to_lower(item); } + +/// Helper function to allow ignore_underscore to be passed to IsMember or Transform +inline std::string ignore_underscore(std::string item) { return detail::remove_underscore(item); } + +/// Helper function to allow checks to ignore spaces to be passed to IsMember or Transform +inline std::string ignore_space(std::string item) { + item.erase(std::remove(std::begin(item), std::end(item), ' '), std::end(item)); + item.erase(std::remove(std::begin(item), std::end(item), '\t'), std::end(item)); + return item; +} + +/// Multiply a number by a factor using given mapping. +/// Can be used to write transforms for SIZE or DURATION inputs. +/// +/// Example: +/// With mapping = `{"b"->1, "kb"->1024, "mb"->1024*1024}` +/// one can recognize inputs like "100", "12kb", "100 MB", +/// that will be automatically transformed to 100, 14448, 104857600. +/// +/// Output number type matches the type in the provided mapping. +/// Therefore, if it is required to interpret real inputs like "0.42 s", +/// the mapping should be of a type or . +class AsNumberWithUnit : public Validator { + public: + /// Adjust AsNumberWithUnit behavior. + /// CASE_SENSITIVE/CASE_INSENSITIVE controls how units are matched. + /// UNIT_OPTIONAL/UNIT_REQUIRED throws ValidationError + /// if UNIT_REQUIRED is set and unit literal is not found. + enum Options { + CASE_SENSITIVE = 0, + CASE_INSENSITIVE = 1, + UNIT_OPTIONAL = 0, + UNIT_REQUIRED = 2, + DEFAULT = CASE_INSENSITIVE | UNIT_OPTIONAL + }; + + template + explicit AsNumberWithUnit(std::map mapping, + Options opts = DEFAULT, + const std::string &unit_name = "UNIT") { + description(generate_description(unit_name, opts)); + validate_mapping(mapping, opts); + + // transform function + func_ = [mapping, opts](std::string &input) -> std::string { + Number num; + + detail::rtrim(input); + if(input.empty()) { + throw ValidationError("Input is empty"); + } + + // Find split position between number and prefix + auto unit_begin = input.end(); + while(unit_begin > input.begin() && std::isalpha(*(unit_begin - 1), std::locale())) { + --unit_begin; + } + + std::string unit{unit_begin, input.end()}; + input.resize(static_cast(std::distance(input.begin(), unit_begin))); + detail::trim(input); + + if(opts & UNIT_REQUIRED && unit.empty()) { + throw ValidationError("Missing mandatory unit"); + } + if(opts & CASE_INSENSITIVE) { + unit = detail::to_lower(unit); + } + + bool converted = detail::lexical_cast(input, num); + if(!converted) { + throw ValidationError("Value " + input + " could not be converted to " + detail::type_name()); + } + + if(unit.empty()) { + // No need to modify input if no unit passed + return {}; + } + + // find corresponding factor + auto it = mapping.find(unit); + if(it == mapping.end()) { + throw ValidationError(unit + + " unit not recognized. " + "Allowed values: " + + detail::generate_map(mapping, true)); + } + + // perform safe multiplication + bool ok = detail::checked_multiply(num, it->second); + if(!ok) { + throw ValidationError(detail::as_string(num) + " multiplied by " + unit + + " factor would cause number overflow. Use smaller value."); + } + input = detail::as_string(num); + + return {}; + }; + } + + private: + /// Check that mapping contains valid units. + /// Update mapping for CASE_INSENSITIVE mode. + template static void validate_mapping(std::map &mapping, Options opts) { + for(auto &kv : mapping) { + if(kv.first.empty()) { + throw ValidationError("Unit must not be empty."); + } + if(!detail::isalpha(kv.first)) { + throw ValidationError("Unit must contain only letters."); + } + } + + // make all units lowercase if CASE_INSENSITIVE + if(opts & CASE_INSENSITIVE) { + std::map lower_mapping; + for(auto &kv : mapping) { + auto s = detail::to_lower(kv.first); + if(lower_mapping.count(s)) { + throw ValidationError("Several matching lowercase unit representations are found: " + s); + } + lower_mapping[detail::to_lower(kv.first)] = kv.second; + } + mapping = std::move(lower_mapping); + } + } + + /// Generate description like this: NUMBER [UNIT] + template static std::string generate_description(const std::string &name, Options opts) { + std::stringstream out; + out << detail::type_name() << ' '; + if(opts & UNIT_REQUIRED) { + out << name; + } else { + out << '[' << name << ']'; + } + return out.str(); + } +}; + +/// Converts a human-readable size string (with unit literal) to uin64_t size. +/// Example: +/// "100" => 100 +/// "1 b" => 100 +/// "10Kb" => 10240 // you can configure this to be interpreted as kilobyte (*1000) or kibibyte (*1024) +/// "10 KB" => 10240 +/// "10 kb" => 10240 +/// "10 kib" => 10240 // *i, *ib are always interpreted as *bibyte (*1024) +/// "10kb" => 10240 +/// "2 MB" => 2097152 +/// "2 EiB" => 2^61 // Units up to exibyte are supported +class AsSizeValue : public AsNumberWithUnit { + public: + using result_t = uint64_t; + + /// If kb_is_1000 is true, + /// interpret 'kb', 'k' as 1000 and 'kib', 'ki' as 1024 + /// (same applies to higher order units as well). + /// Otherwise, interpret all literals as factors of 1024. + /// The first option is formally correct, but + /// the second interpretation is more wide-spread + /// (see https://en.wikipedia.org/wiki/Binary_prefix). + explicit AsSizeValue(bool kb_is_1000) : AsNumberWithUnit(get_mapping(kb_is_1000)) { + if(kb_is_1000) { + description("SIZE [b, kb(=1000b), kib(=1024b), ...]"); + } else { + description("SIZE [b, kb(=1024b), ...]"); + } + } + + private: + /// Get mapping + static std::map init_mapping(bool kb_is_1000) { + std::map m; + result_t k_factor = kb_is_1000 ? 1000 : 1024; + result_t ki_factor = 1024; + result_t k = 1; + result_t ki = 1; + m["b"] = 1; + for(std::string p : {"k", "m", "g", "t", "p", "e"}) { + k *= k_factor; + ki *= ki_factor; + m[p] = k; + m[p + "b"] = k; + m[p + "i"] = ki; + m[p + "ib"] = ki; + } + return m; + } + + /// Cache calculated mapping + static std::map get_mapping(bool kb_is_1000) { + if(kb_is_1000) { + static auto m = init_mapping(true); + return m; + } else { + static auto m = init_mapping(false); + return m; + } + } +}; + +namespace detail { +/// Split a string into a program name and command line arguments /// the string is assumed to contain a file name followed by other arguments -/// the return value contains is a pair with the first argument containing the program name and the second everything -/// else +/// the return value contains is a pair with the first argument containing the program name and the second +/// everything else. inline std::pair split_program_name(std::string commandline) { // try to determine the programName std::pair vals; @@ -1353,6 +2484,7 @@ inline std::pair split_program_name(std::string comman ltrim(vals.second); return vals; } + } // namespace detail /// @} @@ -1400,7 +2532,9 @@ class FormatterBase { FormatterBase() = default; FormatterBase(const FormatterBase &) = default; FormatterBase(FormatterBase &&) = default; - virtual ~FormatterBase() = default; + + /// Adding a destructor in this form to work around bug in GCC 4.7 + virtual ~FormatterBase() noexcept {} // NOLINT(modernize-use-equals-default) /// This is the key method that puts together help virtual std::string make_help(const App *, std::string, AppFormatMode) const = 0; @@ -1444,6 +2578,9 @@ class FormatterLambda final : public FormatterBase { /// Create a FormatterLambda with a lambda function explicit FormatterLambda(funct_t funct) : lambda_(std::move(funct)) {} + /// Adding a destructor (mostly to make GCC 4.7 happy) + ~FormatterLambda() noexcept override {} // NOLINT(modernize-use-equals-default) + /// This will simply call the lambda function std::string make_help(const App *app, std::string name, AppFormatMode mode) const override { return lambda_(app, name, mode); @@ -1533,7 +2670,7 @@ class App; using Option_p = std::unique_ptr