mirror of
https://github.com/CLIUtils/CLI11.git
synced 2025-04-29 20:23:55 +00:00
_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=<TEXT>` 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 <phlptp@gmail.com>
280 lines
11 KiB
C++
280 lines
11 KiB
C++
// Copyright (c) 2017-2024, University of Cincinnati, developed by Henry Schreiner
|
|
// under NSF AWARD 1414736 and by the respective contributors.
|
|
// All rights reserved.
|
|
//
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
#pragma once
|
|
|
|
// IWYU pragma: private, include "CLI/CLI.hpp"
|
|
|
|
// [CLI11:public_includes:set]
|
|
#include <algorithm>
|
|
#include <iomanip>
|
|
#include <locale>
|
|
#include <sstream>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
#include <type_traits>
|
|
#include <vector>
|
|
// [CLI11:public_includes:end]
|
|
|
|
#include "Macros.hpp"
|
|
|
|
namespace CLI {
|
|
|
|
// [CLI11:string_tools_hpp:verbatim]
|
|
|
|
/// Include the items in this namespace to get free conversion of enums to/from streams.
|
|
/// (This is available inside CLI as well, so CLI11 will use this without a using statement).
|
|
namespace enums {
|
|
|
|
/// output streaming for enumerations
|
|
template <typename T, typename = typename std::enable_if<std::is_enum<T>::value>::type>
|
|
std::ostream &operator<<(std::ostream &in, const T &item) {
|
|
// make sure this is out of the detail namespace otherwise it won't be found when needed
|
|
return in << static_cast<typename std::underlying_type<T>::type>(item);
|
|
}
|
|
|
|
} // namespace enums
|
|
|
|
/// Export to CLI namespace
|
|
using enums::operator<<;
|
|
|
|
namespace detail {
|
|
/// a constant defining an expected max vector size defined to be a big number that could be multiplied by 4 and not
|
|
/// produce overflow for some expected uses
|
|
constexpr int expected_max_vector_size{1 << 29};
|
|
// Based on http://stackoverflow.com/questions/236129/split-a-string-in-c
|
|
/// Split a string by a delim
|
|
CLI11_INLINE std::vector<std::string> split(const std::string &s, char delim);
|
|
|
|
/// Simple function to join a string
|
|
template <typename T> std::string join(const T &v, std::string delim = ",") {
|
|
std::ostringstream s;
|
|
auto beg = std::begin(v);
|
|
auto end = std::end(v);
|
|
if(beg != end)
|
|
s << *beg++;
|
|
while(beg != end) {
|
|
s << delim << *beg++;
|
|
}
|
|
auto rval = s.str();
|
|
if(!rval.empty() && delim.size() == 1 && rval.back() == delim[0]) {
|
|
// remove trailing delimiter if the last entry was empty
|
|
rval.pop_back();
|
|
}
|
|
return rval;
|
|
}
|
|
|
|
/// Simple function to join a string from processed elements
|
|
template <typename T,
|
|
typename Callable,
|
|
typename = typename std::enable_if<!std::is_constructible<std::string, Callable>::value>::type>
|
|
std::string join(const T &v, Callable func, std::string delim = ",") {
|
|
std::ostringstream s;
|
|
auto beg = std::begin(v);
|
|
auto end = std::end(v);
|
|
auto loc = s.tellp();
|
|
while(beg != end) {
|
|
auto nloc = s.tellp();
|
|
if(nloc > loc) {
|
|
s << delim;
|
|
loc = nloc;
|
|
}
|
|
s << func(*beg++);
|
|
}
|
|
return s.str();
|
|
}
|
|
|
|
/// Join a string in reverse order
|
|
template <typename T> std::string rjoin(const T &v, std::string delim = ",") {
|
|
std::ostringstream s;
|
|
for(std::size_t start = 0; start < v.size(); start++) {
|
|
if(start > 0)
|
|
s << delim;
|
|
s << v[v.size() - start - 1];
|
|
}
|
|
return s.str();
|
|
}
|
|
|
|
// Based roughly on http://stackoverflow.com/questions/25829143/c-trim-whitespace-from-a-string
|
|
|
|
/// Trim whitespace from left of string
|
|
CLI11_INLINE std::string <rim(std::string &str);
|
|
|
|
/// Trim anything from left of string
|
|
CLI11_INLINE std::string <rim(std::string &str, const std::string &filter);
|
|
|
|
/// Trim whitespace from right of string
|
|
CLI11_INLINE std::string &rtrim(std::string &str);
|
|
|
|
/// Trim anything from right of string
|
|
CLI11_INLINE std::string &rtrim(std::string &str, const std::string &filter);
|
|
|
|
/// Trim whitespace from string
|
|
inline std::string &trim(std::string &str) { return ltrim(rtrim(str)); }
|
|
|
|
/// Trim anything from string
|
|
inline std::string &trim(std::string &str, const std::string filter) { return ltrim(rtrim(str, filter), filter); }
|
|
|
|
/// Make a copy of the string and then trim it
|
|
inline std::string trim_copy(const std::string &str) {
|
|
std::string s = str;
|
|
return trim(s);
|
|
}
|
|
|
|
/// remove quotes at the front and back of a string either '"' or '\''
|
|
CLI11_INLINE std::string &remove_quotes(std::string &str);
|
|
|
|
/// remove quotes from all elements of a string vector and process escaped components
|
|
CLI11_INLINE void remove_quotes(std::vector<std::string> &args);
|
|
|
|
/// Add a leader to the beginning of all new lines (nothing is added
|
|
/// at the start of the first line). `"; "` would be for ini files
|
|
///
|
|
/// Can't use Regex, or this would be a subs.
|
|
CLI11_INLINE std::string fix_newlines(const std::string &leader, std::string input);
|
|
|
|
/// Make a copy of the string and then trim it, any filter string can be used (any char in string is filtered)
|
|
inline std::string trim_copy(const std::string &str, const std::string &filter) {
|
|
std::string s = str;
|
|
return trim(s, filter);
|
|
}
|
|
|
|
/// Print subcommand aliases
|
|
CLI11_INLINE std::ostream &format_aliases(std::ostream &out, const std::vector<std::string> &aliases, std::size_t wid);
|
|
|
|
/// Verify the first character of an option
|
|
/// - is a trigger character, ! has special meaning and new lines would just be annoying to deal with
|
|
template <typename T> bool valid_first_char(T c) {
|
|
return ((c != '-') && (static_cast<unsigned char>(c) > 33)); // space and '!' not allowed
|
|
}
|
|
|
|
/// Verify following characters of an option
|
|
template <typename T> bool valid_later_char(T c) {
|
|
// = and : are value separators, { has special meaning for option defaults,
|
|
// and control codes other than tab would just be annoying to deal with in many places allowing space here has too
|
|
// much potential for inadvertent entry errors and bugs
|
|
return ((c != '=') && (c != ':') && (c != '{') && ((static_cast<unsigned char>(c) > 32) || c == '\t'));
|
|
}
|
|
|
|
/// Verify an option/subcommand name
|
|
CLI11_INLINE bool valid_name_string(const std::string &str);
|
|
|
|
/// Verify an app name
|
|
inline bool valid_alias_name_string(const std::string &str) {
|
|
static const std::string badChars(std::string("\n") + '\0');
|
|
return (str.find_first_of(badChars) == std::string::npos);
|
|
}
|
|
|
|
/// check if a string is a container segment separator (empty or "%%")
|
|
inline bool is_separator(const std::string &str) {
|
|
static const std::string sep("%%");
|
|
return (str.empty() || str == sep);
|
|
}
|
|
|
|
/// Verify that str consists of letters only
|
|
inline bool isalpha(const std::string &str) {
|
|
return std::all_of(str.begin(), str.end(), [](char c) { return std::isalpha(c, std::locale()); });
|
|
}
|
|
|
|
/// Return a lower case version of a string
|
|
inline std::string to_lower(std::string str) {
|
|
std::transform(std::begin(str), std::end(str), std::begin(str), [](const std::string::value_type &x) {
|
|
return std::tolower(x, std::locale());
|
|
});
|
|
return str;
|
|
}
|
|
|
|
/// remove underscores from a string
|
|
inline std::string remove_underscore(std::string str) {
|
|
str.erase(std::remove(std::begin(str), std::end(str), '_'), std::end(str));
|
|
return str;
|
|
}
|
|
|
|
/// Find and replace a substring with another substring
|
|
CLI11_INLINE std::string find_and_replace(std::string str, std::string from, std::string to);
|
|
|
|
/// check if the flag definitions has possible false flags
|
|
inline bool has_default_flag_values(const std::string &flags) {
|
|
return (flags.find_first_of("{!") != std::string::npos);
|
|
}
|
|
|
|
CLI11_INLINE void remove_default_flag_values(std::string &flags);
|
|
|
|
/// Check if a string is a member of a list of strings and optionally ignore case or ignore underscores
|
|
CLI11_INLINE std::ptrdiff_t find_member(std::string name,
|
|
const std::vector<std::string> names,
|
|
bool ignore_case = false,
|
|
bool ignore_underscore = false);
|
|
|
|
/// Find a trigger string and call a modify callable function that takes the current string and starting position of the
|
|
/// trigger and returns the position in the string to search for the next trigger string
|
|
template <typename Callable> inline std::string find_and_modify(std::string str, std::string trigger, Callable modify) {
|
|
std::size_t start_pos = 0;
|
|
while((start_pos = str.find(trigger, start_pos)) != std::string::npos) {
|
|
start_pos = modify(str, start_pos);
|
|
}
|
|
return str;
|
|
}
|
|
|
|
/// close a sequence of characters indicated by a closure character. Brackets allows sub sequences
|
|
/// recognized bracket sequences include "'`[(<{ other closure characters are assumed to be literal strings
|
|
CLI11_INLINE std::size_t close_sequence(const std::string &str, std::size_t start, char closure_char);
|
|
|
|
/// Split a string '"one two" "three"' into 'one two', 'three'
|
|
/// Quote characters can be ` ' or " or bracket characters [{(< with matching to the matching bracket
|
|
CLI11_INLINE std::vector<std::string> split_up(std::string str, char delimiter = '\0');
|
|
|
|
/// get the value of an environmental variable or empty string if empty
|
|
CLI11_INLINE std::string get_environment_value(const std::string &env_name);
|
|
|
|
/// This function detects an equal or colon followed by an escaped quote after an argument
|
|
/// then modifies the string to replace the equality with a space. This is needed
|
|
/// to allow the split up function to work properly and is intended to be used with the find_and_modify function
|
|
/// the return value is the offset+1 which is required by the find_and_modify function.
|
|
CLI11_INLINE std::size_t escape_detect(std::string &str, std::size_t offset);
|
|
|
|
/// @brief detect if a string has escapable characters
|
|
/// @param str the string to do the detection on
|
|
/// @return true if the string has escapable characters
|
|
CLI11_INLINE bool has_escapable_character(const std::string &str);
|
|
|
|
/// @brief escape all escapable characters
|
|
/// @param str the string to escape
|
|
/// @return a string with the escapble characters escaped with '\'
|
|
CLI11_INLINE std::string add_escaped_characters(const std::string &str);
|
|
|
|
/// @brief replace the escaped characters with their equivalent
|
|
CLI11_INLINE std::string remove_escaped_characters(const std::string &str);
|
|
|
|
/// generate a string with all non printable characters escaped to hex codes
|
|
CLI11_INLINE std::string binary_escape_string(const std::string &string_to_escape);
|
|
|
|
CLI11_INLINE bool is_binary_escaped_string(const std::string &escaped_string);
|
|
|
|
/// extract an escaped binary_string
|
|
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]
|
|
|
|
} // namespace CLI
|
|
|
|
#ifndef CLI11_COMPILE
|
|
#include "impl/StringTools_inl.hpp" // IWYU pragma: export
|
|
#endif
|