diff --git a/CHANGELOG.md b/CHANGELOG.md index 443926ae..da782137 100644 --- a/CHANGELOG.md +++ b/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. diff --git a/README.md b/README.md index 6e2cfc59..9ab8b7ae 100644 --- a/README.md +++ b/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` 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)`, 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. diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index c6851229..c4618ca4 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -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) diff --git a/examples/formatter.cpp b/examples/formatter.cpp new file mode 100644 index 00000000..788c0b41 --- /dev/null +++ b/examples/formatter.cpp @@ -0,0 +1,30 @@ +#include + +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; +} diff --git a/examples/subcommands.cpp b/examples/subcommands.cpp index 8872d3fc..580f4d1a 100644 --- a/examples/subcommands.cpp +++ b/examples/subcommands.cpp @@ -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?"); diff --git a/include/CLI/App.hpp b/include/CLI/App.hpp index 078ce71c..7e157b11 100644 --- a/include/CLI/App.hpp +++ b/include/CLI/App.hpp @@ -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 formatter_{AppFormatter()}; + /// The error message printing function INHERITABLE std::function 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 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(&e) != nullptr) { + out << help("", AppFormatMode::All); + return e.get_exit_code(); + } + if(e.get_exit_code() != static_cast(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 get_subcommands(bool parsed = true) const { - if(parsed) { - return parsed_subcommands_; - } else { - std::vector 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 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 get_subcommands(const std::function &filter) const { + std::vector 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 get_subcommands(const std::function &filter) { + std::vector 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 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 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