From e7e8de034686878b9c2bc4db12b6935a58326ead Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Radim=20Kr=C4=8Dm=C3=A1=C5=99?= Date: Wed, 27 Aug 2025 14:17:20 +0200 Subject: [PATCH] formatter: add long_option_alignment_ratio (#1185) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The default formatter has hardcoded ratio at which the long options are aligned. It's currently 1/3 of the column, which makes the default look awkward: ``` -h, --help Print --option Something ``` A ->long_option_alignment_ratio(6/30.f) allows output to look like this: ``` -h, --help Print --option Something ``` The 1/3 ratio is also bad if you want to print "descriptive" long options on a single line, because then you might want to increase the column width, but that means you waste more space on short options. e.g. ->column_width(46) ``` -l, --very-descriptive-long-option Something ``` vs. ->column_width(38) ``` -l, --very-descriptive-long-option Something ``` vs. ->column_width(38) ->long_option_alignment_ratio(6/38.f) ``` -l, --very-descriptive-long-option Something ``` Any absolute offset `X` can be set as `X/column_width`, so provide a ratio-based interface. I would have prefered to give an absolute integer offset, but we still have to preserve the functionality that does 1/3 if user changed nothing, which means that ratio-based interface is simpler. I don't have a good idea for the name, "short_option_ratio" might work as well. The setter does not sanity check that the value is in [0;1] range. --------- Signed-off-by: Radim Krčmář Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- include/CLI/FormatterFwd.hpp | 7 +++++++ include/CLI/impl/Formatter_inl.hpp | 7 ++++--- tests/FormatterTest.cpp | 14 ++++++++++++++ 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/include/CLI/FormatterFwd.hpp b/include/CLI/FormatterFwd.hpp index d87807b1..54d08c53 100644 --- a/include/CLI/FormatterFwd.hpp +++ b/include/CLI/FormatterFwd.hpp @@ -47,6 +47,9 @@ class FormatterBase { /// The width of the left column (options/flags/subcommands) std::size_t column_width_{30}; + /// The alignment ratio for long options within the left column + float long_option_alignment_ratio_{1 / 3.f}; + /// The width of the right column (description of options/flags/subcommands) std::size_t right_column_width_{65}; @@ -91,6 +94,10 @@ class FormatterBase { /// Set the left column width (options/flags/subcommands) void column_width(std::size_t val) { column_width_ = val; } + /// Set the alignment ratio for long options within the left column + /// The ratio is in [0;1] range (e.g. 0.2 = 20% of column width, 6.f/column_width = 6th character) + void long_option_alignment_ratio(float ratio) { long_option_alignment_ratio_ = ratio; } + /// Set the right column width (description of options/flags/subcommands) void right_column_width(std::size_t val) { right_column_width_ = val; } diff --git a/include/CLI/impl/Formatter_inl.hpp b/include/CLI/impl/Formatter_inl.hpp index 1e2ef001..f4e841fc 100644 --- a/include/CLI/impl/Formatter_inl.hpp +++ b/include/CLI/impl/Formatter_inl.hpp @@ -298,9 +298,10 @@ CLI11_INLINE std::string Formatter::make_option(const Option *opt, bool is_posit 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 + // Short names take enough width to align long names at the desired ratio + const auto shortNamesColumnWidth = + static_cast(static_cast(column_width_) * long_option_alignment_ratio_); + const auto longNamesColumnWidth = static_cast(column_width_) - shortNamesColumnWidth; int shortNamesOverSize = 0; // Print short names diff --git a/tests/FormatterTest.cpp b/tests/FormatterTest.cpp index 33d1a99c..95093d5e 100644 --- a/tests/FormatterTest.cpp +++ b/tests/FormatterTest.cpp @@ -261,3 +261,17 @@ TEST_CASE("Formatter: Description", "[formatter]") { CHECK_THAT(help, !Contains("to Pr e s e r v e SPA C ES")); CHECK_THAT(help, !Contains(desc_string)); } + +TEST_CASE("Formatter: LongOptionAlignment", "[formatter]") { + CLI::App app{"My prog"}; + + app.get_formatter()->long_option_alignment_ratio(6 / 30.f); + + int v{0}; + app.add_option("--opt", v, "Something"); + + std::string help = app.help(); + + CHECK_THAT(help, Contains("\n -h, --help Print")); + CHECK_THAT(help, Contains("\n --opt INT Something")); +}