Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,10 @@ int main(int argc, char** argv) {
}
```

For more information about `ensure_utf8` the section on
When adding options the names should not conflict with each other, if an option
is added, or a modifier changed that would cause naming conflicts a run time
error will be thrown in the add_option method. This includes default options for
help `-h, --help`. For more information about `ensure_utf8` the section on
[Unicode support](#unicode-support) below.

<details><summary>Note: If you don't like macros, this is what that macro expands to: (click to expand)</summary><p>
Expand Down Expand Up @@ -500,6 +503,20 @@ Before parsing, you can set the following options:
validation checks for the option to be executed when the option value is
parsed vs. at the end of all parsing. This could cause the callback to be
executed multiple times. Also works with positional options.
- `->callback_priority(CallbackPriority priority)`: 🚧 changes the order in
which the option callback is executed. `CallbackPriority::PreHelpCheck` runs
the option callback or value setting prior to help checks and requirements
checking. `CallbackPriority::PreRequirementsCheck` runs the option callback
after the help check but prior to checking for other requirements.
`CallbackPriority::Normal` is the default and runs the callbacks after
requirements and help have been checked and processed.
`CallbackPriority::Last` runs after all normal priority callbacks have been
executed. This allows fine grained manipulation of when option values are set
and when errors or checks can be triggered from the options. The help pointer
may also be modified to make it run later by default, it normally runs before
the requirements checking but can be modified to run later by giving it
`Normal` or `Last` priority, as opposed to modifying individual options. The
version flag has `PreRequirementsCheck` priority by default.

These options return the `Option` pointer, so you can chain them together, and
even skip storing the pointer entirely. The `each` function takes any function
Expand Down
4 changes: 2 additions & 2 deletions include/CLI/App.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -1303,12 +1303,12 @@ class App {
void _process_env();

/// Process callbacks. Runs on *all* subcommands.
void _process_callbacks();
void _process_callbacks(CallbackPriority priority);

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

/// Verify required options and cross requirements. Subcommands too (only if selected).
void _process_requirements();
Expand Down
15 changes: 15 additions & 0 deletions include/CLI/Option.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ enum class MultiOptionPolicy : char {
Reverse, //!< take only the last Expected number of arguments in reverse order
};

/// @brief enumeration for the callback priority
enum class CallbackPriority : std::int8_t { Last = -1, Normal = 0, PreRequirementsCheck = 1, PreHelpCheck = 2 };

/// This is the CRTP base class for Option and OptionDefaults. It was designed this way
/// to share parts of the class; an OptionDefaults can copy to an Option.
template <typename CRTP> class OptionBase {
Expand Down Expand Up @@ -343,6 +346,9 @@ class Option : public OptionBase<Option> {
bool trigger_on_result_{false};
/// flag indicating that the option should force the callback regardless if any results present
bool force_callback_{false};

/// callback priority indicator
CallbackPriority callback_priority_{CallbackPriority::Normal};
///@}

/// Making an option by hand is not defined, it must be made by the App class
Expand Down Expand Up @@ -420,6 +426,15 @@ class Option : public OptionBase<Option> {
/// Get the current value of run_callback_for_default
CLI11_NODISCARD bool get_run_callback_for_default() const { return run_callback_for_default_; }

/// Set the value of callback priority which controls when the callback function should be called relative to other
/// parsing operations the default This is controlled automatically but could be manipulated by the user.
Option *callback_priority(CallbackPriority value = CallbackPriority::Normal) {
callback_priority_ = value;
return this;
}
/// Get the current value of run_callback_for_default
CLI11_NODISCARD CallbackPriority get_callback_priority() const { return callback_priority_; }

/// Adds a shared validator
Option *check(Validator_p validator);

Expand Down
65 changes: 43 additions & 22 deletions include/CLI/impl/App_inl.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ CLI11_INLINE Option *App::set_help_flag(std::string flag_name, const std::string
// Empty name will simply remove the help flag
if(!flag_name.empty()) {
help_ptr_ = add_flag(flag_name, help_description);
help_ptr_->configurable(false);
help_ptr_->configurable(false)->callback_priority(CallbackPriority::PreRequirementsCheck);
}

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

return help_all_ptr_;
Expand All @@ -319,7 +319,7 @@ App::set_version_flag(std::string flag_name, const std::string &versionString, c
if(!flag_name.empty()) {
version_ptr_ = add_flag_callback(
flag_name, [versionString]() { throw(CLI::CallForVersion(versionString, 0)); }, version_help);
version_ptr_->configurable(false);
version_ptr_->configurable(false)->callback_priority(CallbackPriority::PreRequirementsCheck);
}

return version_ptr_;
Expand All @@ -336,7 +336,7 @@ App::set_version_flag(std::string flag_name, std::function<std::string()> vfunc,
if(!flag_name.empty()) {
version_ptr_ =
add_flag_callback(flag_name, [vfunc]() { throw(CLI::CallForVersion(vfunc(), 0)); }, version_help);
version_ptr_->configurable(false);
version_ptr_->configurable(false)->callback_priority(CallbackPriority::PreRequirementsCheck);
}

return version_ptr_;
Expand Down Expand Up @@ -1239,43 +1239,48 @@ CLI11_INLINE void App::_process_env() {
}
}

CLI11_INLINE void App::_process_callbacks() {
CLI11_INLINE void App::_process_callbacks(CallbackPriority priority) {

for(App_p &sub : subcommands_) {
// process the priority option_groups first
if(sub->get_name().empty() && sub->parse_complete_callback_) {
if(sub->count_all() > 0) {
sub->_process_callbacks();
sub->run_callback();
sub->_process_callbacks(priority);
if(priority == CallbackPriority::Normal) {
// only run the subcommand callback at normal priority
sub->run_callback();
}
}
}
}

for(const Option_p &opt : options_) {
if((*opt) && !opt->get_callback_run()) {
opt->run_callback();
if(opt->get_callback_priority() >= priority) {
if((*opt) && !opt->get_callback_run()) {
opt->run_callback();
}
}
}
for(App_p &sub : subcommands_) {
if(!sub->parse_complete_callback_) {
sub->_process_callbacks();
sub->_process_callbacks(priority);
}
}
}

CLI11_INLINE void App::_process_help_flags(bool trigger_help, bool trigger_all_help) const {
CLI11_INLINE void App::_process_help_flags(CallbackPriority priority, bool trigger_help, bool trigger_all_help) const {
const Option *help_ptr = get_help_ptr();
const Option *help_all_ptr = get_help_all_ptr();

if(help_ptr != nullptr && help_ptr->count() > 0)
if(help_ptr != nullptr && help_ptr->count() > 0 && help_ptr->get_callback_priority() >= priority)
trigger_help = true;
if(help_all_ptr != nullptr && help_all_ptr->count() > 0)
if(help_all_ptr != nullptr && help_all_ptr->count() > 0 && help_all_ptr->get_callback_priority() >= priority)
trigger_all_help = true;

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

// Only the final subcommand should call for help. All help wins over help.
} else if(trigger_all_help) {
Expand Down Expand Up @@ -1416,7 +1421,8 @@ CLI11_INLINE void App::_process_requirements() {
CLI11_INLINE void App::_process() {
// help takes precedence over other potential errors and config and environment shouldn't be processed if help
// throws
_process_help_flags();
_process_callbacks(CallbackPriority::PreHelpCheck);
_process_help_flags(CallbackPriority::PreRequirementsCheck);
std::exception_ptr config_exception;
try {
// the config file might generate a FileError but that should not be processed until later in the process
Expand All @@ -1430,13 +1436,16 @@ CLI11_INLINE void App::_process() {
}
// callbacks and requirements processing can generate exceptions which should take priority
// over the config file error if one exists.
_process_callbacks(CallbackPriority::PreRequirementsCheck);
_process_requirements();

_process_callbacks();
_process_help_flags(CallbackPriority::Normal);
_process_callbacks(CallbackPriority::Normal);

if(config_exception) {
std::rethrow_exception(config_exception);
}
_process_help_flags(CallbackPriority::Last);
_process_callbacks(CallbackPriority::Last);
}

CLI11_INLINE void App::_process_extras() {
Expand Down Expand Up @@ -1497,9 +1506,14 @@ CLI11_INLINE void App::_parse(std::vector<std::string> &args) {
args = remaining_for_passthrough(false);
} else if(parse_complete_callback_) {
_process_env();
_process_callbacks();
_process_help_flags();
_process_callbacks(CallbackPriority::PreHelpCheck);
_process_help_flags(CallbackPriority::PreRequirementsCheck);
_process_callbacks(CallbackPriority::PreRequirementsCheck);
_process_requirements();
_process_help_flags(CallbackPriority::Normal);
_process_callbacks(CallbackPriority::Normal);
_process_help_flags(CallbackPriority::Last);
_process_callbacks(CallbackPriority::Last);
run_callback(false, true);
}
}
Expand Down Expand Up @@ -1620,8 +1634,10 @@ CLI11_INLINE bool App::_parse_single_config(const ConfigItem &item, std::size_t
// check for section close
if(item.name == "--") {
if(configurable_ && parse_complete_callback_) {
_process_callbacks();
_process_callbacks(CallbackPriority::PreRequirementsCheck);
_process_requirements();
_process_callbacks(CallbackPriority::Normal);
_process_callbacks(CallbackPriority::Last);
run_callback();
}
return true;
Expand Down Expand Up @@ -2082,9 +2098,14 @@ App::_parse_arg(std::vector<std::string> &args, detail::Classifier current_type,
// run the parse complete callback since the subcommand processing is now complete
if(sub->parse_complete_callback_) {
sub->_process_env();
sub->_process_callbacks();
sub->_process_help_flags();
sub->_process_callbacks(CallbackPriority::PreHelpCheck);
sub->_process_help_flags(CallbackPriority::PreRequirementsCheck);
sub->_process_callbacks(CallbackPriority::PreRequirementsCheck);
sub->_process_requirements();
sub->_process_help_flags(CallbackPriority::Normal);
sub->_process_callbacks(CallbackPriority::Normal);
sub->_process_help_flags(CallbackPriority::Last);
sub->_process_callbacks(CallbackPriority::Last);
sub->run_callback(false, true);
}
return true;
Expand Down
48 changes: 48 additions & 0 deletions tests/AppTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1025,6 +1025,54 @@ TEST_CASE_METHOD(TApp, "TakeFirstOptMulti", "[app]") {
CHECK(std::vector<int>({1, 2}) == vals);
}

TEST_CASE_METHOD(TApp, "optionPriority", "[app]") {
std::vector<int> results;
auto *opt1 = app.add_flag_callback("-A", [&]() { results.push_back(1); });
auto *opt2 = app.add_flag_callback("-B", [&]() { results.push_back(2); });
auto *opt3 = app.add_flag_callback("-C", [&]() { results.push_back(3); });
auto *opt4 = app.add_flag_callback("-D", [&]() { results.push_back(4); });
auto *opt5 = app.add_flag_callback("-E", [&]() { results.push_back(5); });
CHECK(opt1->get_callback_priority() == CLI::CallbackPriority::Normal);
args = {"-A", "-B", "-C", "-D", "-E"};
run();
CHECK(std::vector<int>({1, 2, 3, 4, 5}) == results);
results.clear();
opt2->callback_priority(CLI::CallbackPriority::PreHelpCheck);
CHECK(opt2->get_callback_priority() == CLI::CallbackPriority::PreHelpCheck);
run();
CHECK(std::vector<int>({2, 1, 3, 4, 5}) == results);
results.clear();
opt4->callback_priority(CLI::CallbackPriority::Last);
CHECK(opt4->get_callback_priority() == CLI::CallbackPriority::Last);
run();
CHECK(std::vector<int>({2, 1, 3, 5, 4}) == results);
results.clear();
opt5->callback_priority(CLI::CallbackPriority::PreRequirementsCheck);
run();
CHECK(std::vector<int>({2, 5, 1, 3, 4}) == results);
results.clear();

args = {"-A", "-B", "-C", "-D", "-E", "--help"};
CHECK_THROWS(run());
CHECK(std::vector<int>({2}) == results);
results.clear();

app.get_help_ptr()->callback_priority(CLI::CallbackPriority::Normal);
CHECK_THROWS(run());
CHECK(std::vector<int>({2, 5}) == results);
results.clear();

app.get_help_ptr()->callback_priority(CLI::CallbackPriority::Last);
CHECK_THROWS(run());
CHECK(std::vector<int>({2, 5, 1, 3}) == results);
results.clear();
opt3->excludes(opt1);
args = {"-A", "-B", "-C", "-D", "-E"};

CHECK_THROWS(run());
CHECK(std::vector<int>({2, 5}) == results);
}

TEST_CASE_METHOD(TApp, "ComplexOptMulti", "[app]") {
std::complex<double> val;
app.add_option("--long", val)->take_first()->allow_extra_args();
Expand Down
17 changes: 17 additions & 0 deletions tests/HelpTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1593,3 +1593,20 @@ TEST_CASE("TVersion: exit", "[help]") {
CHECK(0 == ret);
}
}

TEST_CASE("TVersion: exit_with_required", "[help]") {
// test that the version flag works even if there are required options
CLI::App app;

app.set_version_flag("--version", CLI11_VERSION);
app.add_option("--req")->required();

try {
app.parse("--version");
} catch(const CLI::CallForVersion &v) {
std::ostringstream out;
auto ret = app.exit(v, out);
CHECK_THAT(out.str(), Contains(CLI11_VERSION));
CHECK(0 == ret);
}
}
Loading