From 6ac2747212532f5296b08b37516badb5a79e89a9 Mon Sep 17 00:00:00 2001 From: Pavel Belikov Date: Sat, 2 Dec 2017 14:08:05 +0300 Subject: fix usage line wrapping --- Makefile | 4 +- args.hxx | 154 ++++++++++++++++++++++++++++++++++++++++++++++----------------- test.cxx | 46 ++++++++++++++++++- 3 files changed, 159 insertions(+), 45 deletions(-) diff --git a/Makefile b/Makefile index 4c71e96..5cb8feb 100644 --- a/Makefile +++ b/Makefile @@ -54,8 +54,8 @@ doc/man: doxygen Doxyfile bzip2 doc/man/man3/*.3 -runtests: test - ./test +runtests: ${EXECUTABLE} + ./${EXECUTABLE} %.o: %.cxx $(CXX) $< -o $@ $(CFLAGS) diff --git a/args.hxx b/args.hxx index 03faade..a8818ab 100644 --- a/args.hxx +++ b/args.hxx @@ -30,6 +30,7 @@ #define ARGS_HXX #include +#include #include #include #include @@ -83,54 +84,56 @@ namespace args return length; } - /** (INTERNAL) Wrap a string into a vector of lines + /** (INTERNAL) Wrap a vector of words into a vector of lines * - * This is quick and hacky, but works well enough. You can specify a - * different width for the first line + * Empty words are skipped. Word "\n" forces wrapping. * + * \param begin The begin iterator + * \param end The end iterator * \param width The width of the body - * \param the width of the first line, defaults to the width of the body + * \param firstlinewidth the width of the first line, defaults to the width of the body + * \param firstlineindent the indent of the first line, defaults to 0 * \return the vector of lines */ - inline std::vector Wrap(const std::string &in, const std::string::size_type width, std::string::size_type firstlinewidth = 0) + template + inline std::vector Wrap(It begin, + It end, + const std::string::size_type width, + std::string::size_type firstlinewidth = 0, + std::string::size_type firstlineindent = 0) { - // Preserve existing line breaks - const auto newlineloc = in.find('\n'); - if (newlineloc != in.npos) - { - auto first = Wrap(std::string(in, 0, newlineloc), width); - auto second = Wrap(std::string(in, newlineloc + 1), width); - first.insert( - std::end(first), - std::make_move_iterator(std::begin(second)), - std::make_move_iterator(std::end(second))); - return first; - } + std::vector output; + std::string line(firstlineindent, ' '); + bool empty = true; + if (firstlinewidth == 0) { firstlinewidth = width; } - auto currentwidth = firstlinewidth; - std::istringstream stream(in); - std::vector output; - std::string line; - bool empty = true; + auto currentwidth = firstlinewidth; - for (char c : in) + for (auto it = begin; it != end; ++it) { - if (!isspace(c)) + if (it->empty()) { - break; + continue; } - line += c; - } - while (stream) - { - std::string item; - stream >> item; - auto itemsize = Glyphs(item); + if (*it == "\n") + { + if (!empty) + { + output.push_back(line); + line.clear(); + empty = true; + currentwidth = width; + } + + continue; + } + + auto itemsize = Glyphs(*it); if ((line.length() + 1 + itemsize) > currentwidth) { if (!empty) @@ -141,6 +144,7 @@ namespace args currentwidth = width; } } + if (itemsize > 0) { if (!empty) @@ -148,7 +152,7 @@ namespace args line += ' '; } - line += item; + line += *it; empty = false; } } @@ -157,9 +161,50 @@ namespace args { output.push_back(line); } + return output; } + /** (INTERNAL) Wrap a string into a vector of lines + * + * This is quick and hacky, but works well enough. You can specify a + * different width for the first line + * + * \param width The width of the body + * \param firstlinewid the width of the first line, defaults to the width of the body + * \return the vector of lines + */ + inline std::vector Wrap(const std::string &in, const std::string::size_type width, std::string::size_type firstlinewidth = 0) + { + // Preserve existing line breaks + const auto newlineloc = in.find('\n'); + if (newlineloc != in.npos) + { + auto first = Wrap(std::string(in, 0, newlineloc), width); + auto second = Wrap(std::string(in, newlineloc + 1), width); + first.insert( + std::end(first), + std::make_move_iterator(std::begin(second)), + std::make_move_iterator(std::end(second))); + return first; + } + + std::istringstream stream(in); + std::string::size_type indent = 0; + + for (char c : in) + { + if (!isspace(c)) + { + break; + } + ++indent; + } + + return Wrap(std::istream_iterator(stream), std::istream_iterator(), + width, firstlinewidth, indent); + } + #ifdef ARGS_NOEXCEPT /// Error class, for when ARGS_NOEXCEPT is defined enum class Error @@ -1748,7 +1793,32 @@ namespace args if (!ProglinePostfix().empty()) { - res.push_back(ProglinePostfix()); + std::string line; + for (char c : ProglinePostfix()) + { + if (isspace(c)) + { + if (!line.empty()) + { + res.push_back(line); + line.clear(); + } + + if (c == '\n') + { + res.push_back("\n"); + } + } + else + { + line += c; + } + } + + if (!line.empty()) + { + res.push_back(line); + } } return res; @@ -2387,15 +2457,15 @@ namespace args const bool hasoptions = command.HasFlag(); const bool hasarguments = command.HasPositional(); - std::ostringstream prognameline; - prognameline << helpParams.usageString << Prog(); - - for (const std::string &posname: command.GetProgramLine(helpParams)) - { - prognameline << ' ' << posname; - } + std::vector prognameline; + prognameline.push_back(helpParams.usageString); + prognameline.push_back(Prog()); + auto commandProgLine = command.GetProgramLine(helpParams); + prognameline.insert(prognameline.end(), commandProgLine.begin(), commandProgLine.end()); - const auto proglines = Wrap(prognameline.str(), helpParams.width - (helpParams.progindent + 4), helpParams.width - helpParams.progindent); + const auto proglines = Wrap(prognameline.begin(), prognameline.end(), + helpParams.width - (helpParams.progindent + helpParams.progtailindent), + helpParams.width - helpParams.progindent); auto progit = std::begin(proglines); if (progit != std::end(proglines)) { diff --git a/test.cxx b/test.cxx index 897dcf3..911779d 100644 --- a/test.cxx +++ b/test.cxx @@ -958,6 +958,50 @@ TEST_CASE("GetProgramLine works as expected", "[args]") REQUIRE(line(b) == "b -f [positional]"); } +TEST_CASE("Program line wrapping works as expected", "[args]") +{ + args::ArgumentParser p("parser"); + args::ValueFlag f(p, "foo_name", "f", {"foo"}); + args::ValueFlag g(p, "bar_name", "b", {"bar"}); + args::ValueFlag z(p, "baz_name", "z", {"baz"}); + + p.helpParams.proglineShowFlags = true; + p.helpParams.width = 42; + p.Prog("parser"); + p.ProglinePostfix("\na\nliiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiine line2 line2tail"); + + REQUIRE((p.GetProgramLine(p.helpParams) == std::vector{ + "[--foo ]", + "[--bar ]", + "[--baz ]", + "\n", + "a", + "\n", + "liiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiine", + "line2", + "line2tail", + })); + + std::ostringstream s; + s << p; + REQUIRE(s.str() == R"( parser [--foo ] + [--bar ] + [--baz ] + a + liiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiine + line2 line2tail + + parser + + OPTIONS: + + --foo=[foo_name] f + --bar=[bar_name] b + --baz=[baz_name] z + +)"); +} + TEST_CASE("Matcher validation works as expected", "[args]") { args::ArgumentParser parser("Test command"); @@ -984,7 +1028,7 @@ TEST_CASE("HelpParams work as expected", "[args]") )"); - p.helpParams.usageString = "usage: "; + p.helpParams.usageString = "usage:"; p.helpParams.optionsString = "Options"; p.helpParams.useValueNameOnce = true; REQUIRE(p.Help() == R"( usage: prog {OPTIONS} -- cgit v1.2.1