diff --git a/generate_auto_job b/generate_auto_job index 2dc51105..556b374c 100755 --- a/generate_auto_job +++ b/generate_auto_job @@ -19,7 +19,7 @@ def warn(*args, **kwargs): class Main: - SOURCES = [whoami, 'job.yml'] + SOURCES = [whoami, 'job.yml', 'manual/cli.rst'] DESTS = { 'decl': 'libqpdf/qpdf/auto_job_decl.hh', 'init': 'libqpdf/qpdf/auto_job_init.hh', @@ -87,6 +87,88 @@ class Main: for k, v in hashes.items(): print(f'{k} {v}', file=f) + def generate_doc(self, df, f): + st_top = 0 + st_topic = 1 + st_option = 2 + st_option_help = 3 + state = st_top + + indent = None + topic = None + option = None + short_text = None + long_text = None + + print('this->ap.addHelpFooter("For detailed help, visit' + ' the qpdf manual: https://qpdf.readthedocs.io\\n");', file=f) + + def set_indent(x): + nonlocal indent + indent = ' ' * len(x) + + def append_long_text(line): + nonlocal indent, long_text + if line == '\n': + long_text += '\n' + elif line.startswith(indent): + long_text += line[len(indent):] + else: + long_text = long_text.strip() + if long_text != '': + long_text += '\n' + return True + return False + + lineno = 0 + for line in df.readlines(): + lineno += 1 + if state == st_top: + m = re.match(r'^(\s*\.\. )help-topic (\S+): (.*)$', line) + if m: + set_indent(m.group(1)) + topic = m.group(2) + short_text = m.group(3) + long_text = '' + state = st_topic + continue + m = re.match(r'^(\s*\.\. )qpdf:option:: (([^=\s]+)(=(\S+))?)$', + line) + if m: + if topic is None: + raise Exception('option seen before topic') + set_indent(m.group(1)) + option = m.group(3) + synopsis = m.group(2) + if synopsis.endswith('`'): + raise Exception( + f'stray ` at end of option line (line {lineno})') + if synopsis != option: + long_text = synopsis + '\n' + else: + long_text = '' + state = st_option + continue + elif state == st_topic: + if append_long_text(line): + print(f'this->ap.addHelpTopic("{topic}", "{short_text}",' + f' R"({long_text})");', file=f) + state = st_top + elif state == st_option: + if line == '\n' or line.startswith(indent): + m = re.match(r'^(\s*\.\. )help: (.*)$', line) + if m: + set_indent(m.group(1)) + short_text = m.group(2) + state = st_option_help + else: + state = st_top + elif state == st_option_help: + if append_long_text(line): + print(f'this->ap.addOptionHelp("{option}", "{topic}",' + f' "{short_text}", R"({long_text})");', file=f) + state = st_top + def generate(self): warn(f'{whoami}: regenerating auto job files') @@ -230,6 +312,8 @@ class Main: for j in ft['options']: print('this->ap.copyFromOtherTable' f'("{j}", "{other_table}");', file=f) + with open('manual/cli.rst', 'r') as df: + self.generate_doc(df, f) if __name__ == '__main__': diff --git a/include/qpdf/QPDFArgParser.hh b/include/qpdf/QPDFArgParser.hh index ea51ca67..12ade54b 100644 --- a/include/qpdf/QPDFArgParser.hh +++ b/include/qpdf/QPDFArgParser.hh @@ -30,6 +30,7 @@ #include #include #include +#include // This is not a general-purpose argument parser. It is tightly // crafted to work with qpdf. qpdf's command-line syntax is very @@ -38,7 +39,10 @@ // backward compatibility to ensure we don't break what constitutes a // valid command. This class handles the quirks of qpdf's argument // parsing, bash/zsh completion, and support for @argfile to read -// arguments from a file. +// arguments from a file. For the qpdf CLI, setup of QPDFArgParser is +// done mostly by automatically-generated code (one-off code for +// qpdf), though the handlers themselves are hand-coded. See +// generate_auto_job at the top of the source tree for details. // Note about memory: there is code that expects argv to be a char*[], // meaning that arguments are writable. Several operations, including @@ -119,6 +123,13 @@ class QPDFArgParser std::string const& arg, param_arg_handler_t, bool required, char const** choices); + // The default behavior when an invalid choice is specified with + // an option that takes choices is to list all the choices. This + // may not be good if there are too many choices, so you can + // provide your own handler in this case. + QPDF_DLL + void addInvalidChoiceHandler(std::string const& arg, param_arg_handler_t); + // If an option is shared among multiple tables and uses identical // handlers, you can just copy it instead of repeating the // registration call. @@ -131,6 +142,67 @@ class QPDFArgParser QPDF_DLL void addFinalCheck(bare_arg_handler_t); + // Help generation methods + + // Help is available on topics and options. Options may be + // associated with topics. Users can run --help, --help=topic, or + // --help=--arg to get help. The top-level help tells the user how + // to run help and lists available topics. Help for a topic prints + // a short synopsis about the topic and lists any options that may + // be associated with the topic. Help for an option provides a + // short synopsis for that option. All help output is appended + // with a blurb (if supplied) directing the user to the full + // documentation. Help is not shown for options for which help has + // not been added. This makes it possible to have undocumented + // options for testing, backward-compatibility, etc. Also, it + // could be quite confusing to handle appropriate help for some + // inner options that may be repeated with different semantics + // inside different option tables. There is also no checking for + // whether an option that has help actually exists. In other + // words, it's up to the caller to ensure that help actually + // corresponds to the program's actual options. Rather than this + // being an intentional design decision, it is because this class + // is specifically for qpdf, qpdf generates its help and has other + // means to ensure consistency. + + // Note about newlines: + // + // short_text should fit easily after the topic/option on the same + // line and should not end with a newline. Keep it to around 40 to + // 60 characters. + // + // long_text and footer should end with a single newline. They can + // have embedded newlines. Keep lines to under 80 columns. + // + // QPDFArgParser does reformat the text, but it may add blank + // lines in some situations. Following the above conventions will + // keep the help looking uniform. + + // If provided, this footer is appended to all help, separated by + // a blank line. + QPDF_DLL + void addHelpFooter(std::string const&); + + // Add a help topic along with the text for that topic + QPDF_DLL + void addHelpTopic(std::string const& topic, + std::string const& short_text, + std::string const& long_text); + + // Add help for an option, and associate it with a topic. + QPDF_DLL + void addOptionHelp(std::string const& option_name, + std::string const& topic, + std::string const& short_text, + std::string const& long_text); + + // Return the help text for a topic or option. Passing a null + // pointer returns the top-level help information. Passing an + // unknown value returns a string directing the user to run the + // top-level --help option. + QPDF_DLL + std::string getHelp(char const* topic_or_option); + // Convenience methods for adding member functions of a class as // handlers. template @@ -171,7 +243,8 @@ class QPDFArgParser OptionEntry() : parameter_needed(false), bare_arg_handler(0), - param_arg_handler(0) + param_arg_handler(0), + invalid_choice_handler(0) { } bool parameter_needed; @@ -179,9 +252,24 @@ class QPDFArgParser std::set choices; bare_arg_handler_t bare_arg_handler; param_arg_handler_t param_arg_handler; + param_arg_handler_t invalid_choice_handler; }; typedef std::map option_table_t; + struct HelpTopic + { + HelpTopic() = default; + HelpTopic(std::string const& short_text, std::string const& long_text) : + short_text(short_text), + long_text(long_text) + { + } + + std::string short_text; + std::string long_text; + std::set options; + }; + OptionEntry& registerArg(std::string const& arg); void completionCommon(bool zsh); @@ -189,6 +277,7 @@ class QPDFArgParser void argCompletionBash(); void argCompletionZsh(); void argHelp(char*); + void invalidHelpArg(char*); void checkCompletion(); void handleArgFileArguments(); @@ -202,6 +291,11 @@ class QPDFArgParser option_table_t&, std::string const&, std::string const&); void handleCompletion(); + void getTopHelp(std::ostringstream&); + void getAllHelp(std::ostringstream&); + void getTopicHelp( + std::string const& name, HelpTopic const&, std::ostringstream&); + class Members { friend class QPDFArgParser; @@ -235,6 +329,9 @@ class QPDFArgParser std::vector> bash_argv; PointerHolder argv_ph; PointerHolder bash_argv_ph; + std::map help_topics; + std::map option_help; + std::string help_footer; }; PointerHolder m; }; diff --git a/job.sums b/job.sums index 14c948c7..b70547b8 100644 --- a/job.sums +++ b/job.sums @@ -1,5 +1,6 @@ # Generated by generate_auto_job -generate_auto_job 019081046f1bc19f498134eae00344ecfc65b4e52442ee5f1bc80bff99689443 +generate_auto_job 1f42fc554778d95210d11c44e858214b4854ead907d1c9ea84fe37f993ea1a23 job.yml 25c85cba1ae01dac9cd0f9cb7b734e7e3e531c0023ea2b892dc0d40bda1c1146 libqpdf/qpdf/auto_job_decl.hh 97395ecbe590b23ae04d6cce2080dbd0e998917ff5eeaa5c6aafa91041d3cd6a -libqpdf/qpdf/auto_job_init.hh 465bf46769559ceb77110d1b9d3293ba9b3595850b49848c31aeabd10aadb4ad +libqpdf/qpdf/auto_job_init.hh 2afffb5002ff28a3909f709709f65d77bf2289dd72d5ea3d1598a36664a49c73 +manual/cli.rst f0109cca3366a9da4b0a05e3cce996ece2d776321a3f689aeaa2d6af599eee88 diff --git a/libqpdf/QPDFArgParser.cc b/libqpdf/QPDFArgParser.cc index f32b8759..69a97a5c 100644 --- a/libqpdf/QPDFArgParser.cc +++ b/libqpdf/QPDFArgParser.cc @@ -36,12 +36,15 @@ QPDFArgParser::QPDFArgParser(int argc, char* argv[], char const* progname_env) : { selectHelpOptionTable(); char const* help_choices[] = {"all", 0}; + // More help choices are added dynamically. addChoices( "help", bindParam(&QPDFArgParser::argHelp, this), false, help_choices); + addInvalidChoiceHandler( + "help", bindParam(&QPDFArgParser::invalidHelpArg, this)); addBare("completion-bash", - std::bind(std::mem_fn(&QPDFArgParser::argCompletionBash), this)); + bindBare(&QPDFArgParser::argCompletionBash, this)); addBare("completion-zsh", - std::bind(std::mem_fn(&QPDFArgParser::argCompletionZsh), this)); + bindBare(&QPDFArgParser::argCompletionZsh, this)); selectMainOptionTable(); } @@ -157,6 +160,22 @@ QPDFArgParser::addChoices( } } +void +QPDFArgParser::addInvalidChoiceHandler( + std::string const& arg, param_arg_handler_t handler) +{ + auto i = this->m->option_table->find(arg); + if (i == this->m->option_table->end()) + { + QTC::TC("libtests", "QPDFArgParser invalid choice handler to unknown"); + throw std::logic_error( + "QPDFArgParser: attempt to add invalid choice handler" + " to unknown argument"); + } + auto& oe = i->second; + oe.invalid_choice_handler = handler; +} + void QPDFArgParser::copyFromOtherTable(std::string const& arg, std::string const& other_table) @@ -258,9 +277,17 @@ QPDFArgParser::argCompletionZsh() } void -QPDFArgParser::argHelp(char*) +QPDFArgParser::argHelp(char* p) { - // QXXXQ + std::cout << getHelp(p); + exit(0); +} + +void +QPDFArgParser::invalidHelpArg(char* p) +{ + usage(std::string("unknown help option") + + (p ? (std::string(" ") + p) : "")); } void @@ -640,7 +667,14 @@ QPDFArgParser::parseArgs() { std::string message = "--" + arg_s + " must be given as --" + arg_s + "="; - if (! oe.choices.empty()) + if (oe.invalid_choice_handler) + { + oe.invalid_choice_handler(parameter); + // Method should call usage() or exit. Just in case it + // doesn't... + message += "option"; + } + else if (! oe.choices.empty()) { QTC::TC("libtests", "QPDFArgParser required choices"); message += "{"; @@ -844,3 +878,166 @@ QPDFArgParser::handleCompletion() } exit(0); } + +void +QPDFArgParser::addHelpFooter(std::string const& text) +{ + this->m->help_footer = "\n" + text; +} + +void +QPDFArgParser::addHelpTopic(std::string const& topic, + std::string const& short_text, + std::string const& long_text) +{ + if (topic == "all") + { + QTC::TC("libtests", "QPDFArgParser add reserved help topic"); + throw std::logic_error( + "QPDFArgParser: can't register reserved help topic " + topic); + } + if (! ((topic.length() > 0) && (topic.at(0) != '-'))) + { + QTC::TC("libtests", "QPDFArgParser bad topic for help"); + throw std::logic_error( + "QPDFArgParser: help topics must not start with -"); + } + if (this->m->help_topics.count(topic)) + { + QTC::TC("libtests", "QPDFArgParser add existing topic"); + throw std::logic_error( + "QPDFArgParser: topic " + topic + " has already been added"); + } + + this->m->help_topics[topic] = HelpTopic(short_text, long_text); + this->m->help_option_table["help"].choices.insert(topic); +} + +void +QPDFArgParser::addOptionHelp(std::string const& option_name, + std::string const& topic, + std::string const& short_text, + std::string const& long_text) +{ + if (! ((option_name.length() > 2) && + (option_name.at(0) == '-') && + (option_name.at(1) == '-'))) + { + QTC::TC("libtests", "QPDFArgParser bad option for help"); + throw std::logic_error( + "QPDFArgParser: options for help must start with --"); + } + if (this->m->option_help.count(option_name)) + { + QTC::TC("libtests", "QPDFArgParser duplicate option help"); + throw std::logic_error( + "QPDFArgParser: option " + option_name + " already has help"); + } + auto ht = this->m->help_topics.find(topic); + if (ht == this->m->help_topics.end()) + { + QTC::TC("libtests", "QPDFArgParser add to unknown topic"); + throw std::logic_error( + "QPDFArgParser: unable to add option " + option_name + + " to unknown help topic " + topic); + } + this->m->option_help[option_name] = HelpTopic(short_text, long_text); + ht->second.options.insert(option_name); + this->m->help_option_table["help"].choices.insert(option_name); +} + +void +QPDFArgParser::getTopHelp(std::ostringstream& msg) +{ + msg << "Run \"" << this->m->whoami + << " --help=topic\" for help on a topic." << std::endl + << "Run \"" << this->m->whoami + << " --help=option\" for help on an option." << std::endl + << "Run \"" << this->m->whoami + << " --help=all\" to see all available help." << std::endl + << std::endl + << "Topics:" << std::endl; + for (auto const& i: this->m->help_topics) + { + msg << " " << i.first << ": " << i.second.short_text << std::endl; + } +} + +void +QPDFArgParser::getAllHelp(std::ostringstream& msg) +{ + getTopHelp(msg); + auto show = [this, &msg](std::map& topics, + std::string const& label) { + for (auto const& i: topics) + { + auto const& topic = i.first; + msg << std::endl + << "== " << label << " " << topic + << " (" << i.second.short_text << ") ==" + << std::endl + << std::endl; + getTopicHelp(topic, i.second, msg); + } + }; + show(this->m->help_topics, "topic"); + show(this->m->option_help, "option"); + msg << std::endl << "====" << std::endl; +} + +void +QPDFArgParser::getTopicHelp(std::string const& name, + HelpTopic const& ht, + std::ostringstream& msg) +{ + if (ht.long_text.empty()) + { + msg << ht.short_text << std::endl; + } + else + { + msg << ht.long_text; + } + if (! ht.options.empty()) + { + msg << std::endl << "Related options:" << std::endl; + for (auto const& i: ht.options) + { + msg << " " << i << ": " + << this->m->option_help[i].short_text << std::endl; + } + } +} + +std::string +QPDFArgParser::getHelp(char const* topic_or_option) +{ + std::ostringstream msg; + if ((topic_or_option == nullptr) || (strlen(topic_or_option) == 0)) + { + getTopHelp(msg); + } + else + { + std::string arg(topic_or_option); + if (arg == "all") + { + getAllHelp(msg); + } + else if (this->m->option_help.count(arg)) + { + getTopicHelp(arg, this->m->option_help[arg], msg); + } + else if (this->m->help_topics.count(arg)) + { + getTopicHelp(arg, this->m->help_topics[arg], msg); + } + else + { + // should not be possible + getTopHelp(msg); + } + } + msg << this->m->help_footer; + return msg.str(); +} diff --git a/libqpdf/qpdf/auto_job_init.hh b/libqpdf/qpdf/auto_job_init.hh index 3d7cdd7b..b19b2cc9 100644 --- a/libqpdf/qpdf/auto_job_init.hh +++ b/libqpdf/qpdf/auto_job_init.hh @@ -162,3 +162,4 @@ this->ap.copyFromOtherTable("annotate", "128-bit encryption"); this->ap.copyFromOtherTable("form", "128-bit encryption"); this->ap.copyFromOtherTable("modify-other", "128-bit encryption"); this->ap.copyFromOtherTable("modify", "128-bit encryption"); +this->ap.addHelpFooter("For detailed help, visit the qpdf manual: https://qpdf.readthedocs.io\n"); diff --git a/libtests/arg_parser.cc b/libtests/arg_parser.cc index 3da0206e..340bd8d4 100644 --- a/libtests/arg_parser.cc +++ b/libtests/arg_parser.cc @@ -68,6 +68,18 @@ ArgParser::initOptions() ap.addBare("sheep", [this](){ this->ap.selectOptionTable("sheep"); }); ap.registerOptionTable("sheep", nullptr); ap.copyFromOtherTable("ewe", "baaa"); + + ap.addHelpFooter("For more help, read the manual.\n"); + ap.addHelpTopic( + "quack", "Quack Options", + "Just put stuff after quack to get a count at the end.\n"); + ap.addHelpTopic( + "baaa", "Baaa Options", + "Ewe can do sheepish things.\n" + "For example, ewe can add more ram to your computer.\n"); + ap.addOptionHelp("--ewe", "baaa", + "just for ewe", "You are not a ewe.\n"); + ap.addOptionHelp("--ram", "baaa", "curly horns", ""); } void @@ -152,62 +164,60 @@ ArgParser::finalChecks() void ArgParser::test_exceptions() { - try - { + auto err = [](char const* msg, std::function fn) { + try + { + fn(); + assert(msg == nullptr); + } + catch (std::exception& e) + { + std::cout << msg << ": " << e.what() << std::endl; + } + }; + + err("duplicate handler", [this]() { ap.selectMainOptionTable(); ap.addBare("potato", [](){}); - assert(false); - } - catch (std::exception& e) - { - std::cout << "duplicate handler: " << e.what() << std::endl; - } - try - { + }); + err("duplicate handler", [this]() { ap.selectOptionTable("baaa"); ap.addBare("ram", [](){}); - assert(false); - } - catch (std::exception& e) - { - std::cout << "duplicate handler: " << e.what() << std::endl; - } - try - { + }); + err("duplicate table", [this]() { ap.registerOptionTable("baaa", nullptr); - assert(false); - } - catch (std::exception& e) - { - std::cout << "duplicate table: " << e.what() << std::endl; - } - try - { + }); + err("unknown table", [this]() { ap.selectOptionTable("aardvark"); - assert(false); - } - catch (std::exception& e) - { - std::cout << "unknown table: " << e.what() << std::endl; - } - try - { + }); + err("copy from unknown table", [this]() { ap.copyFromOtherTable("one", "two"); - assert(false); - } - catch (std::exception& e) - { - std::cout << "copy from unknown table: " << e.what() << std::endl; - } - try - { + }); + err("copy unknown from other table", [this]() { ap.copyFromOtherTable("two", "baaa"); - assert(false); - } - catch (std::exception& e) - { - std::cout << "copy unknown from other table: " << e.what() << std::endl; - } + }); + err("add existing help topic", [this]() { + ap.addHelpTopic("baaa", "potato", "salad"); + }); + err("add reserved help topic", [this]() { + ap.addHelpTopic("all", "potato", "salad"); + }); + err("add to unknown topic", [this]() { + ap.addOptionHelp("--new", "oops", "potato", "salad"); + }); + err("bad option for help", [this]() { + ap.addOptionHelp("nodash", "baaa", "potato", "salad"); + }); + err("bad topic for help", [this]() { + ap.addHelpTopic("--dashes", "potato", "salad"); + }); + err("duplicate option help", [this]() { + ap.addOptionHelp("--ewe", "baaa", "potato", "salad"); + }); + err("invalid choice handler to unknown", [this]() { + ap.addInvalidChoiceHandler( + "elephant", [](char*){}); + }); } int main(int argc, char* argv[]) diff --git a/libtests/libtests.testcov b/libtests/libtests.testcov index 69573ab0..70aae578 100644 --- a/libtests/libtests.testcov +++ b/libtests/libtests.testcov @@ -54,3 +54,10 @@ QPDFArgParser unrecognized 0 QPDFArgParser complete choices 0 QPDFArgParser copy from unknown 0 QPDFArgParser copy unknown 0 +QPDFArgParser add reserved help topic 0 +QPDFArgParser add existing topic 0 +QPDFArgParser add to unknown topic 0 +QPDFArgParser duplicate option help 0 +QPDFArgParser bad option for help 0 +QPDFArgParser bad topic for help 0 +QPDFArgParser invalid choice handler to unknown 0 diff --git a/libtests/qtest/arg_parser.test b/libtests/qtest/arg_parser.test index 1d24d507..5079289a 100644 --- a/libtests/qtest/arg_parser.test +++ b/libtests/qtest/arg_parser.test @@ -101,4 +101,30 @@ $td->runtest("args from stdin", {$td->FILE => "stdin.out", $td->EXIT_STATUS => 0}, $td->NORMALIZE_NEWLINES); -$td->report(2 + (2 * scalar(@completion_tests)) + scalar(@arg_tests)); +my @help_tests = ( + '', + '=all', + '=--ewe', + '=quack', + ); +foreach my $i (@help_tests) +{ + my $out = $i; + $out =~ s/[=-]//g; + if ($out ne '') + { + $out = "-$out"; + } + $td->runtest("--help$i", + {$td->COMMAND => "arg_parser --help$i"}, + {$td->FILE => "help$out.out", $td->EXIT_STATUS => 0}, + $td->NORMALIZE_NEWLINES); +} + +$td->runtest("bad help option", + {$td->COMMAND => 'arg_parser --help=--oops'}, + {$td->FILE => "help-bad.out", $td->EXIT_STATUS => 2}, + $td->NORMALIZE_NEWLINES); + +$td->report(3 + (2 * scalar(@completion_tests)) + + scalar(@arg_tests) + scalar(@help_tests)); diff --git a/libtests/qtest/arg_parser/completion-top-arg-zsh.out b/libtests/qtest/arg_parser/completion-top-arg-zsh.out index 5a500d38..5c159957 100644 --- a/libtests/qtest/arg_parser/completion-top-arg-zsh.out +++ b/libtests/qtest/arg_parser/completion-top-arg-zsh.out @@ -2,7 +2,9 @@ --completion-zsh --help --help= +--help=--ewe --help=all +--help=quack --moo --moo= --oink= diff --git a/libtests/qtest/arg_parser/completion-top-arg.out b/libtests/qtest/arg_parser/completion-top-arg.out index 4e69efbd..db3d4b0a 100644 --- a/libtests/qtest/arg_parser/completion-top-arg.out +++ b/libtests/qtest/arg_parser/completion-top-arg.out @@ -1,5 +1,7 @@ --baaa --completion-zsh +--help +--help= --moo --moo= --oink= diff --git a/libtests/qtest/arg_parser/exceptions.out b/libtests/qtest/arg_parser/exceptions.out index 82eef2a7..eb8dbe8a 100644 --- a/libtests/qtest/arg_parser/exceptions.out +++ b/libtests/qtest/arg_parser/exceptions.out @@ -4,3 +4,10 @@ duplicate table: QPDFArgParser: registering already registered option table baaa unknown table: QPDFArgParser: selecting unregistered option table aardvark copy from unknown table: QPDFArgParser: attempt to copy from unknown table two copy unknown from other table: QPDFArgParser: attempt to copy unknown argument two from table baaa +add existing help topic: QPDFArgParser: topic baaa has already been added +add reserved help topic: QPDFArgParser: can't register reserved help topic all +add to unknown topic: QPDFArgParser: unable to add option --new to unknown help topic oops +bad option for help: QPDFArgParser: options for help must start with -- +bad topic for help: QPDFArgParser: help topics must not start with - +duplicate option help: QPDFArgParser: option --ewe already has help +invalid choice handler to unknown: QPDFArgParser: attempt to add invalid choice handler to unknown argument diff --git a/libtests/qtest/arg_parser/help-all.out b/libtests/qtest/arg_parser/help-all.out new file mode 100644 index 00000000..432d4afb --- /dev/null +++ b/libtests/qtest/arg_parser/help-all.out @@ -0,0 +1,32 @@ +Run "arg_parser --help=topic" for help on a topic. +Run "arg_parser --help=option" for help on an option. +Run "arg_parser --help=all" to see all available help. + +Topics: + baaa: Baaa Options + quack: Quack Options + +== topic baaa (Baaa Options) == + +Ewe can do sheepish things. +For example, ewe can add more ram to your computer. + +Related options: + --ewe: just for ewe + --ram: curly horns + +== topic quack (Quack Options) == + +Just put stuff after quack to get a count at the end. + +== option --ewe (just for ewe) == + +You are not a ewe. + +== option --ram (curly horns) == + +curly horns + +==== + +For more help, read the manual. diff --git a/libtests/qtest/arg_parser/help-bad.out b/libtests/qtest/arg_parser/help-bad.out new file mode 100644 index 00000000..e9d753b2 --- /dev/null +++ b/libtests/qtest/arg_parser/help-bad.out @@ -0,0 +1 @@ +usage: unknown help option --oops diff --git a/libtests/qtest/arg_parser/help-ewe.out b/libtests/qtest/arg_parser/help-ewe.out new file mode 100644 index 00000000..7fe4bb0e --- /dev/null +++ b/libtests/qtest/arg_parser/help-ewe.out @@ -0,0 +1,3 @@ +You are not a ewe. + +For more help, read the manual. diff --git a/libtests/qtest/arg_parser/help-quack.out b/libtests/qtest/arg_parser/help-quack.out new file mode 100644 index 00000000..b114471e --- /dev/null +++ b/libtests/qtest/arg_parser/help-quack.out @@ -0,0 +1,3 @@ +Just put stuff after quack to get a count at the end. + +For more help, read the manual. diff --git a/libtests/qtest/arg_parser/help.out b/libtests/qtest/arg_parser/help.out new file mode 100644 index 00000000..0accbe67 --- /dev/null +++ b/libtests/qtest/arg_parser/help.out @@ -0,0 +1,9 @@ +Run "arg_parser --help=topic" for help on a topic. +Run "arg_parser --help=option" for help on an option. +Run "arg_parser --help=all" to see all available help. + +Topics: + baaa: Baaa Options + quack: Quack Options + +For more help, read the manual.