Commit 52817f0a45b9116e55432361b8ddd08d28a606c7
1 parent
8a5ba568
Implement QPDFArgParser based on ArgParser from qpdf.cc
Showing
47 changed files
with
1499 additions
and
0 deletions
cSpell.json
| @@ -32,6 +32,7 @@ | @@ -32,6 +32,7 @@ | ||
| 32 | "autolabel", | 32 | "autolabel", |
| 33 | "automake", | 33 | "automake", |
| 34 | "autotools", | 34 | "autotools", |
| 35 | + "baaa", | ||
| 35 | "backports", | 36 | "backports", |
| 36 | "bashcompinit", | 37 | "bashcompinit", |
| 37 | "berkenbilt", | 38 | "berkenbilt", |
| @@ -319,6 +320,7 @@ | @@ -319,6 +320,7 @@ | ||
| 319 | "qpdf", | 320 | "qpdf", |
| 320 | "qpdfacroformdocumenthelper", | 321 | "qpdfacroformdocumenthelper", |
| 321 | "qpdfannotationobjecthelper", | 322 | "qpdfannotationobjecthelper", |
| 323 | + "qpdfargparser", | ||
| 322 | "qpdfconstants", | 324 | "qpdfconstants", |
| 323 | "qpdfcrypto", | 325 | "qpdfcrypto", |
| 324 | "qpdfcryptoimpl", | 326 | "qpdfcryptoimpl", |
include/qpdf/QPDFArgParser.hh
0 → 100644
| 1 | +// Copyright (c) 2005-2021 Jay Berkenbilt | ||
| 2 | +// | ||
| 3 | +// This file is part of qpdf. | ||
| 4 | +// | ||
| 5 | +// Licensed under the Apache License, Version 2.0 (the "License"); | ||
| 6 | +// you may not use this file except in compliance with the License. | ||
| 7 | +// You may obtain a copy of the License at | ||
| 8 | +// | ||
| 9 | +// http://www.apache.org/licenses/LICENSE-2.0 | ||
| 10 | +// | ||
| 11 | +// Unless required by applicable law or agreed to in writing, software | ||
| 12 | +// distributed under the License is distributed on an "AS IS" BASIS, | ||
| 13 | +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| 14 | +// See the License for the specific language governing permissions and | ||
| 15 | +// limitations under the License. | ||
| 16 | +// | ||
| 17 | +// Versions of qpdf prior to version 7 were released under the terms | ||
| 18 | +// of version 2.0 of the Artistic License. At your option, you may | ||
| 19 | +// continue to consider qpdf to be licensed under those terms. Please | ||
| 20 | +// see the manual for additional information. | ||
| 21 | + | ||
| 22 | +#ifndef QPDFARGPARSER_HH | ||
| 23 | +#define QPDFARGPARSER_HH | ||
| 24 | + | ||
| 25 | +#include <qpdf/DLL.h> | ||
| 26 | +#include <qpdf/PointerHolder.hh> | ||
| 27 | +#include <string> | ||
| 28 | +#include <set> | ||
| 29 | +#include <map> | ||
| 30 | +#include <vector> | ||
| 31 | +#include <functional> | ||
| 32 | +#include <stdexcept> | ||
| 33 | + | ||
| 34 | +// 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 | +// complex because of its long history, and it doesn't really follow | ||
| 37 | +// any kind of normal standard for arguments, but it's important for | ||
| 38 | +// backward compatibility not ensure we don't break what constitutes a | ||
| 39 | +// valid command. This class handles the quirks of qpdf's argument | ||
| 40 | +// parsing, bash/zsh completion, and support for @argfile to read | ||
| 41 | +// arguments from a file. | ||
| 42 | + | ||
| 43 | +// Note about memory: there is code that expects argv to be a char*[], | ||
| 44 | +// meaning that arguments are writable. Several operations, including | ||
| 45 | +// reading arguments from a file or parsing a line for bash | ||
| 46 | +// completion, involve fabricating an argv array. To ensure that the | ||
| 47 | +// memory is valid and is cleaned up properly, we keep various vectors | ||
| 48 | +// of smart character pointers that argv points into. In order for | ||
| 49 | +// those pointers to remain valid, the QPDFArgParser instance must | ||
| 50 | +// remain in scope for the life of any code that may reference | ||
| 51 | +// anything from argv. | ||
| 52 | +class QPDFArgParser | ||
| 53 | +{ | ||
| 54 | + public: | ||
| 55 | + // Usage exception is thrown if there are any errors parsing | ||
| 56 | + // arguments | ||
| 57 | + class QPDF_DLL_CLASS Usage: public std::runtime_error | ||
| 58 | + { | ||
| 59 | + public: | ||
| 60 | + QPDF_DLL | ||
| 61 | + Usage(std::string const&); | ||
| 62 | + }; | ||
| 63 | + | ||
| 64 | + // progname_env is used to override argv[0] when figuring out the | ||
| 65 | + // name of the executable for setting up completion. This may be | ||
| 66 | + // needed if the program is invoked by a wrapper. | ||
| 67 | + QPDF_DLL | ||
| 68 | + QPDFArgParser(int argc, char* argv[], char const* progname_env); | ||
| 69 | + | ||
| 70 | + // Calls exit(0) if a help option is given or if in completion | ||
| 71 | + // mode. If there are argument parsing errors, | ||
| 72 | + // QPDFArgParser::Usage is thrown. | ||
| 73 | + QPDF_DLL | ||
| 74 | + void parseArgs(); | ||
| 75 | + | ||
| 76 | + // Methods for registering arguments. QPDFArgParser starts off | ||
| 77 | + // with the main option table selected. You can add handlers for | ||
| 78 | + // arguments in the current option table, and you can select which | ||
| 79 | + // option table is current. The help option table is special and | ||
| 80 | + // contains arguments that are only valid as the first and only | ||
| 81 | + // option. Named option tables are for subparsers and always start | ||
| 82 | + // a series of options that end with `--`. | ||
| 83 | + | ||
| 84 | + typedef std::function<void()> bare_arg_handler_t; | ||
| 85 | + typedef std::function<void(char*)> param_arg_handler_t; | ||
| 86 | + | ||
| 87 | + QPDF_DLL | ||
| 88 | + void selectMainOptionTable(); | ||
| 89 | + QPDF_DLL | ||
| 90 | + void selectHelpOptionTable(); | ||
| 91 | + QPDF_DLL | ||
| 92 | + void selectOptionTable(std::string const& name); | ||
| 93 | + | ||
| 94 | + // Register a new options table. This also selects the option table. | ||
| 95 | + QPDF_DLL | ||
| 96 | + void registerOptionTable( | ||
| 97 | + std::string const& name, bare_arg_handler_t end_handler); | ||
| 98 | + | ||
| 99 | + // Add handlers for options in the current table | ||
| 100 | + | ||
| 101 | + QPDF_DLL | ||
| 102 | + void addPositional(param_arg_handler_t); | ||
| 103 | + QPDF_DLL | ||
| 104 | + void addBare(std::string const& arg, bare_arg_handler_t); | ||
| 105 | + QPDF_DLL | ||
| 106 | + void addRequiredParameter( | ||
| 107 | + std::string const& arg, | ||
| 108 | + param_arg_handler_t, | ||
| 109 | + char const* parameter_name); | ||
| 110 | + QPDF_DLL | ||
| 111 | + void addOptionalParameter(std::string const& arg, param_arg_handler_t); | ||
| 112 | + QPDF_DLL | ||
| 113 | + void addRequiredChoices( | ||
| 114 | + std::string const& arg, param_arg_handler_t, char const** choices); | ||
| 115 | + // The final check handler is called at the very end of argument | ||
| 116 | + // parsing. | ||
| 117 | + QPDF_DLL | ||
| 118 | + void addFinalCheck(bare_arg_handler_t); | ||
| 119 | + | ||
| 120 | + // Convenience methods for adding member functions of a class as | ||
| 121 | + // handlers. | ||
| 122 | + template <class T> | ||
| 123 | + static bare_arg_handler_t bindBare(void (T::*f)(), T* o) | ||
| 124 | + { | ||
| 125 | + return std::bind(std::mem_fn(f), o); | ||
| 126 | + } | ||
| 127 | + template <class T> | ||
| 128 | + static param_arg_handler_t bindParam(void (T::*f)(char *), T* o) | ||
| 129 | + { | ||
| 130 | + return std::bind(std::mem_fn(f), o, std::placeholders::_1); | ||
| 131 | + } | ||
| 132 | + | ||
| 133 | + // When processing arguments, indicate how many arguments remain | ||
| 134 | + // after the one whose handler is being called. | ||
| 135 | + QPDF_DLL | ||
| 136 | + int argsLeft() const; | ||
| 137 | + | ||
| 138 | + // Indicate whether we are in completion mode. | ||
| 139 | + QPDF_DLL | ||
| 140 | + bool isCompleting() const; | ||
| 141 | + | ||
| 142 | + // Insert a completion during argument parsing; useful for | ||
| 143 | + // customizing completion in the position argument handler. Should | ||
| 144 | + // only be used in completion mode. | ||
| 145 | + QPDF_DLL | ||
| 146 | + void insertCompletion(std::string const&); | ||
| 147 | + | ||
| 148 | + private: | ||
| 149 | + struct OptionEntry | ||
| 150 | + { | ||
| 151 | + OptionEntry() : | ||
| 152 | + parameter_needed(false), | ||
| 153 | + bare_arg_handler(0), | ||
| 154 | + param_arg_handler(0) | ||
| 155 | + { | ||
| 156 | + } | ||
| 157 | + bool parameter_needed; | ||
| 158 | + std::string parameter_name; | ||
| 159 | + std::set<std::string> choices; | ||
| 160 | + bare_arg_handler_t bare_arg_handler; | ||
| 161 | + param_arg_handler_t param_arg_handler; | ||
| 162 | + }; | ||
| 163 | + friend struct OptionEntry; | ||
| 164 | + | ||
| 165 | + OptionEntry& registerArg(std::string const& arg); | ||
| 166 | + | ||
| 167 | + void completionCommon(bool zsh); | ||
| 168 | + | ||
| 169 | + void argCompletionBash(); | ||
| 170 | + void argCompletionZsh(); | ||
| 171 | + | ||
| 172 | + void usage(std::string const& message); | ||
| 173 | + void checkCompletion(); | ||
| 174 | + void handleArgFileArguments(); | ||
| 175 | + void handleBashArguments(); | ||
| 176 | + void readArgsFromFile(char const* filename); | ||
| 177 | + void doFinalChecks(); | ||
| 178 | + void addOptionsToCompletions(); | ||
| 179 | + void addChoicesToCompletions(std::string const&, std::string const&); | ||
| 180 | + void handleCompletion(); | ||
| 181 | + | ||
| 182 | + typedef std::map<std::string, OptionEntry> option_table_t; | ||
| 183 | + | ||
| 184 | + class Members | ||
| 185 | + { | ||
| 186 | + friend class QPDFArgParser; | ||
| 187 | + | ||
| 188 | + public: | ||
| 189 | + QPDF_DLL | ||
| 190 | + ~Members() = default; | ||
| 191 | + | ||
| 192 | + private: | ||
| 193 | + Members(int argc, char* argv[], char const* progname_env); | ||
| 194 | + Members(Members const&) = delete; | ||
| 195 | + | ||
| 196 | + int argc; | ||
| 197 | + char** argv; | ||
| 198 | + char const* whoami; | ||
| 199 | + std::string progname_env; | ||
| 200 | + int cur_arg; | ||
| 201 | + bool bash_completion; | ||
| 202 | + bool zsh_completion; | ||
| 203 | + std::string bash_prev; | ||
| 204 | + std::string bash_cur; | ||
| 205 | + std::string bash_line; | ||
| 206 | + std::set<std::string> completions; | ||
| 207 | + std::map<std::string, option_table_t> option_tables; | ||
| 208 | + option_table_t main_option_table; | ||
| 209 | + option_table_t help_option_table; | ||
| 210 | + option_table_t* option_table; | ||
| 211 | + std::string option_table_name; | ||
| 212 | + bare_arg_handler_t final_check_handler; | ||
| 213 | + std::vector<PointerHolder<char>> new_argv; | ||
| 214 | + std::vector<PointerHolder<char>> bash_argv; | ||
| 215 | + PointerHolder<char*> argv_ph; | ||
| 216 | + PointerHolder<char*> bash_argv_ph; | ||
| 217 | + }; | ||
| 218 | + PointerHolder<Members> m; | ||
| 219 | +}; | ||
| 220 | + | ||
| 221 | +#endif // QPDFARGPARSER_HH |
libqpdf/QPDFArgParser.cc
0 → 100644
| 1 | +#include <qpdf/QPDFArgParser.hh> | ||
| 2 | +#include <qpdf/QUtil.hh> | ||
| 3 | +#include <qpdf/QIntC.hh> | ||
| 4 | +#include <qpdf/QTC.hh> | ||
| 5 | +#include <iostream> | ||
| 6 | +#include <cstring> | ||
| 7 | +#include <cstdlib> | ||
| 8 | + | ||
| 9 | +QPDFArgParser::Usage::Usage(std::string const& msg) : | ||
| 10 | + std::runtime_error(msg) | ||
| 11 | +{ | ||
| 12 | +} | ||
| 13 | + | ||
| 14 | +QPDFArgParser::Members::Members( | ||
| 15 | + int argc, char* argv[], char const* progname_env) : | ||
| 16 | + | ||
| 17 | + argc(argc), | ||
| 18 | + argv(argv), | ||
| 19 | + whoami(QUtil::getWhoami(argv[0])), | ||
| 20 | + progname_env(progname_env), | ||
| 21 | + cur_arg(0), | ||
| 22 | + bash_completion(false), | ||
| 23 | + zsh_completion(false), | ||
| 24 | + option_table(nullptr), | ||
| 25 | + final_check_handler(nullptr) | ||
| 26 | +{ | ||
| 27 | +} | ||
| 28 | + | ||
| 29 | +QPDFArgParser::QPDFArgParser(int argc, char* argv[], char const* progname_env) : | ||
| 30 | + m(new Members(argc, argv, progname_env)) | ||
| 31 | +{ | ||
| 32 | + selectHelpOptionTable(); | ||
| 33 | + addBare("completion-bash", | ||
| 34 | + std::bind(std::mem_fn(&QPDFArgParser::argCompletionBash), this)); | ||
| 35 | + addBare("completion-zsh", | ||
| 36 | + std::bind(std::mem_fn(&QPDFArgParser::argCompletionZsh), this)); | ||
| 37 | + selectMainOptionTable(); | ||
| 38 | +} | ||
| 39 | + | ||
| 40 | +void | ||
| 41 | +QPDFArgParser::selectMainOptionTable() | ||
| 42 | +{ | ||
| 43 | + this->m->option_table = &this->m->main_option_table; | ||
| 44 | + this->m->option_table_name = "main"; | ||
| 45 | +} | ||
| 46 | + | ||
| 47 | +void | ||
| 48 | +QPDFArgParser::selectHelpOptionTable() | ||
| 49 | +{ | ||
| 50 | + this->m->option_table = &this->m->help_option_table; | ||
| 51 | + this->m->option_table_name = "help"; | ||
| 52 | +} | ||
| 53 | + | ||
| 54 | +void | ||
| 55 | +QPDFArgParser::selectOptionTable(std::string const& name) | ||
| 56 | +{ | ||
| 57 | + auto t = this->m->option_tables.find(name); | ||
| 58 | + if (t == this->m->option_tables.end()) | ||
| 59 | + { | ||
| 60 | + QTC::TC("libtests", "QPDFArgParser select unregistered table"); | ||
| 61 | + throw std::logic_error( | ||
| 62 | + "QPDFArgParser: selecting unregistered option table " + name); | ||
| 63 | + } | ||
| 64 | + this->m->option_table = &(t->second); | ||
| 65 | + this->m->option_table_name = name; | ||
| 66 | +} | ||
| 67 | + | ||
| 68 | +void | ||
| 69 | +QPDFArgParser::registerOptionTable( | ||
| 70 | + std::string const& name, | ||
| 71 | + bare_arg_handler_t end_handler) | ||
| 72 | +{ | ||
| 73 | + if (0 != this->m->option_tables.count(name)) | ||
| 74 | + { | ||
| 75 | + QTC::TC("libtests", "QPDFArgParser register registered table"); | ||
| 76 | + throw std::logic_error( | ||
| 77 | + "QPDFArgParser: registering already registered option table " | ||
| 78 | + + name); | ||
| 79 | + } | ||
| 80 | + this->m->option_tables[name]; | ||
| 81 | + selectOptionTable(name); | ||
| 82 | + addBare("--", end_handler); | ||
| 83 | +} | ||
| 84 | + | ||
| 85 | +QPDFArgParser::OptionEntry& | ||
| 86 | +QPDFArgParser::registerArg(std::string const& arg) | ||
| 87 | +{ | ||
| 88 | + if (0 != this->m->option_table->count(arg)) | ||
| 89 | + { | ||
| 90 | + QTC::TC("libtests", "QPDFArgParser duplicate handler"); | ||
| 91 | + throw std::logic_error( | ||
| 92 | + "QPDFArgParser: adding a duplicate handler for option " + | ||
| 93 | + arg + " in " + this->m->option_table_name + | ||
| 94 | + " option table"); | ||
| 95 | + } | ||
| 96 | + return ((*this->m->option_table)[arg]); | ||
| 97 | +} | ||
| 98 | + | ||
| 99 | +void | ||
| 100 | +QPDFArgParser::addPositional(param_arg_handler_t handler) | ||
| 101 | +{ | ||
| 102 | + OptionEntry& oe = registerArg(""); | ||
| 103 | + oe.param_arg_handler = handler; | ||
| 104 | +} | ||
| 105 | + | ||
| 106 | +void | ||
| 107 | +QPDFArgParser::addBare( | ||
| 108 | + std::string const& arg, bare_arg_handler_t handler) | ||
| 109 | +{ | ||
| 110 | + OptionEntry& oe = registerArg(arg); | ||
| 111 | + oe.parameter_needed = false; | ||
| 112 | + oe.bare_arg_handler = handler; | ||
| 113 | +} | ||
| 114 | + | ||
| 115 | +void | ||
| 116 | +QPDFArgParser::addRequiredParameter( | ||
| 117 | + std::string const& arg, | ||
| 118 | + param_arg_handler_t handler, | ||
| 119 | + char const* parameter_name) | ||
| 120 | +{ | ||
| 121 | + OptionEntry& oe = registerArg(arg); | ||
| 122 | + oe.parameter_needed = true; | ||
| 123 | + oe.parameter_name = parameter_name; | ||
| 124 | + oe.param_arg_handler = handler; | ||
| 125 | +} | ||
| 126 | + | ||
| 127 | +void | ||
| 128 | +QPDFArgParser::addOptionalParameter( | ||
| 129 | + std::string const& arg, param_arg_handler_t handler) | ||
| 130 | +{ | ||
| 131 | + OptionEntry& oe = registerArg(arg); | ||
| 132 | + oe.parameter_needed = false; | ||
| 133 | + oe.param_arg_handler = handler; | ||
| 134 | +} | ||
| 135 | + | ||
| 136 | +void | ||
| 137 | +QPDFArgParser::addRequiredChoices( | ||
| 138 | + std::string const& arg, | ||
| 139 | + param_arg_handler_t handler, | ||
| 140 | + char const** choices) | ||
| 141 | +{ | ||
| 142 | + OptionEntry& oe = registerArg(arg); | ||
| 143 | + oe.parameter_needed = true; | ||
| 144 | + oe.param_arg_handler = handler; | ||
| 145 | + for (char const** i = choices; *i; ++i) | ||
| 146 | + { | ||
| 147 | + oe.choices.insert(*i); | ||
| 148 | + } | ||
| 149 | +} | ||
| 150 | + | ||
| 151 | +void | ||
| 152 | +QPDFArgParser::addFinalCheck(bare_arg_handler_t handler) | ||
| 153 | +{ | ||
| 154 | + this->m->final_check_handler = handler; | ||
| 155 | +} | ||
| 156 | + | ||
| 157 | +bool | ||
| 158 | +QPDFArgParser::isCompleting() const | ||
| 159 | +{ | ||
| 160 | + return this->m->bash_completion; | ||
| 161 | +} | ||
| 162 | + | ||
| 163 | +int | ||
| 164 | +QPDFArgParser::argsLeft() const | ||
| 165 | +{ | ||
| 166 | + return this->m->argc - this->m->cur_arg - 1; | ||
| 167 | +} | ||
| 168 | + | ||
| 169 | +void | ||
| 170 | +QPDFArgParser::insertCompletion(std::string const& arg) | ||
| 171 | +{ | ||
| 172 | + this->m->completions.insert(arg); | ||
| 173 | +} | ||
| 174 | + | ||
| 175 | +void | ||
| 176 | +QPDFArgParser::completionCommon(bool zsh) | ||
| 177 | +{ | ||
| 178 | + std::string progname = this->m->argv[0]; | ||
| 179 | + std::string executable; | ||
| 180 | + std::string appdir; | ||
| 181 | + std::string appimage; | ||
| 182 | + if (QUtil::get_env(this->m->progname_env.c_str(), &executable)) | ||
| 183 | + { | ||
| 184 | + progname = executable; | ||
| 185 | + } | ||
| 186 | + else if (QUtil::get_env("APPDIR", &appdir) && | ||
| 187 | + QUtil::get_env("APPIMAGE", &appimage)) | ||
| 188 | + { | ||
| 189 | + // Detect if we're in an AppImage and adjust | ||
| 190 | + if ((appdir.length() < strlen(this->m->argv[0])) && | ||
| 191 | + (strncmp(appdir.c_str(), this->m->argv[0], appdir.length()) == 0)) | ||
| 192 | + { | ||
| 193 | + progname = appimage; | ||
| 194 | + } | ||
| 195 | + } | ||
| 196 | + if (zsh) | ||
| 197 | + { | ||
| 198 | + std::cout << "autoload -U +X bashcompinit && bashcompinit && "; | ||
| 199 | + } | ||
| 200 | + std::cout << "complete -o bashdefault -o default"; | ||
| 201 | + if (! zsh) | ||
| 202 | + { | ||
| 203 | + std::cout << " -o nospace"; | ||
| 204 | + } | ||
| 205 | + std::cout << " -C " << progname << " " << this->m->whoami << std::endl; | ||
| 206 | + // Put output before error so calling from zsh works properly | ||
| 207 | + std::string path = progname; | ||
| 208 | + size_t slash = path.find('/'); | ||
| 209 | + if ((slash != 0) && (slash != std::string::npos)) | ||
| 210 | + { | ||
| 211 | + std::cerr << "WARNING: " << this->m->whoami << " completion enabled" | ||
| 212 | + << " using relative path to executable" << std::endl; | ||
| 213 | + } | ||
| 214 | +} | ||
| 215 | + | ||
| 216 | +void | ||
| 217 | +QPDFArgParser::argCompletionBash() | ||
| 218 | +{ | ||
| 219 | + completionCommon(false); | ||
| 220 | +} | ||
| 221 | + | ||
| 222 | +void | ||
| 223 | +QPDFArgParser::argCompletionZsh() | ||
| 224 | +{ | ||
| 225 | + completionCommon(true); | ||
| 226 | +} | ||
| 227 | + | ||
| 228 | +void | ||
| 229 | +QPDFArgParser::handleArgFileArguments() | ||
| 230 | +{ | ||
| 231 | + // Support reading arguments from files. Create a new argv. Ensure | ||
| 232 | + // that argv itself as well as all its contents are automatically | ||
| 233 | + // deleted by using PointerHolder objects to back the pointers in | ||
| 234 | + // argv. | ||
| 235 | + this->m->new_argv.push_back( | ||
| 236 | + PointerHolder<char>(true, QUtil::copy_string(this->m->argv[0]))); | ||
| 237 | + for (int i = 1; i < this->m->argc; ++i) | ||
| 238 | + { | ||
| 239 | + char* argfile = 0; | ||
| 240 | + if ((strlen(this->m->argv[i]) > 1) && (this->m->argv[i][0] == '@')) | ||
| 241 | + { | ||
| 242 | + argfile = 1 + this->m->argv[i]; | ||
| 243 | + if (strcmp(argfile, "-") != 0) | ||
| 244 | + { | ||
| 245 | + if (! QUtil::file_can_be_opened(argfile)) | ||
| 246 | + { | ||
| 247 | + // The file's not there; treating as regular option | ||
| 248 | + argfile = nullptr; | ||
| 249 | + } | ||
| 250 | + } | ||
| 251 | + } | ||
| 252 | + if (argfile) | ||
| 253 | + { | ||
| 254 | + readArgsFromFile(1 + this->m->argv[i]); | ||
| 255 | + } | ||
| 256 | + else | ||
| 257 | + { | ||
| 258 | + this->m->new_argv.push_back( | ||
| 259 | + PointerHolder<char>( | ||
| 260 | + true, QUtil::copy_string(this->m->argv[i]))); | ||
| 261 | + } | ||
| 262 | + } | ||
| 263 | + this->m->argv_ph = | ||
| 264 | + PointerHolder<char*>(true, new char*[1 + this->m->new_argv.size()]); | ||
| 265 | + this->m->argv = this->m->argv_ph.getPointer(); | ||
| 266 | + for (size_t i = 0; i < this->m->new_argv.size(); ++i) | ||
| 267 | + { | ||
| 268 | + this->m->argv[i] = this->m->new_argv.at(i).getPointer(); | ||
| 269 | + } | ||
| 270 | + this->m->argc = QIntC::to_int(this->m->new_argv.size()); | ||
| 271 | + this->m->argv[this->m->argc] = 0; | ||
| 272 | +} | ||
| 273 | + | ||
| 274 | +void | ||
| 275 | +QPDFArgParser::handleBashArguments() | ||
| 276 | +{ | ||
| 277 | + // Do a minimal job of parsing bash_line into arguments. This | ||
| 278 | + // doesn't do everything the shell does (e.g. $(...), variable | ||
| 279 | + // expansion, arithmetic, globs, etc.), but it should be good | ||
| 280 | + // enough for purposes of handling completion. As we build up the | ||
| 281 | + // new argv, we can't use this->m->new_argv because this code has to | ||
| 282 | + // interoperate with @file arguments, so memory for both ways of | ||
| 283 | + // fabricating argv has to be protected. | ||
| 284 | + | ||
| 285 | + bool last_was_backslash = false; | ||
| 286 | + enum { st_top, st_squote, st_dquote } state = st_top; | ||
| 287 | + std::string arg; | ||
| 288 | + for (std::string::iterator iter = this->m->bash_line.begin(); | ||
| 289 | + iter != this->m->bash_line.end(); ++iter) | ||
| 290 | + { | ||
| 291 | + char ch = (*iter); | ||
| 292 | + if (last_was_backslash) | ||
| 293 | + { | ||
| 294 | + arg.append(1, ch); | ||
| 295 | + last_was_backslash = false; | ||
| 296 | + } | ||
| 297 | + else if (ch == '\\') | ||
| 298 | + { | ||
| 299 | + last_was_backslash = true; | ||
| 300 | + } | ||
| 301 | + else | ||
| 302 | + { | ||
| 303 | + bool append = false; | ||
| 304 | + switch (state) | ||
| 305 | + { | ||
| 306 | + case st_top: | ||
| 307 | + if (QUtil::is_space(ch)) | ||
| 308 | + { | ||
| 309 | + if (! arg.empty()) | ||
| 310 | + { | ||
| 311 | + this->m->bash_argv.push_back( | ||
| 312 | + PointerHolder<char>( | ||
| 313 | + true, QUtil::copy_string(arg.c_str()))); | ||
| 314 | + arg.clear(); | ||
| 315 | + } | ||
| 316 | + } | ||
| 317 | + else if (ch == '"') | ||
| 318 | + { | ||
| 319 | + state = st_dquote; | ||
| 320 | + } | ||
| 321 | + else if (ch == '\'') | ||
| 322 | + { | ||
| 323 | + state = st_squote; | ||
| 324 | + } | ||
| 325 | + else | ||
| 326 | + { | ||
| 327 | + append = true; | ||
| 328 | + } | ||
| 329 | + break; | ||
| 330 | + | ||
| 331 | + case st_squote: | ||
| 332 | + if (ch == '\'') | ||
| 333 | + { | ||
| 334 | + state = st_top; | ||
| 335 | + } | ||
| 336 | + else | ||
| 337 | + { | ||
| 338 | + append = true; | ||
| 339 | + } | ||
| 340 | + break; | ||
| 341 | + | ||
| 342 | + case st_dquote: | ||
| 343 | + if (ch == '"') | ||
| 344 | + { | ||
| 345 | + state = st_top; | ||
| 346 | + } | ||
| 347 | + else | ||
| 348 | + { | ||
| 349 | + append = true; | ||
| 350 | + } | ||
| 351 | + break; | ||
| 352 | + } | ||
| 353 | + if (append) | ||
| 354 | + { | ||
| 355 | + arg.append(1, ch); | ||
| 356 | + } | ||
| 357 | + } | ||
| 358 | + } | ||
| 359 | + if (this->m->bash_argv.empty()) | ||
| 360 | + { | ||
| 361 | + // This can't happen if properly invoked by bash, but ensure | ||
| 362 | + // we have a valid argv[0] regardless. | ||
| 363 | + this->m->bash_argv.push_back( | ||
| 364 | + PointerHolder<char>( | ||
| 365 | + true, QUtil::copy_string(this->m->argv[0]))); | ||
| 366 | + } | ||
| 367 | + // Explicitly discard any non-space-terminated word. The "current | ||
| 368 | + // word" is handled specially. | ||
| 369 | + this->m->bash_argv_ph = | ||
| 370 | + PointerHolder<char*>(true, new char*[1 + this->m->bash_argv.size()]); | ||
| 371 | + this->m->argv = this->m->bash_argv_ph.getPointer(); | ||
| 372 | + for (size_t i = 0; i < this->m->bash_argv.size(); ++i) | ||
| 373 | + { | ||
| 374 | + this->m->argv[i] = this->m->bash_argv.at(i).getPointer(); | ||
| 375 | + } | ||
| 376 | + this->m->argc = QIntC::to_int(this->m->bash_argv.size()); | ||
| 377 | + this->m->argv[this->m->argc] = 0; | ||
| 378 | +} | ||
| 379 | + | ||
| 380 | +void | ||
| 381 | +QPDFArgParser::usage(std::string const& message) | ||
| 382 | +{ | ||
| 383 | + if (this->m->bash_completion) | ||
| 384 | + { | ||
| 385 | + // This will cause bash to fall back to regular file completion. | ||
| 386 | + exit(0); | ||
| 387 | + } | ||
| 388 | + throw Usage(message); | ||
| 389 | +} | ||
| 390 | + | ||
| 391 | +void | ||
| 392 | +QPDFArgParser::readArgsFromFile(char const* filename) | ||
| 393 | +{ | ||
| 394 | + std::list<std::string> lines; | ||
| 395 | + if (strcmp(filename, "-") == 0) | ||
| 396 | + { | ||
| 397 | + QTC::TC("libtests", "QPDFArgParser read args from stdin"); | ||
| 398 | + lines = QUtil::read_lines_from_file(std::cin); | ||
| 399 | + } | ||
| 400 | + else | ||
| 401 | + { | ||
| 402 | + QTC::TC("libtests", "QPDFArgParser read args from file"); | ||
| 403 | + lines = QUtil::read_lines_from_file(filename); | ||
| 404 | + } | ||
| 405 | + for (std::list<std::string>::iterator iter = lines.begin(); | ||
| 406 | + iter != lines.end(); ++iter) | ||
| 407 | + { | ||
| 408 | + this->m->new_argv.push_back( | ||
| 409 | + PointerHolder<char>(true, QUtil::copy_string((*iter).c_str()))); | ||
| 410 | + } | ||
| 411 | +} | ||
| 412 | + | ||
| 413 | +void | ||
| 414 | +QPDFArgParser::checkCompletion() | ||
| 415 | +{ | ||
| 416 | + // See if we're being invoked from bash completion. | ||
| 417 | + std::string bash_point_env; | ||
| 418 | + // On Windows with mingw, there have been times when there appears | ||
| 419 | + // to be no way to distinguish between an empty environment | ||
| 420 | + // variable and an unset variable. There are also conditions under | ||
| 421 | + // which bash doesn't set COMP_LINE. Therefore, enter this logic | ||
| 422 | + // if either COMP_LINE or COMP_POINT are set. They will both be | ||
| 423 | + // set together under ordinary circumstances. | ||
| 424 | + bool got_line = QUtil::get_env("COMP_LINE", &this->m->bash_line); | ||
| 425 | + bool got_point = QUtil::get_env("COMP_POINT", &bash_point_env); | ||
| 426 | + if (got_line || got_point) | ||
| 427 | + { | ||
| 428 | + size_t p = QUtil::string_to_uint(bash_point_env.c_str()); | ||
| 429 | + if (p < this->m->bash_line.length()) | ||
| 430 | + { | ||
| 431 | + // Truncate the line. We ignore everything at or after the | ||
| 432 | + // cursor for completion purposes. | ||
| 433 | + this->m->bash_line = this->m->bash_line.substr(0, p); | ||
| 434 | + } | ||
| 435 | + if (p > this->m->bash_line.length()) | ||
| 436 | + { | ||
| 437 | + p = this->m->bash_line.length(); | ||
| 438 | + } | ||
| 439 | + // Set bash_cur and bash_prev based on bash_line rather than | ||
| 440 | + // relying on argv. This enables us to use bashcompinit to get | ||
| 441 | + // completion in zsh too since bashcompinit sets COMP_LINE and | ||
| 442 | + // COMP_POINT but doesn't invoke the command with options like | ||
| 443 | + // bash does. | ||
| 444 | + | ||
| 445 | + // p is equal to length of the string. Walk backwards looking | ||
| 446 | + // for the first separator. bash_cur is everything after the | ||
| 447 | + // last separator, possibly empty. | ||
| 448 | + char sep(0); | ||
| 449 | + while (p > 0) | ||
| 450 | + { | ||
| 451 | + --p; | ||
| 452 | + char ch = this->m->bash_line.at(p); | ||
| 453 | + if ((ch == ' ') || (ch == '=') || (ch == ':')) | ||
| 454 | + { | ||
| 455 | + sep = ch; | ||
| 456 | + break; | ||
| 457 | + } | ||
| 458 | + } | ||
| 459 | + if (1+p <= this->m->bash_line.length()) | ||
| 460 | + { | ||
| 461 | + this->m->bash_cur = this->m->bash_line.substr( | ||
| 462 | + 1+p, std::string::npos); | ||
| 463 | + } | ||
| 464 | + if ((sep == ':') || (sep == '=')) | ||
| 465 | + { | ||
| 466 | + // Bash sets prev to the non-space separator if any. | ||
| 467 | + // Actually, if there are multiple separators in a row, | ||
| 468 | + // they are all included in prev, but that detail is not | ||
| 469 | + // important to us and not worth coding. | ||
| 470 | + this->m->bash_prev = this->m->bash_line.substr(p, 1); | ||
| 471 | + } | ||
| 472 | + else | ||
| 473 | + { | ||
| 474 | + // Go back to the last separator and set prev based on | ||
| 475 | + // that. | ||
| 476 | + size_t p1 = p; | ||
| 477 | + while (p1 > 0) | ||
| 478 | + { | ||
| 479 | + --p1; | ||
| 480 | + char ch = this->m->bash_line.at(p1); | ||
| 481 | + if ((ch == ' ') || (ch == ':') || (ch == '=')) | ||
| 482 | + { | ||
| 483 | + this->m->bash_prev = | ||
| 484 | + this->m->bash_line.substr(p1 + 1, p - p1 - 1); | ||
| 485 | + break; | ||
| 486 | + } | ||
| 487 | + } | ||
| 488 | + } | ||
| 489 | + if (this->m->bash_prev.empty()) | ||
| 490 | + { | ||
| 491 | + this->m->bash_prev = this->m->bash_line.substr(0, p); | ||
| 492 | + } | ||
| 493 | + if (this->m->argc == 1) | ||
| 494 | + { | ||
| 495 | + // This is probably zsh using bashcompinit. There are a | ||
| 496 | + // few differences in the expected output. | ||
| 497 | + this->m->zsh_completion = true; | ||
| 498 | + } | ||
| 499 | + handleBashArguments(); | ||
| 500 | + this->m->bash_completion = true; | ||
| 501 | + } | ||
| 502 | +} | ||
| 503 | + | ||
| 504 | +void | ||
| 505 | +QPDFArgParser::parseArgs() | ||
| 506 | +{ | ||
| 507 | + selectMainOptionTable(); | ||
| 508 | + checkCompletion(); | ||
| 509 | + handleArgFileArguments(); | ||
| 510 | + for (this->m->cur_arg = 1; | ||
| 511 | + this->m->cur_arg < this->m->argc; | ||
| 512 | + ++this->m->cur_arg) | ||
| 513 | + { | ||
| 514 | + bool help_option = false; | ||
| 515 | + bool end_option = false; | ||
| 516 | + auto oep = this->m->option_table->end(); | ||
| 517 | + char* arg = this->m->argv[this->m->cur_arg]; | ||
| 518 | + char* parameter = nullptr; | ||
| 519 | + std::string o_arg(arg); | ||
| 520 | + std::string arg_s(arg); | ||
| 521 | + if ((strcmp(arg, "--") == 0) && | ||
| 522 | + (this->m->option_table != &this->m->main_option_table)) | ||
| 523 | + { | ||
| 524 | + // Special case for -- option, which is used to break out | ||
| 525 | + // of subparsers. | ||
| 526 | + oep = this->m->option_table->find("--"); | ||
| 527 | + end_option = true; | ||
| 528 | + if (oep == this->m->option_table->end()) | ||
| 529 | + { | ||
| 530 | + // This is registered automatically, so this can't happen. | ||
| 531 | + throw std::logic_error("ArgParser: -- handler not registered"); | ||
| 532 | + } | ||
| 533 | + } | ||
| 534 | + else if ((arg[0] == '-') && (strcmp(arg, "-") != 0)) | ||
| 535 | + { | ||
| 536 | + ++arg; | ||
| 537 | + if (arg[0] == '-') | ||
| 538 | + { | ||
| 539 | + // Be lax about -arg vs --arg | ||
| 540 | + ++arg; | ||
| 541 | + } | ||
| 542 | + else | ||
| 543 | + { | ||
| 544 | + QTC::TC("libtests", "QPDFArgParser single dash"); | ||
| 545 | + } | ||
| 546 | + if (strlen(arg) > 0) | ||
| 547 | + { | ||
| 548 | + // Prevent --=something from being treated as an empty | ||
| 549 | + // arg since the empty string in the option table is | ||
| 550 | + // for positional arguments. | ||
| 551 | + parameter = const_cast<char*>(strchr(1 + arg, '=')); | ||
| 552 | + } | ||
| 553 | + if (parameter) | ||
| 554 | + { | ||
| 555 | + *parameter++ = 0; | ||
| 556 | + } | ||
| 557 | + | ||
| 558 | + arg_s = arg; | ||
| 559 | + | ||
| 560 | + if ((! this->m->bash_completion) && | ||
| 561 | + (this->m->argc == 2) && (this->m->cur_arg == 1) && | ||
| 562 | + this->m->help_option_table.count(arg_s)) | ||
| 563 | + { | ||
| 564 | + // Handle help option, which is only valid as the sole | ||
| 565 | + // option. | ||
| 566 | + QTC::TC("libtests", "QPDFArgParser help option"); | ||
| 567 | + oep = this->m->help_option_table.find(arg_s); | ||
| 568 | + help_option = true; | ||
| 569 | + } | ||
| 570 | + | ||
| 571 | + if (! (help_option || arg_s.empty() || (arg_s.at(0) == '-'))) | ||
| 572 | + { | ||
| 573 | + oep = this->m->option_table->find(arg_s); | ||
| 574 | + } | ||
| 575 | + } | ||
| 576 | + else | ||
| 577 | + { | ||
| 578 | + // The empty string maps to the positional argument | ||
| 579 | + // handler. | ||
| 580 | + QTC::TC("libtests", "QPDFArgParser positional"); | ||
| 581 | + oep = this->m->option_table->find(""); | ||
| 582 | + parameter = arg; | ||
| 583 | + } | ||
| 584 | + | ||
| 585 | + if (oep == this->m->option_table->end()) | ||
| 586 | + { | ||
| 587 | + QTC::TC("libtests", "QPDFArgParser unrecognized"); | ||
| 588 | + std::string message = "unrecognized argument " + o_arg; | ||
| 589 | + if (this->m->option_table != &this->m->main_option_table) | ||
| 590 | + { | ||
| 591 | + message += " (" + this->m->option_table_name + | ||
| 592 | + " options must be terminated with --)"; | ||
| 593 | + } | ||
| 594 | + usage(message); | ||
| 595 | + } | ||
| 596 | + | ||
| 597 | + OptionEntry& oe = oep->second; | ||
| 598 | + if ((oe.parameter_needed && (0 == parameter)) || | ||
| 599 | + ((! oe.choices.empty() && | ||
| 600 | + ((0 == parameter) || | ||
| 601 | + (0 == oe.choices.count(parameter)))))) | ||
| 602 | + { | ||
| 603 | + std::string message = | ||
| 604 | + "--" + arg_s + " must be given as --" + arg_s + "="; | ||
| 605 | + if (! oe.choices.empty()) | ||
| 606 | + { | ||
| 607 | + QTC::TC("libtests", "QPDFArgParser required choices"); | ||
| 608 | + message += "{"; | ||
| 609 | + for (std::set<std::string>::iterator iter = | ||
| 610 | + oe.choices.begin(); | ||
| 611 | + iter != oe.choices.end(); ++iter) | ||
| 612 | + { | ||
| 613 | + if (iter != oe.choices.begin()) | ||
| 614 | + { | ||
| 615 | + message += ","; | ||
| 616 | + } | ||
| 617 | + message += *iter; | ||
| 618 | + } | ||
| 619 | + message += "}"; | ||
| 620 | + } | ||
| 621 | + else if (! oe.parameter_name.empty()) | ||
| 622 | + { | ||
| 623 | + QTC::TC("libtests", "QPDFArgParser required parameter"); | ||
| 624 | + message += oe.parameter_name; | ||
| 625 | + } | ||
| 626 | + else | ||
| 627 | + { | ||
| 628 | + // should not be possible | ||
| 629 | + message += "option"; | ||
| 630 | + } | ||
| 631 | + usage(message); | ||
| 632 | + } | ||
| 633 | + if (oe.bare_arg_handler) | ||
| 634 | + { | ||
| 635 | + oe.bare_arg_handler(); | ||
| 636 | + } | ||
| 637 | + else if (oe.param_arg_handler) | ||
| 638 | + { | ||
| 639 | + oe.param_arg_handler(parameter); | ||
| 640 | + } | ||
| 641 | + if (help_option) | ||
| 642 | + { | ||
| 643 | + exit(0); | ||
| 644 | + } | ||
| 645 | + if (end_option) | ||
| 646 | + { | ||
| 647 | + selectMainOptionTable(); | ||
| 648 | + } | ||
| 649 | + } | ||
| 650 | + if (this->m->bash_completion) | ||
| 651 | + { | ||
| 652 | + handleCompletion(); | ||
| 653 | + } | ||
| 654 | + else | ||
| 655 | + { | ||
| 656 | + doFinalChecks(); | ||
| 657 | + } | ||
| 658 | +} | ||
| 659 | + | ||
| 660 | +void | ||
| 661 | +QPDFArgParser::doFinalChecks() | ||
| 662 | +{ | ||
| 663 | + if (this->m->option_table != &(this->m->main_option_table)) | ||
| 664 | + { | ||
| 665 | + QTC::TC("libtests", "QPDFArgParser missing --"); | ||
| 666 | + usage("missing -- at end of " + this->m->option_table_name + | ||
| 667 | + " options"); | ||
| 668 | + } | ||
| 669 | + if (this->m->final_check_handler != nullptr) | ||
| 670 | + { | ||
| 671 | + this->m->final_check_handler(); | ||
| 672 | + } | ||
| 673 | +} | ||
| 674 | + | ||
| 675 | +void | ||
| 676 | +QPDFArgParser::addChoicesToCompletions(std::string const& option, | ||
| 677 | + std::string const& extra_prefix) | ||
| 678 | +{ | ||
| 679 | + if (this->m->option_table->count(option) != 0) | ||
| 680 | + { | ||
| 681 | + OptionEntry& oe = (*this->m->option_table)[option]; | ||
| 682 | + for (std::set<std::string>::iterator iter = oe.choices.begin(); | ||
| 683 | + iter != oe.choices.end(); ++iter) | ||
| 684 | + { | ||
| 685 | + QTC::TC("libtests", "QPDFArgParser complete choices"); | ||
| 686 | + this->m->completions.insert(extra_prefix + *iter); | ||
| 687 | + } | ||
| 688 | + } | ||
| 689 | +} | ||
| 690 | + | ||
| 691 | +void | ||
| 692 | +QPDFArgParser::addOptionsToCompletions() | ||
| 693 | +{ | ||
| 694 | + for (std::map<std::string, OptionEntry>::iterator iter = | ||
| 695 | + this->m->option_table->begin(); | ||
| 696 | + iter != this->m->option_table->end(); ++iter) | ||
| 697 | + { | ||
| 698 | + std::string const& arg = (*iter).first; | ||
| 699 | + if (arg == "--") | ||
| 700 | + { | ||
| 701 | + continue; | ||
| 702 | + } | ||
| 703 | + OptionEntry& oe = (*iter).second; | ||
| 704 | + std::string base = "--" + arg; | ||
| 705 | + if (oe.param_arg_handler) | ||
| 706 | + { | ||
| 707 | + if (this->m->zsh_completion) | ||
| 708 | + { | ||
| 709 | + // zsh doesn't treat = as a word separator, so add all | ||
| 710 | + // the options so we don't get a space after the =. | ||
| 711 | + addChoicesToCompletions(arg, base + "="); | ||
| 712 | + } | ||
| 713 | + this->m->completions.insert(base + "="); | ||
| 714 | + } | ||
| 715 | + if (! oe.parameter_needed) | ||
| 716 | + { | ||
| 717 | + this->m->completions.insert(base); | ||
| 718 | + } | ||
| 719 | + } | ||
| 720 | +} | ||
| 721 | + | ||
| 722 | +void | ||
| 723 | +QPDFArgParser::handleCompletion() | ||
| 724 | +{ | ||
| 725 | + std::string extra_prefix; | ||
| 726 | + if (this->m->completions.empty()) | ||
| 727 | + { | ||
| 728 | + // Detect --option=... Bash treats the = as a word separator. | ||
| 729 | + std::string choice_option; | ||
| 730 | + if (this->m->bash_cur.empty() && (this->m->bash_prev.length() > 2) && | ||
| 731 | + (this->m->bash_prev.at(0) == '-') && | ||
| 732 | + (this->m->bash_prev.at(1) == '-') && | ||
| 733 | + (this->m->bash_line.at(this->m->bash_line.length() - 1) == '=')) | ||
| 734 | + { | ||
| 735 | + choice_option = this->m->bash_prev.substr(2, std::string::npos); | ||
| 736 | + } | ||
| 737 | + else if ((this->m->bash_prev == "=") && | ||
| 738 | + (this->m->bash_line.length() > | ||
| 739 | + (this->m->bash_cur.length() + 1))) | ||
| 740 | + { | ||
| 741 | + // We're sitting at --option=x. Find previous option. | ||
| 742 | + size_t end_mark = this->m->bash_line.length() - | ||
| 743 | + this->m->bash_cur.length() - 1; | ||
| 744 | + char before_cur = this->m->bash_line.at(end_mark); | ||
| 745 | + if (before_cur == '=') | ||
| 746 | + { | ||
| 747 | + size_t space = this->m->bash_line.find_last_of(' ', end_mark); | ||
| 748 | + if (space != std::string::npos) | ||
| 749 | + { | ||
| 750 | + std::string candidate = | ||
| 751 | + this->m->bash_line.substr( | ||
| 752 | + space + 1, end_mark - space - 1); | ||
| 753 | + if ((candidate.length() > 2) && | ||
| 754 | + (candidate.at(0) == '-') && | ||
| 755 | + (candidate.at(1) == '-')) | ||
| 756 | + { | ||
| 757 | + choice_option = | ||
| 758 | + candidate.substr(2, std::string::npos); | ||
| 759 | + } | ||
| 760 | + } | ||
| 761 | + } | ||
| 762 | + } | ||
| 763 | + if (! choice_option.empty()) | ||
| 764 | + { | ||
| 765 | + if (this->m->zsh_completion) | ||
| 766 | + { | ||
| 767 | + // zsh wants --option=choice rather than just choice | ||
| 768 | + extra_prefix = "--" + choice_option + "="; | ||
| 769 | + } | ||
| 770 | + addChoicesToCompletions(choice_option, extra_prefix); | ||
| 771 | + } | ||
| 772 | + else if ((! this->m->bash_cur.empty()) && | ||
| 773 | + (this->m->bash_cur.at(0) == '-')) | ||
| 774 | + { | ||
| 775 | + addOptionsToCompletions(); | ||
| 776 | + if (this->m->argc == 1) | ||
| 777 | + { | ||
| 778 | + // Help options are valid only by themselves. | ||
| 779 | + for (std::map<std::string, OptionEntry>::iterator iter = | ||
| 780 | + this->m->help_option_table.begin(); | ||
| 781 | + iter != this->m->help_option_table.end(); ++iter) | ||
| 782 | + { | ||
| 783 | + this->m->completions.insert("--" + (*iter).first); | ||
| 784 | + } | ||
| 785 | + } | ||
| 786 | + } | ||
| 787 | + } | ||
| 788 | + std::string prefix = extra_prefix + this->m->bash_cur; | ||
| 789 | + for (std::set<std::string>::iterator iter = this->m->completions.begin(); | ||
| 790 | + iter != this->m->completions.end(); ++iter) | ||
| 791 | + { | ||
| 792 | + if (prefix.empty() || | ||
| 793 | + ((*iter).substr(0, prefix.length()) == prefix)) | ||
| 794 | + { | ||
| 795 | + std::cout << *iter << std::endl; | ||
| 796 | + } | ||
| 797 | + } | ||
| 798 | + exit(0); | ||
| 799 | +} |
libqpdf/build.mk
| @@ -57,6 +57,7 @@ SRCS_libqpdf = \ | @@ -57,6 +57,7 @@ SRCS_libqpdf = \ | ||
| 57 | libqpdf/QPDF.cc \ | 57 | libqpdf/QPDF.cc \ |
| 58 | libqpdf/QPDFAcroFormDocumentHelper.cc \ | 58 | libqpdf/QPDFAcroFormDocumentHelper.cc \ |
| 59 | libqpdf/QPDFAnnotationObjectHelper.cc \ | 59 | libqpdf/QPDFAnnotationObjectHelper.cc \ |
| 60 | + libqpdf/QPDFArgParser.cc \ | ||
| 60 | libqpdf/QPDFCryptoProvider.cc \ | 61 | libqpdf/QPDFCryptoProvider.cc \ |
| 61 | libqpdf/QPDFEFStreamObjectHelper.cc \ | 62 | libqpdf/QPDFEFStreamObjectHelper.cc \ |
| 62 | libqpdf/QPDFEmbeddedFileDocumentHelper.cc \ | 63 | libqpdf/QPDFEmbeddedFileDocumentHelper.cc \ |
libtests/arg_parser.cc
0 → 100644
| 1 | +#include <qpdf/QPDFArgParser.hh> | ||
| 2 | +#include <qpdf/QUtil.hh> | ||
| 3 | +#include <iostream> | ||
| 4 | +#include <cstring> | ||
| 5 | +#include <cassert> | ||
| 6 | + | ||
| 7 | +class ArgParser | ||
| 8 | +{ | ||
| 9 | + public: | ||
| 10 | + ArgParser(int argc, char* argv[]); | ||
| 11 | + void parseArgs(); | ||
| 12 | + | ||
| 13 | + void test_exceptions(); | ||
| 14 | + | ||
| 15 | + private: | ||
| 16 | + void handlePotato(); | ||
| 17 | + void handleSalad(char* p); | ||
| 18 | + void handleMoo(char* p); | ||
| 19 | + void handleOink(char* p); | ||
| 20 | + void handleQuack(char* p); | ||
| 21 | + void startQuack(); | ||
| 22 | + void getQuack(char* p); | ||
| 23 | + void endQuack(); | ||
| 24 | + void finalChecks(); | ||
| 25 | + | ||
| 26 | + void initOptions(); | ||
| 27 | + void output(std::string const&); | ||
| 28 | + | ||
| 29 | + QPDFArgParser ap; | ||
| 30 | + int quacks; | ||
| 31 | +}; | ||
| 32 | + | ||
| 33 | +ArgParser::ArgParser(int argc, char* argv[]) : | ||
| 34 | + ap(QPDFArgParser(argc, argv, "TEST_ARG_PARSER")), | ||
| 35 | + quacks(0) | ||
| 36 | +{ | ||
| 37 | + initOptions(); | ||
| 38 | +} | ||
| 39 | + | ||
| 40 | +void | ||
| 41 | +ArgParser::initOptions() | ||
| 42 | +{ | ||
| 43 | + auto b = [this](void (ArgParser::*f)()) { | ||
| 44 | + return QPDFArgParser::bindBare(f, this); | ||
| 45 | + }; | ||
| 46 | + auto p = [this](void (ArgParser::*f)(char *)) { | ||
| 47 | + return QPDFArgParser::bindParam(f, this); | ||
| 48 | + }; | ||
| 49 | + | ||
| 50 | + ap.addBare("potato", b(&ArgParser::handlePotato)); | ||
| 51 | + ap.addRequiredParameter("salad", p(&ArgParser::handleSalad), "tossed"); | ||
| 52 | + ap.addOptionalParameter("moo", p(&ArgParser::handleMoo)); | ||
| 53 | + char const* choices[] = {"pig", "boar", "sow", 0}; | ||
| 54 | + ap.addRequiredChoices("oink", p(&ArgParser::handleOink), choices); | ||
| 55 | + ap.selectHelpOptionTable(); | ||
| 56 | + ap.addBare("version", [this](){ output("3.14159"); }); | ||
| 57 | + ap.selectMainOptionTable(); | ||
| 58 | + ap.addBare("quack", b(&ArgParser::startQuack)); | ||
| 59 | + ap.registerOptionTable("quack", b(&ArgParser::endQuack)); | ||
| 60 | + ap.addPositional(p(&ArgParser::getQuack)); | ||
| 61 | + ap.addFinalCheck(b(&ArgParser::finalChecks)); | ||
| 62 | + ap.selectMainOptionTable(); | ||
| 63 | + ap.addBare("baaa", [this](){ this->ap.selectOptionTable("baaa"); }); | ||
| 64 | + ap.registerOptionTable("baaa", nullptr); | ||
| 65 | + ap.addBare("ewe", [this](){ output("you"); }); | ||
| 66 | + ap.addBare("ram", [this](){ output("ram"); }); | ||
| 67 | +} | ||
| 68 | + | ||
| 69 | +void | ||
| 70 | +ArgParser::output(std::string const& msg) | ||
| 71 | +{ | ||
| 72 | + if (! this->ap.isCompleting()) | ||
| 73 | + { | ||
| 74 | + std::cout << msg << std::endl; | ||
| 75 | + } | ||
| 76 | +} | ||
| 77 | + | ||
| 78 | +void | ||
| 79 | +ArgParser::handlePotato() | ||
| 80 | +{ | ||
| 81 | + output("got potato"); | ||
| 82 | +} | ||
| 83 | + | ||
| 84 | +void | ||
| 85 | +ArgParser::handleSalad(char* p) | ||
| 86 | +{ | ||
| 87 | + output(std::string("got salad=") + p); | ||
| 88 | +} | ||
| 89 | + | ||
| 90 | +void | ||
| 91 | +ArgParser::handleMoo(char* p) | ||
| 92 | +{ | ||
| 93 | + output(std::string("got moo=") + (p ? p : "(none)")); | ||
| 94 | +} | ||
| 95 | + | ||
| 96 | +void | ||
| 97 | +ArgParser::handleOink(char* p) | ||
| 98 | +{ | ||
| 99 | + output(std::string("got oink=") + p); | ||
| 100 | +} | ||
| 101 | + | ||
| 102 | +void | ||
| 103 | +ArgParser::parseArgs() | ||
| 104 | +{ | ||
| 105 | + this->ap.parseArgs(); | ||
| 106 | +} | ||
| 107 | + | ||
| 108 | +void | ||
| 109 | +ArgParser::startQuack() | ||
| 110 | +{ | ||
| 111 | + this->ap.selectOptionTable("quack"); | ||
| 112 | + if (this->ap.isCompleting()) | ||
| 113 | + { | ||
| 114 | + if (this->ap.isCompleting() && (this->ap.argsLeft() == 0)) | ||
| 115 | + { | ||
| 116 | + this->ap.insertCompletion("something"); | ||
| 117 | + this->ap.insertCompletion("anything"); | ||
| 118 | + } | ||
| 119 | + return; | ||
| 120 | + } | ||
| 121 | +} | ||
| 122 | + | ||
| 123 | +void | ||
| 124 | +ArgParser::getQuack(char* p) | ||
| 125 | +{ | ||
| 126 | + ++this->quacks; | ||
| 127 | + if (this->ap.isCompleting() && (this->ap.argsLeft() == 0)) | ||
| 128 | + { | ||
| 129 | + this->ap.insertCompletion( | ||
| 130 | + std::string("thing-") + QUtil::int_to_string(this->quacks)); | ||
| 131 | + return; | ||
| 132 | + } | ||
| 133 | + output(std::string("got quack: ") + p); | ||
| 134 | +} | ||
| 135 | + | ||
| 136 | +void | ||
| 137 | +ArgParser::endQuack() | ||
| 138 | +{ | ||
| 139 | + output("total quacks so far: " + QUtil::int_to_string(this->quacks)); | ||
| 140 | +} | ||
| 141 | + | ||
| 142 | +void | ||
| 143 | +ArgParser::finalChecks() | ||
| 144 | +{ | ||
| 145 | + output("total quacks: " + QUtil::int_to_string(this->quacks)); | ||
| 146 | +} | ||
| 147 | + | ||
| 148 | +void | ||
| 149 | +ArgParser::test_exceptions() | ||
| 150 | +{ | ||
| 151 | + try | ||
| 152 | + { | ||
| 153 | + ap.selectMainOptionTable(); | ||
| 154 | + ap.addBare("potato", [](){}); | ||
| 155 | + assert(false); | ||
| 156 | + } | ||
| 157 | + catch (std::exception& e) | ||
| 158 | + { | ||
| 159 | + std::cout << "duplicate handler: " << e.what() << std::endl; | ||
| 160 | + } | ||
| 161 | + try | ||
| 162 | + { | ||
| 163 | + ap.selectOptionTable("baaa"); | ||
| 164 | + ap.addBare("ram", [](){}); | ||
| 165 | + assert(false); | ||
| 166 | + } | ||
| 167 | + catch (std::exception& e) | ||
| 168 | + { | ||
| 169 | + std::cout << "duplicate handler: " << e.what() << std::endl; | ||
| 170 | + } | ||
| 171 | + try | ||
| 172 | + { | ||
| 173 | + ap.registerOptionTable("baaa", nullptr); | ||
| 174 | + assert(false); | ||
| 175 | + } | ||
| 176 | + catch (std::exception& e) | ||
| 177 | + { | ||
| 178 | + std::cout << "duplicate table: " << e.what() << std::endl; | ||
| 179 | + } | ||
| 180 | + try | ||
| 181 | + { | ||
| 182 | + ap.selectOptionTable("aardvark"); | ||
| 183 | + assert(false); | ||
| 184 | + } | ||
| 185 | + catch (std::exception& e) | ||
| 186 | + { | ||
| 187 | + std::cout << "unknown table: " << e.what() << std::endl; | ||
| 188 | + } | ||
| 189 | +} | ||
| 190 | + | ||
| 191 | +int main(int argc, char* argv[]) | ||
| 192 | +{ | ||
| 193 | + | ||
| 194 | + ArgParser ap(argc, argv); | ||
| 195 | + if ((argc == 2) && (strcmp(argv[1], "exceptions") == 0)) | ||
| 196 | + { | ||
| 197 | + ap.test_exceptions(); | ||
| 198 | + return 0; | ||
| 199 | + } | ||
| 200 | + try | ||
| 201 | + { | ||
| 202 | + ap.parseArgs(); | ||
| 203 | + } | ||
| 204 | + catch (QPDFArgParser::Usage& e) | ||
| 205 | + { | ||
| 206 | + std::cerr << "usage: " << e.what() << std::endl; | ||
| 207 | + exit(2); | ||
| 208 | + } | ||
| 209 | + catch (std::exception& e) | ||
| 210 | + { | ||
| 211 | + std::cerr << "exception: " << e.what() << std::endl; | ||
| 212 | + exit(3); | ||
| 213 | + } | ||
| 214 | + return 0; | ||
| 215 | +} |
libtests/build.mk
libtests/libtests.testcov
| @@ -39,3 +39,16 @@ JSON key missing in object 0 | @@ -39,3 +39,16 @@ JSON key missing in object 0 | ||
| 39 | JSON wanted array 0 | 39 | JSON wanted array 0 |
| 40 | JSON schema array error 0 | 40 | JSON schema array error 0 |
| 41 | JSON key extra in object 0 | 41 | JSON key extra in object 0 |
| 42 | +QPDFArgParser read args from stdin 0 | ||
| 43 | +QPDFArgParser read args from file 0 | ||
| 44 | +QPDFArgParser required choices 0 | ||
| 45 | +QPDFArgParser required parameter 0 | ||
| 46 | +QPDFArgParser select unregistered table 0 | ||
| 47 | +QPDFArgParser register registered table 0 | ||
| 48 | +QPDFArgParser duplicate handler 0 | ||
| 49 | +QPDFArgParser missing -- 0 | ||
| 50 | +QPDFArgParser single dash 0 | ||
| 51 | +QPDFArgParser help option 0 | ||
| 52 | +QPDFArgParser positional 0 | ||
| 53 | +QPDFArgParser unrecognized 0 | ||
| 54 | +QPDFArgParser complete choices 0 |
libtests/qtest/arg_parser.test
0 → 100644
| 1 | +#!/usr/bin/env perl | ||
| 2 | +require 5.008; | ||
| 3 | +use warnings; | ||
| 4 | +use strict; | ||
| 5 | + | ||
| 6 | +chdir("arg_parser") or die "chdir testdir failed: $!\n"; | ||
| 7 | +unshift(@INC, '.'); | ||
| 8 | +require completion_helpers; | ||
| 9 | + | ||
| 10 | +require TestDriver; | ||
| 11 | + | ||
| 12 | +my $td = new TestDriver('arg_parser'); | ||
| 13 | + | ||
| 14 | +my @completion_tests = ( | ||
| 15 | + ['', 0, 'bad-input-1'], | ||
| 16 | + ['', 1, 'bad-input-2'], | ||
| 17 | + ['', 2, 'bad-input-3'], | ||
| 18 | + ['arg_parser', 2, 'bad-input-4'], | ||
| 19 | + ['arg_parser ', undef, 'top'], | ||
| 20 | + ['arg_parser -', undef, 'top-arg'], | ||
| 21 | + ['arg_parser --po', undef, 'po'], | ||
| 22 | + ['arg_parser --potato ', undef, 'potato'], | ||
| 23 | + ['arg_parser --quack ', undef, 'quack'], | ||
| 24 | + ['arg_parser --quack -', undef, 'quack-'], | ||
| 25 | + ['arg_parser --quack x ', undef, 'quack-x'], | ||
| 26 | + ['arg_parser --quack x x ', undef, 'quack-x-x'], | ||
| 27 | + ['arg_parser --baaa -', undef, 'baaa'], | ||
| 28 | + ['arg_parser --baaa -- --', undef, 'second'], | ||
| 29 | + ['arg_parser @quack-xyz ', undef, 'quack-x-y-z'], | ||
| 30 | + ['arg_parser --quack \'user " password\' ', undef, 'quack-x'], | ||
| 31 | + ['arg_parser --quack \'user password\' ', undef, 'quack-x'], | ||
| 32 | + ['arg_parser --quack "user password" ', undef, 'quack-x'], | ||
| 33 | + ['arg_parser --quack "user pass\'word" ', undef, 'quack-x'], | ||
| 34 | + ['arg_parser --quack user\ password ', undef, 'quack-x'], | ||
| 35 | + ); | ||
| 36 | + | ||
| 37 | +foreach my $c (@completion_tests) | ||
| 38 | +{ | ||
| 39 | + my ($cmd, $point, $description) = @$c; | ||
| 40 | + my $out = "completion-$description.out"; | ||
| 41 | + my $zout = "completion-$description-zsh.out"; | ||
| 42 | + if (! -f $zout) | ||
| 43 | + { | ||
| 44 | + $zout = $out; | ||
| 45 | + } | ||
| 46 | + $td->runtest("bash completion: $description", | ||
| 47 | + {$td->COMMAND => | ||
| 48 | + [@{bash_completion("arg_parser", $cmd, $point)}], | ||
| 49 | + $td->FILTER => "perl filter-completion.pl $out"}, | ||
| 50 | + {$td->FILE => "$out", $td->EXIT_STATUS => 0}, | ||
| 51 | + $td->NORMALIZE_NEWLINES); | ||
| 52 | + $td->runtest("zsh completion: $description", | ||
| 53 | + {$td->COMMAND => | ||
| 54 | + [@{zsh_completion("arg_parser", $cmd, $point)}], | ||
| 55 | + $td->FILTER => "perl filter-completion.pl $zout"}, | ||
| 56 | + {$td->FILE => "$zout", $td->EXIT_STATUS => 0}, | ||
| 57 | + $td->NORMALIZE_NEWLINES); | ||
| 58 | +} | ||
| 59 | + | ||
| 60 | +my @arg_tests = ( | ||
| 61 | + ['--potato', 0], # 0 | ||
| 62 | + ['--oops', 2], # 1 | ||
| 63 | + ['--version', 0], # 2 | ||
| 64 | + ['--version --potato', 2], # 3 | ||
| 65 | + ['--potato --version', 2], # 4 | ||
| 66 | + ['--quack', 2], # 5 | ||
| 67 | + ['--quack --', 0], # 6 | ||
| 68 | + ['--quack 1 2 3 --', 0], # 7 | ||
| 69 | + ['--potato --quack 1 2 3 --' . # 8 | ||
| 70 | + ' --potato --quack a b c --' . | ||
| 71 | + ' --baaa --ram --', 0], | ||
| 72 | + ['--baaa --potato --', 2], # 9 | ||
| 73 | + ['--baaa --ewe', 2], # 10 | ||
| 74 | + ['--oink=baaa', 2], # 11 | ||
| 75 | + ['--oink=sow', 0], # 12 | ||
| 76 | + ['-oink=sow', 0], # 13 | ||
| 77 | + ['@quack-xyz', 2], # 14 | ||
| 78 | + ['@quack-xyz --', 0], # 15 | ||
| 79 | + ['--salad', 2], # 16 | ||
| 80 | + ['--salad=spinach', 0], # 17 | ||
| 81 | + ); | ||
| 82 | + | ||
| 83 | +for (my $i = 0; $i < scalar(@arg_tests); ++$i) | ||
| 84 | +{ | ||
| 85 | + my ($args, $status) = @{$arg_tests[$i]}; | ||
| 86 | + $td->runtest("arg_tests $i", | ||
| 87 | + {$td->COMMAND => "arg_parser $args"}, | ||
| 88 | + {$td->FILE => "args-$i.out", $td->EXIT_STATUS => $status}, | ||
| 89 | + $td->NORMALIZE_NEWLINES); | ||
| 90 | +} | ||
| 91 | + | ||
| 92 | +$td->runtest("exceptions", | ||
| 93 | + {$td->COMMAND => "arg_parser exceptions"}, | ||
| 94 | + {$td->FILE => "exceptions.out", $td->EXIT_STATUS => 0}, | ||
| 95 | + $td->NORMALIZE_NEWLINES); | ||
| 96 | + | ||
| 97 | +$td->runtest("args from stdin", | ||
| 98 | + {$td->COMMAND => 'echo --potato | arg_parser @-'}, | ||
| 99 | + {$td->FILE => "stdin.out", $td->EXIT_STATUS => 0}, | ||
| 100 | + $td->NORMALIZE_NEWLINES); | ||
| 101 | + | ||
| 102 | +$td->report(2 + (2 * scalar(@completion_tests)) + scalar(@arg_tests)); |
libtests/qtest/arg_parser/args-0.out
0 → 100644
libtests/qtest/arg_parser/args-1.out
0 → 100644
| 1 | +usage: unrecognized argument --oops |
libtests/qtest/arg_parser/args-10.out
0 → 100644
libtests/qtest/arg_parser/args-11.out
0 → 100644
| 1 | +usage: --oink must be given as --oink={boar,pig,sow} |
libtests/qtest/arg_parser/args-12.out
0 → 100644
libtests/qtest/arg_parser/args-13.out
0 → 100644
libtests/qtest/arg_parser/args-14.out
0 → 100644
libtests/qtest/arg_parser/args-15.out
0 → 100644
libtests/qtest/arg_parser/args-16.out
0 → 100644
| 1 | +usage: --salad must be given as --salad=tossed |
libtests/qtest/arg_parser/args-17.out
0 → 100644
libtests/qtest/arg_parser/args-2.out
0 → 100644
| 1 | +3.14159 |
libtests/qtest/arg_parser/args-3.out
0 → 100644
| 1 | +usage: unrecognized argument --version |
libtests/qtest/arg_parser/args-4.out
0 → 100644
libtests/qtest/arg_parser/args-5.out
0 → 100644
| 1 | +usage: missing -- at end of quack options |
libtests/qtest/arg_parser/args-6.out
0 → 100644
libtests/qtest/arg_parser/args-7.out
0 → 100644
libtests/qtest/arg_parser/args-8.out
0 → 100644
libtests/qtest/arg_parser/args-9.out
0 → 100644
| 1 | +usage: unrecognized argument --potato (baaa options must be terminated with --) |
libtests/qtest/arg_parser/completion-baaa.out
0 → 100644
libtests/qtest/arg_parser/completion-bad-input-1.out
0 → 100644
| 1 | +! |
libtests/qtest/arg_parser/completion-bad-input-2.out
0 → 100644
| 1 | +! |
libtests/qtest/arg_parser/completion-bad-input-3.out
0 → 100644
| 1 | +! |
libtests/qtest/arg_parser/completion-bad-input-4.out
0 → 100644
| 1 | +! |
libtests/qtest/arg_parser/completion-po.out
0 → 100644
| 1 | +--potato |
libtests/qtest/arg_parser/completion-potato.out
0 → 100644
libtests/qtest/arg_parser/completion-quack-.out
0 → 100644
| 1 | +!-- |
libtests/qtest/arg_parser/completion-quack-x-x.out
0 → 100644
libtests/qtest/arg_parser/completion-quack-x-y-z.out
0 → 100644
libtests/qtest/arg_parser/completion-quack-x.out
0 → 100644
libtests/qtest/arg_parser/completion-quack.out
0 → 100644
libtests/qtest/arg_parser/completion-second-zsh.out
0 → 100644
libtests/qtest/arg_parser/completion-second.out
0 → 100644
libtests/qtest/arg_parser/completion-top-arg-zsh.out
0 → 100644
libtests/qtest/arg_parser/completion-top-arg.out
0 → 100644
libtests/qtest/arg_parser/completion-top.out
0 → 100644
libtests/qtest/arg_parser/exceptions.out
0 → 100644
| 1 | +duplicate handler: QPDFArgParser: adding a duplicate handler for option potato in main option table | ||
| 2 | +duplicate handler: QPDFArgParser: adding a duplicate handler for option ram in baaa option table | ||
| 3 | +duplicate table: QPDFArgParser: registering already registered option table baaa | ||
| 4 | +unknown table: QPDFArgParser: selecting unregistered option table aardvark |
libtests/qtest/arg_parser/quack-xyz
0 → 100644
libtests/qtest/arg_parser/stdin.out
0 → 100644
qpdf/qtest/qpdf.test
| @@ -88,6 +88,11 @@ $td->runtest("UTF-16 encoding errors", | @@ -88,6 +88,11 @@ $td->runtest("UTF-16 encoding errors", | ||
| 88 | {$td->FILE => "unicode-errors.out", $td->EXIT_STATUS => 0}, | 88 | {$td->FILE => "unicode-errors.out", $td->EXIT_STATUS => 0}, |
| 89 | $td->NORMALIZE_NEWLINES); | 89 | $td->NORMALIZE_NEWLINES); |
| 90 | 90 | ||
| 91 | +# Tests to exercise QPDFArgParser belong in arg_parser.test in | ||
| 92 | +# libtests. These tests are supposed to be specific to the qpdf cli. | ||
| 93 | +# Since they were written prior to moving QPDFArgParser into the | ||
| 94 | +# library, there are several tests here that also exercise | ||
| 95 | +# QPDFArgParser logic. | ||
| 91 | my @completion_tests = ( | 96 | my @completion_tests = ( |
| 92 | ['', 0, 'bad-input-1'], | 97 | ['', 0, 'bad-input-1'], |
| 93 | ['', 1, 'bad-input-2'], | 98 | ['', 1, 'bad-input-2'], |