Skip to content

Commit f0e4055

Browse files
phlptpVolkerChristianpre-commit-ci[bot]
authored
feat: add a reverse multi option policy (#918)
use it for the default in `set_config` and simplify and add more flexibility to the the config processing, and potentially in other options as well. The reverse policy returns a vector but in reversed order from normal. This is what we want in the config processing Inspired by #862, and updated with recent code changes. --------- Co-authored-by: Volker Christian <[email protected]> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent c071cb6 commit f0e4055

File tree

8 files changed

+196
-21
lines changed

8 files changed

+196
-21
lines changed

book/chapters/config.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ config flag. The second item is the default file name. If that is specified, the
88
config will try to read that file. The third item is the help string, with a
99
reasonable default, and the final argument is a boolean (default: false) that
1010
indicates that the configuration file is required and an error will be thrown if
11-
the file is not found and this is set to true.
11+
the file is not found and this is set to true. The option pointer returned by
12+
`set_config` is the same type as returned by `add_option` and all modifiers
13+
including validators, and checks are valid.
1214

1315
### Adding a default path
1416

@@ -98,6 +100,21 @@ If it is needed to get the configuration file name used this can be obtained via
98100
`app["--config"]->as<std::string>()` assuming `--config` was the configuration
99101
option name.
100102

103+
### Order of precedence
104+
105+
By default if multiple configuration files are given they are read in reverse
106+
order. With the last one given taking precedence over the earlier ones. This
107+
behavior can be changed through the `multi_option_policy`. For example:
108+
109+
```cpp
110+
app.set_config("--config")
111+
->multi_option_policy(CLI::MultiOptionPolicy::TakeAll);
112+
```
113+
114+
will read the files in the order given, which may be useful in some
115+
circumstances. Using `CLI::MultiOptionPolicy::TakeLast` would work similarly
116+
getting the last `N` files given.
117+
101118
## Configure file format
102119
103120
Here is an example configuration file, in

book/chapters/options.md

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ that to add option modifiers. A full listing of the option modifiers:
222222
| `->allow_extra_args()` | Allow extra argument values to be included when an option is passed. Enabled by default for vector options. |
223223
| `->disable_flag_override()` | specify that flag options cannot be overridden on the command line use `=<newval>` |
224224
| `->delimiter('<CH>')` | specify a character that can be used to separate elements in a command line argument, default is <none>, common values are ',', and ';' |
225-
| `->multi_option_policy( CLI::MultiOptionPolicy::Throw)` | Sets the policy for handling multiple arguments if the option was received on the command line several times. `Throw`ing an error is the default, but `TakeLast`, `TakeFirst`, `TakeAll`, `Join`, and `Sum` are also available. See the next four lines for shortcuts to set this more easily. |
225+
| `->multi_option_policy( CLI::MultiOptionPolicy::Throw)` | Sets the policy for handling multiple arguments if the option was received on the command line several times. `Throw`ing an error is the default, but `TakeLast`, `TakeFirst`, `TakeAll`, `Join`, `Reverse`, and `Sum` are also available. See the next four lines for shortcuts to set this more easily. |
226226
| `->take_last()` | Only use the last option if passed several times. This is always true by default for bool options, regardless of the app default, but can be set to false explicitly with `->multi_option_policy()`. |
227227
| `->take_first()` | sets `->multi_option_policy(CLI::MultiOptionPolicy::TakeFirst)` |
228228
| `->take_all()` | sets `->multi_option_policy(CLI::MultiOptionPolicy::TakeAll)` |
@@ -246,6 +246,28 @@ function of the form `bool function(std::string)` that runs on every value that
246246
the option receives, and returns a value that tells CLI11 whether the check
247247
passed or failed.
248248

249+
### Multi Option policy
250+
251+
The Multi option policy can be used to instruct CLI11 what to do when an option
252+
is called multiple times and how to return those values in a meaningful way.
253+
There are several options can be set through the
254+
`->multi_option_policy( CLI::MultiOptionPolicy::Throw)` option modifier.
255+
`Throw`ing an error is the default, but `TakeLast`, `TakeFirst`, `TakeAll`,
256+
`Join`, `Reverse`, and `Sum`
257+
258+
| Value | Description |
259+
| --------- | --------------------------------------------------------------------------------- |
260+
| Throw | Throws an error if more values are given then expected |
261+
| TakeLast | Selects the last expected number of values given |
262+
| TakeFirst | Selects the first expected number of of values given |
263+
| Join | Joins the strings together using the `delimiter` given |
264+
| TakeAll | Takes all the values |
265+
| Sum | If the values are numeric, it sums them and returns the result |
266+
| Reverse | Selects the last expected number of values given and return them in reverse order |
267+
268+
NOTE: For reverse, the index used for an indexed validator is also applied in
269+
reverse order index 1 will be the last element and 2 second from last and so on.
270+
249271
## Using the `CLI::Option` pointer
250272

251273
Each of the option creation mechanisms returns a pointer to the internally

include/CLI/App.hpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1223,6 +1223,9 @@ class App {
12231223
/// Read and process a configuration file (main app only)
12241224
void _process_config_file();
12251225

1226+
/// Read and process a particular configuration file
1227+
void _process_config_file(const std::string &config_file, bool throw_error);
1228+
12261229
/// Get envname options if not yet passed. Runs on *all* subcommands.
12271230
void _process_env();
12281231

include/CLI/Option.hpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ enum class MultiOptionPolicy : char {
4141
TakeFirst, //!< take only the first Expected number of arguments
4242
Join, //!< merge all the arguments together into a single string via the delimiter character default('\n')
4343
TakeAll, //!< just get all the passed argument regardless
44-
Sum //!< sum all the arguments together if numerical or concatenate directly without delimiter
44+
Sum, //!< sum all the arguments together if numerical or concatenate directly without delimiter
45+
Reverse, //!< take only the last Expected number of arguments in reverse order
4546
};
4647

4748
/// This is the CRTP base class for Option and OptionDefaults. It was designed this way

include/CLI/impl/App_inl.hpp

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -318,8 +318,8 @@ CLI11_INLINE Option *App::set_config(std::string option_name,
318318
config_ptr_->force_callback_ = true;
319319
}
320320
config_ptr_->configurable(false);
321-
// set the option to take the last value given by default
322-
config_ptr_->take_last();
321+
// set the option to take the last value and reverse given by default
322+
config_ptr_->multi_option_policy(MultiOptionPolicy::Reverse);
323323
}
324324

325325
return config_ptr_;
@@ -1013,6 +1013,21 @@ CLI11_NODISCARD CLI11_INLINE detail::Classifier App::_recognize(const std::strin
10131013
return detail::Classifier::NONE;
10141014
}
10151015

1016+
CLI11_INLINE void App::_process_config_file(const std::string &config_file, bool throw_error) {
1017+
auto path_result = detail::check_path(config_file.c_str());
1018+
if(path_result == detail::path_type::file) {
1019+
try {
1020+
std::vector<ConfigItem> values = config_formatter_->from_file(config_file);
1021+
_parse_config(values);
1022+
} catch(const FileError &) {
1023+
if(throw_error)
1024+
throw;
1025+
}
1026+
} else if(throw_error) {
1027+
throw FileError::Missing(config_file);
1028+
}
1029+
}
1030+
10161031
CLI11_INLINE void App::_process_config_file() {
10171032
if(config_ptr_ != nullptr) {
10181033
bool config_required = config_ptr_->get_required();
@@ -1032,20 +1047,8 @@ CLI11_INLINE void App::_process_config_file() {
10321047
}
10331048
return;
10341049
}
1035-
for(auto rit = config_files.rbegin(); rit != config_files.rend(); ++rit) {
1036-
const auto &config_file = *rit;
1037-
auto path_result = detail::check_path(config_file.c_str());
1038-
if(path_result == detail::path_type::file) {
1039-
try {
1040-
std::vector<ConfigItem> values = config_formatter_->from_file(config_file);
1041-
_parse_config(values);
1042-
} catch(const FileError &) {
1043-
if(config_required || file_given)
1044-
throw;
1045-
}
1046-
} else if(config_required || file_given) {
1047-
throw FileError::Missing(config_file);
1048-
}
1050+
for(const auto &config_file : config_files) {
1051+
_process_config_file(config_file, config_required || file_given);
10491052
}
10501053
}
10511054
}

include/CLI/impl/Option_inl.hpp

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -500,7 +500,8 @@ CLI11_INLINE void Option::_validate_results(results_t &res) const {
500500
if(type_size_max_ > 1) { // in this context index refers to the index in the type
501501
int index = 0;
502502
if(get_items_expected_max() < static_cast<int>(res.size()) &&
503-
multi_option_policy_ == CLI::MultiOptionPolicy::TakeLast) {
503+
(multi_option_policy_ == CLI::MultiOptionPolicy::TakeLast ||
504+
multi_option_policy_ == CLI::MultiOptionPolicy::Reverse)) {
504505
// create a negative index for the earliest ones
505506
index = get_items_expected_max() - static_cast<int>(res.size());
506507
}
@@ -518,7 +519,8 @@ CLI11_INLINE void Option::_validate_results(results_t &res) const {
518519
} else {
519520
int index = 0;
520521
if(expected_max_ < static_cast<int>(res.size()) &&
521-
multi_option_policy_ == CLI::MultiOptionPolicy::TakeLast) {
522+
(multi_option_policy_ == CLI::MultiOptionPolicy::TakeLast ||
523+
multi_option_policy_ == CLI::MultiOptionPolicy::Reverse)) {
522524
// create a negative index for the earliest ones
523525
index = expected_max_ - static_cast<int>(res.size());
524526
}
@@ -550,6 +552,15 @@ CLI11_INLINE void Option::_reduce_results(results_t &out, const results_t &origi
550552
out.assign(original.end() - static_cast<results_t::difference_type>(trim_size), original.end());
551553
}
552554
} break;
555+
case MultiOptionPolicy::Reverse: {
556+
// Allow multi-option sizes (including 0)
557+
std::size_t trim_size = std::min<std::size_t>(
558+
static_cast<std::size_t>(std::max<int>(get_items_expected_max(), 1)), original.size());
559+
if(original.size() != trim_size || trim_size > 1) {
560+
out.assign(original.end() - static_cast<results_t::difference_type>(trim_size), original.end());
561+
}
562+
std::reverse(out.begin(), out.end());
563+
} break;
553564
case MultiOptionPolicy::TakeFirst: {
554565
std::size_t trim_size = std::min<std::size_t>(
555566
static_cast<std::size_t>(std::max<int>(get_items_expected_max(), 1)), original.size());

tests/AppTest.cpp

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,21 @@ TEST_CASE_METHOD(TApp, "OneFlagShortValuesAs", "[app]") {
5454
auto vec = opt->as<std::vector<int>>();
5555
CHECK(1 == vec[0]);
5656
CHECK(2 == vec[1]);
57+
58+
flg->multi_option_policy(CLI::MultiOptionPolicy::Sum);
59+
vec = opt->as<std::vector<int>>();
60+
CHECK(3 == vec[0]);
61+
CHECK(vec.size() == 1);
62+
5763
flg->multi_option_policy(CLI::MultiOptionPolicy::Join);
5864
CHECK("1\n2" == opt->as<std::string>());
5965
flg->delimiter(',');
6066
CHECK("1,2" == opt->as<std::string>());
67+
flg->multi_option_policy(CLI::MultiOptionPolicy::Reverse)->expected(1, 300);
68+
vec = opt->as<std::vector<int>>();
69+
REQUIRE(vec.size() == 2U);
70+
CHECK(2 == vec[0]);
71+
CHECK(1 == vec[1]);
6172
}
6273

6374
TEST_CASE_METHOD(TApp, "OneFlagShortWindows", "[app]") {
@@ -866,6 +877,29 @@ TEST_CASE_METHOD(TApp, "SumOptString", "[app]") {
866877
CHECK("i2" == val);
867878
}
868879

880+
TEST_CASE_METHOD(TApp, "ReverseOpt", "[app]") {
881+
882+
std::vector<std::string> val;
883+
auto *opt1 = app.add_option("--val", val)->multi_option_policy(CLI::MultiOptionPolicy::Reverse);
884+
885+
args = {"--val=string1", "--val=string2", "--val", "string3", "string4"};
886+
887+
run();
888+
889+
CHECK(val.size() == 4U);
890+
891+
CHECK(val.front() == "string4");
892+
CHECK(val.back() == "string1");
893+
894+
opt1->expected(1, 2);
895+
run();
896+
CHECK(val.size() == 2U);
897+
898+
CHECK(val.front() == "string4");
899+
CHECK(val.back() == "string3");
900+
CHECK(opt1->get_multi_option_policy() == CLI::MultiOptionPolicy::Reverse);
901+
}
902+
869903
TEST_CASE_METHOD(TApp, "JoinOpt2", "[app]") {
870904

871905
std::string str;

tests/ConfigFileTest.cpp

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -744,6 +744,90 @@ TEST_CASE_METHOD(TApp, "MultiConfig", "[config]") {
744744
CHECK(one == 55);
745745
}
746746

747+
TEST_CASE_METHOD(TApp, "MultiConfig_takelast", "[config]") {
748+
749+
TempFile tmpini{"TestIniTmp.ini"};
750+
TempFile tmpini2{"TestIniTmp2.ini"};
751+
752+
app.set_config("--config")->multi_option_policy(CLI::MultiOptionPolicy::TakeLast)->expected(1, 3);
753+
754+
{
755+
std::ofstream out{tmpini};
756+
out << "[default]" << std::endl;
757+
out << "two=99" << std::endl;
758+
out << "three=3" << std::endl;
759+
}
760+
761+
{
762+
std::ofstream out{tmpini2};
763+
out << "[default]" << std::endl;
764+
out << "one=55" << std::endl;
765+
out << "three=4" << std::endl;
766+
}
767+
768+
int one{0}, two{0}, three{0};
769+
app.add_option("--one", one);
770+
app.add_option("--two", two);
771+
app.add_option("--three", three);
772+
773+
args = {"--config", tmpini, "--config", tmpini2};
774+
run();
775+
776+
CHECK(two == 99);
777+
CHECK(three == 3);
778+
CHECK(one == 55);
779+
780+
two = 0;
781+
args = {"--config", tmpini2, "--config", tmpini};
782+
run();
783+
784+
CHECK(two == 99);
785+
CHECK(three == 4);
786+
CHECK(one == 55);
787+
}
788+
789+
TEST_CASE_METHOD(TApp, "MultiConfig_takeAll", "[config]") {
790+
791+
TempFile tmpini{"TestIniTmp.ini"};
792+
TempFile tmpini2{"TestIniTmp2.ini"};
793+
794+
app.set_config("--config")->multi_option_policy(CLI::MultiOptionPolicy::TakeAll);
795+
796+
{
797+
std::ofstream out{tmpini};
798+
out << "[default]" << std::endl;
799+
out << "two=99" << std::endl;
800+
out << "three=3" << std::endl;
801+
}
802+
803+
{
804+
std::ofstream out{tmpini2};
805+
out << "[default]" << std::endl;
806+
out << "one=55" << std::endl;
807+
out << "three=4" << std::endl;
808+
}
809+
810+
int one{0}, two{0}, three{0};
811+
app.add_option("--one", one);
812+
app.add_option("--two", two);
813+
app.add_option("--three", three);
814+
815+
args = {"--config", tmpini, "--config", tmpini2};
816+
run();
817+
818+
CHECK(two == 99);
819+
CHECK(three == 3);
820+
CHECK(one == 55);
821+
822+
two = 0;
823+
args = {"--config", tmpini2, "--config", tmpini};
824+
run();
825+
826+
CHECK(two == 99);
827+
CHECK(three == 4);
828+
CHECK(one == 55);
829+
}
830+
747831
TEST_CASE_METHOD(TApp, "MultiConfig_single", "[config]") {
748832

749833
TempFile tmpini{"TestIniTmp.ini"};

0 commit comments

Comments
 (0)