diff --git a/cSpell.json b/cSpell.json index 2e162b20..09d00693 100644 --- a/cSpell.json +++ b/cSpell.json @@ -32,6 +32,7 @@ "autolabel", "automake", "autotools", + "baaa", "backports", "bashcompinit", "berkenbilt", @@ -319,6 +320,7 @@ "qpdf", "qpdfacroformdocumenthelper", "qpdfannotationobjecthelper", + "qpdfargparser", "qpdfconstants", "qpdfcrypto", "qpdfcryptoimpl", diff --git a/include/qpdf/QPDFArgParser.hh b/include/qpdf/QPDFArgParser.hh new file mode 100644 index 00000000..2c46c4e0 --- /dev/null +++ b/include/qpdf/QPDFArgParser.hh @@ -0,0 +1,221 @@ +// Copyright (c) 2005-2021 Jay Berkenbilt +// +// This file is part of qpdf. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Versions of qpdf prior to version 7 were released under the terms +// of version 2.0 of the Artistic License. At your option, you may +// continue to consider qpdf to be licensed under those terms. Please +// see the manual for additional information. + +#ifndef QPDFARGPARSER_HH +#define QPDFARGPARSER_HH + +#include +#include +#include +#include +#include +#include +#include +#include + +// This is not a general-purpose argument parser. It is tightly +// crafted to work with qpdf. qpdf's command-line syntax is very +// complex because of its long history, and it doesn't really follow +// any kind of normal standard for arguments, but it's important for +// backward compatibility not ensure we don't break what constitutes a +// valid command. This class handles the quirks of qpdf's argument +// parsing, bash/zsh completion, and support for @argfile to read +// arguments from a file. + +// Note about memory: there is code that expects argv to be a char*[], +// meaning that arguments are writable. Several operations, including +// reading arguments from a file or parsing a line for bash +// completion, involve fabricating an argv array. To ensure that the +// memory is valid and is cleaned up properly, we keep various vectors +// of smart character pointers that argv points into. In order for +// those pointers to remain valid, the QPDFArgParser instance must +// remain in scope for the life of any code that may reference +// anything from argv. +class QPDFArgParser +{ + public: + // Usage exception is thrown if there are any errors parsing + // arguments + class QPDF_DLL_CLASS Usage: public std::runtime_error + { + public: + QPDF_DLL + Usage(std::string const&); + }; + + // progname_env is used to override argv[0] when figuring out the + // name of the executable for setting up completion. This may be + // needed if the program is invoked by a wrapper. + QPDF_DLL + QPDFArgParser(int argc, char* argv[], char const* progname_env); + + // Calls exit(0) if a help option is given or if in completion + // mode. If there are argument parsing errors, + // QPDFArgParser::Usage is thrown. + QPDF_DLL + void parseArgs(); + + // Methods for registering arguments. QPDFArgParser starts off + // with the main option table selected. You can add handlers for + // arguments in the current option table, and you can select which + // option table is current. The help option table is special and + // contains arguments that are only valid as the first and only + // option. Named option tables are for subparsers and always start + // a series of options that end with `--`. + + typedef std::function bare_arg_handler_t; + typedef std::function param_arg_handler_t; + + QPDF_DLL + void selectMainOptionTable(); + QPDF_DLL + void selectHelpOptionTable(); + QPDF_DLL + void selectOptionTable(std::string const& name); + + // Register a new options table. This also selects the option table. + QPDF_DLL + void registerOptionTable( + std::string const& name, bare_arg_handler_t end_handler); + + // Add handlers for options in the current table + + QPDF_DLL + void addPositional(param_arg_handler_t); + QPDF_DLL + void addBare(std::string const& arg, bare_arg_handler_t); + QPDF_DLL + void addRequiredParameter( + std::string const& arg, + param_arg_handler_t, + char const* parameter_name); + QPDF_DLL + void addOptionalParameter(std::string const& arg, param_arg_handler_t); + QPDF_DLL + void addRequiredChoices( + std::string const& arg, param_arg_handler_t, char const** choices); + // The final check handler is called at the very end of argument + // parsing. + QPDF_DLL + void addFinalCheck(bare_arg_handler_t); + + // Convenience methods for adding member functions of a class as + // handlers. + template + static bare_arg_handler_t bindBare(void (T::*f)(), T* o) + { + return std::bind(std::mem_fn(f), o); + } + template + static param_arg_handler_t bindParam(void (T::*f)(char *), T* o) + { + return std::bind(std::mem_fn(f), o, std::placeholders::_1); + } + + // When processing arguments, indicate how many arguments remain + // after the one whose handler is being called. + QPDF_DLL + int argsLeft() const; + + // Indicate whether we are in completion mode. + QPDF_DLL + bool isCompleting() const; + + // Insert a completion during argument parsing; useful for + // customizing completion in the position argument handler. Should + // only be used in completion mode. + QPDF_DLL + void insertCompletion(std::string const&); + + private: + struct OptionEntry + { + OptionEntry() : + parameter_needed(false), + bare_arg_handler(0), + param_arg_handler(0) + { + } + bool parameter_needed; + std::string parameter_name; + std::set choices; + bare_arg_handler_t bare_arg_handler; + param_arg_handler_t param_arg_handler; + }; + friend struct OptionEntry; + + OptionEntry& registerArg(std::string const& arg); + + void completionCommon(bool zsh); + + void argCompletionBash(); + void argCompletionZsh(); + + void usage(std::string const& message); + void checkCompletion(); + void handleArgFileArguments(); + void handleBashArguments(); + void readArgsFromFile(char const* filename); + void doFinalChecks(); + void addOptionsToCompletions(); + void addChoicesToCompletions(std::string const&, std::string const&); + void handleCompletion(); + + typedef std::map option_table_t; + + class Members + { + friend class QPDFArgParser; + + public: + QPDF_DLL + ~Members() = default; + + private: + Members(int argc, char* argv[], char const* progname_env); + Members(Members const&) = delete; + + int argc; + char** argv; + char const* whoami; + std::string progname_env; + int cur_arg; + bool bash_completion; + bool zsh_completion; + std::string bash_prev; + std::string bash_cur; + std::string bash_line; + std::set completions; + std::map option_tables; + option_table_t main_option_table; + option_table_t help_option_table; + option_table_t* option_table; + std::string option_table_name; + bare_arg_handler_t final_check_handler; + std::vector> new_argv; + std::vector> bash_argv; + PointerHolder argv_ph; + PointerHolder bash_argv_ph; + }; + PointerHolder m; +}; + +#endif // QPDFARGPARSER_HH diff --git a/libqpdf/QPDFArgParser.cc b/libqpdf/QPDFArgParser.cc new file mode 100644 index 00000000..81b6557d --- /dev/null +++ b/libqpdf/QPDFArgParser.cc @@ -0,0 +1,799 @@ +#include +#include +#include +#include +#include +#include +#include + +QPDFArgParser::Usage::Usage(std::string const& msg) : + std::runtime_error(msg) +{ +} + +QPDFArgParser::Members::Members( + int argc, char* argv[], char const* progname_env) : + + argc(argc), + argv(argv), + whoami(QUtil::getWhoami(argv[0])), + progname_env(progname_env), + cur_arg(0), + bash_completion(false), + zsh_completion(false), + option_table(nullptr), + final_check_handler(nullptr) +{ +} + +QPDFArgParser::QPDFArgParser(int argc, char* argv[], char const* progname_env) : + m(new Members(argc, argv, progname_env)) +{ + selectHelpOptionTable(); + addBare("completion-bash", + std::bind(std::mem_fn(&QPDFArgParser::argCompletionBash), this)); + addBare("completion-zsh", + std::bind(std::mem_fn(&QPDFArgParser::argCompletionZsh), this)); + selectMainOptionTable(); +} + +void +QPDFArgParser::selectMainOptionTable() +{ + this->m->option_table = &this->m->main_option_table; + this->m->option_table_name = "main"; +} + +void +QPDFArgParser::selectHelpOptionTable() +{ + this->m->option_table = &this->m->help_option_table; + this->m->option_table_name = "help"; +} + +void +QPDFArgParser::selectOptionTable(std::string const& name) +{ + auto t = this->m->option_tables.find(name); + if (t == this->m->option_tables.end()) + { + QTC::TC("libtests", "QPDFArgParser select unregistered table"); + throw std::logic_error( + "QPDFArgParser: selecting unregistered option table " + name); + } + this->m->option_table = &(t->second); + this->m->option_table_name = name; +} + +void +QPDFArgParser::registerOptionTable( + std::string const& name, + bare_arg_handler_t end_handler) +{ + if (0 != this->m->option_tables.count(name)) + { + QTC::TC("libtests", "QPDFArgParser register registered table"); + throw std::logic_error( + "QPDFArgParser: registering already registered option table " + + name); + } + this->m->option_tables[name]; + selectOptionTable(name); + addBare("--", end_handler); +} + +QPDFArgParser::OptionEntry& +QPDFArgParser::registerArg(std::string const& arg) +{ + if (0 != this->m->option_table->count(arg)) + { + QTC::TC("libtests", "QPDFArgParser duplicate handler"); + throw std::logic_error( + "QPDFArgParser: adding a duplicate handler for option " + + arg + " in " + this->m->option_table_name + + " option table"); + } + return ((*this->m->option_table)[arg]); +} + +void +QPDFArgParser::addPositional(param_arg_handler_t handler) +{ + OptionEntry& oe = registerArg(""); + oe.param_arg_handler = handler; +} + +void +QPDFArgParser::addBare( + std::string const& arg, bare_arg_handler_t handler) +{ + OptionEntry& oe = registerArg(arg); + oe.parameter_needed = false; + oe.bare_arg_handler = handler; +} + +void +QPDFArgParser::addRequiredParameter( + std::string const& arg, + param_arg_handler_t handler, + char const* parameter_name) +{ + OptionEntry& oe = registerArg(arg); + oe.parameter_needed = true; + oe.parameter_name = parameter_name; + oe.param_arg_handler = handler; +} + +void +QPDFArgParser::addOptionalParameter( + std::string const& arg, param_arg_handler_t handler) +{ + OptionEntry& oe = registerArg(arg); + oe.parameter_needed = false; + oe.param_arg_handler = handler; +} + +void +QPDFArgParser::addRequiredChoices( + std::string const& arg, + param_arg_handler_t handler, + char const** choices) +{ + OptionEntry& oe = registerArg(arg); + oe.parameter_needed = true; + oe.param_arg_handler = handler; + for (char const** i = choices; *i; ++i) + { + oe.choices.insert(*i); + } +} + +void +QPDFArgParser::addFinalCheck(bare_arg_handler_t handler) +{ + this->m->final_check_handler = handler; +} + +bool +QPDFArgParser::isCompleting() const +{ + return this->m->bash_completion; +} + +int +QPDFArgParser::argsLeft() const +{ + return this->m->argc - this->m->cur_arg - 1; +} + +void +QPDFArgParser::insertCompletion(std::string const& arg) +{ + this->m->completions.insert(arg); +} + +void +QPDFArgParser::completionCommon(bool zsh) +{ + std::string progname = this->m->argv[0]; + std::string executable; + std::string appdir; + std::string appimage; + if (QUtil::get_env(this->m->progname_env.c_str(), &executable)) + { + progname = executable; + } + else if (QUtil::get_env("APPDIR", &appdir) && + QUtil::get_env("APPIMAGE", &appimage)) + { + // Detect if we're in an AppImage and adjust + if ((appdir.length() < strlen(this->m->argv[0])) && + (strncmp(appdir.c_str(), this->m->argv[0], appdir.length()) == 0)) + { + progname = appimage; + } + } + if (zsh) + { + std::cout << "autoload -U +X bashcompinit && bashcompinit && "; + } + std::cout << "complete -o bashdefault -o default"; + if (! zsh) + { + std::cout << " -o nospace"; + } + std::cout << " -C " << progname << " " << this->m->whoami << std::endl; + // Put output before error so calling from zsh works properly + std::string path = progname; + size_t slash = path.find('/'); + if ((slash != 0) && (slash != std::string::npos)) + { + std::cerr << "WARNING: " << this->m->whoami << " completion enabled" + << " using relative path to executable" << std::endl; + } +} + +void +QPDFArgParser::argCompletionBash() +{ + completionCommon(false); +} + +void +QPDFArgParser::argCompletionZsh() +{ + completionCommon(true); +} + +void +QPDFArgParser::handleArgFileArguments() +{ + // Support reading arguments from files. Create a new argv. Ensure + // that argv itself as well as all its contents are automatically + // deleted by using PointerHolder objects to back the pointers in + // argv. + this->m->new_argv.push_back( + PointerHolder(true, QUtil::copy_string(this->m->argv[0]))); + for (int i = 1; i < this->m->argc; ++i) + { + char* argfile = 0; + if ((strlen(this->m->argv[i]) > 1) && (this->m->argv[i][0] == '@')) + { + argfile = 1 + this->m->argv[i]; + if (strcmp(argfile, "-") != 0) + { + if (! QUtil::file_can_be_opened(argfile)) + { + // The file's not there; treating as regular option + argfile = nullptr; + } + } + } + if (argfile) + { + readArgsFromFile(1 + this->m->argv[i]); + } + else + { + this->m->new_argv.push_back( + PointerHolder( + true, QUtil::copy_string(this->m->argv[i]))); + } + } + this->m->argv_ph = + PointerHolder(true, new char*[1 + this->m->new_argv.size()]); + this->m->argv = this->m->argv_ph.getPointer(); + for (size_t i = 0; i < this->m->new_argv.size(); ++i) + { + this->m->argv[i] = this->m->new_argv.at(i).getPointer(); + } + this->m->argc = QIntC::to_int(this->m->new_argv.size()); + this->m->argv[this->m->argc] = 0; +} + +void +QPDFArgParser::handleBashArguments() +{ + // Do a minimal job of parsing bash_line into arguments. This + // doesn't do everything the shell does (e.g. $(...), variable + // expansion, arithmetic, globs, etc.), but it should be good + // enough for purposes of handling completion. As we build up the + // new argv, we can't use this->m->new_argv because this code has to + // interoperate with @file arguments, so memory for both ways of + // fabricating argv has to be protected. + + bool last_was_backslash = false; + enum { st_top, st_squote, st_dquote } state = st_top; + std::string arg; + for (std::string::iterator iter = this->m->bash_line.begin(); + iter != this->m->bash_line.end(); ++iter) + { + char ch = (*iter); + if (last_was_backslash) + { + arg.append(1, ch); + last_was_backslash = false; + } + else if (ch == '\\') + { + last_was_backslash = true; + } + else + { + bool append = false; + switch (state) + { + case st_top: + if (QUtil::is_space(ch)) + { + if (! arg.empty()) + { + this->m->bash_argv.push_back( + PointerHolder( + true, QUtil::copy_string(arg.c_str()))); + arg.clear(); + } + } + else if (ch == '"') + { + state = st_dquote; + } + else if (ch == '\'') + { + state = st_squote; + } + else + { + append = true; + } + break; + + case st_squote: + if (ch == '\'') + { + state = st_top; + } + else + { + append = true; + } + break; + + case st_dquote: + if (ch == '"') + { + state = st_top; + } + else + { + append = true; + } + break; + } + if (append) + { + arg.append(1, ch); + } + } + } + if (this->m->bash_argv.empty()) + { + // This can't happen if properly invoked by bash, but ensure + // we have a valid argv[0] regardless. + this->m->bash_argv.push_back( + PointerHolder( + true, QUtil::copy_string(this->m->argv[0]))); + } + // Explicitly discard any non-space-terminated word. The "current + // word" is handled specially. + this->m->bash_argv_ph = + PointerHolder(true, new char*[1 + this->m->bash_argv.size()]); + this->m->argv = this->m->bash_argv_ph.getPointer(); + for (size_t i = 0; i < this->m->bash_argv.size(); ++i) + { + this->m->argv[i] = this->m->bash_argv.at(i).getPointer(); + } + this->m->argc = QIntC::to_int(this->m->bash_argv.size()); + this->m->argv[this->m->argc] = 0; +} + +void +QPDFArgParser::usage(std::string const& message) +{ + if (this->m->bash_completion) + { + // This will cause bash to fall back to regular file completion. + exit(0); + } + throw Usage(message); +} + +void +QPDFArgParser::readArgsFromFile(char const* filename) +{ + std::list lines; + if (strcmp(filename, "-") == 0) + { + QTC::TC("libtests", "QPDFArgParser read args from stdin"); + lines = QUtil::read_lines_from_file(std::cin); + } + else + { + QTC::TC("libtests", "QPDFArgParser read args from file"); + lines = QUtil::read_lines_from_file(filename); + } + for (std::list::iterator iter = lines.begin(); + iter != lines.end(); ++iter) + { + this->m->new_argv.push_back( + PointerHolder(true, QUtil::copy_string((*iter).c_str()))); + } +} + +void +QPDFArgParser::checkCompletion() +{ + // See if we're being invoked from bash completion. + std::string bash_point_env; + // On Windows with mingw, there have been times when there appears + // to be no way to distinguish between an empty environment + // variable and an unset variable. There are also conditions under + // which bash doesn't set COMP_LINE. Therefore, enter this logic + // if either COMP_LINE or COMP_POINT are set. They will both be + // set together under ordinary circumstances. + bool got_line = QUtil::get_env("COMP_LINE", &this->m->bash_line); + bool got_point = QUtil::get_env("COMP_POINT", &bash_point_env); + if (got_line || got_point) + { + size_t p = QUtil::string_to_uint(bash_point_env.c_str()); + if (p < this->m->bash_line.length()) + { + // Truncate the line. We ignore everything at or after the + // cursor for completion purposes. + this->m->bash_line = this->m->bash_line.substr(0, p); + } + if (p > this->m->bash_line.length()) + { + p = this->m->bash_line.length(); + } + // Set bash_cur and bash_prev based on bash_line rather than + // relying on argv. This enables us to use bashcompinit to get + // completion in zsh too since bashcompinit sets COMP_LINE and + // COMP_POINT but doesn't invoke the command with options like + // bash does. + + // p is equal to length of the string. Walk backwards looking + // for the first separator. bash_cur is everything after the + // last separator, possibly empty. + char sep(0); + while (p > 0) + { + --p; + char ch = this->m->bash_line.at(p); + if ((ch == ' ') || (ch == '=') || (ch == ':')) + { + sep = ch; + break; + } + } + if (1+p <= this->m->bash_line.length()) + { + this->m->bash_cur = this->m->bash_line.substr( + 1+p, std::string::npos); + } + if ((sep == ':') || (sep == '=')) + { + // Bash sets prev to the non-space separator if any. + // Actually, if there are multiple separators in a row, + // they are all included in prev, but that detail is not + // important to us and not worth coding. + this->m->bash_prev = this->m->bash_line.substr(p, 1); + } + else + { + // Go back to the last separator and set prev based on + // that. + size_t p1 = p; + while (p1 > 0) + { + --p1; + char ch = this->m->bash_line.at(p1); + if ((ch == ' ') || (ch == ':') || (ch == '=')) + { + this->m->bash_prev = + this->m->bash_line.substr(p1 + 1, p - p1 - 1); + break; + } + } + } + if (this->m->bash_prev.empty()) + { + this->m->bash_prev = this->m->bash_line.substr(0, p); + } + if (this->m->argc == 1) + { + // This is probably zsh using bashcompinit. There are a + // few differences in the expected output. + this->m->zsh_completion = true; + } + handleBashArguments(); + this->m->bash_completion = true; + } +} + +void +QPDFArgParser::parseArgs() +{ + selectMainOptionTable(); + checkCompletion(); + handleArgFileArguments(); + for (this->m->cur_arg = 1; + this->m->cur_arg < this->m->argc; + ++this->m->cur_arg) + { + bool help_option = false; + bool end_option = false; + auto oep = this->m->option_table->end(); + char* arg = this->m->argv[this->m->cur_arg]; + char* parameter = nullptr; + std::string o_arg(arg); + std::string arg_s(arg); + if ((strcmp(arg, "--") == 0) && + (this->m->option_table != &this->m->main_option_table)) + { + // Special case for -- option, which is used to break out + // of subparsers. + oep = this->m->option_table->find("--"); + end_option = true; + if (oep == this->m->option_table->end()) + { + // This is registered automatically, so this can't happen. + throw std::logic_error("ArgParser: -- handler not registered"); + } + } + else if ((arg[0] == '-') && (strcmp(arg, "-") != 0)) + { + ++arg; + if (arg[0] == '-') + { + // Be lax about -arg vs --arg + ++arg; + } + else + { + QTC::TC("libtests", "QPDFArgParser single dash"); + } + if (strlen(arg) > 0) + { + // Prevent --=something from being treated as an empty + // arg since the empty string in the option table is + // for positional arguments. + parameter = const_cast(strchr(1 + arg, '=')); + } + if (parameter) + { + *parameter++ = 0; + } + + arg_s = arg; + + if ((! this->m->bash_completion) && + (this->m->argc == 2) && (this->m->cur_arg == 1) && + this->m->help_option_table.count(arg_s)) + { + // Handle help option, which is only valid as the sole + // option. + QTC::TC("libtests", "QPDFArgParser help option"); + oep = this->m->help_option_table.find(arg_s); + help_option = true; + } + + if (! (help_option || arg_s.empty() || (arg_s.at(0) == '-'))) + { + oep = this->m->option_table->find(arg_s); + } + } + else + { + // The empty string maps to the positional argument + // handler. + QTC::TC("libtests", "QPDFArgParser positional"); + oep = this->m->option_table->find(""); + parameter = arg; + } + + if (oep == this->m->option_table->end()) + { + QTC::TC("libtests", "QPDFArgParser unrecognized"); + std::string message = "unrecognized argument " + o_arg; + if (this->m->option_table != &this->m->main_option_table) + { + message += " (" + this->m->option_table_name + + " options must be terminated with --)"; + } + usage(message); + } + + OptionEntry& oe = oep->second; + if ((oe.parameter_needed && (0 == parameter)) || + ((! oe.choices.empty() && + ((0 == parameter) || + (0 == oe.choices.count(parameter)))))) + { + std::string message = + "--" + arg_s + " must be given as --" + arg_s + "="; + if (! oe.choices.empty()) + { + QTC::TC("libtests", "QPDFArgParser required choices"); + message += "{"; + for (std::set::iterator iter = + oe.choices.begin(); + iter != oe.choices.end(); ++iter) + { + if (iter != oe.choices.begin()) + { + message += ","; + } + message += *iter; + } + message += "}"; + } + else if (! oe.parameter_name.empty()) + { + QTC::TC("libtests", "QPDFArgParser required parameter"); + message += oe.parameter_name; + } + else + { + // should not be possible + message += "option"; + } + usage(message); + } + if (oe.bare_arg_handler) + { + oe.bare_arg_handler(); + } + else if (oe.param_arg_handler) + { + oe.param_arg_handler(parameter); + } + if (help_option) + { + exit(0); + } + if (end_option) + { + selectMainOptionTable(); + } + } + if (this->m->bash_completion) + { + handleCompletion(); + } + else + { + doFinalChecks(); + } +} + +void +QPDFArgParser::doFinalChecks() +{ + if (this->m->option_table != &(this->m->main_option_table)) + { + QTC::TC("libtests", "QPDFArgParser missing --"); + usage("missing -- at end of " + this->m->option_table_name + + " options"); + } + if (this->m->final_check_handler != nullptr) + { + this->m->final_check_handler(); + } +} + +void +QPDFArgParser::addChoicesToCompletions(std::string const& option, + std::string const& extra_prefix) +{ + if (this->m->option_table->count(option) != 0) + { + OptionEntry& oe = (*this->m->option_table)[option]; + for (std::set::iterator iter = oe.choices.begin(); + iter != oe.choices.end(); ++iter) + { + QTC::TC("libtests", "QPDFArgParser complete choices"); + this->m->completions.insert(extra_prefix + *iter); + } + } +} + +void +QPDFArgParser::addOptionsToCompletions() +{ + for (std::map::iterator iter = + this->m->option_table->begin(); + iter != this->m->option_table->end(); ++iter) + { + std::string const& arg = (*iter).first; + if (arg == "--") + { + continue; + } + OptionEntry& oe = (*iter).second; + std::string base = "--" + arg; + if (oe.param_arg_handler) + { + if (this->m->zsh_completion) + { + // zsh doesn't treat = as a word separator, so add all + // the options so we don't get a space after the =. + addChoicesToCompletions(arg, base + "="); + } + this->m->completions.insert(base + "="); + } + if (! oe.parameter_needed) + { + this->m->completions.insert(base); + } + } +} + +void +QPDFArgParser::handleCompletion() +{ + std::string extra_prefix; + if (this->m->completions.empty()) + { + // Detect --option=... Bash treats the = as a word separator. + std::string choice_option; + if (this->m->bash_cur.empty() && (this->m->bash_prev.length() > 2) && + (this->m->bash_prev.at(0) == '-') && + (this->m->bash_prev.at(1) == '-') && + (this->m->bash_line.at(this->m->bash_line.length() - 1) == '=')) + { + choice_option = this->m->bash_prev.substr(2, std::string::npos); + } + else if ((this->m->bash_prev == "=") && + (this->m->bash_line.length() > + (this->m->bash_cur.length() + 1))) + { + // We're sitting at --option=x. Find previous option. + size_t end_mark = this->m->bash_line.length() - + this->m->bash_cur.length() - 1; + char before_cur = this->m->bash_line.at(end_mark); + if (before_cur == '=') + { + size_t space = this->m->bash_line.find_last_of(' ', end_mark); + if (space != std::string::npos) + { + std::string candidate = + this->m->bash_line.substr( + space + 1, end_mark - space - 1); + if ((candidate.length() > 2) && + (candidate.at(0) == '-') && + (candidate.at(1) == '-')) + { + choice_option = + candidate.substr(2, std::string::npos); + } + } + } + } + if (! choice_option.empty()) + { + if (this->m->zsh_completion) + { + // zsh wants --option=choice rather than just choice + extra_prefix = "--" + choice_option + "="; + } + addChoicesToCompletions(choice_option, extra_prefix); + } + else if ((! this->m->bash_cur.empty()) && + (this->m->bash_cur.at(0) == '-')) + { + addOptionsToCompletions(); + if (this->m->argc == 1) + { + // Help options are valid only by themselves. + for (std::map::iterator iter = + this->m->help_option_table.begin(); + iter != this->m->help_option_table.end(); ++iter) + { + this->m->completions.insert("--" + (*iter).first); + } + } + } + } + std::string prefix = extra_prefix + this->m->bash_cur; + for (std::set::iterator iter = this->m->completions.begin(); + iter != this->m->completions.end(); ++iter) + { + if (prefix.empty() || + ((*iter).substr(0, prefix.length()) == prefix)) + { + std::cout << *iter << std::endl; + } + } + exit(0); +} diff --git a/libqpdf/build.mk b/libqpdf/build.mk index 9f935566..c18bb7df 100644 --- a/libqpdf/build.mk +++ b/libqpdf/build.mk @@ -57,6 +57,7 @@ SRCS_libqpdf = \ libqpdf/QPDF.cc \ libqpdf/QPDFAcroFormDocumentHelper.cc \ libqpdf/QPDFAnnotationObjectHelper.cc \ + libqpdf/QPDFArgParser.cc \ libqpdf/QPDFCryptoProvider.cc \ libqpdf/QPDFEFStreamObjectHelper.cc \ libqpdf/QPDFEmbeddedFileDocumentHelper.cc \ diff --git a/libtests/arg_parser.cc b/libtests/arg_parser.cc new file mode 100644 index 00000000..a5a0bf35 --- /dev/null +++ b/libtests/arg_parser.cc @@ -0,0 +1,215 @@ +#include +#include +#include +#include +#include + +class ArgParser +{ + public: + ArgParser(int argc, char* argv[]); + void parseArgs(); + + void test_exceptions(); + + private: + void handlePotato(); + void handleSalad(char* p); + void handleMoo(char* p); + void handleOink(char* p); + void handleQuack(char* p); + void startQuack(); + void getQuack(char* p); + void endQuack(); + void finalChecks(); + + void initOptions(); + void output(std::string const&); + + QPDFArgParser ap; + int quacks; +}; + +ArgParser::ArgParser(int argc, char* argv[]) : + ap(QPDFArgParser(argc, argv, "TEST_ARG_PARSER")), + quacks(0) +{ + initOptions(); +} + +void +ArgParser::initOptions() +{ + auto b = [this](void (ArgParser::*f)()) { + return QPDFArgParser::bindBare(f, this); + }; + auto p = [this](void (ArgParser::*f)(char *)) { + return QPDFArgParser::bindParam(f, this); + }; + + ap.addBare("potato", b(&ArgParser::handlePotato)); + ap.addRequiredParameter("salad", p(&ArgParser::handleSalad), "tossed"); + ap.addOptionalParameter("moo", p(&ArgParser::handleMoo)); + char const* choices[] = {"pig", "boar", "sow", 0}; + ap.addRequiredChoices("oink", p(&ArgParser::handleOink), choices); + ap.selectHelpOptionTable(); + ap.addBare("version", [this](){ output("3.14159"); }); + ap.selectMainOptionTable(); + ap.addBare("quack", b(&ArgParser::startQuack)); + ap.registerOptionTable("quack", b(&ArgParser::endQuack)); + ap.addPositional(p(&ArgParser::getQuack)); + ap.addFinalCheck(b(&ArgParser::finalChecks)); + ap.selectMainOptionTable(); + ap.addBare("baaa", [this](){ this->ap.selectOptionTable("baaa"); }); + ap.registerOptionTable("baaa", nullptr); + ap.addBare("ewe", [this](){ output("you"); }); + ap.addBare("ram", [this](){ output("ram"); }); +} + +void +ArgParser::output(std::string const& msg) +{ + if (! this->ap.isCompleting()) + { + std::cout << msg << std::endl; + } +} + +void +ArgParser::handlePotato() +{ + output("got potato"); +} + +void +ArgParser::handleSalad(char* p) +{ + output(std::string("got salad=") + p); +} + +void +ArgParser::handleMoo(char* p) +{ + output(std::string("got moo=") + (p ? p : "(none)")); +} + +void +ArgParser::handleOink(char* p) +{ + output(std::string("got oink=") + p); +} + +void +ArgParser::parseArgs() +{ + this->ap.parseArgs(); +} + +void +ArgParser::startQuack() +{ + this->ap.selectOptionTable("quack"); + if (this->ap.isCompleting()) + { + if (this->ap.isCompleting() && (this->ap.argsLeft() == 0)) + { + this->ap.insertCompletion("something"); + this->ap.insertCompletion("anything"); + } + return; + } +} + +void +ArgParser::getQuack(char* p) +{ + ++this->quacks; + if (this->ap.isCompleting() && (this->ap.argsLeft() == 0)) + { + this->ap.insertCompletion( + std::string("thing-") + QUtil::int_to_string(this->quacks)); + return; + } + output(std::string("got quack: ") + p); +} + +void +ArgParser::endQuack() +{ + output("total quacks so far: " + QUtil::int_to_string(this->quacks)); +} + +void +ArgParser::finalChecks() +{ + output("total quacks: " + QUtil::int_to_string(this->quacks)); +} + +void +ArgParser::test_exceptions() +{ + try + { + ap.selectMainOptionTable(); + ap.addBare("potato", [](){}); + assert(false); + } + catch (std::exception& e) + { + std::cout << "duplicate handler: " << e.what() << std::endl; + } + try + { + ap.selectOptionTable("baaa"); + ap.addBare("ram", [](){}); + assert(false); + } + catch (std::exception& e) + { + std::cout << "duplicate handler: " << e.what() << std::endl; + } + try + { + ap.registerOptionTable("baaa", nullptr); + assert(false); + } + catch (std::exception& e) + { + std::cout << "duplicate table: " << e.what() << std::endl; + } + try + { + ap.selectOptionTable("aardvark"); + assert(false); + } + catch (std::exception& e) + { + std::cout << "unknown table: " << e.what() << std::endl; + } +} + +int main(int argc, char* argv[]) +{ + + ArgParser ap(argc, argv); + if ((argc == 2) && (strcmp(argv[1], "exceptions") == 0)) + { + ap.test_exceptions(); + return 0; + } + try + { + ap.parseArgs(); + } + catch (QPDFArgParser::Usage& e) + { + std::cerr << "usage: " << e.what() << std::endl; + exit(2); + } + catch (std::exception& e) + { + std::cerr << "exception: " << e.what() << std::endl; + exit(3); + } + return 0; +} diff --git a/libtests/build.mk b/libtests/build.mk index 6f88de32..1ae95196 100644 --- a/libtests/build.mk +++ b/libtests/build.mk @@ -1,6 +1,7 @@ BINS_libtests = \ cxx11 \ aes \ + arg_parser \ ascii85 \ bits \ buffer \ diff --git a/libtests/libtests.testcov b/libtests/libtests.testcov index 775141d7..884d433f 100644 --- a/libtests/libtests.testcov +++ b/libtests/libtests.testcov @@ -39,3 +39,16 @@ JSON key missing in object 0 JSON wanted array 0 JSON schema array error 0 JSON key extra in object 0 +QPDFArgParser read args from stdin 0 +QPDFArgParser read args from file 0 +QPDFArgParser required choices 0 +QPDFArgParser required parameter 0 +QPDFArgParser select unregistered table 0 +QPDFArgParser register registered table 0 +QPDFArgParser duplicate handler 0 +QPDFArgParser missing -- 0 +QPDFArgParser single dash 0 +QPDFArgParser help option 0 +QPDFArgParser positional 0 +QPDFArgParser unrecognized 0 +QPDFArgParser complete choices 0 diff --git a/libtests/qtest/arg_parser.test b/libtests/qtest/arg_parser.test new file mode 100644 index 00000000..42a80531 --- /dev/null +++ b/libtests/qtest/arg_parser.test @@ -0,0 +1,102 @@ +#!/usr/bin/env perl +require 5.008; +use warnings; +use strict; + +chdir("arg_parser") or die "chdir testdir failed: $!\n"; +unshift(@INC, '.'); +require completion_helpers; + +require TestDriver; + +my $td = new TestDriver('arg_parser'); + +my @completion_tests = ( + ['', 0, 'bad-input-1'], + ['', 1, 'bad-input-2'], + ['', 2, 'bad-input-3'], + ['arg_parser', 2, 'bad-input-4'], + ['arg_parser ', undef, 'top'], + ['arg_parser -', undef, 'top-arg'], + ['arg_parser --po', undef, 'po'], + ['arg_parser --potato ', undef, 'potato'], + ['arg_parser --quack ', undef, 'quack'], + ['arg_parser --quack -', undef, 'quack-'], + ['arg_parser --quack x ', undef, 'quack-x'], + ['arg_parser --quack x x ', undef, 'quack-x-x'], + ['arg_parser --baaa -', undef, 'baaa'], + ['arg_parser --baaa -- --', undef, 'second'], + ['arg_parser @quack-xyz ', undef, 'quack-x-y-z'], + ['arg_parser --quack \'user " password\' ', undef, 'quack-x'], + ['arg_parser --quack \'user password\' ', undef, 'quack-x'], + ['arg_parser --quack "user password" ', undef, 'quack-x'], + ['arg_parser --quack "user pass\'word" ', undef, 'quack-x'], + ['arg_parser --quack user\ password ', undef, 'quack-x'], + ); + +foreach my $c (@completion_tests) +{ + my ($cmd, $point, $description) = @$c; + my $out = "completion-$description.out"; + my $zout = "completion-$description-zsh.out"; + if (! -f $zout) + { + $zout = $out; + } + $td->runtest("bash completion: $description", + {$td->COMMAND => + [@{bash_completion("arg_parser", $cmd, $point)}], + $td->FILTER => "perl filter-completion.pl $out"}, + {$td->FILE => "$out", $td->EXIT_STATUS => 0}, + $td->NORMALIZE_NEWLINES); + $td->runtest("zsh completion: $description", + {$td->COMMAND => + [@{zsh_completion("arg_parser", $cmd, $point)}], + $td->FILTER => "perl filter-completion.pl $zout"}, + {$td->FILE => "$zout", $td->EXIT_STATUS => 0}, + $td->NORMALIZE_NEWLINES); +} + +my @arg_tests = ( + ['--potato', 0], # 0 + ['--oops', 2], # 1 + ['--version', 0], # 2 + ['--version --potato', 2], # 3 + ['--potato --version', 2], # 4 + ['--quack', 2], # 5 + ['--quack --', 0], # 6 + ['--quack 1 2 3 --', 0], # 7 + ['--potato --quack 1 2 3 --' . # 8 + ' --potato --quack a b c --' . + ' --baaa --ram --', 0], + ['--baaa --potato --', 2], # 9 + ['--baaa --ewe', 2], # 10 + ['--oink=baaa', 2], # 11 + ['--oink=sow', 0], # 12 + ['-oink=sow', 0], # 13 + ['@quack-xyz', 2], # 14 + ['@quack-xyz --', 0], # 15 + ['--salad', 2], # 16 + ['--salad=spinach', 0], # 17 + ); + +for (my $i = 0; $i < scalar(@arg_tests); ++$i) +{ + my ($args, $status) = @{$arg_tests[$i]}; + $td->runtest("arg_tests $i", + {$td->COMMAND => "arg_parser $args"}, + {$td->FILE => "args-$i.out", $td->EXIT_STATUS => $status}, + $td->NORMALIZE_NEWLINES); +} + +$td->runtest("exceptions", + {$td->COMMAND => "arg_parser exceptions"}, + {$td->FILE => "exceptions.out", $td->EXIT_STATUS => 0}, + $td->NORMALIZE_NEWLINES); + +$td->runtest("args from stdin", + {$td->COMMAND => 'echo --potato | arg_parser @-'}, + {$td->FILE => "stdin.out", $td->EXIT_STATUS => 0}, + $td->NORMALIZE_NEWLINES); + +$td->report(2 + (2 * scalar(@completion_tests)) + scalar(@arg_tests)); diff --git a/libtests/qtest/arg_parser/args-0.out b/libtests/qtest/arg_parser/args-0.out new file mode 100644 index 00000000..0b34908b --- /dev/null +++ b/libtests/qtest/arg_parser/args-0.out @@ -0,0 +1,2 @@ +got potato +total quacks: 0 diff --git a/libtests/qtest/arg_parser/args-1.out b/libtests/qtest/arg_parser/args-1.out new file mode 100644 index 00000000..38168d32 --- /dev/null +++ b/libtests/qtest/arg_parser/args-1.out @@ -0,0 +1 @@ +usage: unrecognized argument --oops diff --git a/libtests/qtest/arg_parser/args-10.out b/libtests/qtest/arg_parser/args-10.out new file mode 100644 index 00000000..875d684e --- /dev/null +++ b/libtests/qtest/arg_parser/args-10.out @@ -0,0 +1,2 @@ +you +usage: missing -- at end of baaa options diff --git a/libtests/qtest/arg_parser/args-11.out b/libtests/qtest/arg_parser/args-11.out new file mode 100644 index 00000000..5280c8e9 --- /dev/null +++ b/libtests/qtest/arg_parser/args-11.out @@ -0,0 +1 @@ +usage: --oink must be given as --oink={boar,pig,sow} diff --git a/libtests/qtest/arg_parser/args-12.out b/libtests/qtest/arg_parser/args-12.out new file mode 100644 index 00000000..cc710b1a --- /dev/null +++ b/libtests/qtest/arg_parser/args-12.out @@ -0,0 +1,2 @@ +got oink=sow +total quacks: 0 diff --git a/libtests/qtest/arg_parser/args-13.out b/libtests/qtest/arg_parser/args-13.out new file mode 100644 index 00000000..cc710b1a --- /dev/null +++ b/libtests/qtest/arg_parser/args-13.out @@ -0,0 +1,2 @@ +got oink=sow +total quacks: 0 diff --git a/libtests/qtest/arg_parser/args-14.out b/libtests/qtest/arg_parser/args-14.out new file mode 100644 index 00000000..69d153f0 --- /dev/null +++ b/libtests/qtest/arg_parser/args-14.out @@ -0,0 +1,7 @@ +got potato +got potato +got quack: x +total quacks so far: 1 +got quack: y +got quack: z +usage: missing -- at end of quack options diff --git a/libtests/qtest/arg_parser/args-15.out b/libtests/qtest/arg_parser/args-15.out new file mode 100644 index 00000000..3f2cf9fe --- /dev/null +++ b/libtests/qtest/arg_parser/args-15.out @@ -0,0 +1,8 @@ +got potato +got potato +got quack: x +total quacks so far: 1 +got quack: y +got quack: z +total quacks so far: 3 +total quacks: 3 diff --git a/libtests/qtest/arg_parser/args-16.out b/libtests/qtest/arg_parser/args-16.out new file mode 100644 index 00000000..fe34c57c --- /dev/null +++ b/libtests/qtest/arg_parser/args-16.out @@ -0,0 +1 @@ +usage: --salad must be given as --salad=tossed diff --git a/libtests/qtest/arg_parser/args-17.out b/libtests/qtest/arg_parser/args-17.out new file mode 100644 index 00000000..f42f66d2 --- /dev/null +++ b/libtests/qtest/arg_parser/args-17.out @@ -0,0 +1,2 @@ +got salad=spinach +total quacks: 0 diff --git a/libtests/qtest/arg_parser/args-2.out b/libtests/qtest/arg_parser/args-2.out new file mode 100644 index 00000000..41bec393 --- /dev/null +++ b/libtests/qtest/arg_parser/args-2.out @@ -0,0 +1 @@ +3.14159 diff --git a/libtests/qtest/arg_parser/args-3.out b/libtests/qtest/arg_parser/args-3.out new file mode 100644 index 00000000..7c394636 --- /dev/null +++ b/libtests/qtest/arg_parser/args-3.out @@ -0,0 +1 @@ +usage: unrecognized argument --version diff --git a/libtests/qtest/arg_parser/args-4.out b/libtests/qtest/arg_parser/args-4.out new file mode 100644 index 00000000..456b9935 --- /dev/null +++ b/libtests/qtest/arg_parser/args-4.out @@ -0,0 +1,2 @@ +got potato +usage: unrecognized argument --version diff --git a/libtests/qtest/arg_parser/args-5.out b/libtests/qtest/arg_parser/args-5.out new file mode 100644 index 00000000..dec60c4d --- /dev/null +++ b/libtests/qtest/arg_parser/args-5.out @@ -0,0 +1 @@ +usage: missing -- at end of quack options diff --git a/libtests/qtest/arg_parser/args-6.out b/libtests/qtest/arg_parser/args-6.out new file mode 100644 index 00000000..abde45a0 --- /dev/null +++ b/libtests/qtest/arg_parser/args-6.out @@ -0,0 +1,2 @@ +total quacks so far: 0 +total quacks: 0 diff --git a/libtests/qtest/arg_parser/args-7.out b/libtests/qtest/arg_parser/args-7.out new file mode 100644 index 00000000..761fef2d --- /dev/null +++ b/libtests/qtest/arg_parser/args-7.out @@ -0,0 +1,5 @@ +got quack: 1 +got quack: 2 +got quack: 3 +total quacks so far: 3 +total quacks: 3 diff --git a/libtests/qtest/arg_parser/args-8.out b/libtests/qtest/arg_parser/args-8.out new file mode 100644 index 00000000..515eb968 --- /dev/null +++ b/libtests/qtest/arg_parser/args-8.out @@ -0,0 +1,12 @@ +got potato +got quack: 1 +got quack: 2 +got quack: 3 +total quacks so far: 3 +got potato +got quack: a +got quack: b +got quack: c +total quacks so far: 6 +ram +total quacks: 6 diff --git a/libtests/qtest/arg_parser/args-9.out b/libtests/qtest/arg_parser/args-9.out new file mode 100644 index 00000000..85f991e7 --- /dev/null +++ b/libtests/qtest/arg_parser/args-9.out @@ -0,0 +1 @@ +usage: unrecognized argument --potato (baaa options must be terminated with --) diff --git a/libtests/qtest/arg_parser/completion-baaa.out b/libtests/qtest/arg_parser/completion-baaa.out new file mode 100644 index 00000000..ad92c848 --- /dev/null +++ b/libtests/qtest/arg_parser/completion-baaa.out @@ -0,0 +1,3 @@ +--ewe +--ram +!--potato diff --git a/libtests/qtest/arg_parser/completion-bad-input-1.out b/libtests/qtest/arg_parser/completion-bad-input-1.out new file mode 100644 index 00000000..cdf4cb4f --- /dev/null +++ b/libtests/qtest/arg_parser/completion-bad-input-1.out @@ -0,0 +1 @@ +! diff --git a/libtests/qtest/arg_parser/completion-bad-input-2.out b/libtests/qtest/arg_parser/completion-bad-input-2.out new file mode 100644 index 00000000..cdf4cb4f --- /dev/null +++ b/libtests/qtest/arg_parser/completion-bad-input-2.out @@ -0,0 +1 @@ +! diff --git a/libtests/qtest/arg_parser/completion-bad-input-3.out b/libtests/qtest/arg_parser/completion-bad-input-3.out new file mode 100644 index 00000000..cdf4cb4f --- /dev/null +++ b/libtests/qtest/arg_parser/completion-bad-input-3.out @@ -0,0 +1 @@ +! diff --git a/libtests/qtest/arg_parser/completion-bad-input-4.out b/libtests/qtest/arg_parser/completion-bad-input-4.out new file mode 100644 index 00000000..cdf4cb4f --- /dev/null +++ b/libtests/qtest/arg_parser/completion-bad-input-4.out @@ -0,0 +1 @@ +! diff --git a/libtests/qtest/arg_parser/completion-po.out b/libtests/qtest/arg_parser/completion-po.out new file mode 100644 index 00000000..2d80e857 --- /dev/null +++ b/libtests/qtest/arg_parser/completion-po.out @@ -0,0 +1 @@ +--potato diff --git a/libtests/qtest/arg_parser/completion-potato.out b/libtests/qtest/arg_parser/completion-potato.out new file mode 100644 index 00000000..6f84ce0b --- /dev/null +++ b/libtests/qtest/arg_parser/completion-potato.out @@ -0,0 +1,2 @@ +!got +!potato diff --git a/libtests/qtest/arg_parser/completion-quack-.out b/libtests/qtest/arg_parser/completion-quack-.out new file mode 100644 index 00000000..cb2a1ff3 --- /dev/null +++ b/libtests/qtest/arg_parser/completion-quack-.out @@ -0,0 +1 @@ +!-- diff --git a/libtests/qtest/arg_parser/completion-quack-x-x.out b/libtests/qtest/arg_parser/completion-quack-x-x.out new file mode 100644 index 00000000..8884537a --- /dev/null +++ b/libtests/qtest/arg_parser/completion-quack-x-x.out @@ -0,0 +1,4 @@ +thing-2 +!anything +!something +!thing-1 diff --git a/libtests/qtest/arg_parser/completion-quack-x-y-z.out b/libtests/qtest/arg_parser/completion-quack-x-y-z.out new file mode 100644 index 00000000..1532f7d4 --- /dev/null +++ b/libtests/qtest/arg_parser/completion-quack-x-y-z.out @@ -0,0 +1,2 @@ +thing-3 +!thing-2 diff --git a/libtests/qtest/arg_parser/completion-quack-x.out b/libtests/qtest/arg_parser/completion-quack-x.out new file mode 100644 index 00000000..16cba7f7 --- /dev/null +++ b/libtests/qtest/arg_parser/completion-quack-x.out @@ -0,0 +1,4 @@ +thing-1 +!anything +!something +!thing-2 diff --git a/libtests/qtest/arg_parser/completion-quack.out b/libtests/qtest/arg_parser/completion-quack.out new file mode 100644 index 00000000..be1f8e71 --- /dev/null +++ b/libtests/qtest/arg_parser/completion-quack.out @@ -0,0 +1,4 @@ +anything +something +!thing-0 +!thing-1 diff --git a/libtests/qtest/arg_parser/completion-second-zsh.out b/libtests/qtest/arg_parser/completion-second-zsh.out new file mode 100644 index 00000000..cb7b774f --- /dev/null +++ b/libtests/qtest/arg_parser/completion-second-zsh.out @@ -0,0 +1,11 @@ +--baaa +--moo +--moo= +--oink= +--oink=pig +--potato +--salad= +!--completion-zsh +!--ewe +!--ram +!--version diff --git a/libtests/qtest/arg_parser/completion-second.out b/libtests/qtest/arg_parser/completion-second.out new file mode 100644 index 00000000..3c581154 --- /dev/null +++ b/libtests/qtest/arg_parser/completion-second.out @@ -0,0 +1,11 @@ +--baaa +--moo +--moo= +--oink= +--potato +--salad= +!--completion-zsh +!--ewe +!--oink=pig +!--ram +!--version diff --git a/libtests/qtest/arg_parser/completion-top-arg-zsh.out b/libtests/qtest/arg_parser/completion-top-arg-zsh.out new file mode 100644 index 00000000..11bcb3b6 --- /dev/null +++ b/libtests/qtest/arg_parser/completion-top-arg-zsh.out @@ -0,0 +1,11 @@ +--baaa +--completion-zsh +--moo +--moo= +--oink= +--oink=pig +--potato +--salad= +--version +!--ewe +!--ram diff --git a/libtests/qtest/arg_parser/completion-top-arg.out b/libtests/qtest/arg_parser/completion-top-arg.out new file mode 100644 index 00000000..4e69efbd --- /dev/null +++ b/libtests/qtest/arg_parser/completion-top-arg.out @@ -0,0 +1,11 @@ +--baaa +--completion-zsh +--moo +--moo= +--oink= +--potato +--salad= +--version +!--ewe +!--oink=pig +!--ram diff --git a/libtests/qtest/arg_parser/completion-top.out b/libtests/qtest/arg_parser/completion-top.out new file mode 100644 index 00000000..20258955 --- /dev/null +++ b/libtests/qtest/arg_parser/completion-top.out @@ -0,0 +1,4 @@ +!--completion-zsh +!--potato +!--salad=tossed +!--version diff --git a/libtests/qtest/arg_parser/exceptions.out b/libtests/qtest/arg_parser/exceptions.out new file mode 100644 index 00000000..c71159f8 --- /dev/null +++ b/libtests/qtest/arg_parser/exceptions.out @@ -0,0 +1,4 @@ +duplicate handler: QPDFArgParser: adding a duplicate handler for option potato in main option table +duplicate handler: QPDFArgParser: adding a duplicate handler for option ram in baaa option table +duplicate table: QPDFArgParser: registering already registered option table baaa +unknown table: QPDFArgParser: selecting unregistered option table aardvark diff --git a/libtests/qtest/arg_parser/quack-xyz b/libtests/qtest/arg_parser/quack-xyz new file mode 100644 index 00000000..ae5c3734 --- /dev/null +++ b/libtests/qtest/arg_parser/quack-xyz @@ -0,0 +1,8 @@ +--potato +--potato +--quack +x +-- +--quack +y +z diff --git a/libtests/qtest/arg_parser/stdin.out b/libtests/qtest/arg_parser/stdin.out new file mode 100644 index 00000000..0b34908b --- /dev/null +++ b/libtests/qtest/arg_parser/stdin.out @@ -0,0 +1,2 @@ +got potato +total quacks: 0 diff --git a/qpdf/qtest/qpdf.test b/qpdf/qtest/qpdf.test index 7dce31ed..9d7cf674 100644 --- a/qpdf/qtest/qpdf.test +++ b/qpdf/qtest/qpdf.test @@ -88,6 +88,11 @@ $td->runtest("UTF-16 encoding errors", {$td->FILE => "unicode-errors.out", $td->EXIT_STATUS => 0}, $td->NORMALIZE_NEWLINES); +# Tests to exercise QPDFArgParser belong in arg_parser.test in +# libtests. These tests are supposed to be specific to the qpdf cli. +# Since they were written prior to moving QPDFArgParser into the +# library, there are several tests here that also exercise +# QPDFArgParser logic. my @completion_tests = ( ['', 0, 'bad-input-1'], ['', 1, 'bad-input-2'],