aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rwxr-xr-xgenerate_auto_job86
-rw-r--r--include/qpdf/QPDFArgParser.hh101
-rw-r--r--job.sums5
-rw-r--r--libqpdf/QPDFArgParser.cc207
-rw-r--r--libqpdf/qpdf/auto_job_init.hh1
-rw-r--r--libtests/arg_parser.cc106
-rw-r--r--libtests/libtests.testcov7
-rw-r--r--libtests/qtest/arg_parser.test28
-rw-r--r--libtests/qtest/arg_parser/completion-top-arg-zsh.out2
-rw-r--r--libtests/qtest/arg_parser/completion-top-arg.out2
-rw-r--r--libtests/qtest/arg_parser/exceptions.out7
-rw-r--r--libtests/qtest/arg_parser/help-all.out32
-rw-r--r--libtests/qtest/arg_parser/help-bad.out1
-rw-r--r--libtests/qtest/arg_parser/help-ewe.out3
-rw-r--r--libtests/qtest/arg_parser/help-quack.out3
-rw-r--r--libtests/qtest/arg_parser/help.out9
16 files changed, 541 insertions, 59 deletions
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 <vector>
#include <functional>
#include <stdexcept>
+#include <sstream>
// 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 <class T>
@@ -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<std::string> 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<std::string, OptionEntry> 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<std::string> 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<PointerHolder<char>> bash_argv;
PointerHolder<char*> argv_ph;
PointerHolder<char*> bash_argv_ph;
+ std::map<std::string, HelpTopic> help_topics;
+ std::map<std::string, HelpTopic> option_help;
+ std::string help_footer;
};
PointerHolder<Members> 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();
}
@@ -158,6 +161,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<std::string, HelpTopic>& 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<void()> 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.