From 65442ad846e870cf527f67d04f9a16a108d5a2e4 Mon Sep 17 00:00:00 2001 From: Marc <12819635+LostInCompilation@users.noreply.github.com> Date: Mon, 7 Oct 2024 17:13:04 +0200 Subject: [PATCH] A better Help formatter (V2) (#866) _This is the new PR I've mentioned to work on in PR #858_ ## A better Help Formatter _See below for images of the new help page_ Finally, after a lot of planning, understanding CLI11's codebase, testing and coding, the new default Help Formatter is done. There are a lot of changes to make the help page more readable and closer to UNIX standards, see Changelog below for details. One of the highlights is automatic paragraph formatting with correct line wrapping for App and options/flag descriptions as well as the footer. A goal was to provide more flexibility and better readability for the help page while providing full compatibility with Apps using CLI11 (no breaking changes and no changes to Apps required). Also better support for different terminal sizes. Users can now specify three new optional attributes: `right_column_width_`, `description_paragraph_width_` and `footer_paragraph_width_`. See code documentation for more details. The different columns for options/flags now scale with the set `column_width_` value: Single dash flags occupy 33% of the set `column_width_`, double dash flags and options (like REQUIRED) 66%. These new attributes allow for indirectly respecting terminal geometry, footer paragraph formatting has also been added (#355). This PR also implements the issues #353 and #856. The new help page formatting can also be used as an input for man page generation, since it's oriented on the man page style (#413). [help2man](https://www.gnu.org/software/help2man/) can be used to generate man pages from help output (see comment down below for example). I thoroughly tested this code with all possible combinations of flags, options, positionals, subcommands, validators, ... So far everything works great. I hope this PR looks good and meets all requirements. I'm looking forward to the implementation of this PR into CLI11. If you have any questions or suggestions feel free to comment. ### Fixed/implemented issues by this PR - #353 Better options formatting - #856 Space between options - #355 Footer formatting - #413 Man page generation can be achieved using help2man with the new help formatting - https://github.com/CLIUtils/CLI11/issues/384#issuecomment-570066436 Better help formatting can be marked as complete ### What about the failing tests? Of course the tests expect the old help text format. This is why 6 of the tests are failing. Since it is a bit of work to migrate the tests to the new help format, I first wanted to push out this PR and get confirmation before I'll update all the tests. So please let me know if this PR gets implemented, what changes should be made and then I'll migrate the tests to the new help format, either in this PR or I'll make a new one. ## Changelog: #### There are _no breaking changes_. Every App using CLI11 will work with this new formatter with no changes required. - Added empty lines at beginning and end of help text - Removed double new-line between option groups for consistency. Now all sections have the same number of new-lines - Switched usage and description order - Only show "Usage"-string if no App name is present. This provides better readability - Made categories (Options, Positionals, ...) capital - Changed `ConfigBase::to_config` to correctly process capital "OPTIONS"-group (only affects descriptions of the config file, not a breaking change) - Added a paragraph formatter function `streamOutAsParagraph` to StringTools.hpp - Made "description" a paragraph block with correct, word respecting line wrapping and indentation (using the new paragraph formatter function) - Made the footer a paragraph block with correct, word respecting line wrapping and indentation - Updated documentation for `column_width_` to make it more clear - Added new member: `right_column_width_`, added getter and setter for `right_column_width_` - Added new member: `description_paragraph_width_`, added getter and setter for `description_paragraph_width_` - Added new member: `footer_paragraph_width_`, added getter and setter for `footer_paragraph_width_ ` - Positionals description are now formatted as paragraph with correct, word respecting line wrapping - Options description are now formatted as paragraph with correct, word respecting line wrapping - Short and long options/flags/names are now correctly formatted to always be at the right position (also for subcommand options/flags) - Short and long options/flags/names column widths scale linearly with the `column_width_` attribute to better adapt to different `column_width_` sizes - Merged PR #860 ## What's planned for the future? - I'm thinking of better formatting the options of flags (like REQUIRED, TEXT, INT, ...) and make them also in a seperate column. This way they would also always be at the same position. However I decided against it for this PR, since I wanted them to be as close as possible to the actual flag. With my implementation it is quite easy to add this change in the future. - Subcommands: I'm planning on better formatting the Subcommands. With this PR only the short and long flags/options of subcommands are better formatted (like it is with the main flags, see images down below). - Maybe implement a different way to display expected data type options (TEXT, INT, ...). For example: `--file-name=` for long flags only and if `disable_flag_override_` is false. - Maybe add something like this: https://github.com/CLIUtils/CLI11/issues/554 --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Philip Top --- .codecov.yml | 8 + examples/CMakeLists.txt | 8 +- fuzz/fuzzApp.cpp | 1 + include/CLI/App.hpp | 2 +- include/CLI/FormatterFwd.hpp | 44 +++-- include/CLI/Option.hpp | 2 +- include/CLI/StringTools.hpp | 11 +- include/CLI/TypeTools.hpp | 16 +- include/CLI/impl/App_inl.hpp | 6 + include/CLI/impl/Config_inl.hpp | 8 +- include/CLI/impl/Formatter_inl.hpp | 157 +++++++++++++--- include/CLI/impl/StringTools_inl.hpp | 49 +++-- tests/CreationTest.cpp | 4 +- tests/FormatterTest.cpp | 48 ++--- tests/HelpTest.cpp | 269 ++++++++++++++++++++------- tests/OptionTypeTest.cpp | 12 ++ tests/SubcommandTest.cpp | 6 +- 17 files changed, 467 insertions(+), 184 deletions(-) diff --git a/.codecov.yml b/.codecov.yml index 61c2e2f2..7ba78c16 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -5,3 +5,11 @@ ignore: - "docs" - "test_package" - "fuzz" + +parsers: + gcov: + branch_detection: + conditional: yes + loop: yes + method: no + macro: no diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index b95165c6..24a5be64 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -75,8 +75,8 @@ set_property( "Working on count: 2, direct count: 2, opt count: 2" "Some value: 1.2") # test shows that the help prints out for unnamed subcommands add_test(NAME subcom_partitioned_help COMMAND subcom_partitioned --help) -set_property(TEST subcom_partitioned_help PROPERTY PASS_REGULAR_EXPRESSION - "-f,--file TEXT REQUIRED" "-d,--double FLOAT") +set_property(TEST subcom_partitioned_help + PROPERTY PASS_REGULAR_EXPRESSION "-f,[ \\t]*--file TEXT REQUIRED" "-d,--double FLOAT") #################################################### add_cli_exe(config_app config_app.cpp) @@ -145,8 +145,8 @@ add_cli_exe(validators validators.cpp) add_test(NAME validators_help COMMAND validators --help) set_property( TEST validators_help - PROPERTY PASS_REGULAR_EXPRESSION " -f,--file TEXT:FILE[\\r\\n\\t ]+File name" - " -v,--value INT:INT in [3 - 6][\\r\\n\\t ]+Value in range") + PROPERTY PASS_REGULAR_EXPRESSION " -f,[ \\t]*--file TEXT:FILE[\\r\\n\\t ]+File name" + " -v,[ \\t]*--value INT:INT in [3 - 6][\\r\\n\\t ]+Value in range") add_test(NAME validators_file COMMAND validators --file nonex.xxx) set_property( TEST validators_file PROPERTY PASS_REGULAR_EXPRESSION "--file: File does not exist: nonex.xxx" diff --git a/fuzz/fuzzApp.cpp b/fuzz/fuzzApp.cpp index 16123ce1..092c2397 100644 --- a/fuzz/fuzzApp.cpp +++ b/fuzz/fuzzApp.cpp @@ -44,6 +44,7 @@ std::optional> tcomplex; std::string_view vstrv; std::shared_ptr FuzzApp::generateApp() { auto fApp = std::make_shared("fuzzing App", "fuzzer"); fApp->set_config("--config"); + fApp->set_help_all_flag("--help-all"); fApp->add_flag("-a,--flag"); fApp->add_flag("-b,--flag2,!--nflag2", flag1); fApp->add_flag("-c{34},--flag3{1}", flagCnt)->disable_flag_override(); diff --git a/include/CLI/App.hpp b/include/CLI/App.hpp index 892bd1f2..f98463f9 100644 --- a/include/CLI/App.hpp +++ b/include/CLI/App.hpp @@ -275,7 +275,7 @@ class App { App *parent_{nullptr}; /// The group membership INHERITABLE - std::string group_{"Subcommands"}; + std::string group_{"SUBCOMMANDS"}; /// Alias names for the subcommand std::vector aliases_{}; diff --git a/include/CLI/FormatterFwd.hpp b/include/CLI/FormatterFwd.hpp index ad3f49c3..f86226c9 100644 --- a/include/CLI/FormatterFwd.hpp +++ b/include/CLI/FormatterFwd.hpp @@ -44,9 +44,18 @@ class FormatterBase { /// @name Options ///@{ - /// The width of the first column + /// The width of the left column (options/flags/subcommands) std::size_t column_width_{30}; + /// The width of the right column (description of options/flags/subcommands) + std::size_t right_column_width_{65}; + + /// The width of the description paragraph at the top of help + std::size_t description_paragraph_width_{80}; + + /// The width of the footer paragraph + std::size_t footer_paragraph_width_{80}; + /// @brief The required help printout labels (user changeable) /// Values are Needs, Excludes, etc. std::map labels_{}; @@ -75,9 +84,18 @@ class FormatterBase { /// Set the "REQUIRED" label void label(std::string key, std::string val) { labels_[key] = val; } - /// Set the column width + /// Set the left column width (options/flags/subcommands) void column_width(std::size_t val) { column_width_ = val; } + /// Set the right column width (description of options/flags/subcommands) + void right_column_width(std::size_t val) { right_column_width_ = val; } + + /// Set the description paragraph width at the top of help + void description_paragraph_width(std::size_t val) { description_paragraph_width_ = val; } + + /// Set the footer paragraph width + void footer_paragraph_width(std::size_t val) { footer_paragraph_width_ = val; } + ///@} /// @name Getters ///@{ @@ -89,9 +107,18 @@ class FormatterBase { return labels_.at(key); } - /// Get the current column width + /// Get the current left column width (options/flags/subcommands) CLI11_NODISCARD std::size_t get_column_width() const { return column_width_; } + /// Get the current right column width (description of options/flags/subcommands) + CLI11_NODISCARD std::size_t get_right_column_width() const { return right_column_width_; } + + /// Get the current description paragraph width at the top of help + CLI11_NODISCARD std::size_t get_description_paragraph_width() const { return description_paragraph_width_; } + + /// Get the current footer paragraph width + CLI11_NODISCARD std::size_t get_footer_paragraph_width() const { return footer_paragraph_width_; } + ///@} }; @@ -146,7 +173,7 @@ class Formatter : public FormatterBase { 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; + virtual std::string make_expanded(const App *sub, AppFormatMode mode) const; /// This prints out all the groups of options virtual std::string make_footer(const App *app) const; @@ -158,19 +185,14 @@ class Formatter : public FormatterBase { virtual std::string make_usage(const App *app, std::string name) const; /// This puts everything together - std::string make_help(const App * /*app*/, std::string, AppFormatMode) const override; + std::string make_help(const App *app, std::string, AppFormatMode mode) const override; ///@} /// @name Options ///@{ /// This prints out an option help line, either positional or optional form - virtual std::string make_option(const Option *opt, bool is_positional) const { - std::stringstream out; - detail::format_help( - out, make_option_name(opt, is_positional) + make_option_opts(opt), make_option_desc(opt), column_width_); - return out.str(); - } + virtual std::string make_option(const Option *, bool) const; /// @brief This is the name part of an option, Default: left column virtual std::string make_option_name(const Option *, bool) const; diff --git a/include/CLI/Option.hpp b/include/CLI/Option.hpp index 40b7f6f6..f6f848aa 100644 --- a/include/CLI/Option.hpp +++ b/include/CLI/Option.hpp @@ -54,7 +54,7 @@ template class OptionBase { protected: /// The group membership - std::string group_ = std::string("Options"); + std::string group_ = std::string("OPTIONS"); /// True if this is a required option bool required_{false}; diff --git a/include/CLI/StringTools.hpp b/include/CLI/StringTools.hpp index 81bdf152..e2df32fb 100644 --- a/include/CLI/StringTools.hpp +++ b/include/CLI/StringTools.hpp @@ -141,9 +141,6 @@ inline std::string trim_copy(const std::string &str, const std::string &filter) std::string s = str; return trim(s, filter); } -/// Print a two part "help" string -CLI11_INLINE std::ostream & -format_help(std::ostream &out, std::string name, const std::string &description, std::size_t wid); /// Print subcommand aliases CLI11_INLINE std::ostream &format_aliases(std::ostream &out, const std::vector &aliases, std::size_t wid); @@ -263,6 +260,14 @@ CLI11_INLINE std::string extract_binary_string(const std::string &escaped_string /// process a quoted string, remove the quotes and if appropriate handle escaped characters CLI11_INLINE bool process_quoted_string(std::string &str, char string_char = '\"', char literal_char = '\''); +/// This function formats the given text as a paragraph with fixed width and applies correct line wrapping +/// with a custom line prefix. The paragraph will get streamed to the given ostrean. +CLI11_INLINE std::ostream &streamOutAsParagraph(std::ostream &out, + const std::string &text, + std::size_t paragraphWidth, + const std::string &linePrefix = "", + bool skipPrefixOnFirstLine = false); + } // namespace detail // [CLI11:string_tools_hpp:end] diff --git a/include/CLI/TypeTools.hpp b/include/CLI/TypeTools.hpp index a812ed94..4cd7a9f5 100644 --- a/include/CLI/TypeTools.hpp +++ b/include/CLI/TypeTools.hpp @@ -904,7 +904,7 @@ bool integral_conversion(const std::string &input, T &output) noexcept { nstring.erase(std::remove(nstring.begin(), nstring.end(), '\''), nstring.end()); return integral_conversion(nstring, output); } - if(input.compare(0, 2, "0o") == 0) { + if(input.compare(0, 2, "0o") == 0 || input.compare(0, 2, "0O") == 0) { val = nullptr; errno = 0; output_ll = std::strtoull(input.c_str() + 2, &val, 8); @@ -914,7 +914,10 @@ bool integral_conversion(const std::string &input, T &output) noexcept { output = static_cast(output_ll); return (val == (input.c_str() + input.size()) && static_cast(output) == output_ll); } - if(input.compare(0, 2, "0b") == 0) { + if(input.compare(0, 2, "0b") == 0 || input.compare(0, 2, "0B") == 0) { + // LCOV_EXCL_START + // In some new compilers including the coverage testing one binary strings are handled properly in strtoull + // automatically so this coverage is missing but is well tested in other compilers val = nullptr; errno = 0; output_ll = std::strtoull(input.c_str() + 2, &val, 2); @@ -923,6 +926,7 @@ bool integral_conversion(const std::string &input, T &output) noexcept { } output = static_cast(output_ll); return (val == (input.c_str() + input.size()) && static_cast(output) == output_ll); + // LCOV_EXCL_STOP } return false; } @@ -955,7 +959,7 @@ bool integral_conversion(const std::string &input, T &output) noexcept { nstring.erase(std::remove(nstring.begin(), nstring.end(), '\''), nstring.end()); return integral_conversion(nstring, output); } - if(input.compare(0, 2, "0o") == 0) { + if(input.compare(0, 2, "0o") == 0 || input.compare(0, 2, "0O") == 0) { val = nullptr; errno = 0; output_ll = std::strtoll(input.c_str() + 2, &val, 8); @@ -965,7 +969,10 @@ bool integral_conversion(const std::string &input, T &output) noexcept { output = static_cast(output_ll); return (val == (input.c_str() + input.size()) && static_cast(output) == output_ll); } - if(input.compare(0, 2, "0b") == 0) { + if(input.compare(0, 2, "0b") == 0 || input.compare(0, 2, "0B") == 0) { + // LCOV_EXCL_START + // In some new compilers including the coverage testing one binary strings are handled properly in strtoll + // automatically so this coverage is missing but is well tested in other compilers val = nullptr; errno = 0; output_ll = std::strtoll(input.c_str() + 2, &val, 2); @@ -974,6 +981,7 @@ bool integral_conversion(const std::string &input, T &output) noexcept { } output = static_cast(output_ll); return (val == (input.c_str() + input.size()) && static_cast(output) == output_ll); + // LCOV_EXCL_STOP } return false; } diff --git a/include/CLI/impl/App_inl.hpp b/include/CLI/impl/App_inl.hpp index 12b095a2..b66bab7c 100644 --- a/include/CLI/impl/App_inl.hpp +++ b/include/CLI/impl/App_inl.hpp @@ -2265,11 +2265,14 @@ CLI11_INLINE void retire_option(App *app, Option *opt) { ->expected(option_copy->get_expected_min(), option_copy->get_expected_max()) ->allow_extra_args(option_copy->get_allow_extra_args()); + // LCOV_EXCL_START + // something odd with coverage on new compilers Validator retired_warning{[opt2](std::string &) { std::cout << "WARNING " << opt2->get_name() << " is retired and has no effect\n"; return std::string(); }, ""}; + // LCOV_EXCL_STOP retired_warning.application_index(0); opt2->check(retired_warning); } @@ -2287,11 +2290,14 @@ CLI11_INLINE void retire_option(App *app, const std::string &option_name) { ->type_name("RETIRED") ->expected(0, 1) ->default_str("RETIRED"); + // LCOV_EXCL_START + // something odd with coverage on new compilers Validator retired_warning{[opt2](std::string &) { std::cout << "WARNING " << opt2->get_name() << " is retired and has no effect\n"; return std::string(); }, ""}; + // LCOV_EXCL_STOP retired_warning.application_index(0); opt2->check(retired_warning); } diff --git a/include/CLI/impl/Config_inl.hpp b/include/CLI/impl/Config_inl.hpp index 848f0b26..83cd4432 100644 --- a/include/CLI/impl/Config_inl.hpp +++ b/include/CLI/impl/Config_inl.hpp @@ -517,18 +517,18 @@ ConfigBase::to_config(const App *app, bool default_also, bool write_description, std::vector groups = app->get_groups(); bool defaultUsed = false; - groups.insert(groups.begin(), std::string("Options")); + groups.insert(groups.begin(), std::string("OPTIONS")); if(write_description && (app->get_configurable() || app->get_parent() == nullptr || app->get_name().empty())) { out << commentLead << detail::fix_newlines(commentLead, app->get_description()) << '\n'; } for(auto &group : groups) { - if(group == "Options" || group.empty()) { + if(group == "OPTIONS" || group.empty()) { if(defaultUsed) { continue; } defaultUsed = true; } - if(write_description && group != "Options" && !group.empty()) { + if(write_description && group != "OPTIONS" && !group.empty()) { out << '\n' << commentLead << group << " Options\n"; } for(const Option *opt : app->get_options({})) { @@ -536,7 +536,7 @@ ConfigBase::to_config(const App *app, bool default_also, bool write_description, // Only process options that are configurable if(opt->get_configurable()) { if(opt->get_group() != group) { - if(!(group == "Options" && opt->get_group().empty())) { + if(!(group == "OPTIONS" && opt->get_group().empty())) { continue; } } diff --git a/include/CLI/impl/Formatter_inl.hpp b/include/CLI/impl/Formatter_inl.hpp index a5b8d043..9a5e9f7b 100644 --- a/include/CLI/impl/Formatter_inl.hpp +++ b/include/CLI/impl/Formatter_inl.hpp @@ -39,7 +39,7 @@ CLI11_INLINE std::string Formatter::make_positionals(const App *app) const { if(opts.empty()) return {}; - return make_group(get_label("Positionals"), true, opts); + return make_group(get_label("POSITIONALS"), true, opts); } CLI11_INLINE std::string Formatter::make_groups(const App *app, AppFormatMode mode) const { @@ -58,8 +58,9 @@ CLI11_INLINE std::string Formatter::make_groups(const App *app, AppFormatMode mo if(!group.empty() && !opts.empty()) { out << make_group(group, false, opts); - if(group != groups.back()) - out << "\n"; + // Removed double newline between groups for consistency of help text + // if(group != groups.back()) + // out << "\n"; } } @@ -95,12 +96,16 @@ CLI11_INLINE std::string Formatter::make_description(const App *app) const { CLI11_INLINE std::string Formatter::make_usage(const App *app, std::string name) const { std::string usage = app->get_usage(); if(!usage.empty()) { - return usage + "\n"; + return usage + "\n\n"; } std::stringstream out; + out << '\n'; - out << get_label("Usage") << ":" << (name.empty() ? "" : " ") << name; + if(name.empty()) + out << get_label("Usage") << ':'; + else + out << name; std::vector groups = app->get_groups(); @@ -128,13 +133,13 @@ CLI11_INLINE std::string Formatter::make_usage(const App *app, std::string name) if(!app->get_subcommands( [](const CLI::App *subc) { return ((!subc->get_disabled()) && (!subc->get_name().empty())); }) .empty()) { - out << " " << (app->get_require_subcommand_min() == 0 ? "[" : "") + 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 << '\n'; + out << "\n\n"; return out.str(); } @@ -144,29 +149,29 @@ CLI11_INLINE std::string Formatter::make_footer(const App *app) const { if(footer.empty()) { return std::string{}; } - return "\n" + footer + "\n"; + return '\n' + footer + "\n\n"; } CLI11_INLINE std::string Formatter::make_help(const App *app, std::string name, AppFormatMode mode) const { - // This immediately forwards to the make_expanded method. This is done this way so that subcommands can // have overridden formatters if(mode == AppFormatMode::Sub) - return make_expanded(app); + return make_expanded(app, mode); std::stringstream out; if((app->get_name().empty()) && (app->get_parent() != nullptr)) { - if(app->get_group() != "Subcommands") { + if(app->get_group() != "SUBCOMMANDS") { out << app->get_group() << ':'; } } - out << make_description(app); out << make_usage(app, name); + detail::streamOutAsParagraph( + out, make_description(app), description_paragraph_width_, " "); // Format description as paragraph out << make_positionals(app); out << make_groups(app, mode); out << make_subcommands(app, mode); - out << make_footer(app); + detail::streamOutAsParagraph(out, make_footer(app), footer_paragraph_width_); // Format footer as paragraph return out.str(); } @@ -181,7 +186,7 @@ CLI11_INLINE std::string Formatter::make_subcommands(const App *app, AppFormatMo for(const App *com : subcommands) { if(com->get_name().empty()) { if(!com->get_group().empty() && com->get_group().front() != '+') { - out << make_expanded(com); + out << make_expanded(com, mode); } continue; } @@ -195,7 +200,7 @@ CLI11_INLINE std::string Formatter::make_subcommands(const App *app, AppFormatMo // For each group, filter out and print subcommands for(const std::string &group : subcmd_groups_seen) { - out << "\n" << group << ":\n"; + out << '\n' << group << ":\n"; std::vector subcommands_group = app->get_subcommands( [&group](const App *sub_app) { return detail::to_lower(sub_app->get_group()) == detail::to_lower(group); }); for(const App *new_com : subcommands_group) { @@ -205,7 +210,7 @@ CLI11_INLINE std::string Formatter::make_subcommands(const App *app, AppFormatMo out << make_subcommand(new_com); } else { out << new_com->help(new_com->get_name(), AppFormatMode::Sub); - out << "\n"; + out << '\n'; } } } @@ -215,31 +220,123 @@ CLI11_INLINE std::string Formatter::make_subcommands(const App *app, AppFormatMo CLI11_INLINE std::string Formatter::make_subcommand(const App *sub) const { std::stringstream out; - detail::format_help(out, - sub->get_display_name(true) + (sub->get_required() ? " " + get_label("REQUIRED") : ""), - sub->get_description(), - column_width_); + std::string name = " " + sub->get_display_name(true) + (sub->get_required() ? " " + get_label("REQUIRED") : ""); + + out << std::setw(static_cast(column_width_)) << std::left << name; + detail::streamOutAsParagraph( + out, sub->get_description(), right_column_width_, std::string(column_width_, ' '), true); + out << '\n'; return out.str(); } -CLI11_INLINE std::string Formatter::make_expanded(const App *sub) const { +CLI11_INLINE std::string Formatter::make_expanded(const App *sub, AppFormatMode mode) const { std::stringstream out; - out << sub->get_display_name(true) << "\n"; + out << sub->get_display_name(true) << '\n'; + + detail::streamOutAsParagraph( + out, make_description(sub), description_paragraph_width_, " "); // Format description as paragraph - out << make_description(sub); if(sub->get_name().empty() && !sub->get_aliases().empty()) { detail::format_aliases(out, sub->get_aliases(), column_width_ + 2); } + out << make_positionals(sub); - out << make_groups(sub, AppFormatMode::Sub); - out << make_subcommands(sub, AppFormatMode::Sub); + out << make_groups(sub, mode); + out << make_subcommands(sub, mode); + detail::streamOutAsParagraph(out, make_footer(sub), footer_paragraph_width_); // Format footer as paragraph - // Drop blank spaces - std::string tmp = detail::find_and_replace(out.str(), "\n\n", "\n"); - tmp = tmp.substr(0, tmp.size() - 1); // Remove the final '\n' + out << '\n'; + return out.str(); +} - // Indent all but the first line (the name) - return detail::find_and_replace(tmp, "\n", "\n ") + "\n"; +CLI11_INLINE std::string Formatter::make_option(const Option *opt, bool is_positional) const { + std::stringstream out; + if(is_positional) { + const std::string left = " " + make_option_name(opt, true) + make_option_opts(opt); + const std::string desc = make_option_desc(opt); + out << std::setw(static_cast(column_width_)) << std::left << left; + + if(!desc.empty()) { + bool skipFirstLinePrefix = true; + if(left.length() >= column_width_) { + out << '\n'; + skipFirstLinePrefix = false; + } + detail::streamOutAsParagraph( + out, desc, right_column_width_, std::string(column_width_, ' '), skipFirstLinePrefix); + } + } else { + const std::string namesCombined = make_option_name(opt, false); + const std::string opts = make_option_opts(opt); + const std::string desc = make_option_desc(opt); + + // Split all names at comma and sort them into short names and long names + const auto names = detail::split(namesCombined, ','); + std::vector vshortNames; + std::vector vlongNames; + std::for_each(names.begin(), names.end(), [&vshortNames, &vlongNames](const std::string &name) { + if(name.find("--", 0) != std::string::npos) + vlongNames.push_back(name); + else + vshortNames.push_back(name); + }); + + // Assemble short and long names + std::string shortNames = detail::join(vshortNames, ", "); + std::string longNames = detail::join(vlongNames, ", "); + + // Calculate setw sizes + const auto shortNamesColumnWidth = static_cast(column_width_ / 3); // 33% left for short names + const auto longNamesColumnWidth = static_cast(std::ceil( + static_cast(column_width_) / 3.0f * 2.0f)); // 66% right for long names and options, ceil result + int shortNamesOverSize = 0; + + // Print short names + if(shortNames.length() > 0) { + shortNames = " " + shortNames; // Indent + if(longNames.length() == 0 && opts.length() > 0) + shortNames += opts; // Add opts if only short names and no long names + if(longNames.length() > 0) + shortNames += ","; + if(static_cast(shortNames.length()) >= shortNamesColumnWidth) { + shortNames += " "; + shortNamesOverSize = static_cast(shortNames.length()) - shortNamesColumnWidth; + } + out << std::setw(shortNamesColumnWidth) << std::left << shortNames; + } else { + out << std::setw(shortNamesColumnWidth) << std::left << ""; + } + + // Adjust long name column width in case of short names column reaching into long names column + shortNamesOverSize = + (std::min)(shortNamesOverSize, longNamesColumnWidth); // Prevent negative result with unsigned integers + const auto adjustedLongNamesColumnWidth = longNamesColumnWidth - shortNamesOverSize; + + // Print long names + if(longNames.length() > 0) { + if(opts.length() > 0) + longNames += opts; + if(static_cast(longNames.length()) >= adjustedLongNamesColumnWidth) + longNames += " "; + + out << std::setw(adjustedLongNamesColumnWidth) << std::left << longNames; + } else { + out << std::setw(adjustedLongNamesColumnWidth) << std::left << ""; + } + + if(!desc.empty()) { + bool skipFirstLinePrefix = true; + if(out.str().length() > column_width_) { + out << '\n'; + skipFirstLinePrefix = false; + } + detail::streamOutAsParagraph( + out, desc, right_column_width_, std::string(column_width_, ' '), skipFirstLinePrefix); + } + } + + out << '\n'; + return out.str(); } CLI11_INLINE std::string Formatter::make_option_name(const Option *opt, bool is_positional) const { diff --git a/include/CLI/impl/StringTools_inl.hpp b/include/CLI/impl/StringTools_inl.hpp index e3775619..1dffb126 100644 --- a/include/CLI/impl/StringTools_inl.hpp +++ b/include/CLI/impl/StringTools_inl.hpp @@ -95,24 +95,6 @@ CLI11_INLINE std::string fix_newlines(const std::string &leader, std::string inp return input; } -CLI11_INLINE std::ostream & -format_help(std::ostream &out, std::string name, const std::string &description, std::size_t wid) { - name = " " + name; - out << std::setw(static_cast(wid)) << std::left << name; - if(!description.empty()) { - if(name.length() >= wid) - out << "\n" << std::setw(static_cast(wid)) << ""; - for(const char c : description) { - out.put(c); - if(c == '\n') { - out << std::setw(static_cast(wid)) << ""; - } - } - } - out << "\n"; - return out; -} - CLI11_INLINE std::ostream &format_aliases(std::ostream &out, const std::vector &aliases, std::size_t wid) { if(!aliases.empty()) { out << std::setw(static_cast(wid)) << " aliases: "; @@ -573,6 +555,37 @@ std::string get_environment_value(const std::string &env_name) { return ename_string; } +CLI11_INLINE std::ostream &streamOutAsParagraph(std::ostream &out, + const std::string &text, + std::size_t paragraphWidth, + const std::string &linePrefix, + bool skipPrefixOnFirstLine) { + if(!skipPrefixOnFirstLine) + out << linePrefix; // First line prefix + + std::istringstream lss(text); + std::string line = ""; + while(std::getline(lss, line)) { + std::istringstream iss(line); + std::string word = ""; + std::size_t charsWritten = 0; + + while(iss >> word) { + if(word.length() + charsWritten > paragraphWidth) { + out << '\n' << linePrefix; + charsWritten = 0; + } + + out << word << " "; + charsWritten += word.length() + 1; + } + + if(!lss.eof()) + out << '\n' << linePrefix; + } + return out; +} + } // namespace detail // [CLI11:string_tools_inl_hpp:end] } // namespace CLI diff --git a/tests/CreationTest.cpp b/tests/CreationTest.cpp index 46f57770..1907115a 100644 --- a/tests/CreationTest.cpp +++ b/tests/CreationTest.cpp @@ -438,7 +438,7 @@ TEST_CASE_METHOD(TApp, "OptionFromDefaultsSubcommands", "[creation]") { CHECK(!app.option_defaults()->get_ignore_underscore()); CHECK(!app.option_defaults()->get_disable_flag_override()); CHECK(app.option_defaults()->get_configurable()); - CHECK("Options" == app.option_defaults()->get_group()); + CHECK("OPTIONS" == app.option_defaults()->get_group()); app.option_defaults() ->required() @@ -498,7 +498,7 @@ TEST_CASE_METHOD(TApp, "SubcommandDefaults", "[creation]") { CHECK(app.get_usage().empty()); CHECK(app.get_footer().empty()); - CHECK("Subcommands" == app.get_group()); + CHECK("SUBCOMMANDS" == app.get_group()); CHECK(0u == app.get_require_subcommand_min()); CHECK(0u == app.get_require_subcommand_max()); diff --git a/tests/FormatterTest.cpp b/tests/FormatterTest.cpp index 215dcb10..b88232b4 100644 --- a/tests/FormatterTest.cpp +++ b/tests/FormatterTest.cpp @@ -57,11 +57,9 @@ TEST_CASE("Formatter: OptCustomize", "[formatter]") { std::string help = app.help(); CHECK_THAT(help, Contains("(MUST HAVE)")); - CHECK(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"); + CHECK_THAT(help, Contains("Something")); + CHECK_THAT(help, Contains("--opt INT")); + CHECK_THAT(help, Contains("-h, --help Print")); } TEST_CASE("Formatter: OptCustomizeSimple", "[formatter]") { @@ -76,11 +74,10 @@ TEST_CASE("Formatter: OptCustomizeSimple", "[formatter]") { std::string help = app.help(); CHECK_THAT(help, Contains("(MUST HAVE)")); - CHECK(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"); + CHECK_THAT(help, Contains("(MUST HAVE)")); + CHECK_THAT(help, Contains("Something")); + CHECK_THAT(help, Contains("--opt INT")); + CHECK_THAT(help, Contains("-h, --help Print")); } TEST_CASE("Formatter: OptCustomizeOptionText", "[formatter]") { @@ -94,11 +91,6 @@ TEST_CASE("Formatter: OptCustomizeOptionText", "[formatter]") { std::string help = app.help(); CHECK_THAT(help, Contains("(ARG)")); - CHECK(help == "My prog\n" - "Usage: [OPTIONS]\n\n" - "Options:\n" - " -h,--help Print this help message and exit\n" - " --opt (ARG) Something\n"); } TEST_CASE("Formatter: FalseFlagExample", "[formatter]") { @@ -129,16 +121,13 @@ TEST_CASE("Formatter: AppCustomize", "[formatter]") { appfmt->label("Usage", "Run"); app.formatter(appfmt); - app.add_subcommand("subcom2", "This"); + app.add_subcommand("subcom2", "That"); std::string help = app.help(); - CHECK(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"); + CHECK_THAT(help, Contains("Run: [OPTIONS] [SUBCOMMAND]\n\n")); + CHECK_THAT(help, Contains("\nSUBCOMMANDS:\n")); + CHECK_THAT(help, Contains(" subcom1 This \n")); + CHECK_THAT(help, Contains(" subcom2 That \n")); } TEST_CASE("Formatter: AppCustomizeSimple", "[formatter]") { @@ -148,16 +137,13 @@ TEST_CASE("Formatter: AppCustomizeSimple", "[formatter]") { app.get_formatter()->column_width(20); app.get_formatter()->label("Usage", "Run"); - app.add_subcommand("subcom2", "This"); + app.add_subcommand("subcom2", "That"); std::string help = app.help(); - CHECK(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"); + CHECK_THAT(help, Contains("Run: [OPTIONS] [SUBCOMMAND]\n\n")); + CHECK_THAT(help, Contains("\nSUBCOMMANDS:\n")); + CHECK_THAT(help, Contains(" subcom1 This \n")); + CHECK_THAT(help, Contains(" subcom2 That \n")); } TEST_CASE("Formatter: AllSub", "[formatter]") { diff --git a/tests/HelpTest.cpp b/tests/HelpTest.cpp index 50b4cd42..68c5db34 100644 --- a/tests/HelpTest.cpp +++ b/tests/HelpTest.cpp @@ -21,8 +21,8 @@ TEST_CASE("THelp: Basic", "[help]") { std::string help = app.help(); CHECK_THAT(help, Contains("My prog")); - CHECK_THAT(help, Contains("-h,--help")); - CHECK_THAT(help, Contains("Options:")); + CHECK_THAT(help, Contains("-h, --help")); + CHECK_THAT(help, Contains("OPTIONS:")); CHECK_THAT(help, Contains("Usage:")); } @@ -32,9 +32,6 @@ TEST_CASE("THelp: Usage", "[help]") { std::string help = app.help(); - CHECK_THAT(help, Contains("My prog")); - CHECK_THAT(help, Contains("-h,--help")); - CHECK_THAT(help, Contains("Options:")); CHECK_THAT(help, Contains("use: just use it")); } @@ -43,10 +40,6 @@ TEST_CASE("THelp: UsageCallback", "[help]") { app.usage([]() { return "use: just use it"; }); std::string help = app.help(); - - CHECK_THAT(help, Contains("My prog")); - CHECK_THAT(help, Contains("-h,--help")); - CHECK_THAT(help, Contains("Options:")); CHECK_THAT(help, Contains("use: just use it")); } @@ -56,9 +49,6 @@ TEST_CASE("THelp: UsageCallbackBoth", "[help]") { app.usage("like 1, 2, and 3"); std::string help = app.help(); - CHECK_THAT(help, Contains("My prog")); - CHECK_THAT(help, Contains("-h,--help")); - CHECK_THAT(help, Contains("Options:")); CHECK_THAT(help, Contains("use: just use it")); CHECK_THAT(help, Contains("like 1, 2, and 3")); } @@ -69,10 +59,6 @@ TEST_CASE("THelp: Footer", "[help]") { std::string help = app.help(); - CHECK_THAT(help, Contains("My prog")); - CHECK_THAT(help, Contains("-h,--help")); - CHECK_THAT(help, Contains("Options:")); - CHECK_THAT(help, Contains("Usage:")); CHECK_THAT(help, Contains("Report bugs to bugs@example.com")); } @@ -82,10 +68,6 @@ TEST_CASE("THelp: FooterCallback", "[help]") { std::string help = app.help(); - CHECK_THAT(help, Contains("My prog")); - CHECK_THAT(help, Contains("-h,--help")); - CHECK_THAT(help, Contains("Options:")); - CHECK_THAT(help, Contains("Usage:")); CHECK_THAT(help, Contains("Report bugs to bugs@example.com")); } @@ -95,10 +77,6 @@ TEST_CASE("THelp: FooterCallbackBoth", "[help]") { app.footer(" foot!!!!"); std::string help = app.help(); - CHECK_THAT(help, Contains("My prog")); - CHECK_THAT(help, Contains("-h,--help")); - CHECK_THAT(help, Contains("Options:")); - CHECK_THAT(help, Contains("Usage:")); CHECK_THAT(help, Contains("Report bugs to bugs@example.com")); CHECK_THAT(help, Contains("foot!!!!")); } @@ -111,13 +89,11 @@ TEST_CASE("THelp: OptionalPositional", "[help]") { std::string help = app.help(); - CHECK_THAT(help, Contains("My prog")); - CHECK_THAT(help, Contains("-h,--help")); - CHECK_THAT(help, Contains("Options:")); - CHECK_THAT(help, Contains("Positionals:")); + CHECK_THAT(help, Contains("OPTIONS:")); + CHECK_THAT(help, Contains("POSITIONALS:")); CHECK_THAT(help, Contains("something TEXT")); CHECK_THAT(help, Contains("My option here")); - CHECK_THAT(help, Contains("Usage: program [OPTIONS] [something]")); + CHECK_THAT(help, Contains("program [OPTIONS] [something]")); } TEST_CASE("THelp: Hidden", "[help]") { @@ -130,9 +106,7 @@ TEST_CASE("THelp: Hidden", "[help]") { std::string help = app.help(); - CHECK_THAT(help, Contains("My prog")); - CHECK_THAT(help, Contains("-h,--help")); - CHECK_THAT(help, Contains("Options:")); + CHECK_THAT(help, Contains("OPTIONS:")); CHECK_THAT(help, !Contains("[something]")); CHECK_THAT(help, !Contains("something ")); CHECK_THAT(help, !Contains("another")); @@ -176,7 +150,7 @@ TEST_CASE("THelp: deprecatedOptions2", "[help]") { TEST_CASE("THelp: deprecatedOptions3", "[help]") { CLI::App app{"My prog"}; - + app.get_formatter()->right_column_width(100); std::string x; app.add_option("--something", x, "Some Description"); app.add_option("--something_else", x, "Some other description"); @@ -258,9 +232,7 @@ TEST_CASE("THelp: HiddenGroup", "[help]") { std::string help = app.help(); - CHECK_THAT(help, Contains("My prog")); - CHECK_THAT(help, Contains("-h,--help")); - CHECK_THAT(help, Contains("Options:")); + CHECK_THAT(help, Contains("OPTIONS:")); CHECK_THAT(help, !Contains("[something]")); CHECK_THAT(help, !Contains("something ")); CHECK_THAT(help, !Contains("another")); @@ -311,10 +283,7 @@ TEST_CASE("THelp: OptionalPositionalAndOptions", "[help]") { std::string help = app.help(); - CHECK_THAT(help, Contains("My prog")); - CHECK_THAT(help, Contains("-h,--help")); - CHECK_THAT(help, Contains("Options:")); - CHECK_THAT(help, Contains("Usage: AnotherProgram [OPTIONS] [something]")); + CHECK_THAT(help, Contains("AnotherProgram [OPTIONS] [something]")); } TEST_CASE("THelp: RequiredPositionalAndOptions", "[help]") { @@ -326,10 +295,8 @@ TEST_CASE("THelp: RequiredPositionalAndOptions", "[help]") { std::string help = app.help(); - CHECK_THAT(help, Contains("My prog")); - CHECK_THAT(help, Contains("-h,--help")); - CHECK_THAT(help, Contains("Options:")); - CHECK_THAT(help, Contains("Positionals:")); + CHECK_THAT(help, Contains("OPTIONS:")); + CHECK_THAT(help, Contains("POSITIONALS:")); CHECK_THAT(help, Contains("Usage: [OPTIONS] something")); } @@ -342,7 +309,7 @@ TEST_CASE("THelp: MultiOpts", "[help]") { std::string help = app.help(); CHECK_THAT(help, Contains("My prog")); - CHECK_THAT(help, !Contains("Positionals:")); + CHECK_THAT(help, !Contains("POSITIONALS:")); CHECK_THAT(help, Contains("Usage: [OPTIONS]")); CHECK_THAT(help, Contains("INT x 2")); CHECK_THAT(help, Contains("INT ...")); @@ -369,8 +336,8 @@ TEST_CASE("THelp: MultiPosOpts", "[help]") { std::string help = app.help(); CHECK_THAT(help, Contains("My prog")); - CHECK_THAT(help, Contains("Positionals:")); - CHECK_THAT(help, Contains("Usage: program [OPTIONS]")); + CHECK_THAT(help, Contains("POSITIONALS:")); + CHECK_THAT(help, Contains("program [OPTIONS]")); CHECK_THAT(help, Contains("INT x 2")); CHECK_THAT(help, Contains("INT ...")); CHECK_THAT(help, Contains("[quick(2x)]")); @@ -408,7 +375,7 @@ TEST_CASE("THelp: NeedsPositional", "[help]") { std::string help = app.help(); - CHECK_THAT(help, Contains("Positionals:")); + CHECK_THAT(help, Contains("POSITIONALS:")); CHECK_THAT(help, Contains("Needs: op1")); } @@ -433,7 +400,7 @@ TEST_CASE("THelp: ExcludesPositional", "[help]") { std::string help = app.help(); - CHECK_THAT(help, Contains("Positionals:")); + CHECK_THAT(help, Contains("POSITIONALS:")); CHECK_THAT(help, Contains("Excludes: op1")); } @@ -520,7 +487,7 @@ TEST_CASE("THelp: Subcom", "[help]") { CHECK_THAT(help, Contains("Usage: [OPTIONS] SUBCOMMAND")); help = sub1->help(); - CHECK_THAT(help, Contains("Usage: sub1")); + CHECK_THAT(help, Contains("sub1 [OPTIONS]")); char x[] = "./myprogram"; // NOLINT(modernize-avoid-c-arrays) char y[] = "sub2"; // NOLINT(modernize-avoid-c-arrays) @@ -529,7 +496,7 @@ TEST_CASE("THelp: Subcom", "[help]") { app.parse(static_cast(args.size()), args.data()); help = app.help(); - CHECK_THAT(help, Contains("Usage: ./myprogram sub2")); + CHECK_THAT(help, Contains("./myprogram sub2")); } TEST_CASE("THelp: Subcom_alias", "[help]") { @@ -570,7 +537,7 @@ TEST_CASE("THelp: MasterName", "[help]") { std::vector args = {x}; app.parse(static_cast(args.size()), args.data()); - CHECK_THAT(app.help(), Contains("Usage: MyRealName")); + CHECK_THAT(app.help(), Contains("MyRealName")); } TEST_CASE("THelp: IntDefaults", "[help]") { @@ -654,7 +621,7 @@ TEST_CASE("THelp: RemoveHelp", "[help]") { CHECK_THAT(help, Contains("My prog")); CHECK_THAT(help, !Contains("-h,--help")); - CHECK_THAT(help, !Contains("Options:")); + CHECK_THAT(help, !Contains("OPTIONS:")); CHECK_THAT(help, Contains("Usage:")); std::vector input{"--help"}; @@ -675,7 +642,7 @@ TEST_CASE("THelp: RemoveOtherMethodHelp", "[help]") { CHECK_THAT(help, Contains("My prog")); CHECK_THAT(help, !Contains("-h,--help")); - CHECK_THAT(help, !Contains("Options:")); + CHECK_THAT(help, !Contains("OPTIONS:")); CHECK_THAT(help, Contains("Usage:")); std::vector input{"--help"}; @@ -697,7 +664,7 @@ TEST_CASE("THelp: RemoveOtherMethodHelpAll", "[help]") { CHECK_THAT(help, Contains("My prog")); CHECK_THAT(help, !Contains("--help-all")); - CHECK_THAT(help, Contains("Options:")); + CHECK_THAT(help, Contains("OPTIONS:")); CHECK_THAT(help, Contains("Usage:")); std::vector input{"--help-all"}; @@ -716,7 +683,7 @@ TEST_CASE("THelp: NoHelp", "[help]") { CHECK_THAT(help, Contains("My prog")); CHECK_THAT(help, !Contains("-h,--help")); - CHECK_THAT(help, !Contains("Options:")); + CHECK_THAT(help, !Contains("OPTIONS:")); CHECK_THAT(help, Contains("Usage:")); std::vector input{"--help"}; @@ -738,7 +705,7 @@ TEST_CASE("THelp: CustomHelp", "[help]") { CHECK_THAT(help, Contains("My prog")); CHECK_THAT(help, !Contains("-h,--help")); CHECK_THAT(help, Contains("--yelp")); - CHECK_THAT(help, Contains("Options:")); + CHECK_THAT(help, Contains("OPTIONS:")); CHECK_THAT(help, Contains("Usage:")); std::vector input{"--yelp"}; @@ -774,7 +741,176 @@ TEST_CASE("THelp: NextLineShouldBeAlignmentInMultilineDescription", "[help]") { const std::string help = app.help(); const auto width = app.get_formatter()->get_column_width(); - CHECK_THAT(help, Contains(first + "\n" + std::string(width, ' ') + second)); + auto first_loc = help.find("first"); + auto first_new_line = help.find_last_of('\n', first_loc); + auto second_loc = help.find("second"); + auto second_new_line = help.find_last_of('\n', second_loc); + CHECK(first_loc - first_new_line - 1 == width); + CHECK(second_loc - second_new_line - 1 == width); + CHECK(second_new_line > first_loc); +} + +TEST_CASE("THelp: CheckRightWidth", "[help]") { + CLI::App app; + int i{0}; + const std::string first{"first line"}; + const std::string second{"second line"}; + app.add_option("-i,--int", i, first + "\n" + second); + app.get_formatter()->column_width(24); + CHECK(app.get_formatter()->get_column_width() == 24); + const std::string help = app.help(); + auto first_loc = help.find("first"); + auto first_new_line = help.find_last_of('\n', first_loc); + auto second_loc = help.find("second"); + auto second_new_line = help.find_last_of('\n', second_loc); + CHECK(first_loc - first_new_line - 1 == 24); + CHECK(second_loc - second_new_line - 1 == 24); + CHECK(second_new_line > first_loc); +} + +static const std::string long_string{ + "AAARG this is a long line description that will span across multiple lines and still go on and on. This is meant " + "to test how the help handler handles things like this"}; + +TEST_CASE("THelp: longLineAlignment", "[help]") { + CLI::App app; + int i{0}; + + app.add_option("-i,--int,--int_very_long_option_name_that_just_keeps_going_on_and_on_and_on_and_on_and_on_possibly_" + "to_infitinty,--and_another_long_name_just_for_fun", + i, + long_string); + + std::string help = app.help(); + auto width = app.get_formatter()->get_right_column_width(); + auto first_loc = help.find("AAARG"); + auto first_new_line = help.find_first_of('\n', first_loc); + + CHECK(first_new_line - first_loc - 1 < width); + app.get_formatter()->right_column_width(30); + width = app.get_formatter()->get_right_column_width(); + CHECK(width == 30); + help = app.help(); + first_loc = help.find("AAARG"); + first_new_line = help.find_first_of('\n', first_loc); + + CHECK(first_new_line - first_loc - 1 < width); +} + +TEST_CASE("THelp: longPositional", "[help]") { + CLI::App app; + int i{0}; + + app.add_option("int_very_long_option_name_that_just_keeps_going_on_and_on_and_on_and_on_and_on_possibly_" + "to_infitinty", + i, + long_string); + + std::string help = app.help(); + auto width = app.get_formatter()->get_right_column_width(); + auto first_loc = help.find("AAARG"); + auto first_new_line = help.find_first_of('\n', first_loc); + + CHECK(first_new_line - first_loc - 1 < width); +} + +TEST_CASE("THelp: SubcommandNewLineDescription", "[help]") { + + const std::string nl_description{"this is a description with aX \n X\\n in it and just for fun \n\t another"}; + + CLI::App app; + int i{0}; + app.add_option("-i,--int", i); + app.add_subcommand("subcom1", nl_description); + std::string help = app.help(); + auto width = app.get_formatter()->get_column_width(); + auto first_X = help.find_first_of('X'); + auto first_new_line = help.find_first_of('\n', first_X); + auto second_X = help.find_first_of('X', first_new_line); + CHECK(second_X - first_new_line > width); +} + +TEST_CASE("THelp: longDescription", "[help]") { + + CLI::App app(long_string, "long_desc"); + int i{0}; + + app.add_option("-i,--int", i); + + std::string help = app.help(); + auto width = app.get_formatter()->get_description_paragraph_width(); + auto first_loc = help.find("AAARG"); + auto first_new_line = help.find_first_of('\n', first_loc); + + CHECK(first_new_line - first_loc - 1 < width); + app.get_formatter()->description_paragraph_width(30); + width = app.get_formatter()->get_description_paragraph_width(); + CHECK(width == 30); + help = app.help(); + first_loc = help.find("AAARG"); + first_new_line = help.find_first_of('\n', first_loc); + + CHECK(first_new_line - first_loc - 1 < width); +} + +TEST_CASE("THelp: longSubcommandDescription", "[help]") { + + CLI::App app; + int i{0}; + + app.add_option("-i,--int", i); + app.add_subcommand("test1", long_string); + std::string help = app.help(); + auto width = app.get_formatter()->get_right_column_width(); + auto first_loc = help.find("AAARG"); + auto first_new_line = help.find_first_of('\n', first_loc); + + CHECK(first_new_line - first_loc - 1 < width); + app.get_formatter()->right_column_width(30); + width = 30; + help = app.help(); + first_loc = help.find("AAARG"); + first_new_line = help.find_first_of('\n', first_loc); + + CHECK(first_new_line - first_loc - 1 < width); +} + +TEST_CASE("THelp: longSubcommandDescriptionExpanded", "[help]") { + + CLI::App app; + int i{0}; + + app.add_option("-i,--int", i); + app.add_subcommand("test1", long_string); + + auto help = app.help("", CLI::AppFormatMode::All); + auto width = app.get_formatter()->get_description_paragraph_width(); + auto first_loc = help.find("AAARG"); + auto first_new_line = help.find_first_of('\n', first_loc); + + CHECK(first_new_line - first_loc - 1 < width); +} + +TEST_CASE("THelp: longFooter", "[help]") { + CLI::App app("test long footer", "long_desc"); + int i{0}; + app.footer(long_string); + app.add_option("-i,--int", i); + + std::string help = app.help(); + auto width = app.get_formatter()->get_footer_paragraph_width(); + auto first_loc = help.find("AAARG"); + auto first_new_line = help.find_first_of('\n', first_loc); + + CHECK(first_new_line - first_loc - 1 < width); + app.get_formatter()->footer_paragraph_width(30); + width = app.get_formatter()->get_footer_paragraph_width(); + CHECK(width == 30); + help = app.help(); + first_loc = help.find("AAARG"); + first_new_line = help.find_first_of('\n', first_loc); + + CHECK(first_new_line - first_loc - 1 < width); } TEST_CASE("THelp: NiceName", "[help]") { @@ -882,20 +1018,9 @@ TEST_CASE_METHOD(CapturedHelp, "CallForAllHelpOutput", "[help]") { CHECK_THAT(out.str(), Contains("one")); CHECK_THAT(out.str(), Contains("two")); CHECK_THAT(out.str(), Contains("--three")); - - CHECK(out.str() == "My Test Program\n" - "Usage: [OPTIONS] [SUBCOMMAND]\n" - "\n" - "Options:\n" - " -h,--help Print this help message and exit\n" - " --help-all Help all\n" - "\n" - "Subcommands:\n" - "one\n" - " One description\n\n" - "two\n" - " Options:\n" - " --three \n\n"); + CHECK_THAT(out.str(), Contains("SUBCOMMANDS:")); + CHECK_THAT(out.str(), Contains("--help-all")); + CHECK_THAT(out.str(), Contains("My Test Program")); } TEST_CASE_METHOD(CapturedHelp, "NewFormattedHelp", "[help]") { app.formatter_fn([](const CLI::App *, std::string, CLI::AppFormatMode) { return "New Help"; }); diff --git a/tests/OptionTypeTest.cpp b/tests/OptionTypeTest.cpp index b7f99868..5e25a929 100644 --- a/tests/OptionTypeTest.cpp +++ b/tests/OptionTypeTest.cpp @@ -280,6 +280,8 @@ static const std::map testValuesInt{ {"-995'862'275", -995862275}, {"0b11010110", 0xD6}, {"0b1101'0110", 0xD6}, + {"0B11010110", 0xD6}, + {"0B1101'0110", 0xD6}, {"1_2_3_4_5", 12345}, }; @@ -309,6 +311,10 @@ TEST_CASE_METHOD(TApp, "intConversionsErange", "[optiontype]") { args = {"--val", "0b1011000001101011001100110011111000101010101011111111111111111111111001010111011100"}; CHECK_THROWS_AS(run(), CLI::ParseError); + + args = {"--val", "0B1011000001101011001100110011111000101010101011111111111111111111111001010111011100"}; + + CHECK_THROWS_AS(run(), CLI::ParseError); } static const std::map testValuesUInt{ @@ -329,6 +335,8 @@ static const std::map testValuesUInt{ {"995'862'275", 995862275}, {"0b11010110", 0xD6}, {"0b1101'0110", 0xD6}, + {"0B11010110", 0xD6}, + {"0B1101'0110", 0xD6}, {"1_2_3_4_5", 12345}, }; @@ -358,6 +366,10 @@ TEST_CASE_METHOD(TApp, "uintConversionsErange", "[optiontype]") { args = {"--val", "0b1011000001101011001100110011111000101010101011111111111111111111111001010111011100"}; CHECK_THROWS_AS(run(), CLI::ParseError); + + args = {"--val", "0B1011000001101011001100110011111000101010101011111111111111111111111001010111011100"}; + + CHECK_THROWS_AS(run(), CLI::ParseError); } TEST_CASE_METHOD(TApp, "CharOption", "[optiontype]") { diff --git a/tests/SubcommandTest.cpp b/tests/SubcommandTest.cpp index f64f04b9..b7a8bb97 100644 --- a/tests/SubcommandTest.cpp +++ b/tests/SubcommandTest.cpp @@ -1013,18 +1013,18 @@ TEST_CASE_METHOD(SubcommandProgram, "Subcommand Groups", "[subcom]") { std::string help = app.help(); CHECK_THAT(help, !Contains("More Commands:")); - CHECK_THAT(help, Contains("Subcommands:")); + CHECK_THAT(help, Contains("SUBCOMMANDS:")); start->group("More Commands"); help = app.help(); CHECK_THAT(help, Contains("More Commands:")); - CHECK_THAT(help, Contains("Subcommands:")); + CHECK_THAT(help, Contains("SUBCOMMANDS:")); // Case is ignored but for the first subcommand in a group. stop->group("more commands"); help = app.help(); CHECK_THAT(help, Contains("More Commands:")); - CHECK_THAT(help, !Contains("Subcommands:")); + CHECK_THAT(help, !Contains("SUBCOMMANDS:")); } TEST_CASE_METHOD(SubcommandProgram, "Subcommand ExtrasErrors", "[subcom]") {