Commit 52817f0a45b9116e55432361b8ddd08d28a606c7

Authored by Jay Berkenbilt
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
1 BINS_libtests = \ 1 BINS_libtests = \
2 cxx11 \ 2 cxx11 \
3 aes \ 3 aes \
  4 + arg_parser \
4 ascii85 \ 5 ascii85 \
5 bits \ 6 bits \
6 buffer \ 7 buffer \
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
  1 +got potato
  2 +total quacks: 0
libtests/qtest/arg_parser/args-1.out 0 → 100644
  1 +usage: unrecognized argument --oops
libtests/qtest/arg_parser/args-10.out 0 → 100644
  1 +you
  2 +usage: missing -- at end of baaa options
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
  1 +got oink=sow
  2 +total quacks: 0
libtests/qtest/arg_parser/args-13.out 0 → 100644
  1 +got oink=sow
  2 +total quacks: 0
libtests/qtest/arg_parser/args-14.out 0 → 100644
  1 +got potato
  2 +got potato
  3 +got quack: x
  4 +total quacks so far: 1
  5 +got quack: y
  6 +got quack: z
  7 +usage: missing -- at end of quack options
libtests/qtest/arg_parser/args-15.out 0 → 100644
  1 +got potato
  2 +got potato
  3 +got quack: x
  4 +total quacks so far: 1
  5 +got quack: y
  6 +got quack: z
  7 +total quacks so far: 3
  8 +total quacks: 3
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
  1 +got salad=spinach
  2 +total quacks: 0
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
  1 +got potato
  2 +usage: unrecognized argument --version
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
  1 +total quacks so far: 0
  2 +total quacks: 0
libtests/qtest/arg_parser/args-7.out 0 → 100644
  1 +got quack: 1
  2 +got quack: 2
  3 +got quack: 3
  4 +total quacks so far: 3
  5 +total quacks: 3
libtests/qtest/arg_parser/args-8.out 0 → 100644
  1 +got potato
  2 +got quack: 1
  3 +got quack: 2
  4 +got quack: 3
  5 +total quacks so far: 3
  6 +got potato
  7 +got quack: a
  8 +got quack: b
  9 +got quack: c
  10 +total quacks so far: 6
  11 +ram
  12 +total quacks: 6
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
  1 +--ewe
  2 +--ram
  3 +!--potato
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
  1 +!got
  2 +!potato
libtests/qtest/arg_parser/completion-quack-.out 0 → 100644
  1 +!--
libtests/qtest/arg_parser/completion-quack-x-x.out 0 → 100644
  1 +thing-2
  2 +!anything
  3 +!something
  4 +!thing-1
libtests/qtest/arg_parser/completion-quack-x-y-z.out 0 → 100644
  1 +thing-3
  2 +!thing-2
libtests/qtest/arg_parser/completion-quack-x.out 0 → 100644
  1 +thing-1
  2 +!anything
  3 +!something
  4 +!thing-2
libtests/qtest/arg_parser/completion-quack.out 0 → 100644
  1 +anything
  2 +something
  3 +!thing-0
  4 +!thing-1
libtests/qtest/arg_parser/completion-second-zsh.out 0 → 100644
  1 +--baaa
  2 +--moo
  3 +--moo=
  4 +--oink=
  5 +--oink=pig
  6 +--potato
  7 +--salad=
  8 +!--completion-zsh
  9 +!--ewe
  10 +!--ram
  11 +!--version
libtests/qtest/arg_parser/completion-second.out 0 → 100644
  1 +--baaa
  2 +--moo
  3 +--moo=
  4 +--oink=
  5 +--potato
  6 +--salad=
  7 +!--completion-zsh
  8 +!--ewe
  9 +!--oink=pig
  10 +!--ram
  11 +!--version
libtests/qtest/arg_parser/completion-top-arg-zsh.out 0 → 100644
  1 +--baaa
  2 +--completion-zsh
  3 +--moo
  4 +--moo=
  5 +--oink=
  6 +--oink=pig
  7 +--potato
  8 +--salad=
  9 +--version
  10 +!--ewe
  11 +!--ram
libtests/qtest/arg_parser/completion-top-arg.out 0 → 100644
  1 +--baaa
  2 +--completion-zsh
  3 +--moo
  4 +--moo=
  5 +--oink=
  6 +--potato
  7 +--salad=
  8 +--version
  9 +!--ewe
  10 +!--oink=pig
  11 +!--ram
libtests/qtest/arg_parser/completion-top.out 0 → 100644
  1 +!--completion-zsh
  2 +!--potato
  3 +!--salad=tossed
  4 +!--version
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
  1 +--potato
  2 +--potato
  3 +--quack
  4 +x
  5 +--
  6 +--quack
  7 +y
  8 +z
libtests/qtest/arg_parser/stdin.out 0 → 100644
  1 +got potato
  2 +total quacks: 0
qpdf/qtest/qpdf.test
@@ -88,6 +88,11 @@ $td-&gt;runtest(&quot;UTF-16 encoding errors&quot;, @@ -88,6 +88,11 @@ $td-&gt;runtest(&quot;UTF-16 encoding errors&quot;,
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'],