Commit b4bd124be496170937d19742d83c2bad7471fe81
1 parent
5303130c
QPDFArgParser: support adding/printing help information
Showing
16 changed files
with
541 additions
and
59 deletions
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->ap.copyFromOtherTable("annotate", "128-bit encryption"); |
| 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->runtest("args from stdin", |
| 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
libtests/qtest/arg_parser/completion-top-arg.out
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
libtests/qtest/arg_parser/help-quack.out
0 → 100644
libtests/qtest/arg_parser/help.out
0 → 100644