Commit b4bd124be496170937d19742d83c2bad7471fe81

Authored by Jay Berkenbilt
1 parent 5303130c

QPDFArgParser: support adding/printing help information

generate_auto_job
... ... @@ -19,7 +19,7 @@ def warn(*args, **kwargs):
19 19  
20 20  
21 21 class Main:
22   - SOURCES = [whoami, 'job.yml']
  22 + SOURCES = [whoami, 'job.yml', 'manual/cli.rst']
23 23 DESTS = {
24 24 'decl': 'libqpdf/qpdf/auto_job_decl.hh',
25 25 'init': 'libqpdf/qpdf/auto_job_init.hh',
... ... @@ -87,6 +87,88 @@ class Main:
87 87 for k, v in hashes.items():
88 88 print(f'{k} {v}', file=f)
89 89  
  90 + def generate_doc(self, df, f):
  91 + st_top = 0
  92 + st_topic = 1
  93 + st_option = 2
  94 + st_option_help = 3
  95 + state = st_top
  96 +
  97 + indent = None
  98 + topic = None
  99 + option = None
  100 + short_text = None
  101 + long_text = None
  102 +
  103 + print('this->ap.addHelpFooter("For detailed help, visit'
  104 + ' the qpdf manual: https://qpdf.readthedocs.io\\n");', file=f)
  105 +
  106 + def set_indent(x):
  107 + nonlocal indent
  108 + indent = ' ' * len(x)
  109 +
  110 + def append_long_text(line):
  111 + nonlocal indent, long_text
  112 + if line == '\n':
  113 + long_text += '\n'
  114 + elif line.startswith(indent):
  115 + long_text += line[len(indent):]
  116 + else:
  117 + long_text = long_text.strip()
  118 + if long_text != '':
  119 + long_text += '\n'
  120 + return True
  121 + return False
  122 +
  123 + lineno = 0
  124 + for line in df.readlines():
  125 + lineno += 1
  126 + if state == st_top:
  127 + m = re.match(r'^(\s*\.\. )help-topic (\S+): (.*)$', line)
  128 + if m:
  129 + set_indent(m.group(1))
  130 + topic = m.group(2)
  131 + short_text = m.group(3)
  132 + long_text = ''
  133 + state = st_topic
  134 + continue
  135 + m = re.match(r'^(\s*\.\. )qpdf:option:: (([^=\s]+)(=(\S+))?)$',
  136 + line)
  137 + if m:
  138 + if topic is None:
  139 + raise Exception('option seen before topic')
  140 + set_indent(m.group(1))
  141 + option = m.group(3)
  142 + synopsis = m.group(2)
  143 + if synopsis.endswith('`'):
  144 + raise Exception(
  145 + f'stray ` at end of option line (line {lineno})')
  146 + if synopsis != option:
  147 + long_text = synopsis + '\n'
  148 + else:
  149 + long_text = ''
  150 + state = st_option
  151 + continue
  152 + elif state == st_topic:
  153 + if append_long_text(line):
  154 + print(f'this->ap.addHelpTopic("{topic}", "{short_text}",'
  155 + f' R"({long_text})");', file=f)
  156 + state = st_top
  157 + elif state == st_option:
  158 + if line == '\n' or line.startswith(indent):
  159 + m = re.match(r'^(\s*\.\. )help: (.*)$', line)
  160 + if m:
  161 + set_indent(m.group(1))
  162 + short_text = m.group(2)
  163 + state = st_option_help
  164 + else:
  165 + state = st_top
  166 + elif state == st_option_help:
  167 + if append_long_text(line):
  168 + print(f'this->ap.addOptionHelp("{option}", "{topic}",'
  169 + f' "{short_text}", R"({long_text})");', file=f)
  170 + state = st_top
  171 +
90 172 def generate(self):
91 173 warn(f'{whoami}: regenerating auto job files')
92 174  
... ... @@ -230,6 +312,8 @@ class Main:
230 312 for j in ft['options']:
231 313 print('this->ap.copyFromOtherTable'
232 314 f'("{j}", "{other_table}");', file=f)
  315 + with open('manual/cli.rst', 'r') as df:
  316 + self.generate_doc(df, f)
233 317  
234 318  
235 319 if __name__ == '__main__':
... ...
include/qpdf/QPDFArgParser.hh
... ... @@ -30,6 +30,7 @@
30 30 #include <vector>
31 31 #include <functional>
32 32 #include <stdexcept>
  33 +#include <sstream>
33 34  
34 35 // This is not a general-purpose argument parser. It is tightly
35 36 // crafted to work with qpdf. qpdf's command-line syntax is very
... ... @@ -38,7 +39,10 @@
38 39 // backward compatibility to ensure we don't break what constitutes a
39 40 // valid command. This class handles the quirks of qpdf's argument
40 41 // parsing, bash/zsh completion, and support for @argfile to read
41   -// arguments from a file.
  42 +// arguments from a file. For the qpdf CLI, setup of QPDFArgParser is
  43 +// done mostly by automatically-generated code (one-off code for
  44 +// qpdf), though the handlers themselves are hand-coded. See
  45 +// generate_auto_job at the top of the source tree for details.
42 46  
43 47 // Note about memory: there is code that expects argv to be a char*[],
44 48 // meaning that arguments are writable. Several operations, including
... ... @@ -119,6 +123,13 @@ class QPDFArgParser
119 123 std::string const& arg, param_arg_handler_t,
120 124 bool required, char const** choices);
121 125  
  126 + // The default behavior when an invalid choice is specified with
  127 + // an option that takes choices is to list all the choices. This
  128 + // may not be good if there are too many choices, so you can
  129 + // provide your own handler in this case.
  130 + QPDF_DLL
  131 + void addInvalidChoiceHandler(std::string const& arg, param_arg_handler_t);
  132 +
122 133 // If an option is shared among multiple tables and uses identical
123 134 // handlers, you can just copy it instead of repeating the
124 135 // registration call.
... ... @@ -131,6 +142,67 @@ class QPDFArgParser
131 142 QPDF_DLL
132 143 void addFinalCheck(bare_arg_handler_t);
133 144  
  145 + // Help generation methods
  146 +
  147 + // Help is available on topics and options. Options may be
  148 + // associated with topics. Users can run --help, --help=topic, or
  149 + // --help=--arg to get help. The top-level help tells the user how
  150 + // to run help and lists available topics. Help for a topic prints
  151 + // a short synopsis about the topic and lists any options that may
  152 + // be associated with the topic. Help for an option provides a
  153 + // short synopsis for that option. All help output is appended
  154 + // with a blurb (if supplied) directing the user to the full
  155 + // documentation. Help is not shown for options for which help has
  156 + // not been added. This makes it possible to have undocumented
  157 + // options for testing, backward-compatibility, etc. Also, it
  158 + // could be quite confusing to handle appropriate help for some
  159 + // inner options that may be repeated with different semantics
  160 + // inside different option tables. There is also no checking for
  161 + // whether an option that has help actually exists. In other
  162 + // words, it's up to the caller to ensure that help actually
  163 + // corresponds to the program's actual options. Rather than this
  164 + // being an intentional design decision, it is because this class
  165 + // is specifically for qpdf, qpdf generates its help and has other
  166 + // means to ensure consistency.
  167 +
  168 + // Note about newlines:
  169 + //
  170 + // short_text should fit easily after the topic/option on the same
  171 + // line and should not end with a newline. Keep it to around 40 to
  172 + // 60 characters.
  173 + //
  174 + // long_text and footer should end with a single newline. They can
  175 + // have embedded newlines. Keep lines to under 80 columns.
  176 + //
  177 + // QPDFArgParser does reformat the text, but it may add blank
  178 + // lines in some situations. Following the above conventions will
  179 + // keep the help looking uniform.
  180 +
  181 + // If provided, this footer is appended to all help, separated by
  182 + // a blank line.
  183 + QPDF_DLL
  184 + void addHelpFooter(std::string const&);
  185 +
  186 + // Add a help topic along with the text for that topic
  187 + QPDF_DLL
  188 + void addHelpTopic(std::string const& topic,
  189 + std::string const& short_text,
  190 + std::string const& long_text);
  191 +
  192 + // Add help for an option, and associate it with a topic.
  193 + QPDF_DLL
  194 + void addOptionHelp(std::string const& option_name,
  195 + std::string const& topic,
  196 + std::string const& short_text,
  197 + std::string const& long_text);
  198 +
  199 + // Return the help text for a topic or option. Passing a null
  200 + // pointer returns the top-level help information. Passing an
  201 + // unknown value returns a string directing the user to run the
  202 + // top-level --help option.
  203 + QPDF_DLL
  204 + std::string getHelp(char const* topic_or_option);
  205 +
134 206 // Convenience methods for adding member functions of a class as
135 207 // handlers.
136 208 template <class T>
... ... @@ -171,7 +243,8 @@ class QPDFArgParser
171 243 OptionEntry() :
172 244 parameter_needed(false),
173 245 bare_arg_handler(0),
174   - param_arg_handler(0)
  246 + param_arg_handler(0),
  247 + invalid_choice_handler(0)
175 248 {
176 249 }
177 250 bool parameter_needed;
... ... @@ -179,9 +252,24 @@ class QPDFArgParser
179 252 std::set<std::string> choices;
180 253 bare_arg_handler_t bare_arg_handler;
181 254 param_arg_handler_t param_arg_handler;
  255 + param_arg_handler_t invalid_choice_handler;
182 256 };
183 257 typedef std::map<std::string, OptionEntry> option_table_t;
184 258  
  259 + struct HelpTopic
  260 + {
  261 + HelpTopic() = default;
  262 + HelpTopic(std::string const& short_text, std::string const& long_text) :
  263 + short_text(short_text),
  264 + long_text(long_text)
  265 + {
  266 + }
  267 +
  268 + std::string short_text;
  269 + std::string long_text;
  270 + std::set<std::string> options;
  271 + };
  272 +
185 273 OptionEntry& registerArg(std::string const& arg);
186 274  
187 275 void completionCommon(bool zsh);
... ... @@ -189,6 +277,7 @@ class QPDFArgParser
189 277 void argCompletionBash();
190 278 void argCompletionZsh();
191 279 void argHelp(char*);
  280 + void invalidHelpArg(char*);
192 281  
193 282 void checkCompletion();
194 283 void handleArgFileArguments();
... ... @@ -202,6 +291,11 @@ class QPDFArgParser
202 291 option_table_t&, std::string const&, std::string const&);
203 292 void handleCompletion();
204 293  
  294 + void getTopHelp(std::ostringstream&);
  295 + void getAllHelp(std::ostringstream&);
  296 + void getTopicHelp(
  297 + std::string const& name, HelpTopic const&, std::ostringstream&);
  298 +
205 299 class Members
206 300 {
207 301 friend class QPDFArgParser;
... ... @@ -235,6 +329,9 @@ class QPDFArgParser
235 329 std::vector<PointerHolder<char>> bash_argv;
236 330 PointerHolder<char*> argv_ph;
237 331 PointerHolder<char*> bash_argv_ph;
  332 + std::map<std::string, HelpTopic> help_topics;
  333 + std::map<std::string, HelpTopic> option_help;
  334 + std::string help_footer;
238 335 };
239 336 PointerHolder<Members> m;
240 337 };
... ...
job.sums
1 1 # Generated by generate_auto_job
2   -generate_auto_job 019081046f1bc19f498134eae00344ecfc65b4e52442ee5f1bc80bff99689443
  2 +generate_auto_job 1f42fc554778d95210d11c44e858214b4854ead907d1c9ea84fe37f993ea1a23
3 3 job.yml 25c85cba1ae01dac9cd0f9cb7b734e7e3e531c0023ea2b892dc0d40bda1c1146
4 4 libqpdf/qpdf/auto_job_decl.hh 97395ecbe590b23ae04d6cce2080dbd0e998917ff5eeaa5c6aafa91041d3cd6a
5   -libqpdf/qpdf/auto_job_init.hh 465bf46769559ceb77110d1b9d3293ba9b3595850b49848c31aeabd10aadb4ad
  5 +libqpdf/qpdf/auto_job_init.hh 2afffb5002ff28a3909f709709f65d77bf2289dd72d5ea3d1598a36664a49c73
  6 +manual/cli.rst f0109cca3366a9da4b0a05e3cce996ece2d776321a3f689aeaa2d6af599eee88
... ...
libqpdf/QPDFArgParser.cc
... ... @@ -36,12 +36,15 @@ QPDFArgParser::QPDFArgParser(int argc, char* argv[], char const* progname_env) :
36 36 {
37 37 selectHelpOptionTable();
38 38 char const* help_choices[] = {"all", 0};
  39 + // More help choices are added dynamically.
39 40 addChoices(
40 41 "help", bindParam(&QPDFArgParser::argHelp, this), false, help_choices);
  42 + addInvalidChoiceHandler(
  43 + "help", bindParam(&QPDFArgParser::invalidHelpArg, this));
41 44 addBare("completion-bash",
42   - std::bind(std::mem_fn(&QPDFArgParser::argCompletionBash), this));
  45 + bindBare(&QPDFArgParser::argCompletionBash, this));
43 46 addBare("completion-zsh",
44   - std::bind(std::mem_fn(&QPDFArgParser::argCompletionZsh), this));
  47 + bindBare(&QPDFArgParser::argCompletionZsh, this));
45 48 selectMainOptionTable();
46 49 }
47 50  
... ... @@ -158,6 +161,22 @@ QPDFArgParser::addChoices(
158 161 }
159 162  
160 163 void
  164 +QPDFArgParser::addInvalidChoiceHandler(
  165 + std::string const& arg, param_arg_handler_t handler)
  166 +{
  167 + auto i = this->m->option_table->find(arg);
  168 + if (i == this->m->option_table->end())
  169 + {
  170 + QTC::TC("libtests", "QPDFArgParser invalid choice handler to unknown");
  171 + throw std::logic_error(
  172 + "QPDFArgParser: attempt to add invalid choice handler"
  173 + " to unknown argument");
  174 + }
  175 + auto& oe = i->second;
  176 + oe.invalid_choice_handler = handler;
  177 +}
  178 +
  179 +void
161 180 QPDFArgParser::copyFromOtherTable(std::string const& arg,
162 181 std::string const& other_table)
163 182 {
... ... @@ -258,9 +277,17 @@ QPDFArgParser::argCompletionZsh()
258 277 }
259 278  
260 279 void
261   -QPDFArgParser::argHelp(char*)
  280 +QPDFArgParser::argHelp(char* p)
262 281 {
263   - // QXXXQ
  282 + std::cout << getHelp(p);
  283 + exit(0);
  284 +}
  285 +
  286 +void
  287 +QPDFArgParser::invalidHelpArg(char* p)
  288 +{
  289 + usage(std::string("unknown help option") +
  290 + (p ? (std::string(" ") + p) : ""));
264 291 }
265 292  
266 293 void
... ... @@ -640,7 +667,14 @@ QPDFArgParser::parseArgs()
640 667 {
641 668 std::string message =
642 669 "--" + arg_s + " must be given as --" + arg_s + "=";
643   - if (! oe.choices.empty())
  670 + if (oe.invalid_choice_handler)
  671 + {
  672 + oe.invalid_choice_handler(parameter);
  673 + // Method should call usage() or exit. Just in case it
  674 + // doesn't...
  675 + message += "option";
  676 + }
  677 + else if (! oe.choices.empty())
644 678 {
645 679 QTC::TC("libtests", "QPDFArgParser required choices");
646 680 message += "{";
... ... @@ -844,3 +878,166 @@ QPDFArgParser::handleCompletion()
844 878 }
845 879 exit(0);
846 880 }
  881 +
  882 +void
  883 +QPDFArgParser::addHelpFooter(std::string const& text)
  884 +{
  885 + this->m->help_footer = "\n" + text;
  886 +}
  887 +
  888 +void
  889 +QPDFArgParser::addHelpTopic(std::string const& topic,
  890 + std::string const& short_text,
  891 + std::string const& long_text)
  892 +{
  893 + if (topic == "all")
  894 + {
  895 + QTC::TC("libtests", "QPDFArgParser add reserved help topic");
  896 + throw std::logic_error(
  897 + "QPDFArgParser: can't register reserved help topic " + topic);
  898 + }
  899 + if (! ((topic.length() > 0) && (topic.at(0) != '-')))
  900 + {
  901 + QTC::TC("libtests", "QPDFArgParser bad topic for help");
  902 + throw std::logic_error(
  903 + "QPDFArgParser: help topics must not start with -");
  904 + }
  905 + if (this->m->help_topics.count(topic))
  906 + {
  907 + QTC::TC("libtests", "QPDFArgParser add existing topic");
  908 + throw std::logic_error(
  909 + "QPDFArgParser: topic " + topic + " has already been added");
  910 + }
  911 +
  912 + this->m->help_topics[topic] = HelpTopic(short_text, long_text);
  913 + this->m->help_option_table["help"].choices.insert(topic);
  914 +}
  915 +
  916 +void
  917 +QPDFArgParser::addOptionHelp(std::string const& option_name,
  918 + std::string const& topic,
  919 + std::string const& short_text,
  920 + std::string const& long_text)
  921 +{
  922 + if (! ((option_name.length() > 2) &&
  923 + (option_name.at(0) == '-') &&
  924 + (option_name.at(1) == '-')))
  925 + {
  926 + QTC::TC("libtests", "QPDFArgParser bad option for help");
  927 + throw std::logic_error(
  928 + "QPDFArgParser: options for help must start with --");
  929 + }
  930 + if (this->m->option_help.count(option_name))
  931 + {
  932 + QTC::TC("libtests", "QPDFArgParser duplicate option help");
  933 + throw std::logic_error(
  934 + "QPDFArgParser: option " + option_name + " already has help");
  935 + }
  936 + auto ht = this->m->help_topics.find(topic);
  937 + if (ht == this->m->help_topics.end())
  938 + {
  939 + QTC::TC("libtests", "QPDFArgParser add to unknown topic");
  940 + throw std::logic_error(
  941 + "QPDFArgParser: unable to add option " + option_name +
  942 + " to unknown help topic " + topic);
  943 + }
  944 + this->m->option_help[option_name] = HelpTopic(short_text, long_text);
  945 + ht->second.options.insert(option_name);
  946 + this->m->help_option_table["help"].choices.insert(option_name);
  947 +}
  948 +
  949 +void
  950 +QPDFArgParser::getTopHelp(std::ostringstream& msg)
  951 +{
  952 + msg << "Run \"" << this->m->whoami
  953 + << " --help=topic\" for help on a topic." << std::endl
  954 + << "Run \"" << this->m->whoami
  955 + << " --help=option\" for help on an option." << std::endl
  956 + << "Run \"" << this->m->whoami
  957 + << " --help=all\" to see all available help." << std::endl
  958 + << std::endl
  959 + << "Topics:" << std::endl;
  960 + for (auto const& i: this->m->help_topics)
  961 + {
  962 + msg << " " << i.first << ": " << i.second.short_text << std::endl;
  963 + }
  964 +}
  965 +
  966 +void
  967 +QPDFArgParser::getAllHelp(std::ostringstream& msg)
  968 +{
  969 + getTopHelp(msg);
  970 + auto show = [this, &msg](std::map<std::string, HelpTopic>& topics,
  971 + std::string const& label) {
  972 + for (auto const& i: topics)
  973 + {
  974 + auto const& topic = i.first;
  975 + msg << std::endl
  976 + << "== " << label << " " << topic
  977 + << " (" << i.second.short_text << ") =="
  978 + << std::endl
  979 + << std::endl;
  980 + getTopicHelp(topic, i.second, msg);
  981 + }
  982 + };
  983 + show(this->m->help_topics, "topic");
  984 + show(this->m->option_help, "option");
  985 + msg << std::endl << "====" << std::endl;
  986 +}
  987 +
  988 +void
  989 +QPDFArgParser::getTopicHelp(std::string const& name,
  990 + HelpTopic const& ht,
  991 + std::ostringstream& msg)
  992 +{
  993 + if (ht.long_text.empty())
  994 + {
  995 + msg << ht.short_text << std::endl;
  996 + }
  997 + else
  998 + {
  999 + msg << ht.long_text;
  1000 + }
  1001 + if (! ht.options.empty())
  1002 + {
  1003 + msg << std::endl << "Related options:" << std::endl;
  1004 + for (auto const& i: ht.options)
  1005 + {
  1006 + msg << " " << i << ": "
  1007 + << this->m->option_help[i].short_text << std::endl;
  1008 + }
  1009 + }
  1010 +}
  1011 +
  1012 +std::string
  1013 +QPDFArgParser::getHelp(char const* topic_or_option)
  1014 +{
  1015 + std::ostringstream msg;
  1016 + if ((topic_or_option == nullptr) || (strlen(topic_or_option) == 0))
  1017 + {
  1018 + getTopHelp(msg);
  1019 + }
  1020 + else
  1021 + {
  1022 + std::string arg(topic_or_option);
  1023 + if (arg == "all")
  1024 + {
  1025 + getAllHelp(msg);
  1026 + }
  1027 + else if (this->m->option_help.count(arg))
  1028 + {
  1029 + getTopicHelp(arg, this->m->option_help[arg], msg);
  1030 + }
  1031 + else if (this->m->help_topics.count(arg))
  1032 + {
  1033 + getTopicHelp(arg, this->m->help_topics[arg], msg);
  1034 + }
  1035 + else
  1036 + {
  1037 + // should not be possible
  1038 + getTopHelp(msg);
  1039 + }
  1040 + }
  1041 + msg << this->m->help_footer;
  1042 + return msg.str();
  1043 +}
... ...
libqpdf/qpdf/auto_job_init.hh
... ... @@ -162,3 +162,4 @@ this-&gt;ap.copyFromOtherTable(&quot;annotate&quot;, &quot;128-bit encryption&quot;);
162 162 this->ap.copyFromOtherTable("form", "128-bit encryption");
163 163 this->ap.copyFromOtherTable("modify-other", "128-bit encryption");
164 164 this->ap.copyFromOtherTable("modify", "128-bit encryption");
  165 +this->ap.addHelpFooter("For detailed help, visit the qpdf manual: https://qpdf.readthedocs.io\n");
... ...
libtests/arg_parser.cc
... ... @@ -68,6 +68,18 @@ ArgParser::initOptions()
68 68 ap.addBare("sheep", [this](){ this->ap.selectOptionTable("sheep"); });
69 69 ap.registerOptionTable("sheep", nullptr);
70 70 ap.copyFromOtherTable("ewe", "baaa");
  71 +
  72 + ap.addHelpFooter("For more help, read the manual.\n");
  73 + ap.addHelpTopic(
  74 + "quack", "Quack Options",
  75 + "Just put stuff after quack to get a count at the end.\n");
  76 + ap.addHelpTopic(
  77 + "baaa", "Baaa Options",
  78 + "Ewe can do sheepish things.\n"
  79 + "For example, ewe can add more ram to your computer.\n");
  80 + ap.addOptionHelp("--ewe", "baaa",
  81 + "just for ewe", "You are not a ewe.\n");
  82 + ap.addOptionHelp("--ram", "baaa", "curly horns", "");
71 83 }
72 84  
73 85 void
... ... @@ -152,62 +164,60 @@ ArgParser::finalChecks()
152 164 void
153 165 ArgParser::test_exceptions()
154 166 {
155   - try
156   - {
  167 + auto err = [](char const* msg, std::function<void()> fn) {
  168 + try
  169 + {
  170 + fn();
  171 + assert(msg == nullptr);
  172 + }
  173 + catch (std::exception& e)
  174 + {
  175 + std::cout << msg << ": " << e.what() << std::endl;
  176 + }
  177 + };
  178 +
  179 + err("duplicate handler", [this]() {
157 180 ap.selectMainOptionTable();
158 181 ap.addBare("potato", [](){});
159   - assert(false);
160   - }
161   - catch (std::exception& e)
162   - {
163   - std::cout << "duplicate handler: " << e.what() << std::endl;
164   - }
165   - try
166   - {
  182 + });
  183 + err("duplicate handler", [this]() {
167 184 ap.selectOptionTable("baaa");
168 185 ap.addBare("ram", [](){});
169   - assert(false);
170   - }
171   - catch (std::exception& e)
172   - {
173   - std::cout << "duplicate handler: " << e.what() << std::endl;
174   - }
175   - try
176   - {
  186 + });
  187 + err("duplicate table", [this]() {
177 188 ap.registerOptionTable("baaa", nullptr);
178   - assert(false);
179   - }
180   - catch (std::exception& e)
181   - {
182   - std::cout << "duplicate table: " << e.what() << std::endl;
183   - }
184   - try
185   - {
  189 + });
  190 + err("unknown table", [this]() {
186 191 ap.selectOptionTable("aardvark");
187   - assert(false);
188   - }
189   - catch (std::exception& e)
190   - {
191   - std::cout << "unknown table: " << e.what() << std::endl;
192   - }
193   - try
194   - {
  192 + });
  193 + err("copy from unknown table", [this]() {
195 194 ap.copyFromOtherTable("one", "two");
196   - assert(false);
197   - }
198   - catch (std::exception& e)
199   - {
200   - std::cout << "copy from unknown table: " << e.what() << std::endl;
201   - }
202   - try
203   - {
  195 + });
  196 + err("copy unknown from other table", [this]() {
204 197 ap.copyFromOtherTable("two", "baaa");
205   - assert(false);
206   - }
207   - catch (std::exception& e)
208   - {
209   - std::cout << "copy unknown from other table: " << e.what() << std::endl;
210   - }
  198 + });
  199 + err("add existing help topic", [this]() {
  200 + ap.addHelpTopic("baaa", "potato", "salad");
  201 + });
  202 + err("add reserved help topic", [this]() {
  203 + ap.addHelpTopic("all", "potato", "salad");
  204 + });
  205 + err("add to unknown topic", [this]() {
  206 + ap.addOptionHelp("--new", "oops", "potato", "salad");
  207 + });
  208 + err("bad option for help", [this]() {
  209 + ap.addOptionHelp("nodash", "baaa", "potato", "salad");
  210 + });
  211 + err("bad topic for help", [this]() {
  212 + ap.addHelpTopic("--dashes", "potato", "salad");
  213 + });
  214 + err("duplicate option help", [this]() {
  215 + ap.addOptionHelp("--ewe", "baaa", "potato", "salad");
  216 + });
  217 + err("invalid choice handler to unknown", [this]() {
  218 + ap.addInvalidChoiceHandler(
  219 + "elephant", [](char*){});
  220 + });
211 221 }
212 222  
213 223 int main(int argc, char* argv[])
... ...
libtests/libtests.testcov
... ... @@ -54,3 +54,10 @@ QPDFArgParser unrecognized 0
54 54 QPDFArgParser complete choices 0
55 55 QPDFArgParser copy from unknown 0
56 56 QPDFArgParser copy unknown 0
  57 +QPDFArgParser add reserved help topic 0
  58 +QPDFArgParser add existing topic 0
  59 +QPDFArgParser add to unknown topic 0
  60 +QPDFArgParser duplicate option help 0
  61 +QPDFArgParser bad option for help 0
  62 +QPDFArgParser bad topic for help 0
  63 +QPDFArgParser invalid choice handler to unknown 0
... ...
libtests/qtest/arg_parser.test
... ... @@ -101,4 +101,30 @@ $td-&gt;runtest(&quot;args from stdin&quot;,
101 101 {$td->FILE => "stdin.out", $td->EXIT_STATUS => 0},
102 102 $td->NORMALIZE_NEWLINES);
103 103  
104   -$td->report(2 + (2 * scalar(@completion_tests)) + scalar(@arg_tests));
  104 +my @help_tests = (
  105 + '',
  106 + '=all',
  107 + '=--ewe',
  108 + '=quack',
  109 + );
  110 +foreach my $i (@help_tests)
  111 +{
  112 + my $out = $i;
  113 + $out =~ s/[=-]//g;
  114 + if ($out ne '')
  115 + {
  116 + $out = "-$out";
  117 + }
  118 + $td->runtest("--help$i",
  119 + {$td->COMMAND => "arg_parser --help$i"},
  120 + {$td->FILE => "help$out.out", $td->EXIT_STATUS => 0},
  121 + $td->NORMALIZE_NEWLINES);
  122 +}
  123 +
  124 +$td->runtest("bad help option",
  125 + {$td->COMMAND => 'arg_parser --help=--oops'},
  126 + {$td->FILE => "help-bad.out", $td->EXIT_STATUS => 2},
  127 + $td->NORMALIZE_NEWLINES);
  128 +
  129 +$td->report(3 + (2 * scalar(@completion_tests)) +
  130 + scalar(@arg_tests) + scalar(@help_tests));
... ...
libtests/qtest/arg_parser/completion-top-arg-zsh.out
... ... @@ -2,7 +2,9 @@
2 2 --completion-zsh
3 3 --help
4 4 --help=
  5 +--help=--ewe
5 6 --help=all
  7 +--help=quack
6 8 --moo
7 9 --moo=
8 10 --oink=
... ...
libtests/qtest/arg_parser/completion-top-arg.out
1 1 --baaa
2 2 --completion-zsh
  3 +--help
  4 +--help=
3 5 --moo
4 6 --moo=
5 7 --oink=
... ...
libtests/qtest/arg_parser/exceptions.out
... ... @@ -4,3 +4,10 @@ duplicate table: QPDFArgParser: registering already registered option table baaa
4 4 unknown table: QPDFArgParser: selecting unregistered option table aardvark
5 5 copy from unknown table: QPDFArgParser: attempt to copy from unknown table two
6 6 copy unknown from other table: QPDFArgParser: attempt to copy unknown argument two from table baaa
  7 +add existing help topic: QPDFArgParser: topic baaa has already been added
  8 +add reserved help topic: QPDFArgParser: can't register reserved help topic all
  9 +add to unknown topic: QPDFArgParser: unable to add option --new to unknown help topic oops
  10 +bad option for help: QPDFArgParser: options for help must start with --
  11 +bad topic for help: QPDFArgParser: help topics must not start with -
  12 +duplicate option help: QPDFArgParser: option --ewe already has help
  13 +invalid choice handler to unknown: QPDFArgParser: attempt to add invalid choice handler to unknown argument
... ...
libtests/qtest/arg_parser/help-all.out 0 → 100644
  1 +Run "arg_parser --help=topic" for help on a topic.
  2 +Run "arg_parser --help=option" for help on an option.
  3 +Run "arg_parser --help=all" to see all available help.
  4 +
  5 +Topics:
  6 + baaa: Baaa Options
  7 + quack: Quack Options
  8 +
  9 +== topic baaa (Baaa Options) ==
  10 +
  11 +Ewe can do sheepish things.
  12 +For example, ewe can add more ram to your computer.
  13 +
  14 +Related options:
  15 + --ewe: just for ewe
  16 + --ram: curly horns
  17 +
  18 +== topic quack (Quack Options) ==
  19 +
  20 +Just put stuff after quack to get a count at the end.
  21 +
  22 +== option --ewe (just for ewe) ==
  23 +
  24 +You are not a ewe.
  25 +
  26 +== option --ram (curly horns) ==
  27 +
  28 +curly horns
  29 +
  30 +====
  31 +
  32 +For more help, read the manual.
... ...
libtests/qtest/arg_parser/help-bad.out 0 → 100644
  1 +usage: unknown help option --oops
... ...
libtests/qtest/arg_parser/help-ewe.out 0 → 100644
  1 +You are not a ewe.
  2 +
  3 +For more help, read the manual.
... ...
libtests/qtest/arg_parser/help-quack.out 0 → 100644
  1 +Just put stuff after quack to get a count at the end.
  2 +
  3 +For more help, read the manual.
... ...
libtests/qtest/arg_parser/help.out 0 → 100644
  1 +Run "arg_parser --help=topic" for help on a topic.
  2 +Run "arg_parser --help=option" for help on an option.
  3 +Run "arg_parser --help=all" to see all available help.
  4 +
  5 +Topics:
  6 + baaa: Baaa Options
  7 + quack: Quack Options
  8 +
  9 +For more help, read the manual.
... ...