mirror of
https://github.com/qpdf/qpdf.git
synced 2024-12-22 10:58:58 +00:00
786 lines
26 KiB
C++
786 lines
26 KiB
C++
#include <qpdf/QPDFArgParser.hh>
|
|
|
|
#include <qpdf/QIntC.hh>
|
|
#include <qpdf/QPDFLogger.hh>
|
|
#include <qpdf/QPDFUsage.hh>
|
|
#include <qpdf/QTC.hh>
|
|
#include <qpdf/QUtil.hh>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <iostream>
|
|
|
|
QPDFArgParser::Members::Members(int argc, char const* const argv[], char const* progname_env) :
|
|
|
|
argc(argc),
|
|
argv(argv),
|
|
progname_env(progname_env),
|
|
cur_arg(0),
|
|
bash_completion(false),
|
|
zsh_completion(false),
|
|
option_table(nullptr),
|
|
final_check_handler(nullptr)
|
|
{
|
|
auto tmp = QUtil::make_unique_cstr(argv[0]);
|
|
whoami = QUtil::getWhoami(tmp.get());
|
|
}
|
|
|
|
QPDFArgParser::QPDFArgParser(int argc, char const* const argv[], char const* progname_env) :
|
|
m(new Members(argc, argv, progname_env))
|
|
{
|
|
selectHelpOptionTable();
|
|
char const* help_choices[] = {"all", nullptr};
|
|
// More help choices are added dynamically.
|
|
addChoices("help", bindParam(&QPDFArgParser::argHelp, this), false, help_choices);
|
|
addInvalidChoiceHandler("help", bindParam(&QPDFArgParser::invalidHelpArg, this));
|
|
addBare("completion-bash", bindBare(&QPDFArgParser::argCompletionBash, this));
|
|
addBare("completion-zsh", bindBare(&QPDFArgParser::argCompletionZsh, this));
|
|
selectMainOptionTable();
|
|
}
|
|
|
|
void
|
|
QPDFArgParser::selectMainOptionTable()
|
|
{
|
|
m->option_table = &m->main_option_table;
|
|
m->option_table_name = "main";
|
|
}
|
|
|
|
void
|
|
QPDFArgParser::selectHelpOptionTable()
|
|
{
|
|
m->option_table = &m->help_option_table;
|
|
m->option_table_name = "help";
|
|
}
|
|
|
|
void
|
|
QPDFArgParser::selectOptionTable(std::string const& name)
|
|
{
|
|
auto t = m->option_tables.find(name);
|
|
if (t == m->option_tables.end()) {
|
|
QTC::TC("libtests", "QPDFArgParser select unregistered table");
|
|
throw std::logic_error("QPDFArgParser: selecting unregistered option table " + name);
|
|
}
|
|
m->option_table = &(t->second);
|
|
m->option_table_name = name;
|
|
}
|
|
|
|
void
|
|
QPDFArgParser::registerOptionTable(std::string const& name, bare_arg_handler_t end_handler)
|
|
{
|
|
if (0 != m->option_tables.count(name)) {
|
|
QTC::TC("libtests", "QPDFArgParser register registered table");
|
|
throw std::logic_error(
|
|
"QPDFArgParser: registering already registered option table " + name);
|
|
}
|
|
m->option_tables[name];
|
|
selectOptionTable(name);
|
|
addBare("--", end_handler);
|
|
}
|
|
|
|
QPDFArgParser::OptionEntry&
|
|
QPDFArgParser::registerArg(std::string const& arg)
|
|
{
|
|
if (0 != m->option_table->count(arg)) {
|
|
QTC::TC("libtests", "QPDFArgParser duplicate handler");
|
|
throw std::logic_error(
|
|
"QPDFArgParser: adding a duplicate handler for option " + arg + " in " +
|
|
m->option_table_name + " option table");
|
|
}
|
|
return ((*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::addChoices(
|
|
std::string const& arg, param_arg_handler_t handler, bool required, char const** choices)
|
|
{
|
|
OptionEntry& oe = registerArg(arg);
|
|
oe.parameter_needed = required;
|
|
oe.param_arg_handler = handler;
|
|
for (char const** i = choices; *i; ++i) {
|
|
oe.choices.insert(*i);
|
|
}
|
|
}
|
|
|
|
void
|
|
QPDFArgParser::addInvalidChoiceHandler(std::string const& arg, param_arg_handler_t handler)
|
|
{
|
|
auto i = m->option_table->find(arg);
|
|
if (i == m->option_table->end()) {
|
|
QTC::TC("libtests", "QPDFArgParser invalid choice handler to unknown");
|
|
throw std::logic_error(
|
|
"QPDFArgParser: attempt to add invalid choice handler to unknown argument");
|
|
}
|
|
auto& oe = i->second;
|
|
oe.invalid_choice_handler = handler;
|
|
}
|
|
|
|
void
|
|
QPDFArgParser::addFinalCheck(bare_arg_handler_t handler)
|
|
{
|
|
m->final_check_handler = handler;
|
|
}
|
|
|
|
bool
|
|
QPDFArgParser::isCompleting() const
|
|
{
|
|
return m->bash_completion;
|
|
}
|
|
|
|
int
|
|
QPDFArgParser::argsLeft() const
|
|
{
|
|
return m->argc - m->cur_arg - 1;
|
|
}
|
|
|
|
void
|
|
QPDFArgParser::insertCompletion(std::string const& arg)
|
|
{
|
|
m->completions.insert(arg);
|
|
}
|
|
|
|
void
|
|
QPDFArgParser::completionCommon(bool zsh)
|
|
{
|
|
std::string progname = m->argv[0];
|
|
std::string executable;
|
|
std::string appdir;
|
|
std::string appimage;
|
|
if (QUtil::get_env(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(m->argv[0])) &&
|
|
(strncmp(appdir.c_str(), 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 << "\" " << 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: " << m->whoami << " completion enabled"
|
|
<< " using relative path to executable" << std::endl;
|
|
}
|
|
}
|
|
|
|
void
|
|
QPDFArgParser::argCompletionBash()
|
|
{
|
|
completionCommon(false);
|
|
}
|
|
|
|
void
|
|
QPDFArgParser::argCompletionZsh()
|
|
{
|
|
completionCommon(true);
|
|
}
|
|
|
|
void
|
|
QPDFArgParser::argHelp(std::string const& p)
|
|
{
|
|
QPDFLogger::defaultLogger()->info(getHelp(p));
|
|
exit(0);
|
|
}
|
|
|
|
void
|
|
QPDFArgParser::invalidHelpArg(std::string const& p)
|
|
{
|
|
usage(std::string("unknown help option") + (p.empty() ? "" : (" " + p)));
|
|
}
|
|
|
|
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 shared pointers back to the pointers in
|
|
// argv.
|
|
m->new_argv.push_back(QUtil::make_shared_cstr(m->argv[0]));
|
|
for (int i = 1; i < m->argc; ++i) {
|
|
char const* argfile = nullptr;
|
|
if ((strlen(m->argv[i]) > 1) && (m->argv[i][0] == '@')) {
|
|
argfile = 1 + 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 + m->argv[i]);
|
|
} else {
|
|
m->new_argv.push_back(QUtil::make_shared_cstr(m->argv[i]));
|
|
}
|
|
}
|
|
m->argv_ph = QUtil::make_shared_array<char const*>(1 + m->new_argv.size());
|
|
for (size_t i = 0; i < m->new_argv.size(); ++i) {
|
|
m->argv_ph.get()[i] = m->new_argv.at(i).get();
|
|
}
|
|
m->argc = QIntC::to_int(m->new_argv.size());
|
|
m->argv_ph.get()[m->argc] = nullptr;
|
|
m->argv = m->argv_ph.get();
|
|
}
|
|
|
|
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 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 (char ch: m->bash_line) {
|
|
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()) {
|
|
m->bash_argv.push_back(QUtil::make_shared_cstr(arg));
|
|
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 (m->bash_argv.empty()) {
|
|
// This can't happen if properly invoked by bash, but ensure we have a valid argv[0]
|
|
// regardless.
|
|
m->bash_argv.push_back(QUtil::make_shared_cstr(m->argv[0]));
|
|
}
|
|
// Explicitly discard any non-space-terminated word. The "current word" is handled specially.
|
|
m->bash_argv_ph = QUtil::make_shared_array<char const*>(1 + m->bash_argv.size());
|
|
for (size_t i = 0; i < m->bash_argv.size(); ++i) {
|
|
m->bash_argv_ph.get()[i] = m->bash_argv.at(i).get();
|
|
}
|
|
m->argc = QIntC::to_int(m->bash_argv.size());
|
|
m->bash_argv_ph.get()[m->argc] = nullptr;
|
|
m->argv = m->bash_argv_ph.get();
|
|
}
|
|
|
|
void
|
|
QPDFArgParser::usage(std::string const& message)
|
|
{
|
|
if (m->bash_completion) {
|
|
// This will cause bash to fall back to regular file completion.
|
|
exit(0);
|
|
}
|
|
throw QPDFUsage(message);
|
|
}
|
|
|
|
void
|
|
QPDFArgParser::readArgsFromFile(std::string const& filename)
|
|
{
|
|
std::list<std::string> lines;
|
|
if (filename == "-") {
|
|
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.c_str());
|
|
}
|
|
for (auto const& line: lines) {
|
|
m->new_argv.push_back(QUtil::make_shared_cstr(line));
|
|
}
|
|
}
|
|
|
|
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", &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 < m->bash_line.length()) {
|
|
// Truncate the line. We ignore everything at or after the
|
|
// cursor for completion purposes.
|
|
m->bash_line = m->bash_line.substr(0, p);
|
|
}
|
|
if (p > m->bash_line.length()) {
|
|
p = 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 = m->bash_line.at(p);
|
|
if ((ch == ' ') || (ch == '=') || (ch == ':')) {
|
|
sep = ch;
|
|
break;
|
|
}
|
|
}
|
|
if (1 + p <= m->bash_line.length()) {
|
|
m->bash_cur = 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.
|
|
m->bash_prev = 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 = m->bash_line.at(p1);
|
|
if ((ch == ' ') || (ch == ':') || (ch == '=')) {
|
|
m->bash_prev = m->bash_line.substr(p1 + 1, p - p1 - 1);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (m->bash_prev.empty()) {
|
|
m->bash_prev = m->bash_line.substr(0, p);
|
|
}
|
|
if (m->argc == 1) {
|
|
// This is probably zsh using bashcompinit. There are a few differences in the expected
|
|
// output.
|
|
m->zsh_completion = true;
|
|
}
|
|
handleBashArguments();
|
|
m->bash_completion = true;
|
|
}
|
|
}
|
|
|
|
void
|
|
QPDFArgParser::parseArgs()
|
|
{
|
|
selectMainOptionTable();
|
|
checkCompletion();
|
|
handleArgFileArguments();
|
|
for (m->cur_arg = 1; m->cur_arg < m->argc; ++m->cur_arg) {
|
|
bool help_option = false;
|
|
bool end_option = false;
|
|
auto oep = m->option_table->end();
|
|
char const* arg = m->argv[m->cur_arg];
|
|
std::string parameter;
|
|
bool have_parameter = false;
|
|
std::string o_arg(arg);
|
|
std::string arg_s(arg);
|
|
if (strcmp(arg, "--") == 0) {
|
|
// Special case for -- option, which is used to break out of subparsers.
|
|
oep = m->option_table->find("--");
|
|
end_option = true;
|
|
if (oep == m->option_table->end()) {
|
|
// This is registered automatically, so this can't happen.
|
|
throw std::logic_error("QPDFArgParser: -- 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");
|
|
}
|
|
|
|
// Prevent --=something from being treated as an empty arg by searching for = from after
|
|
// the first character. We do this since the empty string in the option table is for
|
|
// positional arguments. Besides, it doesn't make sense to have an empty option.
|
|
arg_s = arg;
|
|
size_t equal_pos = std::string::npos;
|
|
if (arg_s.length() > 0) {
|
|
equal_pos = arg_s.find('=', 1);
|
|
}
|
|
if (equal_pos != std::string::npos) {
|
|
have_parameter = true;
|
|
parameter = arg_s.substr(equal_pos + 1);
|
|
arg_s = arg_s.substr(0, equal_pos);
|
|
}
|
|
|
|
if ((!m->bash_completion) && (m->argc == 2) && (m->cur_arg == 1) &&
|
|
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 = m->help_option_table.find(arg_s);
|
|
help_option = true;
|
|
}
|
|
|
|
if (!(help_option || arg_s.empty() || (arg_s.at(0) == '-'))) {
|
|
oep = m->option_table->find(arg_s);
|
|
}
|
|
} else {
|
|
// The empty string maps to the positional argument handler.
|
|
QTC::TC("libtests", "QPDFArgParser positional");
|
|
oep = m->option_table->find("");
|
|
parameter = arg;
|
|
}
|
|
|
|
if (oep == m->option_table->end()) {
|
|
QTC::TC("libtests", "QPDFArgParser unrecognized");
|
|
std::string message = "unrecognized argument " + o_arg;
|
|
if (m->option_table != &m->main_option_table) {
|
|
message += " (" + m->option_table_name + " options must be terminated with --)";
|
|
}
|
|
usage(message);
|
|
}
|
|
|
|
OptionEntry& oe = oep->second;
|
|
if ((oe.parameter_needed && (!have_parameter)) ||
|
|
((!oe.choices.empty() && have_parameter && (0 == oe.choices.count(parameter))))) {
|
|
std::string message = "--" + arg_s + " must be given as --" + arg_s + "=";
|
|
if (oe.invalid_choice_handler) {
|
|
oe.invalid_choice_handler(parameter);
|
|
// Method should call usage() or exit. Just in case it doesn't...
|
|
message += "option";
|
|
} else if (!oe.choices.empty()) {
|
|
QTC::TC("libtests", "QPDFArgParser required choices");
|
|
message += "{";
|
|
bool first = true;
|
|
for (auto const& choice: oe.choices) {
|
|
if (first) {
|
|
first = false;
|
|
} else {
|
|
message += ",";
|
|
}
|
|
message += choice;
|
|
}
|
|
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 (m->bash_completion) {
|
|
handleCompletion();
|
|
} else {
|
|
doFinalChecks();
|
|
}
|
|
}
|
|
|
|
std::string
|
|
QPDFArgParser::getProgname()
|
|
{
|
|
return m->whoami;
|
|
}
|
|
|
|
void
|
|
QPDFArgParser::doFinalChecks()
|
|
{
|
|
if (m->option_table != &(m->main_option_table)) {
|
|
QTC::TC("libtests", "QPDFArgParser missing --");
|
|
usage("missing -- at end of " + m->option_table_name + " options");
|
|
}
|
|
if (m->final_check_handler != nullptr) {
|
|
m->final_check_handler();
|
|
}
|
|
}
|
|
|
|
void
|
|
QPDFArgParser::addChoicesToCompletions(
|
|
option_table_t& option_table, std::string const& option, std::string const& extra_prefix)
|
|
{
|
|
if (option_table.count(option) != 0) {
|
|
OptionEntry& oe = option_table[option];
|
|
for (auto const& choice: oe.choices) {
|
|
QTC::TC("libtests", "QPDFArgParser complete choices");
|
|
m->completions.insert(extra_prefix + choice);
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
QPDFArgParser::addOptionsToCompletions(option_table_t& option_table)
|
|
{
|
|
for (auto& iter: option_table) {
|
|
std::string const& arg = iter.first;
|
|
if (arg == "--") {
|
|
continue;
|
|
}
|
|
OptionEntry& oe = iter.second;
|
|
std::string base = "--" + arg;
|
|
if (oe.param_arg_handler) {
|
|
if (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(option_table, arg, base + "=");
|
|
}
|
|
m->completions.insert(base + "=");
|
|
}
|
|
if (!oe.parameter_needed) {
|
|
m->completions.insert(base);
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
QPDFArgParser::insertCompletions(
|
|
option_table_t& option_table, std::string const& choice_option, std::string const& extra_prefix)
|
|
{
|
|
if (!choice_option.empty()) {
|
|
addChoicesToCompletions(option_table, choice_option, extra_prefix);
|
|
} else if ((!m->bash_cur.empty()) && (m->bash_cur.at(0) == '-')) {
|
|
addOptionsToCompletions(option_table);
|
|
}
|
|
}
|
|
|
|
void
|
|
QPDFArgParser::handleCompletion()
|
|
{
|
|
std::string extra_prefix;
|
|
if (m->completions.empty()) {
|
|
// Detect --option=... Bash treats the = as a word separator.
|
|
std::string choice_option;
|
|
if (m->bash_cur.empty() && (m->bash_prev.length() > 2) && (m->bash_prev.at(0) == '-') &&
|
|
(m->bash_prev.at(1) == '-') && (m->bash_line.at(m->bash_line.length() - 1) == '=')) {
|
|
choice_option = m->bash_prev.substr(2, std::string::npos);
|
|
} else if ((m->bash_prev == "=") && (m->bash_line.length() > (m->bash_cur.length() + 1))) {
|
|
// We're sitting at --option=x. Find previous option.
|
|
size_t end_mark = m->bash_line.length() - m->bash_cur.length() - 1;
|
|
char before_cur = m->bash_line.at(end_mark);
|
|
if (before_cur == '=') {
|
|
size_t space = m->bash_line.find_last_of(' ', end_mark);
|
|
if (space != std::string::npos) {
|
|
std::string candidate = 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 (m->zsh_completion && (!choice_option.empty())) {
|
|
// zsh wants --option=choice rather than just choice
|
|
extra_prefix = "--" + choice_option + "=";
|
|
}
|
|
insertCompletions(*m->option_table, choice_option, extra_prefix);
|
|
if (m->argc == 1) {
|
|
// Help options are valid only by themselves.
|
|
insertCompletions(m->help_option_table, choice_option, extra_prefix);
|
|
}
|
|
}
|
|
std::string prefix = extra_prefix + m->bash_cur;
|
|
for (auto const& iter: m->completions) {
|
|
if (prefix.empty() || (iter.substr(0, prefix.length()) == prefix)) {
|
|
std::cout << iter << std::endl;
|
|
}
|
|
}
|
|
exit(0);
|
|
}
|
|
|
|
void
|
|
QPDFArgParser::addHelpFooter(std::string const& text)
|
|
{
|
|
m->help_footer = "\n" + text;
|
|
}
|
|
|
|
void
|
|
QPDFArgParser::addHelpTopic(
|
|
std::string const& topic, std::string const& short_text, std::string const& long_text)
|
|
{
|
|
if (topic == "all") {
|
|
QTC::TC("libtests", "QPDFArgParser add reserved help topic");
|
|
throw std::logic_error("QPDFArgParser: can't register reserved help topic " + topic);
|
|
}
|
|
if (!((topic.length() > 0) && (topic.at(0) != '-'))) {
|
|
QTC::TC("libtests", "QPDFArgParser bad topic for help");
|
|
throw std::logic_error("QPDFArgParser: help topics must not start with -");
|
|
}
|
|
if (m->help_topics.count(topic)) {
|
|
QTC::TC("libtests", "QPDFArgParser add existing topic");
|
|
throw std::logic_error("QPDFArgParser: topic " + topic + " has already been added");
|
|
}
|
|
|
|
m->help_topics[topic] = HelpTopic(short_text, long_text);
|
|
m->help_option_table["help"].choices.insert(topic);
|
|
}
|
|
|
|
void
|
|
QPDFArgParser::addOptionHelp(
|
|
std::string const& option_name,
|
|
std::string const& topic,
|
|
std::string const& short_text,
|
|
std::string const& long_text)
|
|
{
|
|
if (!((option_name.length() > 2) && (option_name.at(0) == '-') && (option_name.at(1) == '-'))) {
|
|
QTC::TC("libtests", "QPDFArgParser bad option for help");
|
|
throw std::logic_error("QPDFArgParser: options for help must start with --");
|
|
}
|
|
if (m->option_help.count(option_name)) {
|
|
QTC::TC("libtests", "QPDFArgParser duplicate option help");
|
|
throw std::logic_error("QPDFArgParser: option " + option_name + " already has help");
|
|
}
|
|
auto ht = m->help_topics.find(topic);
|
|
if (ht == m->help_topics.end()) {
|
|
QTC::TC("libtests", "QPDFArgParser add to unknown topic");
|
|
throw std::logic_error(
|
|
"QPDFArgParser: unable to add option " + option_name + " to unknown help topic " +
|
|
topic);
|
|
}
|
|
m->option_help[option_name] = HelpTopic(short_text, long_text);
|
|
ht->second.options.insert(option_name);
|
|
m->help_option_table["help"].choices.insert(option_name);
|
|
}
|
|
|
|
void
|
|
QPDFArgParser::getTopHelp(std::ostringstream& msg)
|
|
{
|
|
msg << "Run \"" << m->whoami << " --help=topic\" for help on a topic." << std::endl
|
|
<< "Run \"" << m->whoami << " --help=--option\" for help on an option." << std::endl
|
|
<< "Run \"" << m->whoami << " --help=all\" to see all available help." << std::endl
|
|
<< std::endl
|
|
<< "Topics:" << std::endl;
|
|
for (auto const& i: m->help_topics) {
|
|
msg << " " << i.first << ": " << i.second.short_text << std::endl;
|
|
}
|
|
}
|
|
|
|
void
|
|
QPDFArgParser::getAllHelp(std::ostringstream& msg)
|
|
{
|
|
getTopHelp(msg);
|
|
auto show = [this, &msg](std::map<std::string, HelpTopic>& topics) {
|
|
for (auto const& i: topics) {
|
|
auto const& topic = i.first;
|
|
msg << std::endl
|
|
<< "== " << topic << " (" << i.second.short_text << ") ==" << std::endl
|
|
<< std::endl;
|
|
getTopicHelp(topic, i.second, msg);
|
|
}
|
|
};
|
|
show(m->help_topics);
|
|
show(m->option_help);
|
|
msg << std::endl << "====" << std::endl;
|
|
}
|
|
|
|
void
|
|
QPDFArgParser::getTopicHelp(std::string const& name, HelpTopic const& ht, std::ostringstream& msg)
|
|
{
|
|
if (ht.long_text.empty()) {
|
|
msg << ht.short_text << std::endl;
|
|
} else {
|
|
msg << ht.long_text;
|
|
}
|
|
if (!ht.options.empty()) {
|
|
msg << std::endl << "Related options:" << std::endl;
|
|
for (auto const& i: ht.options) {
|
|
msg << " " << i << ": " << m->option_help[i].short_text << std::endl;
|
|
}
|
|
}
|
|
}
|
|
|
|
std::string
|
|
QPDFArgParser::getHelp(std::string const& arg)
|
|
{
|
|
std::ostringstream msg;
|
|
if (arg.empty()) {
|
|
getTopHelp(msg);
|
|
} else {
|
|
if (arg == "all") {
|
|
getAllHelp(msg);
|
|
} else if (m->option_help.count(arg)) {
|
|
getTopicHelp(arg, m->option_help[arg], msg);
|
|
} else if (m->help_topics.count(arg)) {
|
|
getTopicHelp(arg, m->help_topics[arg], msg);
|
|
} else {
|
|
// should not be possible
|
|
getTopHelp(msg);
|
|
}
|
|
}
|
|
msg << m->help_footer;
|
|
return msg.str();
|
|
}
|