Skip to content

Commit 0104dce

Browse files
VolkerChristianphlptppre-commit-ci[bot]
authored
Option callback priority v2 (#1226)
Extension allowing all possible priority combinations. Add a field callback_priority to OptionBase. --------- Co-authored-by: Philip Top <phlptp@gmail.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent a41ba81 commit 0104dce

File tree

7 files changed

+195
-26
lines changed

7 files changed

+195
-26
lines changed

README.md

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,10 @@ int main(int argc, char** argv) {
211211
}
212212
```
213213
214-
For more information about `ensure_utf8` the section on
214+
When adding options the names should not conflict with each other, if an option
215+
is added, or a modifier changed that would cause naming conflicts a run time
216+
error will be thrown in the add_option method. This includes default options for
217+
help `-h, --help`. For more information about `ensure_utf8` the section on
215218
[Unicode support](#unicode-support) below.
216219
217220
<details><summary>Note: If you don't like macros, this is what that macro expands to: (click to expand)</summary><p>
@@ -500,6 +503,34 @@ Before parsing, you can set the following options:
500503
validation checks for the option to be executed when the option value is
501504
parsed vs. at the end of all parsing. This could cause the callback to be
502505
executed multiple times. Also works with positional options.
506+
- `->callback_priority(CallbackPriority priority)`: 🚧 changes the order in
507+
which the option callback is executed. Four principal callback call-points
508+
are available. `CallbackPriority::First` executes at the very beginning of
509+
processing, before configuration files are read and environment variables are
510+
interpreted. `CallbackPriority::PreRequirementsCheck` executes after
511+
configuration and environment processing but before requirements checking.
512+
`CallbackPriority::Normal` executes after the requirements check but before
513+
any previously potentially raised exceptions are re-thrown.
514+
`CallbackPriority::Last` executes after exception handling is completed.
515+
For each position, both ordinary option callbacks and help callbacks are
516+
invoked. The relative order between them can be controlled using the
517+
corresponding `PreHelp` variants. `CallbackPriority::FirstPreHelp` executes
518+
ordinary option callbacks before help callbacks at the very beginning of
519+
processing. `CallbackPriority::PreRequirementsCheckPreHelp` executes ordinary
520+
option callbacks before help callbacks after configuration and environment
521+
processing but before requirements checking. `CallbackPriority::NormalPreHelp`
522+
executes ordinary option callbacks before help callbacks after the
523+
requirements check but before exception re-throwing.
524+
`CallbackPriority::LastPreHelp` executes ordinary option callbacks before help
525+
callbacks after exception handling has completed. When using the standard
526+
priorities (`CallbackPriority::First`,
527+
`CallbackPriority::PreRequirementsCheck`, `CallbackPriority::Normal`,
528+
`CallbackPriority::Last`), help callbacks are executed before ordinary option
529+
callbacks. By default, help callbacks use `CallbackPriority::First`, and
530+
ordinary option callbacks use `CallbackPriority::Normal`. This mechanism
531+
provides fine-grained control over when option values are set and when help or
532+
requirement checks occur, enabling precise customization of the processing
533+
sequence.
503534

504535
These options return the `Option` pointer, so you can chain them together, and
505536
even skip storing the pointer entirely. The `each` function takes any function
@@ -639,7 +670,7 @@ setting `CLI11_ENABLE_EXTRA_VALIDATORS` to 1
639670
write permission. Requires C++17.
640671
- `CLI::ExecPermission`: Requires that the file given exist and have execution
641672
permission. Requires C++17.
642-
-
673+
-
643674
644675
#### Validator Usage
645676

include/CLI/App.hpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1303,12 +1303,12 @@ class App {
13031303
void _process_env();
13041304

13051305
/// Process callbacks. Runs on *all* subcommands.
1306-
void _process_callbacks();
1306+
void _process_callbacks(CallbackPriority priority);
13071307

13081308
/// Run help flag processing if any are found.
13091309
///
13101310
/// The flags allow recursive calls to remember if there was a help flag on a parent.
1311-
void _process_help_flags(bool trigger_help = false, bool trigger_all_help = false) const;
1311+
void _process_help_flags(CallbackPriority priority, bool trigger_help = false, bool trigger_all_help = false) const;
13121312

13131313
/// Verify required options and cross requirements. Subcommands too (only if selected).
13141314
void _process_requirements();

include/CLI/Option.hpp

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,18 @@ enum class MultiOptionPolicy : char {
5050
Reverse, //!< take only the last Expected number of arguments in reverse order
5151
};
5252

53+
/// @brief enumeration for the callback priority
54+
enum class CallbackPriority : std::uint8_t {
55+
FirstPreHelp = 0,
56+
First = 1,
57+
PreRequirementsCheckPreHelp = 2,
58+
PreRequirementsCheck = 3,
59+
NormalPreHelp = 4,
60+
Normal = 5,
61+
LastPreHelp = 6,
62+
Last = 7
63+
}; // namespace CLI
64+
5365
/// This is the CRTP base class for Option and OptionDefaults. It was designed this way
5466
/// to share parts of the class; an OptionDefaults can copy to an Option.
5567
template <typename CRTP> class OptionBase {
@@ -84,6 +96,9 @@ template <typename CRTP> class OptionBase {
8496
/// Policy for handling multiple arguments beyond the expected Max
8597
MultiOptionPolicy multi_option_policy_{MultiOptionPolicy::Throw};
8698

99+
/// Priority of callback
100+
CallbackPriority callback_priority_{CallbackPriority::Normal};
101+
87102
/// Copy the contents to another similar class (one based on OptionBase)
88103
template <typename T> void copy_to(T *other) const;
89104

@@ -142,6 +157,9 @@ template <typename CRTP> class OptionBase {
142157
/// The status of the multi option policy
143158
CLI11_NODISCARD MultiOptionPolicy get_multi_option_policy() const { return multi_option_policy_; }
144159

160+
/// The priority of callback
161+
CLI11_NODISCARD CallbackPriority get_callback_priority() const { return callback_priority_; }
162+
145163
// Shortcuts for multi option policy
146164

147165
/// Set the multi option policy to take last
@@ -201,6 +219,12 @@ class OptionDefaults : public OptionBase<OptionDefaults> {
201219

202220
// Methods here need a different implementation if they are Option vs. OptionDefault
203221

222+
/// Set the callback priority
223+
OptionDefaults *callback_priority(CallbackPriority value = CallbackPriority::Normal) {
224+
callback_priority_ = value;
225+
return this;
226+
}
227+
204228
/// Take the last argument if given multiple times
205229
OptionDefaults *multi_option_policy(MultiOptionPolicy value = MultiOptionPolicy::Throw) {
206230
multi_option_policy_ = value;
@@ -343,7 +367,6 @@ class Option : public OptionBase<Option> {
343367
bool trigger_on_result_{false};
344368
/// flag indicating that the option should force the callback regardless if any results present
345369
bool force_callback_{false};
346-
///@}
347370

348371
/// Making an option by hand is not defined, it must be made by the App class
349372
Option(std::string option_name,
@@ -420,6 +443,13 @@ class Option : public OptionBase<Option> {
420443
/// Get the current value of run_callback_for_default
421444
CLI11_NODISCARD bool get_run_callback_for_default() const { return run_callback_for_default_; }
422445

446+
/// Set the value of callback priority which controls when the callback function should be called relative to other
447+
/// parsing operations the default This is controlled automatically but could be manipulated by the user.
448+
Option *callback_priority(CallbackPriority value = CallbackPriority::Normal) {
449+
callback_priority_ = value;
450+
return this;
451+
}
452+
423453
/// Adds a shared validator
424454
Option *check(Validator_p validator);
425455

include/CLI/impl/App_inl.hpp

Lines changed: 66 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,7 @@ CLI11_INLINE Option *App::set_help_flag(std::string flag_name, const std::string
285285
// Empty name will simply remove the help flag
286286
if(!flag_name.empty()) {
287287
help_ptr_ = add_flag(flag_name, help_description);
288-
help_ptr_->configurable(false);
288+
help_ptr_->configurable(false)->callback_priority(CallbackPriority::First);
289289
}
290290

291291
return help_ptr_;
@@ -301,7 +301,7 @@ CLI11_INLINE Option *App::set_help_all_flag(std::string help_name, const std::st
301301
// Empty name will simply remove the help all flag
302302
if(!help_name.empty()) {
303303
help_all_ptr_ = add_flag(help_name, help_description);
304-
help_all_ptr_->configurable(false);
304+
help_all_ptr_->configurable(false)->callback_priority(CallbackPriority::First);
305305
}
306306

307307
return help_all_ptr_;
@@ -319,7 +319,7 @@ App::set_version_flag(std::string flag_name, const std::string &versionString, c
319319
if(!flag_name.empty()) {
320320
version_ptr_ = add_flag_callback(
321321
flag_name, [versionString]() { throw(CLI::CallForVersion(versionString, 0)); }, version_help);
322-
version_ptr_->configurable(false);
322+
version_ptr_->configurable(false)->callback_priority(CallbackPriority::First);
323323
}
324324

325325
return version_ptr_;
@@ -336,7 +336,7 @@ App::set_version_flag(std::string flag_name, std::function<std::string()> vfunc,
336336
if(!flag_name.empty()) {
337337
version_ptr_ =
338338
add_flag_callback(flag_name, [vfunc]() { throw(CLI::CallForVersion(vfunc(), 0)); }, version_help);
339-
version_ptr_->configurable(false);
339+
version_ptr_->configurable(false)->callback_priority(CallbackPriority::First);
340340
}
341341

342342
return version_ptr_;
@@ -1239,43 +1239,48 @@ CLI11_INLINE void App::_process_env() {
12391239
}
12401240
}
12411241

1242-
CLI11_INLINE void App::_process_callbacks() {
1242+
CLI11_INLINE void App::_process_callbacks(CallbackPriority priority) {
12431243

12441244
for(App_p &sub : subcommands_) {
12451245
// process the priority option_groups first
12461246
if(sub->get_name().empty() && sub->parse_complete_callback_) {
12471247
if(sub->count_all() > 0) {
1248-
sub->_process_callbacks();
1249-
sub->run_callback();
1248+
sub->_process_callbacks(priority);
1249+
if(priority == CallbackPriority::Normal) {
1250+
// only run the subcommand callback at normal priority
1251+
sub->run_callback();
1252+
}
12501253
}
12511254
}
12521255
}
12531256

12541257
for(const Option_p &opt : options_) {
1255-
if((*opt) && !opt->get_callback_run()) {
1256-
opt->run_callback();
1258+
if(opt->get_callback_priority() == priority) {
1259+
if((*opt) && !opt->get_callback_run()) {
1260+
opt->run_callback();
1261+
}
12571262
}
12581263
}
12591264
for(App_p &sub : subcommands_) {
12601265
if(!sub->parse_complete_callback_) {
1261-
sub->_process_callbacks();
1266+
sub->_process_callbacks(priority);
12621267
}
12631268
}
12641269
}
12651270

1266-
CLI11_INLINE void App::_process_help_flags(bool trigger_help, bool trigger_all_help) const {
1271+
CLI11_INLINE void App::_process_help_flags(CallbackPriority priority, bool trigger_help, bool trigger_all_help) const {
12671272
const Option *help_ptr = get_help_ptr();
12681273
const Option *help_all_ptr = get_help_all_ptr();
12691274

1270-
if(help_ptr != nullptr && help_ptr->count() > 0)
1275+
if(help_ptr != nullptr && help_ptr->count() > 0 && help_ptr->get_callback_priority() == priority)
12711276
trigger_help = true;
1272-
if(help_all_ptr != nullptr && help_all_ptr->count() > 0)
1277+
if(help_all_ptr != nullptr && help_all_ptr->count() > 0 && help_all_ptr->get_callback_priority() == priority)
12731278
trigger_all_help = true;
12741279

12751280
// If there were parsed subcommands, call those. First subcommand wins if there are multiple ones.
12761281
if(!parsed_subcommands_.empty()) {
12771282
for(const App *sub : parsed_subcommands_)
1278-
sub->_process_help_flags(trigger_help, trigger_all_help);
1283+
sub->_process_help_flags(priority, trigger_help, trigger_all_help);
12791284

12801285
// Only the final subcommand should call for help. All help wins over help.
12811286
} else if(trigger_all_help) {
@@ -1416,7 +1421,10 @@ CLI11_INLINE void App::_process_requirements() {
14161421
CLI11_INLINE void App::_process() {
14171422
// help takes precedence over other potential errors and config and environment shouldn't be processed if help
14181423
// throws
1419-
_process_help_flags();
1424+
_process_callbacks(CallbackPriority::FirstPreHelp);
1425+
_process_help_flags(CallbackPriority::First);
1426+
_process_callbacks(CallbackPriority::First);
1427+
14201428
std::exception_ptr config_exception;
14211429
try {
14221430
// the config file might generate a FileError but that should not be processed until later in the process
@@ -1430,13 +1438,23 @@ CLI11_INLINE void App::_process() {
14301438
}
14311439
// callbacks and requirements processing can generate exceptions which should take priority
14321440
// over the config file error if one exists.
1441+
_process_callbacks(CallbackPriority::PreRequirementsCheckPreHelp);
1442+
_process_help_flags(CallbackPriority::PreRequirementsCheck);
1443+
_process_callbacks(CallbackPriority::PreRequirementsCheck);
1444+
14331445
_process_requirements();
14341446

1435-
_process_callbacks();
1447+
_process_callbacks(CallbackPriority::NormalPreHelp);
1448+
_process_help_flags(CallbackPriority::Normal);
1449+
_process_callbacks(CallbackPriority::Normal);
14361450

14371451
if(config_exception) {
14381452
std::rethrow_exception(config_exception);
14391453
}
1454+
1455+
_process_callbacks(CallbackPriority::LastPreHelp);
1456+
_process_help_flags(CallbackPriority::Last);
1457+
_process_callbacks(CallbackPriority::Last);
14401458
}
14411459

14421460
CLI11_INLINE void App::_process_extras() {
@@ -1496,10 +1514,20 @@ CLI11_INLINE void App::_parse(std::vector<std::string> &args) {
14961514
// Convert missing (pairs) to extras (string only) ready for processing in another app
14971515
args = remaining_for_passthrough(false);
14981516
} else if(parse_complete_callback_) {
1517+
_process_callbacks(CallbackPriority::FirstPreHelp);
1518+
_process_help_flags(CallbackPriority::First);
1519+
_process_callbacks(CallbackPriority::First);
14991520
_process_env();
1500-
_process_callbacks();
1501-
_process_help_flags();
1521+
_process_callbacks(CallbackPriority::PreRequirementsCheckPreHelp);
1522+
_process_help_flags(CallbackPriority::PreRequirementsCheck);
1523+
_process_callbacks(CallbackPriority::PreRequirementsCheck);
15021524
_process_requirements();
1525+
_process_callbacks(CallbackPriority::NormalPreHelp);
1526+
_process_help_flags(CallbackPriority::Normal);
1527+
_process_callbacks(CallbackPriority::Normal);
1528+
_process_callbacks(CallbackPriority::LastPreHelp);
1529+
_process_help_flags(CallbackPriority::Last);
1530+
_process_callbacks(CallbackPriority::Last);
15031531
run_callback(false, true);
15041532
}
15051533
}
@@ -1620,8 +1648,15 @@ CLI11_INLINE bool App::_parse_single_config(const ConfigItem &item, std::size_t
16201648
// check for section close
16211649
if(item.name == "--") {
16221650
if(configurable_ && parse_complete_callback_) {
1623-
_process_callbacks();
1651+
_process_callbacks(CallbackPriority::FirstPreHelp);
1652+
_process_callbacks(CallbackPriority::First);
1653+
_process_callbacks(CallbackPriority::PreRequirementsCheckPreHelp);
1654+
_process_callbacks(CallbackPriority::PreRequirementsCheck);
16241655
_process_requirements();
1656+
_process_callbacks(CallbackPriority::NormalPreHelp);
1657+
_process_callbacks(CallbackPriority::Normal);
1658+
_process_callbacks(CallbackPriority::LastPreHelp);
1659+
_process_callbacks(CallbackPriority::Last);
16251660
run_callback();
16261661
}
16271662
return true;
@@ -2081,10 +2116,20 @@ App::_parse_arg(std::vector<std::string> &args, detail::Classifier current_type,
20812116
_trigger_pre_parse(args.size());
20822117
// run the parse complete callback since the subcommand processing is now complete
20832118
if(sub->parse_complete_callback_) {
2119+
sub->_process_callbacks(CallbackPriority::FirstPreHelp);
2120+
sub->_process_help_flags(CallbackPriority::First);
2121+
sub->_process_callbacks(CallbackPriority::First);
20842122
sub->_process_env();
2085-
sub->_process_callbacks();
2086-
sub->_process_help_flags();
2123+
sub->_process_callbacks(CallbackPriority::PreRequirementsCheckPreHelp);
2124+
sub->_process_help_flags(CallbackPriority::PreRequirementsCheck);
2125+
sub->_process_callbacks(CallbackPriority::PreRequirementsCheck);
20872126
sub->_process_requirements();
2127+
sub->_process_callbacks(CallbackPriority::NormalPreHelp);
2128+
sub->_process_help_flags(CallbackPriority::Normal);
2129+
sub->_process_callbacks(CallbackPriority::Normal);
2130+
sub->_process_callbacks(CallbackPriority::LastPreHelp);
2131+
sub->_process_help_flags(CallbackPriority::Last);
2132+
sub->_process_callbacks(CallbackPriority::Last);
20882133
sub->run_callback(false, true);
20892134
}
20902135
return true;

include/CLI/impl/Option_inl.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ template <typename CRTP> template <typename T> void OptionBase<CRTP>::copy_to(T
3232
other->delimiter(delimiter_);
3333
other->always_capture_default(always_capture_default_);
3434
other->multi_option_policy(multi_option_policy_);
35+
other->callback_priority(callback_priority_);
3536
}
3637

3738
CLI11_INLINE Option *Option::expected(int value) {

tests/AppTest.cpp

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1025,6 +1025,51 @@ TEST_CASE_METHOD(TApp, "TakeFirstOptMulti", "[app]") {
10251025
CHECK(std::vector<int>({1, 2}) == vals);
10261026
}
10271027

1028+
TEST_CASE_METHOD(TApp, "optionPriority", "[app]") {
1029+
std::vector<int> results;
1030+
auto *opt1 = app.add_flag_callback("-A", [&]() { results.push_back(1); });
1031+
auto *opt2 = app.add_flag_callback("-B", [&]() { results.push_back(2); });
1032+
auto *opt3 = app.add_flag_callback("-C", [&]() { results.push_back(3); });
1033+
auto *opt4 = app.add_flag_callback("-D", [&]() { results.push_back(4); });
1034+
auto *opt5 = app.add_flag_callback("-E", [&]() { results.push_back(5); });
1035+
args = {"-A", "-B", "-C", "-D", "-E"};
1036+
run();
1037+
CHECK(std::vector<int>({1, 2, 3, 4, 5}) == results);
1038+
results.clear();
1039+
opt2->callback_priority(CLI::CallbackPriority::FirstPreHelp);
1040+
run();
1041+
CHECK(std::vector<int>({2, 1, 3, 4, 5}) == results);
1042+
results.clear();
1043+
opt4->callback_priority(CLI::CallbackPriority::Last);
1044+
run();
1045+
CHECK(std::vector<int>({2, 1, 3, 5, 4}) == results);
1046+
results.clear();
1047+
opt5->callback_priority(CLI::CallbackPriority::PreRequirementsCheck);
1048+
run();
1049+
CHECK(std::vector<int>({2, 5, 1, 3, 4}) == results);
1050+
results.clear();
1051+
1052+
args = {"-A", "-B", "-C", "-D", "-E", "--help"};
1053+
CHECK_THROWS(run());
1054+
CHECK(std::vector<int>({2}) == results);
1055+
results.clear();
1056+
1057+
app.get_help_ptr()->callback_priority(CLI::CallbackPriority::Normal);
1058+
CHECK_THROWS(run());
1059+
CHECK(std::vector<int>({2, 5}) == results);
1060+
results.clear();
1061+
1062+
app.get_help_ptr()->callback_priority(CLI::CallbackPriority::Last);
1063+
CHECK_THROWS(run());
1064+
CHECK(std::vector<int>({2, 5, 1, 3}) == results);
1065+
results.clear();
1066+
opt3->excludes(opt1);
1067+
args = {"-A", "-B", "-C", "-D", "-E"};
1068+
1069+
CHECK_THROWS(run());
1070+
CHECK(std::vector<int>({2, 5}) == results);
1071+
}
1072+
10281073
TEST_CASE_METHOD(TApp, "ComplexOptMulti", "[app]") {
10291074
std::complex<double> val;
10301075
app.add_option("--long", val)->take_first()->allow_extra_args();

0 commit comments

Comments
 (0)