Skip to content

Support for size values (640 KB) and numbers with unit in general #253

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
May 18, 2019
Merged
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ The `add_option_function<type>(...` function will typically require the template
🚧 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
app.add_flag("--flag,!--no-flag,result,"help for flag"); // 🚧
app.add_flag("--flag,!--no-flag",result,"help for flag"); // 🚧
```

specifies that if `--flag` is passed on the command line result will be true or contain a value of 1. If `--no-flag` is
Expand Down Expand Up @@ -341,6 +341,8 @@ CLI11 has several Validators built-in that perform some common checks
- `CLI::IsMember(...)`: 🚧 Require an option be a member of a given set. See [Transforming Validators](#transforming-validators) for more details.
- `CLI::Transformer(...)`: 🚧 Modify the input using a map. See [Transforming Validators](#transforming-validators) for more details.
- `CLI::CheckedTransformer(...)`: 🚧 Modify the input using a map, and require that the input is either in the set or already one of the outputs of the set. See [Transforming Validators](#transforming-validators) for more details.
- `CLI::AsNumberWithUnit(...)`: Modify the `<NUMBER> <UNIT>` pair by matching the unit and multiplying the number by the corresponding factor. It can be used as a base for transformers, that accept things like size values (`1 KB`) or durations (`0.33 ms`).
- `CLI::AsSizeValue(...)`: Convert inputs like `100b`, `42 KB`, `101 Mb`, `11 Mib` to absolute values. `KB` can be configured to be interpreted as 10^3 or 2^10.
- `CLI::ExistingFile`: Requires that the file exists if given.
- `CLI::ExistingDirectory`: Requires that the directory exists.
- `CLI::ExistingPath`: Requires that the path (file or directory) exists.
Expand Down
5 changes: 5 additions & 0 deletions include/CLI/StringTools.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,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) {
Expand Down
231 changes: 226 additions & 5 deletions include/CLI/Validators.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
#include "CLI/StringTools.hpp"
#include "CLI/TypeTools.hpp"

#include <cmath>
#include <functional>
#include <iostream>
#include <map>
#include <memory>
#include <string>

Expand Down Expand Up @@ -443,14 +445,17 @@ template <typename T> std::string generate_set(const T &set) {
}

/// Generate a string representation of a map
template <typename T> std::string generate_map(const T &map) {
template <typename T> std::string generate_map(const T &map, bool key_only = false) {
using element_t = typename detail::element_type<T>::type;
using iteration_type_t = typename detail::pair_adaptor<element_t>::value_type; // the type of the object pair
std::string out(1, '{');
out.append(detail::join(detail::smart_deref(map),
[](const iteration_type_t &v) {
return detail::as_string(detail::pair_adaptor<element_t>::first(v)) + "->" +
detail::as_string(detail::pair_adaptor<element_t>::second(v));
[key_only](const iteration_type_t &v) {
auto res = detail::as_string(detail::pair_adaptor<element_t>::first(v));
if(!key_only) {
res += "->" + detail::as_string(detail::pair_adaptor<element_t>::second(v));
}
return res;
},
","));
out.push_back('}');
Expand Down Expand Up @@ -505,6 +510,31 @@ auto search(const T &set, const V &val, const std::function<V(V)> &filter_functi
return {(it != std::end(setref)), it};
}

/// Performs a *= b; if it doesn't cause integer overflow. Returns false otherwise.
template <typename T> typename std::enable_if<std::is_integral<T>::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 T>
typename std::enable_if<std::is_floating_point<T>::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 {
Expand Down Expand Up @@ -707,7 +737,7 @@ class CheckedTransformer : public Validator {
: CheckedTransformer(std::forward<T>(mapping),
[filter_fn_1, filter_fn_2](std::string a) { return filter_fn_2(filter_fn_1(a)); },
other...) {}
}; // namespace CLI
};

/// 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); }
Expand All @@ -722,6 +752,197 @@ inline std::string ignore_space(std::string 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 <string, float> or <string, double>.
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 <typename Number>
explicit AsNumberWithUnit(std::map<std::string, Number> mapping,
Options opts = DEFAULT,
const std::string &unit_name = "UNIT") {
description(generate_description<Number>(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(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<Number>());
}

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 <typename Number> static void validate_mapping(std::map<std::string, Number> &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<std::string, Number> 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 <typename Number> static std::string generate_description(const std::string &name, Options opts) {
std::stringstream out;
out << detail::type_name<Number>() << ' ';
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 <size unit, factor> mapping
static std::map<std::string, result_t> init_mapping(bool kb_is_1000) {
std::map<std::string, result_t> 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<std::string, result_t> 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
Expand Down
Loading