aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJay Berkenbilt <ejb@ql.org>2021-12-30 21:50:31 +0100
committerJay Berkenbilt <ejb@ql.org>2022-01-30 19:11:02 +0100
commit52817f0a45b9116e55432361b8ddd08d28a606c7 (patch)
tree8204c9028da805e20aa523a522654df6934ef4b0
parent8a5ba5686c8cd6f3a9bd65e58ef001e1ae4f15b8 (diff)
downloadqpdf-52817f0a45b9116e55432361b8ddd08d28a606c7.tar.zst
Implement QPDFArgParser based on ArgParser from qpdf.cc
-rw-r--r--cSpell.json2
-rw-r--r--include/qpdf/QPDFArgParser.hh221
-rw-r--r--libqpdf/QPDFArgParser.cc799
-rw-r--r--libqpdf/build.mk1
-rw-r--r--libtests/arg_parser.cc215
-rw-r--r--libtests/build.mk1
-rw-r--r--libtests/libtests.testcov13
-rw-r--r--libtests/qtest/arg_parser.test102
-rw-r--r--libtests/qtest/arg_parser/args-0.out2
-rw-r--r--libtests/qtest/arg_parser/args-1.out1
-rw-r--r--libtests/qtest/arg_parser/args-10.out2
-rw-r--r--libtests/qtest/arg_parser/args-11.out1
-rw-r--r--libtests/qtest/arg_parser/args-12.out2
-rw-r--r--libtests/qtest/arg_parser/args-13.out2
-rw-r--r--libtests/qtest/arg_parser/args-14.out7
-rw-r--r--libtests/qtest/arg_parser/args-15.out8
-rw-r--r--libtests/qtest/arg_parser/args-16.out1
-rw-r--r--libtests/qtest/arg_parser/args-17.out2
-rw-r--r--libtests/qtest/arg_parser/args-2.out1
-rw-r--r--libtests/qtest/arg_parser/args-3.out1
-rw-r--r--libtests/qtest/arg_parser/args-4.out2
-rw-r--r--libtests/qtest/arg_parser/args-5.out1
-rw-r--r--libtests/qtest/arg_parser/args-6.out2
-rw-r--r--libtests/qtest/arg_parser/args-7.out5
-rw-r--r--libtests/qtest/arg_parser/args-8.out12
-rw-r--r--libtests/qtest/arg_parser/args-9.out1
-rw-r--r--libtests/qtest/arg_parser/completion-baaa.out3
-rw-r--r--libtests/qtest/arg_parser/completion-bad-input-1.out1
-rw-r--r--libtests/qtest/arg_parser/completion-bad-input-2.out1
-rw-r--r--libtests/qtest/arg_parser/completion-bad-input-3.out1
-rw-r--r--libtests/qtest/arg_parser/completion-bad-input-4.out1
-rw-r--r--libtests/qtest/arg_parser/completion-po.out1
-rw-r--r--libtests/qtest/arg_parser/completion-potato.out2
-rw-r--r--libtests/qtest/arg_parser/completion-quack-.out1
-rw-r--r--libtests/qtest/arg_parser/completion-quack-x-x.out4
-rw-r--r--libtests/qtest/arg_parser/completion-quack-x-y-z.out2
-rw-r--r--libtests/qtest/arg_parser/completion-quack-x.out4
-rw-r--r--libtests/qtest/arg_parser/completion-quack.out4
-rw-r--r--libtests/qtest/arg_parser/completion-second-zsh.out11
-rw-r--r--libtests/qtest/arg_parser/completion-second.out11
-rw-r--r--libtests/qtest/arg_parser/completion-top-arg-zsh.out11
-rw-r--r--libtests/qtest/arg_parser/completion-top-arg.out11
-rw-r--r--libtests/qtest/arg_parser/completion-top.out4
-rw-r--r--libtests/qtest/arg_parser/exceptions.out4
-rw-r--r--libtests/qtest/arg_parser/quack-xyz8
-rw-r--r--libtests/qtest/arg_parser/stdin.out2
-rw-r--r--qpdf/qtest/qpdf.test5
47 files changed, 1499 insertions, 0 deletions
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 <qpdf/DLL.h>
+#include <qpdf/PointerHolder.hh>
+#include <string>
+#include <set>
+#include <map>
+#include <vector>
+#include <functional>
+#include <stdexcept>
+
+// 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<void()> bare_arg_handler_t;
+ typedef std::function<void(char*)> 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 <class T>
+ static bare_arg_handler_t bindBare(void (T::*f)(), T* o)
+ {
+ return std::bind(std::mem_fn(f), o);
+ }
+ template <class T>
+ 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<std::string> 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<std::string, OptionEntry> 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<std::string> completions;
+ std::map<std::string, option_table_t> 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<PointerHolder<char>> new_argv;
+ std::vector<PointerHolder<char>> bash_argv;
+ PointerHolder<char*> argv_ph;
+ PointerHolder<char*> bash_argv_ph;
+ };
+ PointerHolder<Members> 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 <qpdf/QPDFArgParser.hh>
+#include <qpdf/QUtil.hh>
+#include <qpdf/QIntC.hh>
+#include <qpdf/QTC.hh>
+#include <iostream>
+#include <cstring>
+#include <cstdlib>
+
+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<char>(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<char>(
+ true, QUtil::copy_string(this->m->argv[i])));
+ }
+ }
+ this->m->argv_ph =
+ PointerHolder<char*>(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<char>(
+ 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<char>(
+ 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<char*>(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<std::string> 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<std::string>::iterator iter = lines.begin();
+ iter != lines.end(); ++iter)
+ {
+ this->m->new_argv.push_back(
+ PointerHolder<char>(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<char*>(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<std::string>::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<std::string>::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<std::string, OptionEntry>::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<std::string, OptionEntry>::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<std::string>::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 <qpdf/QPDFArgParser.hh>
+#include <qpdf/QUtil.hh>
+#include <iostream>
+#include <cstring>
+#include <cassert>
+
+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'],