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:
parent
a061275eba
commit
952f2913e3
26
CHANGELOG.md
26
CHANGELOG.md
@ -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.
|
||||
|
||||
|
19
README.md
19
README.md
@ -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.
|
||||
|
||||
|
@ -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
30
examples/formatter.cpp
Normal 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;
|
||||
}
|
@ -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?");
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
185
include/CLI/AppFormatter.hpp
Normal file
185
include/CLI/AppFormatter.hpp
Normal 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
|
@ -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"
|
||||
|
@ -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
190
include/CLI/Formatter.hpp
Normal 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
|
@ -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
|
||||
|
65
include/CLI/OptionFormatter.hpp
Normal file
65
include/CLI/OptionFormatter.hpp
Normal 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
|
@ -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
|
||||
|
@ -27,6 +27,7 @@ set(CLI11_TESTS
|
||||
CreationTest
|
||||
SubcommandTest
|
||||
HelpTest
|
||||
FormatterTest
|
||||
NewParseTest
|
||||
OptionalTest
|
||||
)
|
||||
|
@ -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
77
tests/FormatterTest.cpp
Normal 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"));
|
||||
}
|
@ -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));
|
||||
|
@ -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"};
|
||||
|
Loading…
x
Reference in New Issue
Block a user