From ab9b093c1004a1ce03e1bee579d1b7b7217913bd Mon Sep 17 00:00:00 2001 From: Pavel Belikov Date: Mon, 6 Nov 2017 10:55:19 +0300 Subject: refactor GetDescription --- args.hxx | 145 +++++++++++++++++++++++++++++++++++---------------------------- test.cxx | 6 +-- 2 files changed, 83 insertions(+), 68 deletions(-) diff --git a/args.hxx b/args.hxx index 75240b1..53b8718 100644 --- a/args.hxx +++ b/args.hxx @@ -431,6 +431,57 @@ namespace args return static_cast(static_cast(lhs) & static_cast(rhs)); } + /** A simple structure of parameters for easy user-modifyable help menus + */ + struct HelpParams + { + /** The width of the help menu + */ + unsigned int width = 80; + /** The indent of the program line + */ + unsigned int progindent = 2; + /** The indent of the program trailing lines for long parameters + */ + unsigned int progtailindent = 4; + /** The indent of the description and epilogs + */ + unsigned int descriptionindent = 4; + /** The indent of the flags + */ + unsigned int flagindent = 6; + /** The indent of the flag descriptions + */ + unsigned int helpindent = 40; + /** The additional indent each group adds + */ + unsigned int eachgroupindent = 2; + + /** The minimum gutter between each flag and its help + */ + unsigned int gutter = 1; + + /** Show the terminator when both options and positional parameters are present + */ + bool showTerminator = true; + + /** Show the {OPTIONS} on the prog line when this is true + */ + bool showProglineOptions = true; + + /** Show the positionals on the prog line when this is true + */ + bool showProglinePositionals = true; + + std::string shortPrefix; + + std::string longPrefix; + + std::string shortSeparator; + + std::string longSeparator; + }; + class FlagBase; class PositionalBase; class Command; @@ -474,7 +525,7 @@ namespace args return Matched(); } - virtual std::vector> GetDescription(const std::string &, const std::string &, const std::string &, const std::string &, unsigned indentLevel) const + virtual std::vector> GetDescription(const HelpParams ¶ms, unsigned indentLevel) const { std::tuple description; std::get<1>(description) = help; @@ -560,7 +611,7 @@ namespace args NamedBase(const std::string &name_, const std::string &help_, Options options_ = {}) : Base(help_, options_), name(name_), kickout(false) {} virtual ~NamedBase() {} - virtual std::vector> GetDescription(const std::string &, const std::string &, const std::string &, const std::string &, unsigned indentLevel) const override + virtual std::vector> GetDescription(const HelpParams ¶ms, unsigned indentLevel) const override { std::tuple description; std::get<0>(description) = Name(); @@ -643,10 +694,10 @@ namespace args } } - virtual std::vector> GetDescription(const std::string &shortPrefix, const std::string &longPrefix, const std::string &, const std::string &, unsigned indentLevel) const override + virtual std::vector> GetDescription(const HelpParams ¶ms, unsigned indentLevel) const override { std::tuple description; - const auto flagStrings = matcher.GetFlagStrings(shortPrefix, longPrefix); + const auto flagStrings = matcher.GetFlagStrings(params.shortPrefix, params.longPrefix); std::ostringstream flagstream; for (auto it = std::begin(flagStrings); it != std::end(flagStrings); ++it) { @@ -689,10 +740,10 @@ namespace args ValueFlagBase(const std::string &name_, const std::string &help_, Matcher &&matcher_, Options options_) : FlagBase(name_, help_, std::move(matcher_), options_) {} virtual ~ValueFlagBase() {} - virtual std::vector> GetDescription(const std::string &shortPrefix, const std::string &longPrefix, const std::string &shortSeparator, const std::string &longSeparator, unsigned indentLevel) const override + virtual std::vector> GetDescription(const HelpParams ¶ms, unsigned indentLevel) const override { std::tuple description; - const auto flagStrings = matcher.GetFlagStrings(shortPrefix, longPrefix, Name(), shortSeparator, longSeparator); + const auto flagStrings = matcher.GetFlagStrings(params.shortPrefix, params.longPrefix, Name(), params.shortSeparator, params.longSeparator); std::ostringstream flagstream; for (auto it = std::begin(flagStrings); it != std::end(flagStrings); ++it) { @@ -922,7 +973,7 @@ namespace args /** Get all the child descriptions for help generation */ - virtual std::vector> GetDescription(const std::string &shortPrefix, const std::string &longPrefix, const std::string &shortSeparator, const std::string &longSeparator, const unsigned int indent) const + virtual std::vector> GetDescription(const HelpParams ¶ms, const unsigned int indent) const { std::vector> descriptions; @@ -941,7 +992,7 @@ namespace args continue; } - auto groupDescriptions = child->GetDescription(shortPrefix, longPrefix, shortSeparator, longSeparator, indent + addindent); + auto groupDescriptions = child->GetDescription(params, indent + addindent); descriptions.insert( std::end(descriptions), std::make_move_iterator(std::begin(groupDescriptions)), @@ -1568,63 +1619,17 @@ namespace args } public: - /** A simple structure of parameters for easy user-modifyable help menus - */ - struct HelpParams - { - /** The width of the help menu - */ - unsigned int width = 80; - /** The indent of the program line - */ - unsigned int progindent = 2; - /** The indent of the program trailing lines for long parameters - */ - unsigned int progtailindent = 4; - /** The indent of the description and epilogs - */ - unsigned int descriptionindent = 4; - /** The indent of the flags - */ - unsigned int flagindent = 6; - /** The indent of the flag descriptions - */ - unsigned int helpindent = 40; - /** The additional indent each group adds - */ - unsigned int eachgroupindent = 2; - - /** The minimum gutter between each flag and its help - */ - unsigned int gutter = 1; - - /** Show the terminator when both options and positional parameters are present - */ - bool showTerminator = true; - - /** Show the {OPTIONS} on the prog line when this is true - */ - bool showProglineOptions = true; - - /** Show the positionals on the prog line when this is true - */ - bool showProglinePositionals = true; - } helpParams; + HelpParams helpParams; + ArgumentParser(const std::string &description_, const std::string &epilog_ = std::string()) : description(description_), epilog(epilog_), - longprefix("--"), - shortprefix("-"), - longseparator("="), - terminator("--"), - allowJoinedShortValue(true), - allowJoinedLongValue(true), - allowSeparateShortValue(true), - allowSeparateLongValue(true) {} - - ArgumentParser(Group &global_, const std::string &description_, const std::string &epilog_ = std::string()) : ArgumentParser(description_, epilog_) + terminator("--") { - Add(global_); + LongPrefix("--"); + ShortPrefix("-"); + LongSeparator("="); + SetArgumentSeparations(true, true, true, true); } /** The program name for help generation @@ -1653,7 +1658,7 @@ namespace args */ void Description(const std::string &description_) { this->description = description_; } - + /** The description that appears below options */ const std::string &Epilog() const @@ -1670,7 +1675,10 @@ namespace args /** The prefix for long flags */ void LongPrefix(const std::string &longprefix_) - { this->longprefix = longprefix_; } + { + this->longprefix = longprefix_; + this->helpParams.longPrefix = longprefix_; + } /** The prefix for short flags */ @@ -1679,7 +1687,10 @@ namespace args /** The prefix for short flags */ void ShortPrefix(const std::string &shortprefix_) - { this->shortprefix = shortprefix_; } + { + this->shortprefix = shortprefix_; + this->helpParams.shortPrefix = shortprefix_; + } /** The separator for long flags */ @@ -1699,6 +1710,7 @@ namespace args } else { this->longseparator = longseparator_; + this->helpParams.longSeparator = allowJoinedLongValue ? longseparator_ : " "; } } @@ -1744,6 +1756,9 @@ namespace args this->allowJoinedLongValue = allowJoinedLongValue_; this->allowSeparateShortValue = allowSeparateShortValue_; this->allowSeparateLongValue = allowSeparateLongValue_; + + this->helpParams.longSeparator = allowJoinedLongValue ? longseparator : " "; + this->helpParams.shortSeparator = allowJoinedShortValue ? "" : " "; } /** Pass the help menu into an ostream @@ -1797,7 +1812,7 @@ namespace args } help_ << "\n"; help_ << std::string(helpParams.progindent, ' ') << "OPTIONS:\n\n"; - for (const auto &desc: Group::GetDescription(shortprefix, longprefix, allowJoinedShortValue ? "" : " ", allowJoinedLongValue ? longseparator : " ", 0)) + for (const auto &desc: Group::GetDescription(helpParams, 0)) { const auto groupindent = std::get<2>(desc) * helpParams.eachgroupindent; const auto flags = Wrap(std::get<0>(desc), helpParams.width - (helpParams.flagindent + helpParams.helpindent + helpParams.gutter)); diff --git a/test.cxx b/test.cxx index eddf7fd..a9991fc 100644 --- a/test.cxx +++ b/test.cxx @@ -599,11 +599,11 @@ TEST_CASE("Hidden options are excluded from help", "[args]") args::ValueFlag foo1(group, "foo", "foo", {'f', "foo"}, args::Options::Hidden); args::ValueFlag bar2(group, "bar", "bar", {'b'}); - auto desc = parser1.GetDescription("", "", "", "", 0); + auto desc = parser1.GetDescription(parser1.helpParams, 0); REQUIRE(desc.size() == 3); - REQUIRE(std::get<0>(desc[0]) == "b[bar]"); + REQUIRE(std::get<0>(desc[0]) == "-b[bar]"); REQUIRE(std::get<0>(desc[1]) == "group"); - REQUIRE(std::get<0>(desc[2]) == "b[bar]"); + REQUIRE(std::get<0>(desc[2]) == "-b[bar]"); } TEST_CASE("Implicit values work as expected", "[args]") -- cgit v1.2.1 From ff6f09803274b41d695e9e632db41b380d9e9c99 Mon Sep 17 00:00:00 2001 From: Pavel Belikov Date: Mon, 6 Nov 2017 10:57:00 +0300 Subject: add description for commands --- args.hxx | 90 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------ test.cxx | 30 ++++++++++++++++++++++ 2 files changed, 112 insertions(+), 8 deletions(-) diff --git a/args.hxx b/args.hxx index 53b8718..ca4d966 100644 --- a/args.hxx +++ b/args.hxx @@ -473,12 +473,20 @@ namespace args */ bool showProglinePositionals = true; + /** The prefix for short flags + */ std::string shortPrefix; + /** The prefix for long flags + */ std::string longPrefix; + /** The separator for short flags + */ std::string shortSeparator; + /** The separator for long flags + */ std::string longSeparator; }; @@ -1081,10 +1089,11 @@ namespace args std::vector args; std::vector kicked; ArgumentParser &parser; + Command &command; bool isParsed = false; public: - Subparser(std::vector args_, ArgumentParser &parser_) : args(std::move(args_)), parser(parser_) + Subparser(std::vector args_, ArgumentParser &parser_, Command &command_) : args(std::move(args_)), parser(parser_), command(command_) { } @@ -1093,6 +1102,11 @@ namespace args Subparser &operator = (const Subparser&) = delete; Subparser &operator = (Subparser&&) = delete; + Command &GetCommand() + { + return command; + } + bool IsParsed() const { return isParsed; @@ -1109,10 +1123,13 @@ namespace args class Command : public Group { private: + friend class Subparser; + std::string name; std::string description; std::function parserCoroutine; bool commandIsRequired = false; + std::vector> subparserDescription; protected: @@ -1146,6 +1163,17 @@ namespace args return selectedCommand != nullptr ? selectedCommand->GetCoroutine() : parserCoroutine; } + Command &SelectedCommand() + { + Command *res = this; + while (res->selectedCommand != nullptr) + { + res = res->selectedCommand; + } + + return *res; + } + public: Command(Group &base_, std::string name_, std::string description_, std::function coroutine_ = {}) : name(std::move(name_)), description(std::move(description_)), parserCoroutine(std::move(coroutine_)) @@ -1300,6 +1328,54 @@ namespace args return { this }; } + virtual std::vector> GetDescription(const HelpParams ¶ms, const unsigned int indent) const + { + if (selectedCommand != nullptr) + { + return selectedCommand->GetDescription(params, indent); + } + + std::vector> descriptions; + unsigned addindent = 0; + + std::tuple desc; + std::get<0>(desc) = Name(); + std::get<1>(desc) = description; + std::get<2>(desc) = indent; + if (!Name().empty()) + { + addindent = 1; + descriptions.push_back(desc); + } + + if (!Matched()) + { + return descriptions; + } + + for (Base *child: Children()) + { + if ((child->GetOptions() & Options::Hidden) != Options::None) + { + continue; + } + + auto groupDescriptions = child->GetDescription(params, indent + addindent); + descriptions.insert( + std::end(descriptions), + std::make_move_iterator(std::begin(groupDescriptions)), + std::make_move_iterator(std::end(groupDescriptions))); + } + + for (auto childDescription: subparserDescription) + { + std::get<2>(childDescription) += indent + addindent; + descriptions.push_back(std::move(childDescription)); + } + + return descriptions; + } + virtual void Validate(const std::string &shortprefix, const std::string &longprefix) override { for (Base *child: Children()) @@ -1630,6 +1706,7 @@ namespace args ShortPrefix("-"); LongSeparator("="); SetArgumentSeparations(true, true, true, true); + matched = true; } /** The program name for help generation @@ -1921,18 +1998,15 @@ namespace args } }; - Command::RaiiSubparser::RaiiSubparser(ArgumentParser &parser_, std::vector args_) : command(&parser_), parser(std::move(args_), parser_) + Command::RaiiSubparser::RaiiSubparser(ArgumentParser &parser_, std::vector args_) : command(&parser_), parser(std::move(args_), parser_, parser_.SelectedCommand()) { - while (command->selectedCommand != nullptr) - { - command = command->selectedCommand; - } - - command->subparser = &parser; + parser.GetCommand().subparser = &parser; } void Subparser::Parse() { + isParsed = true; + command.subparserDescription = GetDescription(parser.helpParams, 0); auto it = parser.Parse(args.begin(), args.end()); kicked.assign(it, args.end()); } diff --git a/test.cxx b/test.cxx index a9991fc..e95786e 100644 --- a/test.cxx +++ b/test.cxx @@ -746,6 +746,36 @@ TEST_CASE("Subparser commands with kick-out flags work as expected", "[args]") REQUIRE((kickedOut == std::vector{"A", "B", "C", "D"})); } +TEST_CASE("Subparser help works as expected", "[args]") +{ + args::ArgumentParser p("git-like parser"); + args::Flag g(p, "GLOBAL", "global flag", {'g'}, args::Options::Global); + + args::Command add(p, "add", "add file contents to the index", [&](args::Subparser &c) + { + args::Flag flag(c, "FLAG", "flag", {'f'}); + c.Parse(); + }); + + args::Command commit(p, "commit", "record changes to the repository", [&](args::Subparser &c) + { + args::Flag flag(c, "FLAG", "flag", {'f'}); + c.Parse(); + }); + + auto d = p.GetDescription(p.helpParams, 0); + REQUIRE(d.size() == 3); + REQUIRE(std::get<0>(d[0]) == "-g"); + REQUIRE(std::get<0>(d[1]) == "add"); + REQUIRE(std::get<0>(d[2]) == "commit"); + + p.ParseArgs(std::vector{"add"}); + d = p.GetDescription(p.helpParams, 0); + REQUIRE(d.size() == 2); + REQUIRE(std::get<0>(d[0]) == "add"); + REQUIRE(std::get<0>(d[1]) == "-f"); +} + #undef ARGS_HXX #define ARGS_TESTNAMESPACE #define ARGS_NOEXCEPT -- cgit v1.2.1 From 3419d110deb32ef2b840cfc3094b89b9e02efe88 Mon Sep 17 00:00:00 2001 From: Pavel Belikov Date: Mon, 6 Nov 2017 10:58:48 +0300 Subject: add help output for commands --- args.hxx | 174 ++++++++++++++++++++++++++++++++++++++------------------------- test.cxx | 35 ++++++++++--- 2 files changed, 133 insertions(+), 76 deletions(-) diff --git a/args.hxx b/args.hxx index ca4d966..f439d9f 100644 --- a/args.hxx +++ b/args.hxx @@ -488,6 +488,10 @@ namespace args /** The separator for long flags */ std::string longSeparator; + + /** The program name for help generation + */ + std::string programName; }; class FlagBase; @@ -1126,13 +1130,21 @@ namespace args friend class Subparser; std::string name; - std::string description; + std::string help; std::function parserCoroutine; bool commandIsRequired = false; + std::vector> subparserDescription; + std::vector subparserProgramLine; + bool subparserHasFlag = false; protected: + std::string description; + std::string epilog; + std::string proglinePostfix; + + Command *selectedCommand = nullptr; Subparser *subparser = nullptr; @@ -1174,13 +1186,54 @@ namespace args return *res; } + const Command &SelectedCommand() const + { + const Command *res = this; + while (res->selectedCommand != nullptr) + { + res = res->selectedCommand; + } + + return *res; + } + public: - Command(Group &base_, std::string name_, std::string description_, std::function coroutine_ = {}) - : name(std::move(name_)), description(std::move(description_)), parserCoroutine(std::move(coroutine_)) + Command(Group &base_, std::string name_, std::string help_, std::function coroutine_ = {}) + : name(std::move(name_)), help(std::move(help_)), parserCoroutine(std::move(coroutine_)) { base_.Add(*this); } + /** The description that appears on the prog line after options + */ + const std::string &ProglinePostfix() const + { return proglinePostfix; } + + /** The description that appears on the prog line after options + */ + void ProglinePostfix(const std::string &proglinePostfix_) + { this->proglinePostfix = proglinePostfix_; } + + /** The description that appears above options + */ + const std::string &Description() const + { return description; } + /** The description that appears above options + */ + + void Description(const std::string &description_) + { this->description = description_; } + + /** The description that appears below options + */ + const std::string &Epilog() const + { return epilog; } + + /** The description that appears below options + */ + void Epilog(const std::string &epilog_) + { this->epilog = epilog_; } + const std::function &GetCoroutine() const { return parserCoroutine; @@ -1191,6 +1244,11 @@ namespace args return name; } + const std::string &Help() const + { + return help; + } + virtual bool IsGroup() const override { return false; @@ -1295,7 +1353,7 @@ namespace args return selectedCommand->HasFlag(); } - return Matched() ? Group::HasFlag() : false; + return Matched() ? subparserHasFlag || Group::HasFlag() : false; } virtual std::vector GetProgramLine() const @@ -1307,7 +1365,9 @@ namespace args if (Matched()) { - return Group::GetProgramLine(); + auto res = Group::GetProgramLine(); + res.insert(res.end(), subparserProgramLine.begin(), subparserProgramLine.end()); + return res; } return {}; @@ -1336,20 +1396,14 @@ namespace args } std::vector> descriptions; - unsigned addindent = 0; - - std::tuple desc; - std::get<0>(desc) = Name(); - std::get<1>(desc) = description; - std::get<2>(desc) = indent; - if (!Name().empty()) - { - addindent = 1; - descriptions.push_back(desc); - } if (!Matched()) { + if (!Name().empty()) + { + descriptions.emplace_back(Name(), help, indent); + } + return descriptions; } @@ -1360,7 +1414,7 @@ namespace args continue; } - auto groupDescriptions = child->GetDescription(params, indent + addindent); + auto groupDescriptions = child->GetDescription(params, indent); descriptions.insert( std::end(descriptions), std::make_move_iterator(std::begin(groupDescriptions)), @@ -1369,7 +1423,7 @@ namespace args for (auto childDescription: subparserDescription) { - std::get<2>(childDescription) += indent + addindent; + std::get<2>(childDescription) += indent; descriptions.push_back(std::move(childDescription)); } @@ -1409,11 +1463,6 @@ namespace args friend class Subparser; private: - std::string prog; - std::string proglinePostfix; - std::string description; - std::string epilog; - std::string longprefix; std::string shortprefix; @@ -1697,14 +1746,14 @@ namespace args public: HelpParams helpParams; - ArgumentParser(const std::string &description_, const std::string &epilog_ = std::string()) : - description(description_), - epilog(epilog_), - terminator("--") + ArgumentParser(const std::string &description_, const std::string &epilog_ = std::string()) { + Description(description_); + Epilog(epilog); LongPrefix("--"); ShortPrefix("-"); LongSeparator("="); + Terminator("--"); SetArgumentSeparations(true, true, true, true); matched = true; } @@ -1712,38 +1761,11 @@ namespace args /** The program name for help generation */ const std::string &Prog() const - { return prog; } + { return helpParams.programName; } /** The program name for help generation */ void Prog(const std::string &prog_) - { this->prog = prog_; } - - /** The description that appears on the prog line after options - */ - const std::string &ProglinePostfix() const - { return proglinePostfix; } - /** The description that appears on the prog line after options - */ - void ProglinePostfix(const std::string &proglinePostfix_) - { this->proglinePostfix = proglinePostfix_; } - - /** The description that appears above options - */ - const std::string &Description() const - { return description; } - /** The description that appears above options - */ - void Description(const std::string &description_) - { this->description = description_; } - - /** The description that appears below options - */ - const std::string &Epilog() const - { return epilog; } - /** The description that appears below options - */ - void Epilog(const std::string &epilog_) - { this->epilog = epilog_; } + { this->helpParams.programName = prog_; } /** The prefix for long flags */ @@ -1845,10 +1867,19 @@ namespace args bool hasoptions = false; bool hasarguments = false; - const auto description_text = Wrap(this->description, helpParams.width - helpParams.descriptionindent); - const auto epilog_text = Wrap(this->epilog, helpParams.width - helpParams.descriptionindent); + auto &command = SelectedCommand(); + const auto &description = command.Description().empty() ? command.Help() : command.Description(); + const auto description_text = Wrap(description, helpParams.width - helpParams.descriptionindent); + const auto epilog_text = Wrap(command.Epilog(), helpParams.width - helpParams.descriptionindent); + std::ostringstream prognameline; - prognameline << prog; + prognameline << Prog(); + + if (!command.Name().empty()) + { + prognameline << ' ' << command.Name(); + } + if (HasFlag()) { hasoptions = true; @@ -1857,7 +1888,8 @@ namespace args prognameline << " {OPTIONS}"; } } - for (const std::string &posname: GetProgramLine()) + + for (const std::string &posname: command.GetProgramLine()) { hasarguments = true; if (helpParams.showProglinePositionals) @@ -1865,9 +1897,9 @@ namespace args prognameline << " [" << posname << ']'; } } - if (!proglinePostfix.empty()) + if (!command.ProglinePostfix().empty()) { - prognameline << ' ' << proglinePostfix; + prognameline << ' ' << command.ProglinePostfix(); } const auto proglines = Wrap(prognameline.str(), helpParams.width - (helpParams.progindent + 4), helpParams.width - helpParams.progindent); auto progit = std::begin(proglines); @@ -1883,13 +1915,17 @@ namespace args help_ << '\n'; - for (const auto &line: description_text) + if (!description_text.empty()) { - help_ << std::string(helpParams.descriptionindent, ' ') << line << "\n"; + for (const auto &line: description_text) + { + help_ << std::string(helpParams.descriptionindent, ' ') << line << "\n"; + } + help_ << "\n"; } - help_ << "\n"; + help_ << std::string(helpParams.progindent, ' ') << "OPTIONS:\n\n"; - for (const auto &desc: Group::GetDescription(helpParams, 0)) + for (const auto &desc: command.GetDescription(helpParams, 0)) { const auto groupindent = std::get<2>(desc) * helpParams.eachgroupindent; const auto flags = Wrap(std::get<0>(desc), helpParams.width - (helpParams.flagindent + helpParams.helpindent + helpParams.gutter)); @@ -1989,9 +2025,9 @@ namespace args */ bool ParseCLI(const int argc, const char * const * argv) { - if (prog.empty()) + if (Prog().empty()) { - prog.assign(argv[0]); + Prog(argv[0]); } const std::vector args(argv + 1, argv + argc); return ParseArgs(args) == std::end(args); @@ -2007,6 +2043,8 @@ namespace args { isParsed = true; command.subparserDescription = GetDescription(parser.helpParams, 0); + command.subparserHasFlag = HasFlag(); + command.subparserProgramLine = GetProgramLine(); auto it = parser.Parse(args.begin(), args.end()); kicked.assign(it, args.end()); } diff --git a/test.cxx b/test.cxx index e95786e..18b8c69 100644 --- a/test.cxx +++ b/test.cxx @@ -763,17 +763,36 @@ TEST_CASE("Subparser help works as expected", "[args]") c.Parse(); }); + p.Prog("git"); + + std::ostringstream s; + auto d = p.GetDescription(p.helpParams, 0); - REQUIRE(d.size() == 3); - REQUIRE(std::get<0>(d[0]) == "-g"); - REQUIRE(std::get<0>(d[1]) == "add"); - REQUIRE(std::get<0>(d[2]) == "commit"); + s << p; + REQUIRE(s.str() == R"( git {OPTIONS} + + git-like parser + + OPTIONS: + + -g global flag + add add file contents to the index + commit record changes to the repository + +)"); p.ParseArgs(std::vector{"add"}); - d = p.GetDescription(p.helpParams, 0); - REQUIRE(d.size() == 2); - REQUIRE(std::get<0>(d[0]) == "add"); - REQUIRE(std::get<0>(d[1]) == "-f"); + s.str(""); + s << p; + REQUIRE(s.str() == R"( git add {OPTIONS} + + add file contents to the index + + OPTIONS: + + -f flag + +)"); } #undef ARGS_HXX -- cgit v1.2.1 From 3f6154ebeb153389ebf6ec1dded6a348dc3d9b58 Mon Sep 17 00:00:00 2001 From: Pavel Belikov Date: Mon, 6 Nov 2017 11:02:34 +0300 Subject: add HelpParams::showCommandChildren and HelpParams::showCommandFullHelp --- args.hxx | 267 +++++++++++++++++++++++++++++++++++++++++++++++---------------- test.cxx | 63 +++++++++++++++ 2 files changed, 263 insertions(+), 67 deletions(-) diff --git a/args.hxx b/args.hxx index f439d9f..1e4b1ab 100644 --- a/args.hxx +++ b/args.hxx @@ -492,6 +492,18 @@ namespace args /** The program name for help generation */ std::string programName; + + /** Show command's flags + */ + bool showCommandChildren = false; + + /** Show command's descriptions and epilog + */ + bool showCommandFullHelp = false; + + /** The postfix for progline when showProglineOptions is true and command has any flags + */ + std::string proglineOptions = "{OPTIONS}"; }; class FlagBase; @@ -570,7 +582,12 @@ namespace args return false; } - virtual std::vector GetProgramLine() const + virtual bool HasPositional() const + { + return false; + } + + virtual std::vector GetProgramLine(const HelpParams &) const { return {}; } @@ -809,9 +826,14 @@ namespace args return Ready() ? this : nullptr; } - virtual std::vector GetProgramLine() const override + virtual bool HasPositional() const override { - return {Name()}; + return true; + } + + virtual std::vector GetProgramLine(const HelpParams ¶ms) const override + { + return { "[" + Name() + ']' }; } virtual void Validate(const std::string &, const std::string &) override @@ -962,6 +984,15 @@ namespace args return std::any_of(Children().begin(), Children().end(), [](Base *child) { return child->HasFlag(); }); } + /** Get whether this has any PositionalBase children + * + * \return Whether or not there are any PositionalBase children + */ + virtual bool HasPositional() const override + { + return std::any_of(Children().begin(), Children().end(), [](Base *child) { return child->HasPositional(); }); + } + /** Count the number of matched children this group has */ std::vector::size_type MatchedChildren() const @@ -1015,12 +1046,17 @@ namespace args /** Get the names of positional parameters */ - virtual std::vector GetProgramLine() const override + virtual std::vector GetProgramLine(const HelpParams ¶ms) const override { std::vector names; for (Base *child: Children()) { - auto groupNames = child->GetProgramLine(); + if ((child->GetOptions() & Options::Hidden) != Options::None) + { + continue; + } + + auto groupNames = child->GetProgramLine(params); names.insert( std::end(names), std::make_move_iterator(std::begin(groupNames)), @@ -1047,6 +1083,8 @@ namespace args virtual void Reset() noexcept override { + Base::Reset(); + for (auto &child: Children()) { child->Reset(); @@ -1092,12 +1130,18 @@ namespace args private: std::vector args; std::vector kicked; - ArgumentParser &parser; - Command &command; + ArgumentParser *parser = nullptr; + const HelpParams &helpParams; + const Command &command; bool isParsed = false; public: - Subparser(std::vector args_, ArgumentParser &parser_, Command &command_) : args(std::move(args_)), parser(parser_), command(command_) + Subparser(std::vector args_, ArgumentParser &parser_, const Command &command_, const HelpParams &helpParams_) + : args(std::move(args_)), parser(&parser_), helpParams(helpParams_), command(command_) + { + } + + Subparser(const Command &command_, const HelpParams &helpParams_) : helpParams(helpParams_), command(command_) { } @@ -1106,7 +1150,7 @@ namespace args Subparser &operator = (const Subparser&) = delete; Subparser &operator = (Subparser&&) = delete; - Command &GetCommand() + const Command &GetCommand() { return command; } @@ -1131,31 +1175,31 @@ namespace args std::string name; std::string help; - std::function parserCoroutine; - bool commandIsRequired = false; - - std::vector> subparserDescription; - std::vector subparserProgramLine; - bool subparserHasFlag = false; - - protected: - std::string description; std::string epilog; std::string proglinePostfix; - + std::function parserCoroutine; + bool commandIsRequired = false; Command *selectedCommand = nullptr; - Subparser *subparser = nullptr; + + mutable std::vector> subparserDescription; + mutable std::vector subparserProgramLine; + mutable bool subparserHasFlag = false; + mutable bool subparserHasPositional = false; + mutable Subparser *subparser = nullptr; + + protected: class RaiiSubparser { public: RaiiSubparser(ArgumentParser &parser_, std::vector args_); + RaiiSubparser(const Command &command_, const HelpParams ¶ms_); ~RaiiSubparser() { - command->subparser = nullptr; + command.subparser = oldSubparser; } Subparser &Parser() @@ -1164,8 +1208,9 @@ namespace args } private: - Command *command; + const Command &command; Subparser parser; + Subparser *oldSubparser; }; Command() = default; @@ -1356,21 +1401,43 @@ namespace args return Matched() ? subparserHasFlag || Group::HasFlag() : false; } - virtual std::vector GetProgramLine() const + virtual bool HasPositional() const { if (selectedCommand != nullptr) { - return selectedCommand->GetProgramLine(); + return selectedCommand->HasPositional(); } - if (Matched()) + return Matched() ? subparserHasPositional || Group::HasPositional() : false; + } + + std::vector GetCommandProgramLine(const HelpParams ¶ms) const + { + auto res = Group::GetProgramLine(params); + res.insert(res.end(), subparserProgramLine.begin(), subparserProgramLine.end()); + + if (!Name().empty()) { - auto res = Group::GetProgramLine(); - res.insert(res.end(), subparserProgramLine.begin(), subparserProgramLine.end()); - return res; + res.insert(res.begin(), Name()); } - return {}; + if ((subparserHasFlag || Group::HasFlag()) && params.showProglineOptions) + { + res.push_back(params.proglineOptions); + } + + if (!ProglinePostfix().empty()) + { + res.push_back(ProglinePostfix()); + } + + return res; + } + + virtual std::vector GetProgramLine(const HelpParams ¶ms) const + { + auto &command = SelectedCommand(); + return command.Matched() ? command.GetCommandProgramLine(params) : std::vector(); } virtual std::vector GetCommands() override @@ -1396,15 +1463,65 @@ namespace args } std::vector> descriptions; + unsigned addindent = 0; + + if ((params.showCommandChildren || params.showCommandFullHelp) && !Matched() && parserCoroutine) + { + RaiiSubparser coro(*this, params); +#ifndef ARGS_NOEXCEPT + try + { + parserCoroutine(coro.Parser()); + } + catch (args::Help) + { + } +#else + parserCoroutine(coro.Parser()); +#endif + } + if (!Matched()) { - if (!Name().empty()) + if (params.showCommandFullHelp) + { + std::ostringstream s; + bool empty = true; + for (const auto &progline : GetCommandProgramLine(params)) + { + if (!empty) + { + s << ' '; + } + else + { + empty = false; + } + + s << progline; + } + + descriptions.emplace_back(s.str(), "", indent); + } + else { descriptions.emplace_back(Name(), help, indent); } - return descriptions; + if (!params.showCommandChildren && !params.showCommandFullHelp) + { + return descriptions; + } + + addindent = 1; + } + + if (params.showCommandFullHelp && !Matched()) + { + descriptions.emplace_back("", "", indent + addindent); + descriptions.emplace_back(Description().empty() ? Help() : Description(), "", indent + addindent); + descriptions.emplace_back("", "", indent + addindent); } for (Base *child: Children()) @@ -1414,7 +1531,7 @@ namespace args continue; } - auto groupDescriptions = child->GetDescription(params, indent); + auto groupDescriptions = child->GetDescription(params, indent + addindent); descriptions.insert( std::end(descriptions), std::make_move_iterator(std::begin(groupDescriptions)), @@ -1423,10 +1540,20 @@ namespace args for (auto childDescription: subparserDescription) { - std::get<2>(childDescription) += indent; + std::get<2>(childDescription) += indent + addindent; descriptions.push_back(std::move(childDescription)); } + if (params.showCommandFullHelp && !Matched()) + { + descriptions.emplace_back("", "", indent + addindent); + if (!Epilog().empty()) + { + descriptions.emplace_back(Epilog(), "", indent + addindent); + descriptions.emplace_back("", "", indent + addindent); + } + } + return descriptions; } @@ -1453,6 +1580,10 @@ namespace args { Group::Reset(); selectedCommand = nullptr; + subparserProgramLine.clear(); + subparserDescription.clear(); + subparserHasFlag = false; + subparserHasPositional = false; } }; @@ -1749,7 +1880,7 @@ namespace args ArgumentParser(const std::string &description_, const std::string &epilog_ = std::string()) { Description(description_); - Epilog(epilog); + Epilog(epilog_); LongPrefix("--"); ShortPrefix("-"); LongSeparator("="); @@ -1864,8 +1995,8 @@ namespace args */ void Help(std::ostream &help_) const { - bool hasoptions = false; - bool hasarguments = false; + const bool hasoptions = HasFlag(); + const bool hasarguments = HasPositional(); auto &command = SelectedCommand(); const auto &description = command.Description().empty() ? command.Help() : command.Description(); @@ -1875,32 +2006,11 @@ namespace args std::ostringstream prognameline; prognameline << Prog(); - if (!command.Name().empty()) - { - prognameline << ' ' << command.Name(); - } - - if (HasFlag()) + for (const std::string &posname: command.GetProgramLine(helpParams)) { - hasoptions = true; - if (helpParams.showProglineOptions) - { - prognameline << " {OPTIONS}"; - } + prognameline << ' ' << posname; } - for (const std::string &posname: command.GetProgramLine()) - { - hasarguments = true; - if (helpParams.showProglinePositionals) - { - prognameline << " [" << posname << ']'; - } - } - if (!command.ProglinePostfix().empty()) - { - prognameline << ' ' << command.ProglinePostfix(); - } const auto proglines = Wrap(prognameline.str(), helpParams.width - (helpParams.progindent + 4), helpParams.width - helpParams.progindent); auto progit = std::begin(proglines); if (progit != std::end(proglines)) @@ -1924,9 +2034,12 @@ namespace args help_ << "\n"; } + bool lastDescriptionIsNewline = false; + help_ << std::string(helpParams.progindent, ' ') << "OPTIONS:\n\n"; for (const auto &desc: command.GetDescription(helpParams, 0)) { + lastDescriptionIsNewline = std::get<0>(desc).empty() && std::get<1>(desc).empty(); const auto groupindent = std::get<2>(desc) * helpParams.eachgroupindent; const auto flags = Wrap(std::get<0>(desc), helpParams.width - (helpParams.flagindent + helpParams.helpindent + helpParams.gutter)); const auto info = Wrap(std::get<1>(desc), helpParams.width - (helpParams.helpindent + groupindent)); @@ -1960,13 +2073,18 @@ namespace args } if (hasoptions && hasarguments && helpParams.showTerminator) { + lastDescriptionIsNewline = false; for (const auto &item: Wrap(std::string("\"") + terminator + "\" can be used to terminate flag options and force all following arguments to be treated as positional options", helpParams.width - helpParams.flagindent)) { help_ << std::string(helpParams.flagindent, ' ') << item << '\n'; } } - help_ << "\n"; + if (!lastDescriptionIsNewline) + { + help_ << "\n"; + } + for (const auto &line: epilog_text) { help_ << std::string(helpParams.descriptionindent, ' ') << line << "\n"; @@ -2002,8 +2120,6 @@ namespace args // Reset all Matched statuses and errors Reset(); return Parse(begin, end); - - //TODO: check for required subparsers } /** Parse all arguments. @@ -2034,18 +2150,35 @@ namespace args } }; - Command::RaiiSubparser::RaiiSubparser(ArgumentParser &parser_, std::vector args_) : command(&parser_), parser(std::move(args_), parser_, parser_.SelectedCommand()) + Command::RaiiSubparser::RaiiSubparser(ArgumentParser &parser_, std::vector args_) + : command(parser_.SelectedCommand()), parser(std::move(args_), parser_, command, parser_.helpParams), oldSubparser(command.subparser) + { + command.subparser = &parser; + } + + Command::RaiiSubparser::RaiiSubparser(const Command &command_, const HelpParams ¶ms_): command(command_), parser(command, params_) { - parser.GetCommand().subparser = &parser; + command.subparser = &parser; } void Subparser::Parse() { isParsed = true; - command.subparserDescription = GetDescription(parser.helpParams, 0); + command.subparserDescription = GetDescription(helpParams, 0); command.subparserHasFlag = HasFlag(); - command.subparserProgramLine = GetProgramLine(); - auto it = parser.Parse(args.begin(), args.end()); + command.subparserHasPositional = HasPositional(); + command.subparserProgramLine = GetProgramLine(helpParams); + if (parser == nullptr) + { +#ifndef ARGS_NOEXCEPT + throw args::Help(""); +#else + error = Error::Help; + return; +#endif + } + + auto it = parser->Parse(args.begin(), args.end()); kicked.assign(it, args.end()); } diff --git a/test.cxx b/test.cxx index 18b8c69..597eaae 100644 --- a/test.cxx +++ b/test.cxx @@ -793,6 +793,69 @@ TEST_CASE("Subparser help works as expected", "[args]") -f flag )"); + + p.ParseArgs(std::vector{}); + s.str(""); + s << p; + REQUIRE(s.str() == R"( git {OPTIONS} + + git-like parser + + OPTIONS: + + -g global flag + add add file contents to the index + commit record changes to the repository + +)"); + + + p.helpParams.showCommandChildren = true; + p.ParseArgs(std::vector{}); + s.str(""); + s << p; + REQUIRE(s.str() == R"( git {OPTIONS} + + git-like parser + + OPTIONS: + + -g global flag + add add file contents to the index + -f flag + commit record changes to the repository + -f flag + +)"); + + commit.Epilog("epilog"); + p.helpParams.showCommandFullHelp = true; + p.ParseArgs(std::vector{}); + s.str(""); + s << p; + REQUIRE(s.str() == R"( git {OPTIONS} + + git-like parser + + OPTIONS: + + -g global flag + add {OPTIONS} + + add file contents to the index + + -f flag + + commit {OPTIONS} + + record changes to the repository + + -f flag + + epilog + +)"); + } #undef ARGS_HXX -- cgit v1.2.1 From 34c1a3bbfb094ac1e794a70d8e7d9e5d9b03900c Mon Sep 17 00:00:00 2001 From: Pavel Belikov Date: Mon, 6 Nov 2017 13:58:51 +0300 Subject: add COMMAND to program line --- args.hxx | 46 ++++++++++++++++++++++++++++++++-------------- test.cxx | 9 ++++----- 2 files changed, 36 insertions(+), 19 deletions(-) diff --git a/args.hxx b/args.hxx index 1e4b1ab..afefafe 100644 --- a/args.hxx +++ b/args.hxx @@ -504,6 +504,10 @@ namespace args /** The postfix for progline when showProglineOptions is true and command has any flags */ std::string proglineOptions = "{OPTIONS}"; + + /** The prefix for progline when command has any subcommands + */ + std::string proglineCommand = "COMMAND"; }; class FlagBase; @@ -587,6 +591,11 @@ namespace args return false; } + virtual bool HasCommand() const + { + return false; + } + virtual std::vector GetProgramLine(const HelpParams &) const { return {}; @@ -993,6 +1002,15 @@ namespace args return std::any_of(Children().begin(), Children().end(), [](Base *child) { return child->HasPositional(); }); } + /** Get whether this has any Command children + * + * \return Whether or not there are any Command children + */ + virtual bool HasCommand() const override + { + return std::any_of(Children().begin(), Children().end(), [](Base *child) { return child->HasCommand(); }); + } + /** Count the number of matched children this group has */ std::vector::size_type MatchedChildren() const @@ -1393,22 +1411,17 @@ namespace args virtual bool HasFlag() const { - if (selectedCommand != nullptr) - { - return selectedCommand->HasFlag(); - } - - return Matched() ? subparserHasFlag || Group::HasFlag() : false; + return subparserHasFlag || Group::HasFlag(); } virtual bool HasPositional() const { - if (selectedCommand != nullptr) - { - return selectedCommand->HasPositional(); - } + return subparserHasPositional || Group::HasPositional(); + } - return Matched() ? subparserHasPositional || Group::HasPositional() : false; + virtual bool HasCommand() const + { + return true; } std::vector GetCommandProgramLine(const HelpParams ¶ms) const @@ -1416,6 +1429,11 @@ namespace args auto res = Group::GetProgramLine(params); res.insert(res.end(), subparserProgramLine.begin(), subparserProgramLine.end()); + if (!params.proglineCommand.empty() && Group::HasCommand()) + { + res.insert(res.begin(), commandIsRequired ? params.proglineCommand : "[" + params.proglineCommand + "]"); + } + if (!Name().empty()) { res.insert(res.begin(), Name()); @@ -1995,14 +2013,14 @@ namespace args */ void Help(std::ostream &help_) const { - const bool hasoptions = HasFlag(); - const bool hasarguments = HasPositional(); - auto &command = SelectedCommand(); const auto &description = command.Description().empty() ? command.Help() : command.Description(); const auto description_text = Wrap(description, helpParams.width - helpParams.descriptionindent); const auto epilog_text = Wrap(command.Epilog(), helpParams.width - helpParams.descriptionindent); + const bool hasoptions = command.HasFlag(); + const bool hasarguments = command.HasPositional(); + std::ostringstream prognameline; prognameline << Prog(); diff --git a/test.cxx b/test.cxx index 597eaae..0726619 100644 --- a/test.cxx +++ b/test.cxx @@ -769,7 +769,7 @@ TEST_CASE("Subparser help works as expected", "[args]") auto d = p.GetDescription(p.helpParams, 0); s << p; - REQUIRE(s.str() == R"( git {OPTIONS} + REQUIRE(s.str() == R"( git [COMMAND] {OPTIONS} git-like parser @@ -797,7 +797,7 @@ TEST_CASE("Subparser help works as expected", "[args]") p.ParseArgs(std::vector{}); s.str(""); s << p; - REQUIRE(s.str() == R"( git {OPTIONS} + REQUIRE(s.str() == R"( git [COMMAND] {OPTIONS} git-like parser @@ -809,12 +809,11 @@ TEST_CASE("Subparser help works as expected", "[args]") )"); - p.helpParams.showCommandChildren = true; p.ParseArgs(std::vector{}); s.str(""); s << p; - REQUIRE(s.str() == R"( git {OPTIONS} + REQUIRE(s.str() == R"( git [COMMAND] {OPTIONS} git-like parser @@ -833,7 +832,7 @@ TEST_CASE("Subparser help works as expected", "[args]") p.ParseArgs(std::vector{}); s.str(""); s << p; - REQUIRE(s.str() == R"( git {OPTIONS} + REQUIRE(s.str() == R"( git [COMMAND] {OPTIONS} git-like parser -- cgit v1.2.1