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,7 +19,7 @@ def warn(*args, **kwargs):
19 19
20 20
21 class Main: 21 class Main:
22 - SOURCES = [whoami, 'job.yml'] 22 + SOURCES = [whoami, 'job.yml', 'manual/cli.rst']
23 DESTS = { 23 DESTS = {
24 'decl': 'libqpdf/qpdf/auto_job_decl.hh', 24 'decl': 'libqpdf/qpdf/auto_job_decl.hh',
25 'init': 'libqpdf/qpdf/auto_job_init.hh', 25 'init': 'libqpdf/qpdf/auto_job_init.hh',
@@ -87,6 +87,88 @@ class Main: @@ -87,6 +87,88 @@ class Main:
87 for k, v in hashes.items(): 87 for k, v in hashes.items():
88 print(f'{k} {v}', file=f) 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 def generate(self): 172 def generate(self):
91 warn(f'{whoami}: regenerating auto job files') 173 warn(f'{whoami}: regenerating auto job files')
92 174
@@ -230,6 +312,8 @@ class Main: @@ -230,6 +312,8 @@ class Main:
230 for j in ft['options']: 312 for j in ft['options']:
231 print('this->ap.copyFromOtherTable' 313 print('this->ap.copyFromOtherTable'
232 f'("{j}", "{other_table}");', file=f) 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 if __name__ == '__main__': 319 if __name__ == '__main__':
include/qpdf/QPDFArgParser.hh
@@ -30,6 +30,7 @@ @@ -30,6 +30,7 @@
30 #include <vector> 30 #include <vector>
31 #include <functional> 31 #include <functional>
32 #include <stdexcept> 32 #include <stdexcept>
  33 +#include <sstream>
33 34
34 // This is not a general-purpose argument parser. It is tightly 35 // This is not a general-purpose argument parser. It is tightly
35 // crafted to work with qpdf. qpdf's command-line syntax is very 36 // crafted to work with qpdf. qpdf's command-line syntax is very
@@ -38,7 +39,10 @@ @@ -38,7 +39,10 @@
38 // backward compatibility to ensure we don't break what constitutes a 39 // backward compatibility to ensure we don't break what constitutes a
39 // valid command. This class handles the quirks of qpdf's argument 40 // valid command. This class handles the quirks of qpdf's argument
40 // parsing, bash/zsh completion, and support for @argfile to read 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 // Note about memory: there is code that expects argv to be a char*[], 47 // Note about memory: there is code that expects argv to be a char*[],
44 // meaning that arguments are writable. Several operations, including 48 // meaning that arguments are writable. Several operations, including
@@ -119,6 +123,13 @@ class QPDFArgParser @@ -119,6 +123,13 @@ class QPDFArgParser
119 std::string const& arg, param_arg_handler_t, 123 std::string const& arg, param_arg_handler_t,
120 bool required, char const** choices); 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 // If an option is shared among multiple tables and uses identical 133 // If an option is shared among multiple tables and uses identical
123 // handlers, you can just copy it instead of repeating the 134 // handlers, you can just copy it instead of repeating the
124 // registration call. 135 // registration call.
@@ -131,6 +142,67 @@ class QPDFArgParser @@ -131,6 +142,67 @@ class QPDFArgParser
131 QPDF_DLL 142 QPDF_DLL
132 void addFinalCheck(bare_arg_handler_t); 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 // Convenience methods for adding member functions of a class as 206 // Convenience methods for adding member functions of a class as
135 // handlers. 207 // handlers.
136 template <class T> 208 template <class T>
@@ -171,7 +243,8 @@ class QPDFArgParser @@ -171,7 +243,8 @@ class QPDFArgParser
171 OptionEntry() : 243 OptionEntry() :
172 parameter_needed(false), 244 parameter_needed(false),
173 bare_arg_handler(0), 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 bool parameter_needed; 250 bool parameter_needed;
@@ -179,9 +252,24 @@ class QPDFArgParser @@ -179,9 +252,24 @@ class QPDFArgParser
179 std::set<std::string> choices; 252 std::set<std::string> choices;
180 bare_arg_handler_t bare_arg_handler; 253 bare_arg_handler_t bare_arg_handler;
181 param_arg_handler_t param_arg_handler; 254 param_arg_handler_t param_arg_handler;
  255 + param_arg_handler_t invalid_choice_handler;
182 }; 256 };
183 typedef std::map<std::string, OptionEntry> option_table_t; 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 OptionEntry& registerArg(std::string const& arg); 273 OptionEntry& registerArg(std::string const& arg);
186 274
187 void completionCommon(bool zsh); 275 void completionCommon(bool zsh);
@@ -189,6 +277,7 @@ class QPDFArgParser @@ -189,6 +277,7 @@ class QPDFArgParser
189 void argCompletionBash(); 277 void argCompletionBash();
190 void argCompletionZsh(); 278 void argCompletionZsh();
191 void argHelp(char*); 279 void argHelp(char*);
  280 + void invalidHelpArg(char*);
192 281
193 void checkCompletion(); 282 void checkCompletion();
194 void handleArgFileArguments(); 283 void handleArgFileArguments();
@@ -202,6 +291,11 @@ class QPDFArgParser @@ -202,6 +291,11 @@ class QPDFArgParser
202 option_table_t&, std::string const&, std::string const&); 291 option_table_t&, std::string const&, std::string const&);
203 void handleCompletion(); 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 class Members 299 class Members
206 { 300 {
207 friend class QPDFArgParser; 301 friend class QPDFArgParser;
@@ -235,6 +329,9 @@ class QPDFArgParser @@ -235,6 +329,9 @@ class QPDFArgParser
235 std::vector<PointerHolder<char>> bash_argv; 329 std::vector<PointerHolder<char>> bash_argv;
236 PointerHolder<char*> argv_ph; 330 PointerHolder<char*> argv_ph;
237 PointerHolder<char*> bash_argv_ph; 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 PointerHolder<Members> m; 336 PointerHolder<Members> m;
240 }; 337 };
job.sums
1 # Generated by generate_auto_job 1 # Generated by generate_auto_job
2 -generate_auto_job 019081046f1bc19f498134eae00344ecfc65b4e52442ee5f1bc80bff99689443 2 +generate_auto_job 1f42fc554778d95210d11c44e858214b4854ead907d1c9ea84fe37f993ea1a23
3 job.yml 25c85cba1ae01dac9cd0f9cb7b734e7e3e531c0023ea2b892dc0d40bda1c1146 3 job.yml 25c85cba1ae01dac9cd0f9cb7b734e7e3e531c0023ea2b892dc0d40bda1c1146
4 libqpdf/qpdf/auto_job_decl.hh 97395ecbe590b23ae04d6cce2080dbd0e998917ff5eeaa5c6aafa91041d3cd6a 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,12 +36,15 @@ QPDFArgParser::QPDFArgParser(int argc, char* argv[], char const* progname_env) :
36 { 36 {
37 selectHelpOptionTable(); 37 selectHelpOptionTable();
38 char const* help_choices[] = {"all", 0}; 38 char const* help_choices[] = {"all", 0};
  39 + // More help choices are added dynamically.
39 addChoices( 40 addChoices(
40 "help", bindParam(&QPDFArgParser::argHelp, this), false, help_choices); 41 "help", bindParam(&QPDFArgParser::argHelp, this), false, help_choices);
  42 + addInvalidChoiceHandler(
  43 + "help", bindParam(&QPDFArgParser::invalidHelpArg, this));
41 addBare("completion-bash", 44 addBare("completion-bash",
42 - std::bind(std::mem_fn(&QPDFArgParser::argCompletionBash), this)); 45 + bindBare(&QPDFArgParser::argCompletionBash, this));
43 addBare("completion-zsh", 46 addBare("completion-zsh",
44 - std::bind(std::mem_fn(&QPDFArgParser::argCompletionZsh), this)); 47 + bindBare(&QPDFArgParser::argCompletionZsh, this));
45 selectMainOptionTable(); 48 selectMainOptionTable();
46 } 49 }
47 50
@@ -158,6 +161,22 @@ QPDFArgParser::addChoices( @@ -158,6 +161,22 @@ QPDFArgParser::addChoices(
158 } 161 }
159 162
160 void 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 QPDFArgParser::copyFromOtherTable(std::string const& arg, 180 QPDFArgParser::copyFromOtherTable(std::string const& arg,
162 std::string const& other_table) 181 std::string const& other_table)
163 { 182 {
@@ -258,9 +277,17 @@ QPDFArgParser::argCompletionZsh() @@ -258,9 +277,17 @@ QPDFArgParser::argCompletionZsh()
258 } 277 }
259 278
260 void 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 void 293 void
@@ -640,7 +667,14 @@ QPDFArgParser::parseArgs() @@ -640,7 +667,14 @@ QPDFArgParser::parseArgs()
640 { 667 {
641 std::string message = 668 std::string message =
642 "--" + arg_s + " must be given as --" + arg_s + "="; 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 QTC::TC("libtests", "QPDFArgParser required choices"); 679 QTC::TC("libtests", "QPDFArgParser required choices");
646 message += "{"; 680 message += "{";
@@ -844,3 +878,166 @@ QPDFArgParser::handleCompletion() @@ -844,3 +878,166 @@ QPDFArgParser::handleCompletion()
844 } 878 }
845 exit(0); 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,3 +162,4 @@ this-&gt;ap.copyFromOtherTable(&quot;annotate&quot;, &quot;128-bit encryption&quot;);
162 this->ap.copyFromOtherTable("form", "128-bit encryption"); 162 this->ap.copyFromOtherTable("form", "128-bit encryption");
163 this->ap.copyFromOtherTable("modify-other", "128-bit encryption"); 163 this->ap.copyFromOtherTable("modify-other", "128-bit encryption");
164 this->ap.copyFromOtherTable("modify", "128-bit encryption"); 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,6 +68,18 @@ ArgParser::initOptions()
68 ap.addBare("sheep", [this](){ this->ap.selectOptionTable("sheep"); }); 68 ap.addBare("sheep", [this](){ this->ap.selectOptionTable("sheep"); });
69 ap.registerOptionTable("sheep", nullptr); 69 ap.registerOptionTable("sheep", nullptr);
70 ap.copyFromOtherTable("ewe", "baaa"); 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 void 85 void
@@ -152,62 +164,60 @@ ArgParser::finalChecks() @@ -152,62 +164,60 @@ ArgParser::finalChecks()
152 void 164 void
153 ArgParser::test_exceptions() 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 ap.selectMainOptionTable(); 180 ap.selectMainOptionTable();
158 ap.addBare("potato", [](){}); 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 ap.selectOptionTable("baaa"); 184 ap.selectOptionTable("baaa");
168 ap.addBare("ram", [](){}); 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 ap.registerOptionTable("baaa", nullptr); 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 ap.selectOptionTable("aardvark"); 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 ap.copyFromOtherTable("one", "two"); 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 ap.copyFromOtherTable("two", "baaa"); 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 int main(int argc, char* argv[]) 223 int main(int argc, char* argv[])
libtests/libtests.testcov
@@ -54,3 +54,10 @@ QPDFArgParser unrecognized 0 @@ -54,3 +54,10 @@ QPDFArgParser unrecognized 0
54 QPDFArgParser complete choices 0 54 QPDFArgParser complete choices 0
55 QPDFArgParser copy from unknown 0 55 QPDFArgParser copy from unknown 0
56 QPDFArgParser copy unknown 0 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,4 +101,30 @@ $td-&gt;runtest(&quot;args from stdin&quot;,
101 {$td->FILE => "stdin.out", $td->EXIT_STATUS => 0}, 101 {$td->FILE => "stdin.out", $td->EXIT_STATUS => 0},
102 $td->NORMALIZE_NEWLINES); 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,7 +2,9 @@
2 --completion-zsh 2 --completion-zsh
3 --help 3 --help
4 --help= 4 --help=
  5 +--help=--ewe
5 --help=all 6 --help=all
  7 +--help=quack
6 --moo 8 --moo
7 --moo= 9 --moo=
8 --oink= 10 --oink=
libtests/qtest/arg_parser/completion-top-arg.out
1 --baaa 1 --baaa
2 --completion-zsh 2 --completion-zsh
  3 +--help
  4 +--help=
3 --moo 5 --moo
4 --moo= 6 --moo=
5 --oink= 7 --oink=
libtests/qtest/arg_parser/exceptions.out
@@ -4,3 +4,10 @@ duplicate table: QPDFArgParser: registering already registered option table baaa @@ -4,3 +4,10 @@ duplicate table: QPDFArgParser: registering already registered option table baaa
4 unknown table: QPDFArgParser: selecting unregistered option table aardvark 4 unknown table: QPDFArgParser: selecting unregistered option table aardvark
5 copy from unknown table: QPDFArgParser: attempt to copy from unknown table two 5 copy from unknown table: QPDFArgParser: attempt to copy from unknown table two
6 copy unknown from other table: QPDFArgParser: attempt to copy unknown argument two from table baaa 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.