From 3d7596765bc2aa10ffdd210fb3531d647dd932f5 Mon Sep 17 00:00:00 2001 From: He Shiming Date: Mon, 30 Jan 2023 19:28:07 +0800 Subject: [PATCH 1/5] implements column-aligned multi-line help message for arguments (issue #248) --- include/argparse/argparse.hpp | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/include/argparse/argparse.hpp b/include/argparse/argparse.hpp index d0ce31da..3f5ddee1 100644 --- a/include/argparse/argparse.hpp +++ b/include/argparse/argparse.hpp @@ -649,6 +649,7 @@ class Argument { friend std::ostream &operator<<(std::ostream &stream, const Argument &argument) { + auto stream_width = stream.width(); std::stringstream name_stream; name_stream << " "; // indent if (argument.is_positional(argument.m_names.front(), @@ -668,7 +669,31 @@ class Argument { name_stream << " " << argument.m_metavar; } } - stream << name_stream.str() << "\t" << argument.m_help; + auto spacing = name_stream.str().size(); + auto pos = 0; + auto prev = 0; + auto first_line = true; + stream << name_stream.str(); + while ((pos = argument.m_help.find('\n', prev)) != std::string::npos) { + auto line = argument.m_help.substr(prev, pos - prev + 1); + if (first_line) { + stream << "\t" << line; + first_line = false; + } else { + stream.width(stream_width); + stream << std::string(spacing, ' ') << "\t" << line; + } + prev += pos - prev + 1; + } + if (first_line) + stream << "\t" << argument.m_help; + else { + auto leftover = argument.m_help.substr(prev, argument.m_help.size() - prev); + if (leftover.size() > 0) { + stream.width(stream_width); + stream << std::string(spacing, ' ') << "\t" << leftover; + } + } // print nargs spec if (!argument.m_help.empty()) { From aa996952bb839599eab17199b48ca74162d99fa2 Mon Sep 17 00:00:00 2001 From: Fanurs Date: Sat, 22 Apr 2023 02:35:46 -0400 Subject: [PATCH 2/5] Fixed clang-tidy issues See https://github.com/p-ranav/argparse/pull/259#pullrequestreview-1284243420 --- include/argparse/argparse.hpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/include/argparse/argparse.hpp b/include/argparse/argparse.hpp index 3f5ddee1..7099bcd0 100644 --- a/include/argparse/argparse.hpp +++ b/include/argparse/argparse.hpp @@ -685,11 +685,11 @@ class Argument { } prev += pos - prev + 1; } - if (first_line) + if (first_line) { stream << "\t" << argument.m_help; - else { + } else { auto leftover = argument.m_help.substr(prev, argument.m_help.size() - prev); - if (leftover.size() > 0) { + if (!leftover.empty()) { stream.width(stream_width); stream << std::string(spacing, ' ') << "\t" << leftover; } From 5595375786213ffc0b9ef5155580bf797d6d8d29 Mon Sep 17 00:00:00 2001 From: Fanurs Date: Sat, 22 Apr 2023 15:15:39 -0400 Subject: [PATCH 3/5] Changed padding from `\t` to spaces --- include/argparse/argparse.hpp | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/include/argparse/argparse.hpp b/include/argparse/argparse.hpp index 7099bcd0..77f8e480 100644 --- a/include/argparse/argparse.hpp +++ b/include/argparse/argparse.hpp @@ -649,7 +649,6 @@ class Argument { friend std::ostream &operator<<(std::ostream &stream, const Argument &argument) { - auto stream_width = stream.width(); std::stringstream name_stream; name_stream << " "; // indent if (argument.is_positional(argument.m_names.front(), @@ -669,29 +668,34 @@ class Argument { name_stream << " " << argument.m_metavar; } } - auto spacing = name_stream.str().size(); + + // align multiline help message + auto stream_width = stream.width(); + auto name_padding = std::string(name_stream.str().size(), ' '); auto pos = 0; auto prev = 0; auto first_line = true; + const char* hspace = " "; // minimal space between name and help message stream << name_stream.str(); + std::string_view help_view(argument.m_help); while ((pos = argument.m_help.find('\n', prev)) != std::string::npos) { - auto line = argument.m_help.substr(prev, pos - prev + 1); + auto line = help_view.substr(prev, pos - prev + 1); if (first_line) { - stream << "\t" << line; + stream << hspace << line; first_line = false; } else { stream.width(stream_width); - stream << std::string(spacing, ' ') << "\t" << line; + stream << name_padding << hspace << line; } prev += pos - prev + 1; } if (first_line) { - stream << "\t" << argument.m_help; + stream << hspace << argument.m_help; } else { - auto leftover = argument.m_help.substr(prev, argument.m_help.size() - prev); + auto leftover = help_view.substr(prev, argument.m_help.size() - prev); if (!leftover.empty()) { stream.width(stream_width); - stream << std::string(spacing, ' ') << "\t" << leftover; + stream << name_padding << hspace << leftover; } } From 19d85eadb00bf642bc6f9e57a7a00b56f962eced Mon Sep 17 00:00:00 2001 From: Fanurs Date: Sat, 22 Apr 2023 15:16:47 -0400 Subject: [PATCH 4/5] Passed test for multiline help message alignment --- test/test_help.cpp | 56 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/test/test_help.cpp b/test/test_help.cpp index a81af422..92292c92 100644 --- a/test/test_help.cpp +++ b/test/test_help.cpp @@ -1,6 +1,8 @@ #include #include +#include #include +#include using doctest::test_suite; @@ -73,3 +75,57 @@ TEST_CASE("Users can replace default -h/--help" * test_suite("help")) { program.parse_args({"test", "--help"}); REQUIRE_FALSE(buffer.str().empty()); } + +TEST_CASE("Multiline help message alignment") { + argparse::ArgumentParser program("program"); + program.add_argument("input1") + .help( + "This is the first line of help message.\n" + "And this is the second line of help message." + ); + program.add_argument("program_input2") + .help("There is only one line."); + program.add_argument("-p", "--prog_input3") + .help( +R"(Lorem ipsum dolor sit amet, consectetur adipiscing elit. +Sed ut perspiciatis unde omnis iste natus error sit voluptatem +accusantium doloremque laudantium, totam rem aperiam...)" + ); + + std::ostringstream stream; + stream << program; + std::string help_text = stream.str(); + + // Split the help text into lines + std::istringstream iss(help_text); + std::vector help_lines; + std::string line; + while (std::getline(iss, line)) { + help_lines.push_back(line); + } + + // A map to store the starting position of the help text for each argument + std::unordered_map help_start_positions; + + for (const auto& line : help_lines) { + // Find the argument names in the line + std::smatch match; + if (std::regex_search(line, match, std::regex(R"((input1|program_input2|--prog_input3))"))) { + std::string arg_name = match.str(); + + // Find the position of the first non-space character after the argument name + int position = line.find_first_not_of(' ', match.position() + arg_name.size()); + if (position == std::string::npos) { + continue; + } + help_start_positions[arg_name] = position; + } + } + + // Check if the positions are the same for all arguments + REQUIRE(!help_start_positions.empty()); + int first_position = help_start_positions.begin()->second; + for (const auto& entry : help_start_positions) { + REQUIRE(entry.second == first_position); + } +} From e82653c2d97cef36dbed3c36abeab2236e189dde Mon Sep 17 00:00:00 2001 From: Fanurs Date: Sat, 22 Apr 2023 19:23:17 -0400 Subject: [PATCH 5/5] Fixed test for multiline help message alignment --- include/argparse/argparse.hpp | 2 +- test/test_help.cpp | 59 ++++++++++++++--------------------- 2 files changed, 25 insertions(+), 36 deletions(-) diff --git a/include/argparse/argparse.hpp b/include/argparse/argparse.hpp index 77f8e480..0b9326f9 100644 --- a/include/argparse/argparse.hpp +++ b/include/argparse/argparse.hpp @@ -675,7 +675,7 @@ class Argument { auto pos = 0; auto prev = 0; auto first_line = true; - const char* hspace = " "; // minimal space between name and help message + auto hspace = " "; // minimal space between name and help message stream << name_stream.str(); std::string_view help_view(argument.m_help); while ((pos = argument.m_help.find('\n', prev)) != std::string::npos) { diff --git a/test/test_help.cpp b/test/test_help.cpp index 92292c92..293daef1 100644 --- a/test/test_help.cpp +++ b/test/test_help.cpp @@ -1,8 +1,6 @@ #include #include -#include #include -#include using doctest::test_suite; @@ -77,55 +75,46 @@ TEST_CASE("Users can replace default -h/--help" * test_suite("help")) { } TEST_CASE("Multiline help message alignment") { + // '#' is used at the beginning of each help message line to simplify testing. + // It is important to ensure that this character doesn't appear elsewhere in the test case. + // Default arguments (e.g., -h/--help, -v/--version) are not included in this test. argparse::ArgumentParser program("program"); - program.add_argument("input1") + program.add_argument("INPUT1") .help( - "This is the first line of help message.\n" - "And this is the second line of help message." + "#This is the first line of help message.\n" + "#And this is the second line of help message." ); program.add_argument("program_input2") - .help("There is only one line."); + .help("#There is only one line."); program.add_argument("-p", "--prog_input3") .help( -R"(Lorem ipsum dolor sit amet, consectetur adipiscing elit. -Sed ut perspiciatis unde omnis iste natus error sit voluptatem -accusantium doloremque laudantium, totam rem aperiam...)" +R"(#Lorem ipsum dolor sit amet, consectetur adipiscing elit. +#Sed ut perspiciatis unde omnis iste natus error sit voluptatem +#accusantium doloremque laudantium, totam rem aperiam...)" ); + program.add_argument("--verbose").default_value(false).implicit_value(true); std::ostringstream stream; stream << program; - std::string help_text = stream.str(); + std::istringstream iss(stream.str()); - // Split the help text into lines - std::istringstream iss(help_text); - std::vector help_lines; + int help_message_start = -1; std::string line; while (std::getline(iss, line)) { - help_lines.push_back(line); - } - - // A map to store the starting position of the help text for each argument - std::unordered_map help_start_positions; + // Find the position of '#', which indicates the start of the help message line + auto pos = line.find('#'); - for (const auto& line : help_lines) { - // Find the argument names in the line - std::smatch match; - if (std::regex_search(line, match, std::regex(R"((input1|program_input2|--prog_input3))"))) { - std::string arg_name = match.str(); + if (pos == std::string::npos) { + continue; + } - // Find the position of the first non-space character after the argument name - int position = line.find_first_not_of(' ', match.position() + arg_name.size()); - if (position == std::string::npos) { - continue; - } - help_start_positions[arg_name] = position; + if (help_message_start == -1) { + help_message_start = pos; + } else { + REQUIRE(pos == help_message_start); } } - // Check if the positions are the same for all arguments - REQUIRE(!help_start_positions.empty()); - int first_position = help_start_positions.begin()->second; - for (const auto& entry : help_start_positions) { - REQUIRE(entry.second == first_position); - } + // Make sure we have at least one help message + REQUIRE(help_message_start != -1); }