1
0
mirror of https://github.com/CLIUtils/CLI11.git synced 2025-04-29 12:13:52 +00:00

First attempt at formatter (app and option)

in progress: formatters

Getting closer

Working on apps

One test actually runs

All builds, added filter functions

Reverting a few behavours as needed

Repairs

All tests pass

Fixing error with adding help flag

Labels are simpler mappings, normalized setters

Adding help_all

Adding a few more tests

One more line tested

Adding one more check

Adding to readme

Simplify naming

Adding default constructors

Fixing spacing issues with subcommand all printout

Adding a couple of tests
This commit is contained in:
Henry Fredrick Schreiner 2018-04-23 16:27:33 +02:00 committed by Henry Schreiner
parent a061275eba
commit 952f2913e3
18 changed files with 896 additions and 221 deletions

View File

@ -1,3 +1,29 @@
### Version 1.6: Formatters
Added a new formatting system. You can now set the formatter on Apps and Options.
* Added `AppFormatter` and `OptionFormatter`, and `formatter` slot
* Added `help_all` support (not added by default)
* Added filter argument to `get_subcommands`, `get_options`; use empty filter `{}` to avoid filtering
* Added `get_groups()` to get groups
* Added getters for the missing parts of options (help no longer uses any private parts)
Changes to the help system (most normal users will not notice this):
* Renamed `single_name` to `get_name(false, false)` (the default)
* The old `get_name()` is now `get_name(false, true)`
* The old `get_pname()` is now `get_name(true, false)`
* Removed `help_*` functions
* Protected function `_has_help_positional` removed
* `format_help` can now be chained
Other small changes:
* Testing (only) now uses submodules.
* Removed `requires` in favor of `needs` (deprecated in last version)
* Better CMake policy handling
### Version 1.5.3: Compiler compatibility
This version fixes older AppleClang compilers by removing the optimization for casting. The minimum version of Boost Optional supported has been clarified to be 1.58. CUDA 7.0 NVCC is now supported.

View File

@ -193,6 +193,7 @@ The add commands return a pointer to an internally stored `Option`. If you set t
* `->check(CLI::Range(min,max))`: Requires that the option be between min and max (make sure to use floating point if needed). Min defaults to 0.
* `->transform(std::string(std::string))`: Converts the input string into the output string, in-place in the parsed options.
* `->configurable(false)`: Disable this option from being in an ini configuration file.
* `->formatter(fmt)`: Set the formatter, with signature `std::string(const Option*, OptionFormatMode)`. See Formatting for more details.
These options return the `Option` pointer, so you can chain them together, and even skip storing the pointer entirely. Check takes any function that has the signature `void(const std::string&)`; it should throw a `ValidationError` when validation fails. The help message will have the name of the parent option prepended. Since `check` and `transform` use the same underlying mechanism, you can chain as many as you want, and they will be executed in order. If you just want to see the unconverted values, use `.results()` to get the `std::vector<std::string>` of results.
@ -247,6 +248,7 @@ There are several options that are supported on the main app and subcommands. Th
* `.get_parent()`: Get the parent App or nullptr if called on master App.
* `.get_options()`: Get the list of all defined option pointers (useful for processing the app for custom output formats).
* `.parse_order()`: Get the list of option pointers in the order they were parsed (including duplicates).
* `.formatter(fmt)`: Set a formatter, with signature `std::string(const App*, std::string, AppFormatMode)`. See Formatting for more details.
* `.get_description()`: Access the description.
* `.parsed()`: True if this subcommand was given on the command line.
* `.set_name(name)`: Add or change the name.
@ -254,6 +256,8 @@ There are several options that are supported on the main app and subcommands. Th
* `.allow_extras()`: Do not throw an error if extra arguments are left over.
* `.prefix_command()`: Like `allow_extras`, but stop immediately on the first unrecognised item. It is ideal for allowing your app or subcommand to be a "prefix" to calling another app.
* `.set_footer(message)`: Set text to appear at the bottom of the help string.
* `.set_help_flag(name, message)`: Set the help flag name and message, returns a pointer to the created option.
* `.set_help_all_flag(name, message)`: Set the help all flag name and message, returns a pointer to the created option. Expands subcommands.
* `.set_failure_message(func)`: Set the failure message function. Two provided: `CLI::FailureMessage::help` and `CLI::FailureMessage::simple` (the default).
* `.group(name)`: Set a group name, defaults to `"Subcommands"`. Setting `""` will be hide the subcommand.
@ -300,11 +304,22 @@ app.option_defaults()->required();
The default settings for options are inherited to subcommands, as well.
## Formatting
The job of formatting help printouts is delegated to a formatter callable object on Apps and Options. You are free to replace either formatter by calling `formatter(fmt)` on an `App` or `Option`.
CLI11 comes with a default App formatter functional, `AppFormatter`, and a default `OptionFormatter` as well. They are both customizable; you can set `label(key, value)` to replace the default labels like `REQUIRED`, and `column_width(n)` to set the width of the columns before you add the functional to the app or option. You can also override almost any stage of the formatting process in a subclass of either formatter. If you want to make a new formatter from scratch, you can do
that too; you just need to implement the correct signature. The first argument is a const pointer to the App or Option in question. The App formatter will get a `std::string` usage name as the second option, and both end with with a mode. Both formatters return a `std::string`.
The `AppFormatMode` can be `Normal`, `All`, or `Sub`, and it indicates the situation the help was called in. `Sub` is optional, but the default formatter uses it to make sure expanded subcommands are called with
their own formatter since you can't access anything but the call operator once a formatter has been set.
The `OptionFormatMode` also has three values, depending on the calling situation: `Positional`, `Optional`, and `Usage`. The default formatter uses these to print out the option in each of these situations.
## Subclassing
The App class was designed allow toolkits to subclass it, to provide preset default options (see above) and setup/teardown code. Subcommands remain an unsubclassed `App`, since those are not expected to need setup and teardown. The default `App` only adds a help flag, `-h,--help`, than can removed/replaced using `.set_help_flag(name, help_string)`. You can remove options if you have pointers to them using `.remove_option(opt)`. You can add a `pre_callback` override to customize the after parse
The App class was designed allow toolkits to subclass it, to provide preset default options (see above) and setup/teardown code. Subcommands remain an unsubclassed `App`, since those are not expected to need setup and teardown. The default `App` only adds a help flag, `-h,--help`, than can removed/replaced using `.set_help_flag(name, help_string)`. You can also set a help-all flag with `.set_help_all_flag(name, help_string)`; this will expand the subcommands (one level only). You can remove options if you have pointers to them using `.remove_option(opt)`. You can add a `pre_callback` override to customize the after parse
but before run behavior, while
still giving the user freedom to `set_callback` on the main app.
still giving the user freedom to `set_callback` on the main app.
The most important parse function is `parse(std::vector<std::string>)`, which takes a reversed list of arguments (so that `pop_back` processes the args in the correct order). `get_help_ptr` and `get_config_ptr` give you access to the help/config option pointers. The standard `parse` manually sets the name from the first argument, so it should not be in this vector.

View File

@ -71,7 +71,7 @@ add_cli_exe(enum enum.cpp)
add_test(NAME enum_pass COMMAND enum -l 1)
add_test(NAME enum_fail COMMAND enum -l 4)
set_property(TEST enum_fail PROPERTY PASS_REGULAR_EXPRESSION
"Could not convert: -l,--level = 4")
"Could not convert: --level = 4")
add_cli_exe(modhelp modhelp.cpp)
add_test(NAME modhelp COMMAND modhelp -a test -h)
@ -83,3 +83,5 @@ add_test(NAME subcom_in_files COMMAND subcommand_main subcommand_a -f this.txt -
set_property(TEST subcom_in_files PROPERTY PASS_REGULAR_EXPRESSION
"Working on file: this\.txt"
"Using foo!")
add_cli_exe(formatter formatter.cpp)

30
examples/formatter.cpp Normal file
View File

@ -0,0 +1,30 @@
#include <CLI/CLI.hpp>
class MyFormatter : public CLI::OptionFormatter {
public:
std::string make_opts(const CLI::Option *) const override { return " OPTION"; }
};
int main(int argc, char **argv) {
CLI::App app;
app.set_help_all_flag("--help-all", "Show all help");
app.option_defaults()->formatter(MyFormatter());
CLI::AppFormatter fmt;
fmt.column_width(15);
app.formatter(fmt);
app.add_flag("--flag", "This is a flag");
auto sub1 = app.add_subcommand("one", "Description One");
sub1->add_flag("--oneflag", "Some flag");
auto sub2 = app.add_subcommand("two", "Description Two");
sub2->add_flag("--twoflag", "Some other flag");
CLI11_PARSE(app, argc, argv);
std::cout << "This app was meant to show off the formatter, run with -h" << std::endl;
return 0;
}

View File

@ -3,6 +3,7 @@
int main(int argc, char **argv) {
CLI::App app("K3Pi goofit fitter");
app.set_help_all_flag("--help-all", "Expand all help");
app.add_flag("--random", "Some random flag");
CLI::App *start = app.add_subcommand("start", "A great subcommand");
CLI::App *stop = app.add_subcommand("stop", "Do you really want to stop?");

View File

@ -24,6 +24,7 @@
#include "CLI/Split.hpp"
#include "CLI/StringTools.hpp"
#include "CLI/TypeTools.hpp"
#include "CLI/Formatter.hpp"
namespace CLI {
@ -102,6 +103,12 @@ class App {
/// A pointer to the help flag if there is one INHERITABLE
Option *help_ptr_{nullptr};
/// A pointer to the help all flag if there is one INHERITABLE
Option *help_all_ptr_{nullptr};
/// This is the formatter for help printing. Default provided. INHERITABLE
std::function<std::string(const App *, std::string, AppFormatMode)> formatter_{AppFormatter()};
/// The error message printing function INHERITABLE
std::function<std::string(const App *, const Error &e)> failure_message_ = FailureMessage::simple;
@ -141,7 +148,7 @@ class App {
/// True if this command/subcommand was parsed
bool parsed_{false};
/// Minimum required subcommands
/// Minimum required subcommands (not inheritable!)
size_t require_subcommand_min_ = 0;
/// Max number of subcommands allowed (parsing stops after this number). 0 is unlimited INHERITABLE
@ -171,7 +178,10 @@ class App {
// Inherit if not from a nullptr
if(parent_ != nullptr) {
if(parent_->help_ptr_ != nullptr)
set_help_flag(parent_->help_ptr_->get_name(), parent_->help_ptr_->get_description());
set_help_flag(parent_->help_ptr_->get_name(false, true), parent_->help_ptr_->get_description());
if(parent_->help_all_ptr_ != nullptr)
set_help_all_flag(parent_->help_all_ptr_->get_name(false, true),
parent_->help_all_ptr_->get_description());
/// OptionDefaults
option_defaults_ = parent_->option_defaults_;
@ -185,6 +195,7 @@ class App {
fallthrough_ = parent_->fallthrough_;
group_ = parent_->group_;
footer_ = parent_->footer_;
formatter_ = parent_->formatter_;
require_subcommand_max_ = parent_->require_subcommand_max_;
}
}
@ -250,6 +261,12 @@ class App {
return this;
}
/// Set the help formatter
App *formatter(std::function<std::string(const App *, std::string, AppFormatMode)> fmt) {
formatter_ = fmt;
return this;
}
/// Check to see if this subcommand was parsed, true only if received on command line.
bool parsed() const { return parsed_; }
@ -388,6 +405,22 @@ class App {
return help_ptr_;
}
/// Set a help all flag, replaced the existing one if present
Option *set_help_all_flag(std::string name = "", std::string description = "") {
if(help_all_ptr_ != nullptr) {
remove_option(help_all_ptr_);
help_all_ptr_ = nullptr;
}
// Empty name will simply remove the help flag
if(!name.empty()) {
help_all_ptr_ = add_flag(name, description);
help_all_ptr_->configurable(false);
}
return help_all_ptr_;
}
/// Add option for flag
Option *add_flag(std::string name, std::string description = "") {
CLI::callback_t fun = [](CLI::results_t) { return true; };
@ -764,6 +797,11 @@ class App {
return e.get_exit_code();
}
if(dynamic_cast<const CLI::CallForAllHelp *>(&e) != nullptr) {
out << help("", AppFormatMode::All);
return e.get_exit_code();
}
if(e.get_exit_code() != static_cast<int>(ExitCodes::Success)) {
if(failure_message_)
err << failure_message_(this, e) << std::flush;
@ -801,18 +839,43 @@ class App {
throw OptionNotFound(name);
}
/// Get a subcommand pointer list to the currently selected subcommands (after parsing by default, in command line
/// order)
std::vector<App *> get_subcommands(bool parsed = true) const {
if(parsed) {
return parsed_subcommands_;
} else {
std::vector<App *> subcomms(subcommands_.size());
std::transform(std::begin(subcommands_), std::end(subcommands_), std::begin(subcomms), [](const App_p &v) {
return v.get();
});
return subcomms;
/// Get a subcommand pointer list to the currently selected subcommands (after parsing by by default, in command
/// line order; use parsed = false to get the original definition list.)
std::vector<App *> get_subcommands() const { return parsed_subcommands_; }
/// Get a filtered subcommand pointer list from the original definition list. An empty function will provide all
/// subcommands (const)
std::vector<const App *> get_subcommands(const std::function<bool(const App *)> &filter) const {
std::vector<const App *> subcomms(subcommands_.size());
std::transform(std::begin(subcommands_), std::end(subcommands_), std::begin(subcomms), [](const App_p &v) {
return v.get();
});
if(filter) {
subcomms.erase(std::remove_if(std::begin(subcomms),
std::end(subcomms),
[&filter](const App *app) { return !filter(app); }),
std::end(subcomms));
}
return subcomms;
}
/// Get a filtered subcommand pointer list from the original definition list. An empty function will provide all
/// subcommands
std::vector<App *> get_subcommands(const std::function<bool(App *)> &filter) {
std::vector<App *> subcomms(subcommands_.size());
std::transform(std::begin(subcommands_), std::end(subcommands_), std::begin(subcomms), [](const App_p &v) {
return v.get();
});
if(filter) {
subcomms.erase(
std::remove_if(std::begin(subcomms), std::end(subcomms), [&filter](App *app) { return !filter(app); }),
std::end(subcomms));
}
return subcomms;
}
/// Check to see if given subcommand was selected
@ -885,105 +948,20 @@ class App {
return out.str();
}
/// Makes a help message, with a column wid for column 1
std::string help(size_t wid = 30, std::string prev = "") const {
// Delegate to subcommand if needed
/// Makes a help message, using the currently configured formatter
/// Will only do one subcommand at a time
std::string help(std::string prev = "", AppFormatMode mode = AppFormatMode::Normal) const {
if(prev.empty())
prev = name_;
prev = get_name();
else
prev += " " + name_;
prev += " " + get_name();
// Delegate to subcommand if needed
auto selected_subcommands = get_subcommands();
if(!selected_subcommands.empty())
return selected_subcommands.at(0)->help(wid, prev);
std::stringstream out;
out << description_ << std::endl;
out << "Usage:" << (prev.empty() ? "" : " ") << prev;
// Check for options_
bool npos = false;
std::vector<std::string> groups;
for(const Option_p &opt : options_) {
if(opt->nonpositional()) {
npos = true;
// Add group if it is not already in there
if(std::find(groups.begin(), groups.end(), opt->get_group()) == groups.end()) {
groups.push_back(opt->get_group());
}
}
}
if(npos)
out << " [OPTIONS]";
// Positionals
bool pos = false;
for(const Option_p &opt : options_)
if(opt->get_positional()) {
// A hidden positional should still show up in the usage statement
// if(detail::to_lower(opt->get_group()).empty())
// continue;
out << " " << opt->help_positional();
if(opt->_has_help_positional())
pos = true;
}
if(!subcommands_.empty()) {
if(require_subcommand_min_ > 0)
out << " SUBCOMMAND";
else
out << " [SUBCOMMAND]";
}
out << std::endl;
// Positional descriptions
if(pos) {
out << std::endl << "Positionals:" << std::endl;
for(const Option_p &opt : options_) {
if(detail::to_lower(opt->get_group()).empty())
continue; // Hidden
if(opt->_has_help_positional())
detail::format_help(out, opt->help_pname(), opt->get_description(), wid);
}
}
// Options
if(npos) {
for(const std::string &group : groups) {
if(detail::to_lower(group).empty())
continue; // Hidden
out << std::endl << group << ":" << std::endl;
for(const Option_p &opt : options_) {
if(opt->nonpositional() && opt->get_group() == group)
detail::format_help(out, opt->help_name(true), opt->get_description(), wid);
}
}
}
// Subcommands
if(!subcommands_.empty()) {
std::set<std::string> subcmd_groups_seen;
for(const App_p &com : subcommands_) {
const std::string &group_key = detail::to_lower(com->get_group());
if(group_key.empty() || subcmd_groups_seen.count(group_key) != 0)
continue; // Hidden or not in a group
subcmd_groups_seen.insert(group_key);
out << std::endl << com->get_group() << ":" << std::endl;
for(const App_p &new_com : subcommands_)
if(detail::to_lower(new_com->get_group()) == group_key)
detail::format_help(out, new_com->get_name(), new_com->description_, wid);
}
}
if(!footer_.empty()) {
out << std::endl << footer_ << std::endl;
}
return out.str();
return selected_subcommands.at(0)->help(prev);
else
return formatter_(this, prev, mode);
}
///@}
@ -993,12 +971,20 @@ class App {
/// Get the app or subcommand description
std::string get_description() const { return description_; }
/// Get the list of options (user facing function, so returns raw pointers)
std::vector<Option *> get_options() const {
std::vector<Option *> options(options_.size());
/// Get the list of options (user facing function, so returns raw pointers), has optional filter function
std::vector<const Option *> get_options(const std::function<bool(const Option *)> filter = {}) const {
std::vector<const Option *> options(options_.size());
std::transform(std::begin(options_), std::end(options_), std::begin(options), [](const Option_p &val) {
return val.get();
});
if(filter) {
options.erase(std::remove_if(std::begin(options),
std::end(options),
[&filter](const Option *opt) { return !filter(opt); }),
std::end(options));
}
return options;
}
@ -1035,6 +1021,9 @@ class App {
/// Get a pointer to the help flag. (const)
const Option *get_help_ptr() const { return help_ptr_; }
/// Get a pointer to the help all flag. (const)
const Option *get_help_all_ptr() const { return help_all_ptr_; }
/// Get a pointer to the config option.
Option *get_config_ptr() { return config_ptr_; }
@ -1058,6 +1047,20 @@ class App {
return local_name == name_to_check;
}
/// Get the groups available directly from this option (in order)
std::vector<std::string> get_groups() const {
std::vector<std::string> groups;
for(const Option_p &opt : options_) {
// Add group if it is not already in there
if(std::find(groups.begin(), groups.end(), opt->get_group()) == groups.end()) {
groups.push_back(opt->get_group());
}
}
return groups;
}
/// This gets a vector of pointers with the original parse order
const std::vector<Option *> &parse_order() const { return parse_order_; }
@ -1161,6 +1164,10 @@ class App {
throw CallForHelp();
}
if(help_all_ptr_ != nullptr && help_all_ptr_->count() > 0) {
throw CallForAllHelp();
}
// Process an INI file
if(config_ptr_ != nullptr) {
if(*config_ptr_) {
@ -1221,20 +1228,20 @@ class App {
if(opt->get_required() || opt->count() != 0) {
// Make sure enough -N arguments parsed (+N is already handled in parsing function)
if(opt->get_items_expected() < 0 && opt->count() < static_cast<size_t>(-opt->get_items_expected()))
throw ArgumentMismatch::AtLeast(opt->single_name(), -opt->get_items_expected());
throw ArgumentMismatch::AtLeast(opt->get_name(), -opt->get_items_expected());
// Required but empty
if(opt->get_required() && opt->count() == 0)
throw RequiredError(opt->single_name());
throw RequiredError(opt->get_name());
}
// Requires
for(const Option *opt_req : opt->requires_)
if(opt->count() > 0 && opt_req->count() == 0)
throw RequiresError(opt->single_name(), opt_req->single_name());
throw RequiresError(opt->get_name(), opt_req->get_name());
// Excludes
for(const Option *opt_ex : opt->excludes_)
if(opt->count() > 0 && opt_ex->count() != 0)
throw ExcludesError(opt->single_name(), opt_ex->single_name());
throw ExcludesError(opt->get_name(), opt_ex->get_name());
}
auto selected_subcommands = get_subcommands();
@ -1508,7 +1515,7 @@ class App {
}
if(num > 0) {
throw ArgumentMismatch::TypedAtLeast(op->single_name(), num, op->get_type_name());
throw ArgumentMismatch::TypedAtLeast(op->get_name(), num, op->get_type_name());
}
}
@ -1524,7 +1531,7 @@ namespace FailureMessage {
inline std::string simple(const App *app, const Error &e) {
std::string header = std::string(e.what()) + "\n";
if(app->get_help_ptr() != nullptr)
header += "Run with " + app->get_help_ptr()->single_name() + " for more information.\n";
header += "Run with " + app->get_help_ptr()->get_name() + " for more information.\n";
return header;
}

View File

@ -0,0 +1,185 @@
#pragma once
// Distributed under the 3-Clause BSD License. See accompanying
// file LICENSE or https://github.com/CLIUtils/CLI11 for details.
#include <string>
#include "CLI/App.hpp"
#include "CLI/Formatter.hpp"
namespace CLI {
inline std::string
AppFormatter::make_group(std::string group, std::vector<const Option *> opts, OptionFormatMode mode) const {
std::stringstream out;
out << "\n" << group << ":\n";
for(const Option *opt : opts) {
out << opt->help(mode);
}
return out.str();
}
inline std::string AppFormatter::make_groups(const App *app, AppFormatMode mode) const {
std::stringstream out;
std::vector<std::string> groups = app->get_groups();
std::vector<const Option *> positionals =
app->get_options([](const Option *opt) { return !opt->get_group().empty() && opt->get_positional(); });
if(!positionals.empty())
out << make_group(get_label("Positionals"), positionals, OptionFormatMode::Positional);
// Options
for(const std::string &group : groups) {
std::vector<const Option *> grouped_items =
app->get_options([&group](const Option *opt) { return opt->nonpositional() && opt->get_group() == group; });
if(mode == AppFormatMode::Sub) {
grouped_items.erase(std::remove_if(grouped_items.begin(),
grouped_items.end(),
[app](const Option *opt) {
return app->get_help_ptr() == opt || app->get_help_all_ptr() == opt;
}),
grouped_items.end());
}
if(!group.empty() && !grouped_items.empty()) {
out << make_group(group, grouped_items, OptionFormatMode::Optional);
if(group != groups.back())
out << "\n";
}
}
return out.str();
}
inline std::string AppFormatter::make_description(const App *app) const {
std::string desc = app->get_description();
if(!desc.empty())
return desc + "\n";
else
return "";
}
inline std::string AppFormatter::make_usage(const App *app, std::string name) const {
std::stringstream out;
out << get_label("Usage") << ":" << (name.empty() ? "" : " ") << name;
std::vector<std::string> groups = app->get_groups();
// Print an Options badge if any options exist
std::vector<const Option *> non_pos_options =
app->get_options([](const Option *opt) { return opt->nonpositional(); });
if(!non_pos_options.empty())
out << " [" << get_label("OPTIONS") << "]";
// Positionals need to be listed here
std::vector<const Option *> positionals = app->get_options([](const Option *opt) { return opt->get_positional(); });
// Print out positionals if any are left
if(!positionals.empty()) {
// Convert to help names
std::vector<std::string> positional_names(positionals.size());
std::transform(positionals.begin(), positionals.end(), positional_names.begin(), [](const Option *opt) {
return opt->help(OptionFormatMode::Usage);
});
out << " " << detail::join(positional_names, " ");
}
// Add a marker if subcommands are expected or optional
if(!app->get_subcommands({}).empty()) {
out << " " << (app->get_require_subcommand_min() == 0 ? "[" : "")
<< get_label(app->get_require_subcommand_max() < 2 || app->get_require_subcommand_min() > 1 ? "SUBCOMMAND"
: "SUBCOMMANDS")
<< (app->get_require_subcommand_min() == 0 ? "]" : "");
}
out << std::endl;
return out.str();
}
inline std::string AppFormatter::make_footer(const App *app) const {
std::string footer = app->get_footer();
if(!footer.empty())
return footer + "\n";
else
return "";
}
inline std::string AppFormatter::operator()(const App *app, std::string name, AppFormatMode mode) const {
std::stringstream out;
if(mode == AppFormatMode::Normal) {
out << make_description(app);
out << make_usage(app, name);
out << make_groups(app, mode);
out << make_subcommands(app, mode);
out << make_footer(app);
} else if(mode == AppFormatMode::Sub) {
out << make_expanded(app);
} else if(mode == AppFormatMode::All) {
out << make_description(app);
out << make_usage(app, name);
out << make_groups(app, mode);
out << make_subcommands(app, mode);
}
return out.str();
}
inline std::string AppFormatter::make_subcommands(const App *app, AppFormatMode mode) const {
std::stringstream out;
std::vector<const App *> subcommands = app->get_subcommands({});
// Make a list in definition order of the groups seen
std::vector<std::string> subcmd_groups_seen;
for(const App *com : subcommands) {
std::string group_key = com->get_group();
if(!group_key.empty() &&
std::find_if(subcmd_groups_seen.begin(), subcmd_groups_seen.end(), [&group_key](std::string a) {
return detail::to_lower(a) == detail::to_lower(group_key);
}) == subcmd_groups_seen.end())
subcmd_groups_seen.push_back(group_key);
}
// For each group, filter out and print subcommands
for(const std::string &group : subcmd_groups_seen) {
out << "\n" << group << ":\n";
if(mode == AppFormatMode::All)
out << "\n";
std::vector<const App *> subcommands_group = app->get_subcommands(
[&group](const App *app) { return detail::to_lower(app->get_group()) == detail::to_lower(group); });
for(const App *new_com : subcommands_group) {
if(mode != AppFormatMode::All) {
out << make_subcommand(new_com);
} else {
out << new_com->help(new_com->get_name(), AppFormatMode::Sub);
if(new_com != subcommands_group.back())
out << "\n";
}
}
}
return out.str();
}
inline std::string AppFormatter::make_subcommand(const App *sub) const {
std::stringstream out;
detail::format_help(out, sub->get_name(), sub->get_description(), column_width_);
return out.str();
}
inline std::string AppFormatter::make_expanded(const App *sub) const {
std::stringstream out;
out << sub->get_name() << "\n " << sub->get_description();
out << make_groups(sub, AppFormatMode::Sub);
return out.str();
}
} // namespace CLI

View File

@ -24,6 +24,12 @@
#include "CLI/Validators.hpp"
#include "CLI/Formatter.hpp"
#include "CLI/Option.hpp"
#include "CLI/OptionFormatter.hpp"
#include "CLI/App.hpp"
#include "CLI/AppFormatter.hpp"

View File

@ -157,6 +157,13 @@ class CallForHelp : public ParseError {
CallForHelp() : CallForHelp("This should be caught in your main function, see examples", ExitCodes::Success) {}
};
/// Usually somethign like --help-all on command line
class CallForAllHelp : public ParseError {
CLI11_ERROR_DEF(ParseError, CallForAllHelp)
CallForAllHelp()
: CallForAllHelp("This should be caught in your main function, see examples", ExitCodes::Success) {}
};
/// Does not output a diagnostic in CLI11_PARSE, but allows to return from main() with a specific error code.
class RuntimeError : public ParseError {
CLI11_ERROR_DEF(ParseError, RuntimeError)

190
include/CLI/Formatter.hpp Normal file
View File

@ -0,0 +1,190 @@
#pragma once
// Distributed under the 3-Clause BSD License. See accompanying
// file LICENSE or https://github.com/CLIUtils/CLI11 for details.
#include <string>
#include <map>
#include "CLI/StringTools.hpp"
namespace CLI {
class Option;
class App;
/// This enum signifies what situation the option is beig printed in.
///
/// This is passed in as part of the built in formatter in App; it is
/// possible that a custom App formatter could avoid using it, however.
enum class OptionFormatMode {
Usage, //< In the program usage line
Positional, //< In the positionals
Optional //< In the normal optionals
};
/// This enum signifies the type of help requested
///
/// This is passed in by App; all user classes must accept this as
/// the second argument.
enum class AppFormatMode {
Normal, //< The normal, detailed help
All, //< A fully expanded help
Sub, //< Used when printed as part of expanded subcommand
};
/// This is an example formatter (and also the default formatter)
/// For option help.
class OptionFormatter {
protected:
/// @name Options
///@{
/// @brief The required help printout labels (user changeable)
/// Values are REQUIRED, NEEDS, EXCLUDES
std::map<std::string, std::string> labels_{{"REQUIRED", "(REQUIRED)"}};
/// The width of the first column
size_t column_width_{30};
///@}
/// @name Basic
///@{
public:
OptionFormatter() = default;
OptionFormatter(const OptionFormatter &) = default;
OptionFormatter(OptionFormatter &&) = default;
///@}
/// @name Setters
///@{
/// Set the "REQUIRED" label
void label(std::string key, std::string val) { labels_[key] = val; }
/// Set the column width
void column_width(size_t val) { column_width_ = val; }
///@}
/// @name Getters
///@{
/// Get the current value of a name (REQUIRED, etc.)
std::string get_label(std::string key) const {
if(labels_.find(key) == labels_.end())
return key;
else
return labels_.at(key);
}
/// Get the current column width
size_t get_column_width() const { return column_width_; }
///@}
/// @name Overridables
///@{
/// @brief This is the name part of an option, Default: left column
virtual std::string make_name(const Option *, OptionFormatMode) const;
/// @brief This is the options part of the name, Default: combined into left column
virtual std::string make_opts(const Option *) const;
/// @brief This is the description. Default: Right column, on new line if left column too large
virtual std::string make_desc(const Option *) const;
/// @brief This is used to print the name on the USAGE line (by App formatter)
virtual std::string make_usage(const Option *opt) const;
/// @brief This is the standard help combiner that does the "default" thing.
virtual std::string operator()(const Option *opt, OptionFormatMode mode) const {
std::stringstream out;
if(mode == OptionFormatMode::Usage)
out << make_usage(opt);
else
detail::format_help(out, make_name(opt, mode) + make_opts(opt), make_desc(opt), column_width_);
return out.str();
}
///@}
};
class AppFormatter {
/// @name Options
///@{
/// The width of the first column
size_t column_width_{30};
/// @brief The required help printout labels (user changeable)
/// Values are Needs, Excludes, etc.
std::map<std::string, std::string> labels_;
///@}
/// @name Basic
///@{
public:
AppFormatter() = default;
AppFormatter(const AppFormatter &) = default;
AppFormatter(AppFormatter &&) = default;
///@}
/// @name Setters
///@{
/// Set the "REQUIRED" label
void label(std::string key, std::string val) { labels_[key] = val; }
/// Set the column width
void column_width(size_t val) { column_width_ = val; }
///@}
/// @name Getters
///@{
/// Get the current value of a name (REQUIRED, etc.)
std::string get_label(std::string key) const {
if(labels_.find(key) == labels_.end())
return key;
else
return labels_.at(key);
}
/// Get the current column width
size_t get_column_width() const { return column_width_; }
/// @name Overridables
///@{
/// This prints out a group of options
virtual std::string make_group(std::string group, std::vector<const Option *> opts, OptionFormatMode mode) const;
/// This prints out all the groups of options
virtual std::string make_groups(const App *app, AppFormatMode mode) const;
/// This prints out all the subcommands
virtual std::string make_subcommands(const App *app, AppFormatMode mode) const;
/// This prints out a subcommand
virtual std::string make_subcommand(const App *sub) const;
/// This prints out a subcommand in help-all
virtual std::string make_expanded(const App *sub) const;
/// This prints out all the groups of options
virtual std::string make_footer(const App *app) const;
/// This displays the description line
virtual std::string make_description(const App *app) const;
/// This displays the usage line
virtual std::string make_usage(const App *app, std::string name) const;
/// This puts everything together
virtual std::string operator()(const App *, std::string, AppFormatMode) const;
///@}
};
} // namespace CLI

View File

@ -16,6 +16,7 @@
#include "CLI/Macros.hpp"
#include "CLI/Split.hpp"
#include "CLI/StringTools.hpp"
#include "CLI/Formatter.hpp"
namespace CLI {
@ -48,12 +49,17 @@ template <typename CRTP> class OptionBase {
/// Policy for multiple arguments when `expected_ == 1` (can be set on bool flags, too)
MultiOptionPolicy multi_option_policy_{MultiOptionPolicy::Throw};
/// @brief A formatter to print out help
/// Used by the default App help formatter.
std::function<std::string(const Option *, OptionFormatMode)> formatter_{OptionFormatter()};
template <typename T> void copy_to(T *other) const {
other->group(group_);
other->required(required_);
other->ignore_case(ignore_case_);
other->configurable(configurable_);
other->multi_option_policy(multi_option_policy_);
other->formatter(formatter_);
}
public:
@ -75,6 +81,12 @@ template <typename CRTP> class OptionBase {
/// Support Plumbum term
CRTP *mandatory(bool value = true) { return required(value); }
/// Set a formatter for this option
CRTP *formatter(std::function<std::string(const Option *, OptionFormatMode)> value) {
formatter_ = value;
return static_cast<CRTP *>(this);
}
// Getters
/// Get the group of this option
@ -250,11 +262,11 @@ class Option : public OptionBase<Option> {
Option *expected(int value) {
// Break if this is a flag
if(type_size_ == 0)
throw IncorrectConstruction::SetFlag(single_name());
throw IncorrectConstruction::SetFlag(get_name(true, true));
// Setting 0 is not allowed
else if(value == 0)
throw IncorrectConstruction::Set0Opt(single_name());
throw IncorrectConstruction::Set0Opt(get_name());
// No change is okay, quit now
else if(expected_ == value)
@ -262,11 +274,11 @@ class Option : public OptionBase<Option> {
// Type must be a vector
else if(type_size_ >= 0)
throw IncorrectConstruction::ChangeNotVector(single_name());
throw IncorrectConstruction::ChangeNotVector(get_name());
// TODO: Can support multioption for non-1 values (except for join)
else if(value != 1 && multi_option_policy_ != MultiOptionPolicy::Throw)
throw IncorrectConstruction::AfterMultiOpt(single_name());
throw IncorrectConstruction::AfterMultiOpt(get_name());
expected_ = value;
return this;
@ -295,7 +307,7 @@ class Option : public OptionBase<Option> {
Option *needs(Option *opt) {
auto tup = requires_.insert(opt);
if(!tup.second)
throw OptionAlreadyAdded::Requires(single_name(), opt->single_name());
throw OptionAlreadyAdded::Requires(get_name(), opt->get_name());
return this;
}
@ -356,7 +368,7 @@ class Option : public OptionBase<Option> {
for(const Option_p &opt : parent->options_)
if(opt.get() != this && *opt == *this)
throw OptionAlreadyAdded(opt->get_name());
throw OptionAlreadyAdded(opt->get_name(true, true));
return this;
}
@ -365,7 +377,7 @@ class Option : public OptionBase<Option> {
Option *multi_option_policy(MultiOptionPolicy value = MultiOptionPolicy::Throw) {
if(get_items_expected() < 0)
throw IncorrectConstruction::MultiOptionPolicy(single_name());
throw IncorrectConstruction::MultiOptionPolicy(get_name());
multi_option_policy_ = value;
return this;
}
@ -377,6 +389,21 @@ class Option : public OptionBase<Option> {
/// The number of arguments the option expects
int get_type_size() const { return type_size_; }
/// The type name (for help printing)
std::string get_typeval() const { return typeval_; }
/// The environment variable associated to this value
std::string get_envname() const { return envname_; }
/// The set of options needed
std::set<Option *> get_needs() const { return requires_; }
/// The set of options excluded
std::set<Option *> get_excludes() const { return excludes_; }
/// The default value (for help printing)
std::string get_defaultval() const { return defaultval_; }
/// The number of times the option expects to be included
int get_expected() const { return expected_; }
@ -416,91 +443,65 @@ class Option : public OptionBase<Option> {
/// Get the description
const std::string &get_description() const { return description_; }
// Just the pname
std::string get_pname() const { return pname_; }
///@}
/// @name Help tools
///@{
/// Gets a , sep list of names. Does not include the positional name if opt_only=true.
std::string get_name(bool opt_only = false) const {
std::vector<std::string> name_list;
if(!opt_only && pname_.length() > 0)
name_list.push_back(pname_);
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);
}
/// \brief Gets a comma seperated list of names.
/// Will include / prefer the positional name if positional is true.
/// If all_options is false, pick just the most descriptive name to show.
/// Use `get_name(true)` to get the positional name (replaces `get_pname`)
std::string get_name(bool positional = false, //<[input] Show the positional name
bool all_options = false //<[input] Show every option
) const {
/// The name and any extras needed for positionals
std::string help_positional() const {
std::string out = pname_;
if(get_expected() > 1)
out = out + "(" + std::to_string(get_expected()) + "x)";
else if(get_expected() == -1)
out = out + "...";
out = get_required() ? out : "[" + out + "]";
return out;
}
if(all_options) {
/// The most descriptive name available
std::string single_name() const {
if(!lnames_.empty())
return std::string("--") + lnames_[0];
else if(!snames_.empty())
return std::string("-") + snames_[0];
else
return pname_;
}
std::vector<std::string> name_list;
/// The first half of the help print, name plus default, etc. Setting opt_only to true avoids the positional name.
std::string help_name(bool opt_only = false) const {
std::stringstream out;
out << get_name(opt_only) << help_aftername();
return out.str();
}
/// The all list wil never include a positional unless asked or that's the only name.
if((positional && pname_.length()) || (snames_.empty() && lnames_.empty()))
name_list.push_back(pname_);
/// pname with type info
std::string help_pname() const {
std::stringstream out;
out << get_pname() << help_aftername();
return out.str();
}
for(const std::string &sname : snames_)
name_list.push_back("-" + sname);
/// This is the part after the name is printed but before the description
std::string help_aftername() const {
std::stringstream out;
for(const std::string &lname : lnames_)
name_list.push_back("--" + lname);
if(get_type_size() != 0) {
if(!typeval_.empty())
out << " " << typeval_;
if(!defaultval_.empty())
out << "=" << defaultval_;
if(get_expected() > 1)
out << " x " << get_expected();
if(get_expected() == -1)
out << " ...";
if(get_required())
out << " (REQUIRED)";
return detail::join(name_list);
} else {
// This returns the positional name no matter what
if(positional)
return pname_;
// Prefer long name
else if(!lnames_.empty())
return std::string("--") + lnames_[0];
// Or short name if no long name
else if(!snames_.empty())
return std::string("-") + snames_[0];
// If positional is the only name, it's okay to use that
else
return pname_;
}
if(!envname_.empty())
out << " (env:" << envname_ << ")";
if(!requires_.empty()) {
out << " Needs:";
for(const Option *opt : requires_)
out << " " << opt->single_name();
}
if(!excludes_.empty()) {
out << " Excludes:";
for(const Option *opt : excludes_)
out << " " << opt->single_name();
}
return out.str();
}
/// \brief Call this with a OptionFormatMode to run the currently configured help formatter.
///
/// Changed in Version 1.6:
///
/// * `help_positinoal` MOVED TO `help_usage` (name not included) or Usage mode
/// * `help_name` CHANGED to `help_name` with different true/false flags
/// * `pname` with type info MOVED to `help_name`
/// * `help_aftername()` MOVED to `help_opts()`
/// * Instead of `opt->help_mode()` use `opt->help(mode)`
std::string help(OptionFormatMode mode) const { return formatter_(this, mode); }
///@}
/// @name Parser tools
///@{
@ -514,7 +515,7 @@ class Option : public OptionBase<Option> {
for(const std::function<std::string(std::string &)> &vali : validators_) {
std::string err_msg = vali(result);
if(!err_msg.empty())
throw ValidationError(single_name(), err_msg);
throw ValidationError(get_name(), err_msg);
}
}
@ -542,7 +543,7 @@ class Option : public OptionBase<Option> {
// For now, vector of non size 1 types are not supported but possibility included here
if((get_items_expected() > 0 && results_.size() != static_cast<size_t>(get_items_expected())) ||
(get_items_expected() < 0 && results_.size() < static_cast<size_t>(-get_items_expected())))
throw ArgumentMismatch(single_name(), get_items_expected(), results_.size());
throw ArgumentMismatch(get_name(), get_items_expected(), results_.size());
else
local_result = !callback_(results_);
}
@ -651,17 +652,6 @@ class Option : public OptionBase<Option> {
/// Get the typename for this option
std::string get_type_name() const { return typeval_; }
///@}
protected:
/// @name App Helpers
///@{
/// Can print positional name detailed option if true
bool _has_help_positional() const {
return get_positional() && (has_description() || !requires_.empty() || !excludes_.empty());
}
///@}
};
} // namespace CLI

View File

@ -0,0 +1,65 @@
#pragma once
// Distributed under the 3-Clause BSD License. See accompanying
// file LICENSE or https://github.com/CLIUtils/CLI11 for details.
#include <string>
#include "CLI/Option.hpp"
#include "CLI/Formatter.hpp"
namespace CLI {
inline std::string OptionFormatter::make_name(const Option *opt, OptionFormatMode mode) const {
if(mode == OptionFormatMode::Optional)
return opt->get_name(false, true);
else
return opt->get_name(true, false);
}
inline std::string OptionFormatter::make_opts(const Option *opt) const {
std::stringstream out;
if(opt->get_type_size() != 0) {
if(!opt->get_typeval().empty())
out << " " << get_label(opt->get_typeval());
if(!opt->get_defaultval().empty())
out << "=" << opt->get_defaultval();
if(opt->get_expected() > 1)
out << " x " << opt->get_expected();
if(opt->get_expected() == -1)
out << " ...";
if(opt->get_required())
out << " " << get_label("REQUIRED");
}
if(!opt->get_envname().empty())
out << " (" << get_label("Env") << ":" << opt->get_envname() << ")";
if(!opt->get_needs().empty()) {
out << " " << get_label("Needs") << ":";
for(const Option *op : opt->get_needs())
out << " " << op->get_name();
}
if(!opt->get_excludes().empty()) {
out << " " << get_label("Excludes") << ":";
for(const Option *op : opt->get_excludes())
out << " " << op->get_name();
}
return out.str();
}
inline std::string OptionFormatter::make_desc(const Option *opt) const { return opt->get_description(); }
inline std::string OptionFormatter::make_usage(const Option *opt) const {
// Note that these are positionals usages
std::stringstream out;
out << make_name(opt, OptionFormatMode::Usage);
if(opt->get_expected() > 1)
out << "(" << std::to_string(opt->get_expected()) << "x)";
else if(opt->get_expected() < 0)
out << "...";
return opt->get_required() ? out.str() : "[" + out.str() + "]";
}
} // namespace CLI

View File

@ -104,15 +104,16 @@ inline std::string trim_copy(const std::string &str, const std::string &filter)
return trim(s, filter);
}
/// Print a two part "help" string
inline void format_help(std::stringstream &out, std::string name, std::string description, size_t wid) {
inline std::ostream &format_help(std::ostream &out, std::string name, std::string description, size_t wid) {
name = " " + name;
out << std::setw(static_cast<int>(wid)) << std::left << name;
if(!description.empty()) {
if(name.length() >= wid)
out << std::endl << std::setw(static_cast<int>(wid)) << "";
out << "\n" << std::setw(static_cast<int>(wid)) << "";
out << description;
}
out << std::endl;
out << "\n";
return out;
}
/// Verify the first character of an option

View File

@ -27,6 +27,7 @@ set(CLI11_TESTS
CreationTest
SubcommandTest
HelpTest
FormatterTest
NewParseTest
OptionalTest
)

View File

@ -75,7 +75,7 @@ TEST_F(TApp, RecoverSubcommands) {
CLI::App *app3 = app.add_subcommand("app3");
CLI::App *app4 = app.add_subcommand("app4");
EXPECT_EQ(app.get_subcommands(false), std::vector<CLI::App *>({app1, app2, app3, app4}));
EXPECT_EQ(app.get_subcommands({}), std::vector<CLI::App *>({app1, app2, app3, app4}));
}
TEST_F(TApp, MultipleSubcomMatchingWithCase) {
@ -329,10 +329,17 @@ TEST_F(TApp, GetNameCheck) {
auto a = app.add_flag("--that");
auto b = app.add_flag("-x");
auto c = app.add_option("pos", x);
auto d = app.add_option("one,-o,--other", x);
EXPECT_EQ(a->get_name(), "--that");
EXPECT_EQ(b->get_name(), "-x");
EXPECT_EQ(c->get_name(), "pos");
EXPECT_EQ(a->get_name(false, true), "--that");
EXPECT_EQ(b->get_name(false, true), "-x");
EXPECT_EQ(c->get_name(false, true), "pos");
EXPECT_EQ(d->get_name(), "--other");
EXPECT_EQ(d->get_name(false, false), "--other");
EXPECT_EQ(d->get_name(false, true), "-o,--other");
EXPECT_EQ(d->get_name(true, true), "one,-o,--other");
EXPECT_EQ(d->get_name(true, false), "one");
}
TEST_F(TApp, SubcommandDefaults) {

77
tests/FormatterTest.cpp Normal file
View File

@ -0,0 +1,77 @@
#ifdef CLI11_SINGLE_FILE
#include "CLI11.hpp"
#else
#include "CLI/CLI.hpp"
#endif
#include "gtest/gtest.h"
#include "gmock/gmock.h"
#include <fstream>
using ::testing::HasSubstr;
using ::testing::Not;
TEST(Formatter, Nothing) {
CLI::App app{"My prog"};
app.formatter(
[](const CLI::App *, std::string, CLI::AppFormatMode) { return std::string("This is really simple"); });
std::string help = app.help();
EXPECT_EQ(help, "This is really simple");
}
TEST(Formatter, OptCustomize) {
CLI::App app{"My prog"};
CLI::OptionFormatter optfmt;
optfmt.column_width(25);
optfmt.label("REQUIRED", "(MUST HAVE)");
app.option_defaults()->formatter(optfmt);
int v;
app.add_option("--opt", v, "Something")->required();
std::string help = app.help();
EXPECT_THAT(help, HasSubstr("(MUST HAVE)"));
EXPECT_EQ(help,
"My prog\n"
"Usage: [OPTIONS]\n\n"
"Options:\n"
" -h,--help Print this help message and exit\n"
" --opt INT (MUST HAVE) Something\n");
}
TEST(Formatter, AptCustomize) {
CLI::App app{"My prog"};
app.add_subcommand("subcom1", "This");
CLI::AppFormatter appfmt;
appfmt.column_width(20);
appfmt.label("Usage", "Run");
app.formatter(appfmt);
app.add_subcommand("subcom2", "This");
std::string help = app.help();
EXPECT_EQ(help,
"My prog\n"
"Run: [OPTIONS] [SUBCOMMAND]\n\n"
"Options:\n"
" -h,--help Print this help message and exit\n\n"
"Subcommands:\n"
" subcom1 This\n"
" subcom2 This\n");
}
TEST(Formatter, AllSub) {
CLI::App app{"My prog"};
CLI::App *sub = app.add_subcommand("subcom", "This");
sub->add_flag("--insub", "MyFlag");
std::string help = app.help("", CLI::AppFormatMode::All);
EXPECT_THAT(help, HasSubstr("--insub"));
EXPECT_THAT(help, HasSubstr("subcom"));
}

View File

@ -317,6 +317,27 @@ TEST(THelp, OnlyOneHelp) {
EXPECT_THROW(app.parse(input), CLI::ExtrasError);
}
TEST(THelp, OnlyOneAllHelp) {
CLI::App app{"My prog"};
// It is not supported to have more than one help flag, last one wins
app.set_help_all_flag("--help-all", "No short name allowed");
app.set_help_all_flag("--yelp", "Alias for help");
std::vector<std::string> input{"--help-all"};
EXPECT_THROW(app.parse(input), CLI::ExtrasError);
app.reset();
std::vector<std::string> input2{"--yelp"};
EXPECT_THROW(app.parse(input2), CLI::CallForAllHelp);
// Remove the flag
app.set_help_all_flag();
app.reset();
std::vector<std::string> input3{"--yelp"};
EXPECT_THROW(app.parse(input3), CLI::ExtrasError);
}
TEST(THelp, RemoveHelp) {
CLI::App app{"My prog"};
app.set_help_flag();
@ -385,9 +406,9 @@ TEST(THelp, NiceName) {
auto short_name = app.add_option("more,-x,-y", x);
auto positional = app.add_option("posit", x);
EXPECT_EQ(long_name->single_name(), "--long");
EXPECT_EQ(short_name->single_name(), "-x");
EXPECT_EQ(positional->single_name(), "posit");
EXPECT_EQ(long_name->get_name(), "--long");
EXPECT_EQ(short_name->get_name(), "-x");
EXPECT_EQ(positional->get_name(), "posit");
}
TEST(Exit, ErrorWithHelp) {
@ -401,6 +422,18 @@ TEST(Exit, ErrorWithHelp) {
}
}
TEST(Exit, ErrorWithAllHelp) {
CLI::App app{"My prog"};
app.set_help_all_flag("--help-all", "All help");
std::vector<std::string> input{"--help-all"};
try {
app.parse(input);
} catch(const CLI::CallForAllHelp &e) {
EXPECT_EQ(static_cast<int>(CLI::ExitCodes::Success), e.get_exit_code());
}
}
TEST(Exit, ErrorWithoutHelp) {
CLI::App app{"My prog"};
@ -453,6 +486,29 @@ TEST_F(CapturedHelp, CallForHelp) {
EXPECT_EQ(out.str(), app.help());
EXPECT_EQ(err.str(), "");
}
TEST_F(CapturedHelp, CallForAllHelp) {
EXPECT_EQ(run(CLI::CallForAllHelp()), 0);
EXPECT_EQ(out.str(), app.help("", CLI::AppFormatMode::All));
EXPECT_EQ(err.str(), "");
}
TEST_F(CapturedHelp, CallForAllHelpOutput) {
app.add_subcommand("one");
CLI::App *sub = app.add_subcommand("two");
sub->add_flag("--three");
EXPECT_EQ(run(CLI::CallForAllHelp()), 0);
EXPECT_EQ(out.str(), app.help("", CLI::AppFormatMode::All));
EXPECT_EQ(err.str(), "");
EXPECT_THAT(out.str(), HasSubstr("one"));
EXPECT_THAT(out.str(), HasSubstr("two"));
EXPECT_THAT(out.str(), HasSubstr("--three"));
}
TEST_F(CapturedHelp, NewFormattedHelp) {
app.formatter([](const CLI::App *, std::string, CLI::AppFormatMode) { return "New Help"; });
EXPECT_EQ(run(CLI::CallForHelp()), 0);
EXPECT_EQ(out.str(), "New Help");
EXPECT_EQ(err.str(), "");
}
TEST_F(CapturedHelp, NormalError) {
EXPECT_EQ(run(CLI::ExtrasError({"Thing"})), static_cast<int>(CLI::ExitCodes::ExtrasError));

View File

@ -448,6 +448,13 @@ TEST_F(TApp, PrefixSubcom) {
EXPECT_EQ(subc->remaining(), std::vector<std::string>({"other", "--simple", "--mine"}));
}
TEST_F(TApp, InheritHelpAllFlag) {
app.set_help_all_flag("--help-all");
auto subc = app.add_subcommand("subc");
auto help_opt_list = subc->get_options([](const CLI::Option *opt) { return opt->get_name() == "--help-all"; });
EXPECT_EQ(help_opt_list.size(), (size_t)1);
}
struct SubcommandProgram : public TApp {
CLI::App *start;
@ -536,6 +543,8 @@ TEST_F(TApp, SubcomInheritCaseCheck) {
run();
EXPECT_EQ((size_t)0, app.get_subcommands().size());
EXPECT_EQ((size_t)2, app.get_subcommands({}).size());
EXPECT_EQ((size_t)1, app.get_subcommands([](const CLI::App *s) { return s->get_name() == "sub1"; }).size());
app.reset();
args = {"SuB1"};