1
0
mirror of https://github.com/CLIUtils/CLI11.git synced 2025-04-30 04:33:53 +00:00
CLI11/include/CLI/impl/Option_inl.hpp
Philip Top 91220babfc
regular and literal strings (#964)
Add escaping to quoted strings, differentiate between literal and
regular strings.

The goal is to make string processing in config files as close as
possible to toml standards. This means handing escape sequences
including unicode, and differentiating between literal strings and
regular strings in files and when splitting the command line. Also
allowing variable names in the files to be quoted.

This PR gets partway there. Removes some hacks from the previous PR to
deal with unusual option names and replaces with the quoted names.

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-12-30 06:54:41 -08:00

689 lines
24 KiB
C++

// Copyright (c) 2017-2023, University of Cincinnati, developed by Henry Schreiner
// under NSF AWARD 1414736 and by the respective contributors.
// All rights reserved.
//
// SPDX-License-Identifier: BSD-3-Clause
#pragma once
// This include is only needed for IDEs to discover symbols
#include <CLI/Option.hpp>
// [CLI11:public_includes:set]
#include <algorithm>
#include <string>
#include <utility>
#include <vector>
// [CLI11:public_includes:end]
namespace CLI {
// [CLI11:option_inl_hpp:verbatim]
template <typename CRTP> template <typename T> void OptionBase<CRTP>::copy_to(T *other) const {
other->group(group_);
other->required(required_);
other->ignore_case(ignore_case_);
other->ignore_underscore(ignore_underscore_);
other->configurable(configurable_);
other->disable_flag_override(disable_flag_override_);
other->delimiter(delimiter_);
other->always_capture_default(always_capture_default_);
other->multi_option_policy(multi_option_policy_);
}
CLI11_INLINE Option *Option::expected(int value) {
if(value < 0) {
expected_min_ = -value;
if(expected_max_ < expected_min_) {
expected_max_ = expected_min_;
}
allow_extra_args_ = true;
flag_like_ = false;
} else if(value == detail::expected_max_vector_size) {
expected_min_ = 1;
expected_max_ = detail::expected_max_vector_size;
allow_extra_args_ = true;
flag_like_ = false;
} else {
expected_min_ = value;
expected_max_ = value;
flag_like_ = (expected_min_ == 0);
}
return this;
}
CLI11_INLINE Option *Option::expected(int value_min, int value_max) {
if(value_min < 0) {
value_min = -value_min;
}
if(value_max < 0) {
value_max = detail::expected_max_vector_size;
}
if(value_max < value_min) {
expected_min_ = value_max;
expected_max_ = value_min;
} else {
expected_max_ = value_max;
expected_min_ = value_min;
}
return this;
}
CLI11_INLINE Option *Option::check(Validator validator, const std::string &validator_name) {
validator.non_modifying();
validators_.push_back(std::move(validator));
if(!validator_name.empty())
validators_.back().name(validator_name);
return this;
}
CLI11_INLINE Option *Option::check(std::function<std::string(const std::string &)> Validator,
std::string Validator_description,
std::string Validator_name) {
validators_.emplace_back(Validator, std::move(Validator_description), std::move(Validator_name));
validators_.back().non_modifying();
return this;
}
CLI11_INLINE Option *Option::transform(Validator Validator, const std::string &Validator_name) {
validators_.insert(validators_.begin(), std::move(Validator));
if(!Validator_name.empty())
validators_.front().name(Validator_name);
return this;
}
CLI11_INLINE Option *Option::transform(const std::function<std::string(std::string)> &func,
std::string transform_description,
std::string transform_name) {
validators_.insert(validators_.begin(),
Validator(
[func](std::string &val) {
val = func(val);
return std::string{};
},
std::move(transform_description),
std::move(transform_name)));
return this;
}
CLI11_INLINE Option *Option::each(const std::function<void(std::string)> &func) {
validators_.emplace_back(
[func](std::string &inout) {
func(inout);
return std::string{};
},
std::string{});
return this;
}
CLI11_INLINE Validator *Option::get_validator(const std::string &Validator_name) {
for(auto &Validator : validators_) {
if(Validator_name == Validator.get_name()) {
return &Validator;
}
}
if((Validator_name.empty()) && (!validators_.empty())) {
return &(validators_.front());
}
throw OptionNotFound(std::string{"Validator "} + Validator_name + " Not Found");
}
CLI11_INLINE Validator *Option::get_validator(int index) {
// This is an signed int so that it is not equivalent to a pointer.
if(index >= 0 && index < static_cast<int>(validators_.size())) {
return &(validators_[static_cast<decltype(validators_)::size_type>(index)]);
}
throw OptionNotFound("Validator index is not valid");
}
CLI11_INLINE bool Option::remove_needs(Option *opt) {
auto iterator = std::find(std::begin(needs_), std::end(needs_), opt);
if(iterator == std::end(needs_)) {
return false;
}
needs_.erase(iterator);
return true;
}
CLI11_INLINE Option *Option::excludes(Option *opt) {
if(opt == this) {
throw(IncorrectConstruction("and option cannot exclude itself"));
}
excludes_.insert(opt);
// Help text should be symmetric - excluding a should exclude b
opt->excludes_.insert(this);
// Ignoring the insert return value, excluding twice is now allowed.
// (Mostly to allow both directions to be excluded by user, even though the library does it for you.)
return this;
}
CLI11_INLINE bool Option::remove_excludes(Option *opt) {
auto iterator = std::find(std::begin(excludes_), std::end(excludes_), opt);
if(iterator == std::end(excludes_)) {
return false;
}
excludes_.erase(iterator);
return true;
}
template <typename T> Option *Option::ignore_case(bool value) {
if(!ignore_case_ && value) {
ignore_case_ = value;
auto *parent = static_cast<T *>(parent_);
for(const Option_p &opt : parent->options_) {
if(opt.get() == this) {
continue;
}
const auto &omatch = opt->matching_name(*this);
if(!omatch.empty()) {
ignore_case_ = false;
throw OptionAlreadyAdded("adding ignore case caused a name conflict with " + omatch);
}
}
} else {
ignore_case_ = value;
}
return this;
}
template <typename T> Option *Option::ignore_underscore(bool value) {
if(!ignore_underscore_ && value) {
ignore_underscore_ = value;
auto *parent = static_cast<T *>(parent_);
for(const Option_p &opt : parent->options_) {
if(opt.get() == this) {
continue;
}
const auto &omatch = opt->matching_name(*this);
if(!omatch.empty()) {
ignore_underscore_ = false;
throw OptionAlreadyAdded("adding ignore underscore caused a name conflict with " + omatch);
}
}
} else {
ignore_underscore_ = value;
}
return this;
}
CLI11_INLINE Option *Option::multi_option_policy(MultiOptionPolicy value) {
if(value != multi_option_policy_) {
if(multi_option_policy_ == MultiOptionPolicy::Throw && expected_max_ == detail::expected_max_vector_size &&
expected_min_ > 1) { // this bizarre condition is to maintain backwards compatibility
// with the previous behavior of expected_ with vectors
expected_max_ = expected_min_;
}
multi_option_policy_ = value;
current_option_state_ = option_state::parsing;
}
return this;
}
CLI11_NODISCARD CLI11_INLINE std::string Option::get_name(bool positional, bool all_options) const {
if(get_group().empty())
return {}; // Hidden
if(all_options) {
std::vector<std::string> name_list;
/// The all list will never include a positional unless asked or that's the only name.
if((positional && (!pname_.empty())) || (snames_.empty() && lnames_.empty())) {
name_list.push_back(pname_);
}
if((get_items_expected() == 0) && (!fnames_.empty())) {
for(const std::string &sname : snames_) {
name_list.push_back("-" + sname);
if(check_fname(sname)) {
name_list.back() += "{" + get_flag_value(sname, "") + "}";
}
}
for(const std::string &lname : lnames_) {
name_list.push_back("--" + lname);
if(check_fname(lname)) {
name_list.back() += "{" + get_flag_value(lname, "") + "}";
}
}
} else {
for(const std::string &sname : snames_)
name_list.push_back("-" + sname);
for(const std::string &lname : lnames_)
name_list.push_back("--" + lname);
}
return detail::join(name_list);
}
// This returns the positional name no matter what
if(positional)
return pname_;
// Prefer long name
if(!lnames_.empty())
return std::string(2, '-') + lnames_[0];
// Or short name if no long name
if(!snames_.empty())
return std::string(1, '-') + snames_[0];
// If positional is the only name, it's okay to use that
return pname_;
}
CLI11_INLINE void Option::run_callback() {
if(force_callback_ && results_.empty()) {
add_result(default_str_);
}
if(current_option_state_ == option_state::parsing) {
_validate_results(results_);
current_option_state_ = option_state::validated;
}
if(current_option_state_ < option_state::reduced) {
_reduce_results(proc_results_, results_);
current_option_state_ = option_state::reduced;
}
if(current_option_state_ >= option_state::reduced) {
current_option_state_ = option_state::callback_run;
if(!(callback_)) {
return;
}
const results_t &send_results = proc_results_.empty() ? results_ : proc_results_;
bool local_result = callback_(send_results);
if(!local_result)
throw ConversionError(get_name(), results_);
}
}
CLI11_NODISCARD CLI11_INLINE const std::string &Option::matching_name(const Option &other) const {
static const std::string estring;
for(const std::string &sname : snames_) {
if(other.check_sname(sname))
return sname;
if(other.check_lname(sname))
return sname;
}
for(const std::string &lname : lnames_) {
if(other.check_lname(lname))
return lname;
if(lname.size() == 1) {
if(other.check_sname(lname)) {
return lname;
}
}
}
if(snames_.empty() && lnames_.empty() && !pname_.empty()) {
if(other.check_sname(pname_) || other.check_lname(pname_) || pname_ == other.pname_)
return pname_;
}
if(other.snames_.empty() && other.fnames_.empty() && !other.pname_.empty()) {
if(check_sname(other.pname_) || check_lname(other.pname_) || (pname_ == other.pname_))
return other.pname_;
}
if(ignore_case_ ||
ignore_underscore_) { // We need to do the inverse, in case we are ignore_case or ignore underscore
for(const std::string &sname : other.snames_)
if(check_sname(sname))
return sname;
for(const std::string &lname : other.lnames_)
if(check_lname(lname))
return lname;
}
return estring;
}
CLI11_NODISCARD CLI11_INLINE bool Option::check_name(const std::string &name) const {
if(name.length() > 2 && name[0] == '-' && name[1] == '-')
return check_lname(name.substr(2));
if(name.length() > 1 && name.front() == '-')
return check_sname(name.substr(1));
if(!pname_.empty()) {
std::string local_pname = pname_;
std::string local_name = name;
if(ignore_underscore_) {
local_pname = detail::remove_underscore(local_pname);
local_name = detail::remove_underscore(local_name);
}
if(ignore_case_) {
local_pname = detail::to_lower(local_pname);
local_name = detail::to_lower(local_name);
}
if(local_name == local_pname) {
return true;
}
}
if(!envname_.empty()) {
// this needs to be the original since envname_ shouldn't match on case insensitivity
return (name == envname_);
}
return false;
}
CLI11_NODISCARD CLI11_INLINE std::string Option::get_flag_value(const std::string &name,
std::string input_value) const {
static const std::string trueString{"true"};
static const std::string falseString{"false"};
static const std::string emptyString{"{}"};
// check for disable flag override_
if(disable_flag_override_) {
if(!((input_value.empty()) || (input_value == emptyString))) {
auto default_ind = detail::find_member(name, fnames_, ignore_case_, ignore_underscore_);
if(default_ind >= 0) {
// We can static cast this to std::size_t because it is more than 0 in this block
if(default_flag_values_[static_cast<std::size_t>(default_ind)].second != input_value) {
if(input_value == default_str_ && force_callback_) {
return input_value;
}
throw(ArgumentMismatch::FlagOverride(name));
}
} else {
if(input_value != trueString) {
throw(ArgumentMismatch::FlagOverride(name));
}
}
}
}
auto ind = detail::find_member(name, fnames_, ignore_case_, ignore_underscore_);
if((input_value.empty()) || (input_value == emptyString)) {
if(flag_like_) {
return (ind < 0) ? trueString : default_flag_values_[static_cast<std::size_t>(ind)].second;
}
return (ind < 0) ? default_str_ : default_flag_values_[static_cast<std::size_t>(ind)].second;
}
if(ind < 0) {
return input_value;
}
if(default_flag_values_[static_cast<std::size_t>(ind)].second == falseString) {
errno = 0;
auto val = detail::to_flag_value(input_value);
if(errno != 0) {
errno = 0;
return input_value;
}
return (val == 1) ? falseString : (val == (-1) ? trueString : std::to_string(-val));
}
return input_value;
}
CLI11_INLINE Option *Option::add_result(std::string s) {
_add_result(std::move(s), results_);
current_option_state_ = option_state::parsing;
return this;
}
CLI11_INLINE Option *Option::add_result(std::string s, int &results_added) {
results_added = _add_result(std::move(s), results_);
current_option_state_ = option_state::parsing;
return this;
}
CLI11_INLINE Option *Option::add_result(std::vector<std::string> s) {
current_option_state_ = option_state::parsing;
for(auto &str : s) {
_add_result(std::move(str), results_);
}
return this;
}
CLI11_NODISCARD CLI11_INLINE results_t Option::reduced_results() const {
results_t res = proc_results_.empty() ? results_ : proc_results_;
if(current_option_state_ < option_state::reduced) {
if(current_option_state_ == option_state::parsing) {
res = results_;
_validate_results(res);
}
if(!res.empty()) {
results_t extra;
_reduce_results(extra, res);
if(!extra.empty()) {
res = std::move(extra);
}
}
}
return res;
}
CLI11_INLINE Option *Option::type_size(int option_type_size) {
if(option_type_size < 0) {
// this section is included for backwards compatibility
type_size_max_ = -option_type_size;
type_size_min_ = -option_type_size;
expected_max_ = detail::expected_max_vector_size;
} else {
type_size_max_ = option_type_size;
if(type_size_max_ < detail::expected_max_vector_size) {
type_size_min_ = option_type_size;
} else {
inject_separator_ = true;
}
if(type_size_max_ == 0)
required_ = false;
}
return this;
}
CLI11_INLINE Option *Option::type_size(int option_type_size_min, int option_type_size_max) {
if(option_type_size_min < 0 || option_type_size_max < 0) {
// this section is included for backwards compatibility
expected_max_ = detail::expected_max_vector_size;
option_type_size_min = (std::abs)(option_type_size_min);
option_type_size_max = (std::abs)(option_type_size_max);
}
if(option_type_size_min > option_type_size_max) {
type_size_max_ = option_type_size_min;
type_size_min_ = option_type_size_max;
} else {
type_size_min_ = option_type_size_min;
type_size_max_ = option_type_size_max;
}
if(type_size_max_ == 0) {
required_ = false;
}
if(type_size_max_ >= detail::expected_max_vector_size) {
inject_separator_ = true;
}
return this;
}
CLI11_NODISCARD CLI11_INLINE std::string Option::get_type_name() const {
std::string full_type_name = type_name_();
if(!validators_.empty()) {
for(const auto &Validator : validators_) {
std::string vtype = Validator.get_description();
if(!vtype.empty()) {
full_type_name += ":" + vtype;
}
}
}
return full_type_name;
}
CLI11_INLINE void Option::_validate_results(results_t &res) const {
// Run the Validators (can change the string)
if(!validators_.empty()) {
if(type_size_max_ > 1) { // in this context index refers to the index in the type
int index = 0;
if(get_items_expected_max() < static_cast<int>(res.size()) &&
(multi_option_policy_ == CLI::MultiOptionPolicy::TakeLast ||
multi_option_policy_ == CLI::MultiOptionPolicy::Reverse)) {
// create a negative index for the earliest ones
index = get_items_expected_max() - static_cast<int>(res.size());
}
for(std::string &result : res) {
if(detail::is_separator(result) && type_size_max_ != type_size_min_ && index >= 0) {
index = 0; // reset index for variable size chunks
continue;
}
auto err_msg = _validate(result, (index >= 0) ? (index % type_size_max_) : index);
if(!err_msg.empty())
throw ValidationError(get_name(), err_msg);
++index;
}
} else {
int index = 0;
if(expected_max_ < static_cast<int>(res.size()) &&
(multi_option_policy_ == CLI::MultiOptionPolicy::TakeLast ||
multi_option_policy_ == CLI::MultiOptionPolicy::Reverse)) {
// create a negative index for the earliest ones
index = expected_max_ - static_cast<int>(res.size());
}
for(std::string &result : res) {
auto err_msg = _validate(result, index);
++index;
if(!err_msg.empty())
throw ValidationError(get_name(), err_msg);
}
}
}
}
CLI11_INLINE void Option::_reduce_results(results_t &out, const results_t &original) const {
// max num items expected or length of vector, always at least 1
// Only valid for a trimming policy
out.clear();
// Operation depends on the policy setting
switch(multi_option_policy_) {
case MultiOptionPolicy::TakeAll:
break;
case MultiOptionPolicy::TakeLast: {
// Allow multi-option sizes (including 0)
std::size_t trim_size = std::min<std::size_t>(
static_cast<std::size_t>(std::max<int>(get_items_expected_max(), 1)), original.size());
if(original.size() != trim_size) {
out.assign(original.end() - static_cast<results_t::difference_type>(trim_size), original.end());
}
} break;
case MultiOptionPolicy::Reverse: {
// Allow multi-option sizes (including 0)
std::size_t trim_size = std::min<std::size_t>(
static_cast<std::size_t>(std::max<int>(get_items_expected_max(), 1)), original.size());
if(original.size() != trim_size || trim_size > 1) {
out.assign(original.end() - static_cast<results_t::difference_type>(trim_size), original.end());
}
std::reverse(out.begin(), out.end());
} break;
case MultiOptionPolicy::TakeFirst: {
std::size_t trim_size = std::min<std::size_t>(
static_cast<std::size_t>(std::max<int>(get_items_expected_max(), 1)), original.size());
if(original.size() != trim_size) {
out.assign(original.begin(), original.begin() + static_cast<results_t::difference_type>(trim_size));
}
} break;
case MultiOptionPolicy::Join:
if(results_.size() > 1) {
out.push_back(detail::join(original, std::string(1, (delimiter_ == '\0') ? '\n' : delimiter_)));
}
break;
case MultiOptionPolicy::Sum:
out.push_back(detail::sum_string_vector(original));
break;
case MultiOptionPolicy::Throw:
default: {
auto num_min = static_cast<std::size_t>(get_items_expected_min());
auto num_max = static_cast<std::size_t>(get_items_expected_max());
if(num_min == 0) {
num_min = 1;
}
if(num_max == 0) {
num_max = 1;
}
if(original.size() < num_min) {
throw ArgumentMismatch::AtLeast(get_name(), static_cast<int>(num_min), original.size());
}
if(original.size() > num_max) {
if(original.size() == 2 && num_max == 1 && original[1] == "%%" && original[0] == "{}") {
// this condition is a trap for the following empty indicator check on config files
out = original;
} else {
throw ArgumentMismatch::AtMost(get_name(), static_cast<int>(num_max), original.size());
}
}
break;
}
}
// this check is to allow an empty vector in certain circumstances but not if expected is not zero.
// {} is the indicator for an empty container
if(out.empty()) {
if(original.size() == 1 && original[0] == "{}" && get_items_expected_min() > 0) {
out.push_back("{}");
out.push_back("%%");
}
} else if(out.size() == 1 && out[0] == "{}" && get_items_expected_min() > 0) {
out.push_back("%%");
}
}
CLI11_INLINE std::string Option::_validate(std::string &result, int index) const {
std::string err_msg;
if(result.empty() && expected_min_ == 0) {
// an empty with nothing expected is allowed
return err_msg;
}
for(const auto &vali : validators_) {
auto v = vali.get_application_index();
if(v == -1 || v == index) {
try {
err_msg = vali(result);
} catch(const ValidationError &err) {
err_msg = err.what();
}
if(!err_msg.empty())
break;
}
}
return err_msg;
}
CLI11_INLINE int Option::_add_result(std::string &&result, std::vector<std::string> &res) const {
int result_count = 0;
if(allow_extra_args_ && !result.empty() && result.front() == '[' &&
result.back() == ']') { // this is now a vector string likely from the default or user entry
result.pop_back();
for(auto &var : CLI::detail::split(result.substr(1), ',')) {
if(!var.empty()) {
result_count += _add_result(std::move(var), res);
}
}
return result_count;
}
if(delimiter_ == '\0') {
res.push_back(std::move(result));
++result_count;
} else {
if((result.find_first_of(delimiter_) != std::string::npos)) {
for(const auto &var : CLI::detail::split(result, delimiter_)) {
if(!var.empty()) {
res.push_back(var);
++result_count;
}
}
} else {
res.push_back(std::move(result));
++result_count;
}
}
return result_count;
}
// [CLI11:option_inl_hpp:end]
} // namespace CLI