Commit dd1aca552c912e1675d8927d9c945d6642cb8241

Authored by Jay Berkenbilt
1 parent 3c075fc0

Support bash completion using complete -C

ChangeLog
  1 +2018-12-21 Jay Berkenbilt <ejb@ql.org>
  2 +
  3 + * You can now use eval $(qpdf --completion-bash) to enable bash
  4 + completion for qpdf. It's not perfect, but it works pretty well.
  5 +
1 6 2018-12-19 Jay Berkenbilt <ejb@ql.org>
2 7  
3 8 * When splitting pages using --split-pages, the outlines
... ...
qpdf/qpdf.cc
... ... @@ -322,7 +322,7 @@ class ArgParser
322 322 void argShowLinearization();
323 323 void argShowXref();
324 324 void argShowObject(char* parameter);
325   - void argShowObject();
  325 + void argRawStreamData();
326 326 void argFilteredStreamData();
327 327 void argShowNpages();
328 328 void argShowPages();
... ... @@ -344,10 +344,16 @@ class ArgParser
344 344 void argEndEncrypt();
345 345  
346 346 void usage(std::string const& message);
  347 + void checkCompletion();
347 348 void initOptionTable();
348   - void handleHelpVersion();
  349 + void handleHelpArgs();
349 350 void handleArgFileArguments();
  351 + void handleBashArguments();
350 352 void readArgsFromFile(char const* filename);
  353 + void doFinalChecks();
  354 + void addOptionsToCompletions();
  355 + void addChoicesToCompletions(std::string const&);
  356 + void handleCompletion();
351 357 std::vector<PageSpec> parsePagesOptions();
352 358 void parseRotationParameter(std::string const&);
353 359 std::vector<int> parseNumrange(char const* range, int max,
... ... @@ -359,6 +365,12 @@ class ArgParser
359 365 char** argv;
360 366 Options& o;
361 367 int cur_arg;
  368 + bool bash_completion;
  369 + std::string bash_prev;
  370 + std::string bash_cur;
  371 + std::string bash_line;
  372 + size_t bash_point;
  373 + std::set<std::string> completions;
362 374  
363 375 std::map<std::string, OptionEntry>* option_table;
364 376 std::map<std::string, OptionEntry> main_option_table;
... ... @@ -366,14 +378,18 @@ class ArgParser
366 378 std::map<std::string, OptionEntry> encrypt128_option_table;
367 379 std::map<std::string, OptionEntry> encrypt256_option_table;
368 380 std::vector<PointerHolder<char> > new_argv;
  381 + std::vector<PointerHolder<char> > bash_argv;
369 382 PointerHolder<char*> argv_ph;
  383 + PointerHolder<char*> bash_argv_ph;
370 384 };
371 385  
372 386 ArgParser::ArgParser(int argc, char* argv[], Options& o) :
373 387 argc(argc),
374 388 argv(argv),
375 389 o(o),
376   - cur_arg(0)
  390 + cur_arg(0),
  391 + bash_completion(false),
  392 + bash_point(0)
377 393 {
378 394 option_table = &main_option_table;
379 395 initOptionTable();
... ... @@ -496,7 +512,7 @@ ArgParser::initOptionTable()
496 512 (*t)["show-xref"] = oe_bare(&ArgParser::argShowXref);
497 513 (*t)["show-object"] = oe_requiredParameter(
498 514 &ArgParser::argShowObject, "obj[,gen]");
499   - (*t)["raw-stream-data"] = oe_bare(&ArgParser::argShowObject);
  515 + (*t)["raw-stream-data"] = oe_bare(&ArgParser::argRawStreamData);
500 516 (*t)["filtered-stream-data"] = oe_bare(&ArgParser::argFilteredStreamData);
501 517 (*t)["show-npages"] = oe_bare(&ArgParser::argShowNpages);
502 518 (*t)["show-pages"] = oe_bare(&ArgParser::argShowPages);
... ... @@ -573,9 +589,30 @@ void
573 589 ArgParser::argEncrypt()
574 590 {
575 591 ++cur_arg;
576   - if (cur_arg + 3 >= argc)
  592 + if (cur_arg + 3 > argc)
577 593 {
578   - usage("insufficient arguments to --encrypt");
  594 + if (this->bash_completion)
  595 + {
  596 + if (cur_arg == argc)
  597 + {
  598 + this->completions.insert("user-password");
  599 + }
  600 + else if (cur_arg + 1 == argc)
  601 + {
  602 + this->completions.insert("owner-password");
  603 + }
  604 + else if (cur_arg + 2 == argc)
  605 + {
  606 + this->completions.insert("40");
  607 + this->completions.insert("128");
  608 + this->completions.insert("256");
  609 + }
  610 + return;
  611 + }
  612 + else
  613 + {
  614 + usage("insufficient arguments to --encrypt");
  615 + }
579 616 }
580 617 o.user_password = argv[cur_arg++];
581 618 o.owner_password = argv[cur_arg++];
... ... @@ -904,7 +941,7 @@ ArgParser::argShowObject(char* parameter)
904 941 }
905 942  
906 943 void
907   -ArgParser::argShowObject()
  944 +ArgParser::argRawStreamData()
908 945 {
909 946 o.show_raw_stream_data = true;
910 947 }
... ... @@ -1098,14 +1135,55 @@ ArgParser::handleArgFileArguments()
1098 1135 argv[argc] = 0;
1099 1136 }
1100 1137  
1101   -// Note: let's not be too noisy about documenting the fact that this
1102   -// software purposely fails to enforce the distinction between user
1103   -// and owner passwords. A user password is sufficient to gain full
1104   -// access to the PDF file, so there is nothing this software can do
1105   -// with an owner password that it couldn't do with a user password
1106   -// other than changing the /P value in the encryption dictionary.
1107   -// (Setting this value requires the owner password.) The
1108   -// documentation discusses this as well.
  1138 +void
  1139 +ArgParser::handleBashArguments()
  1140 +{
  1141 + // Do a minimal job of parsing bash_line into arguments. This
  1142 + // doesn't do everything the shell does, but it should be good
  1143 + // enough for purposes of handling completion. We can't use
  1144 + // new_argv because this has to interoperate with @file arguments.
  1145 +
  1146 + enum { st_top, st_quote } state = st_top;
  1147 + std::string arg;
  1148 + for (std::string::iterator iter = bash_line.begin();
  1149 + iter != bash_line.end(); ++iter)
  1150 + {
  1151 + char ch = (*iter);
  1152 + if ((state == st_top) && QUtil::is_space(ch) && (! arg.empty()))
  1153 + {
  1154 + bash_argv.push_back(
  1155 + PointerHolder<char>(
  1156 + true, QUtil::copy_string(arg.c_str())));
  1157 + arg.clear();
  1158 + }
  1159 + else
  1160 + {
  1161 + if (ch == '"')
  1162 + {
  1163 + state = (state == st_top ? st_quote : st_top);
  1164 + }
  1165 + arg.append(1, ch);
  1166 + }
  1167 + }
  1168 + if (bash_argv.empty())
  1169 + {
  1170 + // This can't happen if properly invoked by bash, but ensure
  1171 + // we have a valid argv[0] regardless.
  1172 + bash_argv.push_back(
  1173 + PointerHolder<char>(
  1174 + true, QUtil::copy_string(argv[0])));
  1175 + }
  1176 + // Explicitly discard any non-space-terminated word. The "current
  1177 + // word" is handled specially.
  1178 + bash_argv_ph = PointerHolder<char*>(true, new char*[1+bash_argv.size()]);
  1179 + argv = bash_argv_ph.getPointer();
  1180 + for (size_t i = 0; i < bash_argv.size(); ++i)
  1181 + {
  1182 + argv[i] = bash_argv.at(i).getPointer();
  1183 + }
  1184 + argc = static_cast<int>(bash_argv.size());
  1185 + argv[argc] = 0;
  1186 +}
1109 1187  
1110 1188 char const* ArgParser::help = "\
1111 1189 \n\
... ... @@ -1127,6 +1205,7 @@ Basic Options\n\
1127 1205 --version show version of qpdf\n\
1128 1206 --copyright show qpdf's copyright and license information\n\
1129 1207 --help show command-line argument help\n\
  1208 +--completion-bash output a bash complete command you can eval\n\
1130 1209 --password=password specify a password for accessing encrypted files\n\
1131 1210 --verbose provide additional informational output\n\
1132 1211 --progress give progress indicators while writing output\n\
... ... @@ -1412,7 +1491,15 @@ void usageExit(std::string const&amp; msg)
1412 1491 void
1413 1492 ArgParser::usage(std::string const& message)
1414 1493 {
1415   - usageExit(message);
  1494 + if (this->bash_completion)
  1495 + {
  1496 + // This will cause bash to fall back to regular file completion.
  1497 + exit(0);
  1498 + }
  1499 + else
  1500 + {
  1501 + usageExit(message);
  1502 + }
1416 1503 }
1417 1504  
1418 1505 static JSON json_schema()
... ... @@ -1718,13 +1805,33 @@ ArgParser::readArgsFromFile(char const* filename)
1718 1805 }
1719 1806  
1720 1807 void
1721   -ArgParser::handleHelpVersion()
  1808 +ArgParser::handleHelpArgs()
1722 1809 {
1723   - // Make sure the output looks right on an 80-column display.
  1810 + // Handle special-case informational options that are only
  1811 + // available as the sole option.
  1812 +
  1813 + // The options processed here are also handled as a special case
  1814 + // in handleCompletion.
1724 1815  
1725   - if ((argc == 2) &&
1726   - ((strcmp(argv[1], "--version") == 0) ||
1727   - (strcmp(argv[1], "-version") == 0)))
  1816 + if (argc != 2)
  1817 + {
  1818 + return;
  1819 + }
  1820 + char* arg = argv[1];
  1821 + if (*arg != '-')
  1822 + {
  1823 + return;
  1824 + }
  1825 + ++arg;
  1826 + if (*arg == '-')
  1827 + {
  1828 + ++arg;
  1829 + }
  1830 + if (! *arg)
  1831 + {
  1832 + return;
  1833 + }
  1834 + if (strcmp(arg, "version") == 0)
1728 1835 {
1729 1836 std::cout
1730 1837 << whoami << " version " << QPDF::QPDFVersion() << std::endl
... ... @@ -1733,10 +1840,9 @@ ArgParser::handleHelpVersion()
1733 1840 exit(0);
1734 1841 }
1735 1842  
1736   - if ((argc == 2) &&
1737   - ((strcmp(argv[1], "--copyright") == 0) ||
1738   - (strcmp(argv[1], "-copyright") == 0)))
  1843 + if (strcmp(arg, "copyright") == 0)
1739 1844 {
  1845 + // Make sure the output looks right on an 80-column display.
1740 1846 // 1 2 3 4 5 6 7 8
1741 1847 // 12345678901234567890123456789012345678901234567890123456789012345678901234567890
1742 1848 std::cout
... ... @@ -1776,13 +1882,25 @@ ArgParser::handleHelpVersion()
1776 1882 exit(0);
1777 1883 }
1778 1884  
1779   - if ((argc == 2) &&
1780   - ((strcmp(argv[1], "--help") == 0) ||
1781   - (strcmp(argv[1], "-help") == 0)))
  1885 + if (strcmp(arg, "help") == 0)
1782 1886 {
1783 1887 std::cout << help;
1784 1888 exit(0);
1785 1889 }
  1890 +
  1891 + if (strcmp(arg, "completion-bash") == 0)
  1892 + {
  1893 + std::string path = argv[0];
  1894 + size_t slash = path.find('/');
  1895 + if ((slash != 0) && (slash != std::string::npos))
  1896 + {
  1897 + std::cerr << "WARNING: qpdf completion enabled"
  1898 + << " using relative path to qpdf" << std::endl;
  1899 + }
  1900 + std::cout << "complete -o bashdefault -o default -o nospace"
  1901 + << " -C " << argv[0] << " " << whoami << std::endl;
  1902 + exit(0);
  1903 + }
1786 1904 }
1787 1905  
1788 1906 void
... ... @@ -1851,9 +1969,37 @@ ArgParser::parseRotationParameter(std::string const&amp; parameter)
1851 1969 }
1852 1970  
1853 1971 void
  1972 +ArgParser::checkCompletion()
  1973 +{
  1974 + // See if we're being invoked from bash completion.
  1975 + std::string bash_point_env;
  1976 + if (QUtil::get_env("COMP_LINE", &bash_line) &&
  1977 + QUtil::get_env("COMP_POINT", &bash_point_env))
  1978 + {
  1979 + int p = QUtil::string_to_int(bash_point_env.c_str());
  1980 + if ((p > 0) && (p <= static_cast<int>(bash_line.length())))
  1981 + {
  1982 + // Point to the last character
  1983 + bash_point = static_cast<size_t>(p) - 1;
  1984 + }
  1985 + if (argc >= 4)
  1986 + {
  1987 + bash_cur = argv[2];
  1988 + bash_prev = argv[3];
  1989 + handleBashArguments();
  1990 + bash_completion = true;
  1991 + }
  1992 + }
  1993 +}
  1994 +
  1995 +void
1854 1996 ArgParser::parseOptions()
1855 1997 {
1856   - handleHelpVersion(); // QXXXQ calls std::cout
  1998 + checkCompletion();
  1999 + if (! this->bash_completion)
  2000 + {
  2001 + handleHelpArgs();
  2002 + }
1857 2003 handleArgFileArguments();
1858 2004 for (cur_arg = 1; cur_arg < argc; ++cur_arg)
1859 2005 {
... ... @@ -1957,7 +2103,19 @@ ArgParser::parseOptions()
1957 2103 usage(std::string("unknown argument ") + arg);
1958 2104 }
1959 2105 }
  2106 + if (this->bash_completion)
  2107 + {
  2108 + handleCompletion();
  2109 + }
  2110 + else
  2111 + {
  2112 + doFinalChecks();
  2113 + }
  2114 +}
1960 2115  
  2116 +void
  2117 +ArgParser::doFinalChecks()
  2118 +{
1961 2119 if (this->option_table != &(this->main_option_table))
1962 2120 {
1963 2121 usage("missing -- at end of options");
... ... @@ -2002,6 +2160,107 @@ ArgParser::parseOptions()
2002 2160 }
2003 2161 }
2004 2162  
  2163 +void
  2164 +ArgParser::addChoicesToCompletions(std::string const& option)
  2165 +{
  2166 + if (this->option_table->count(option) != 0)
  2167 + {
  2168 + OptionEntry& oe = (*this->option_table)[option];
  2169 + for (std::set<std::string>::iterator iter = oe.choices.begin();
  2170 + iter != oe.choices.end(); ++iter)
  2171 + {
  2172 + completions.insert(*iter);
  2173 + }
  2174 + }
  2175 +}
  2176 +
  2177 +void
  2178 +ArgParser::addOptionsToCompletions()
  2179 +{
  2180 + for (std::map<std::string, OptionEntry>::iterator iter =
  2181 + this->option_table->begin();
  2182 + iter != this->option_table->end(); ++iter)
  2183 + {
  2184 + std::string const& arg = (*iter).first;
  2185 + OptionEntry& oe = (*iter).second;
  2186 + std::string base = "--" + arg;
  2187 + if (oe.param_arg_handler)
  2188 + {
  2189 + completions.insert(base + "=");
  2190 + }
  2191 + if (! oe.parameter_needed)
  2192 + {
  2193 + completions.insert(base);
  2194 + }
  2195 + }
  2196 +}
  2197 +
  2198 +void
  2199 +ArgParser::handleCompletion()
  2200 +{
  2201 + if (this->completions.empty())
  2202 + {
  2203 + // Detect --option=... Bash treats the = as a word separator.
  2204 + std::string choice_option;
  2205 + if (bash_cur.empty() && (bash_prev.length() > 2) &&
  2206 + (bash_prev.at(0) == '-') &&
  2207 + (bash_prev.at(1) == '-') &&
  2208 + (bash_line.at(bash_point) == '='))
  2209 + {
  2210 + choice_option = bash_prev.substr(2, std::string::npos);
  2211 + }
  2212 + else if ((bash_prev == "=") &&
  2213 + (bash_line.length() > (bash_cur.length() + 1)))
  2214 + {
  2215 + // We're sitting at --option=x. Find previous option.
  2216 + size_t end_mark = bash_line.length() - bash_cur.length() - 1;
  2217 + char before_cur = bash_line.at(end_mark);
  2218 + if (before_cur == '=')
  2219 + {
  2220 + size_t space = bash_line.find_last_of(' ', end_mark);
  2221 + if (space != std::string::npos)
  2222 + {
  2223 + std::string candidate =
  2224 + bash_line.substr(space + 1, end_mark - space - 1);
  2225 + if ((candidate.length() > 2) &&
  2226 + (candidate.at(0) == '-') &&
  2227 + (candidate.at(1) == '-'))
  2228 + {
  2229 + choice_option =
  2230 + candidate.substr(2, std::string::npos);
  2231 + }
  2232 + }
  2233 + }
  2234 + }
  2235 + if (! choice_option.empty())
  2236 + {
  2237 + addChoicesToCompletions(choice_option);
  2238 + }
  2239 + else if ((! bash_cur.empty()) && (bash_cur.at(0) == '-'))
  2240 + {
  2241 + addOptionsToCompletions();
  2242 + if (this->argc == 1)
  2243 + {
  2244 + // Handle options usually handled by handleHelpArgs.
  2245 + this->completions.insert("--help");
  2246 + this->completions.insert("--version");
  2247 + this->completions.insert("--copyright");
  2248 + this->completions.insert("--completion-bash");
  2249 + }
  2250 + }
  2251 + }
  2252 + for (std::set<std::string>::iterator iter = completions.begin();
  2253 + iter != completions.end(); ++iter)
  2254 + {
  2255 + if (this->bash_cur.empty() ||
  2256 + ((*iter).substr(0, bash_cur.length()) == bash_cur))
  2257 + {
  2258 + std::cout << *iter << std::endl;
  2259 + }
  2260 + }
  2261 + exit(0);
  2262 +}
  2263 +
2005 2264 static void set_qpdf_options(QPDF& pdf, Options& o)
2006 2265 {
2007 2266 if (o.ignore_xref_streams)
... ...
qpdf/qtest/qpdf.test
... ... @@ -100,6 +100,36 @@ $td-&gt;runtest(&quot;UTF-16 encoding errors&quot;,
100 100 {$td->FILE => "unicode-errors.out", $td->EXIT_STATUS => 0},
101 101 $td->NORMALIZE_NEWLINES);
102 102  
  103 +my @completion_tests = (
  104 + ['qpdf ', undef, 'top'],
  105 + ['qpdf -', undef, 'top-arg'],
  106 + ['qpdf --enc', undef, 'enc'],
  107 + ['qpdf --encrypt ', undef, 'encrypt'],
  108 + ['qpdf --encrypt u ', undef, 'encrypt-u'],
  109 + ['qpdf --encrypt u o ', undef, 'encrypt-u-o'],
  110 + ['qpdf @encrypt-u o ', undef, 'encrypt-u-o'],
  111 + ['qpdf --encrypt u o 40 --', undef, 'encrypt-40'],
  112 + ['qpdf --encrypt u o 128 --', undef, 'encrypt-128'],
  113 + ['qpdf --encrypt u o 256 --', undef, 'encrypt-256'],
  114 + ['qpdf --encrypt u o bad --', undef, 'encrypt-bad'],
  115 + ['qpdf --split-pag', undef, 'split'],
  116 + ['qpdf --decode-l', undef, 'decode-l'],
  117 + ['qpdf --decode-lzzz', 15, 'decode-l'],
  118 + ['qpdf --decode-level=', undef, 'decode-level'],
  119 + ['qpdf --check -', undef, 'later-arg'],
  120 + );
  121 +$n_tests += scalar(@completion_tests);
  122 +foreach my $c (@completion_tests)
  123 +{
  124 + my ($cmd, $point, $description) = @$c;
  125 + my $out = "completion-$description.out";
  126 + $td->runtest("bash completion: $description",
  127 + {$td->COMMAND => [@{bash_completion($cmd, $point)}],
  128 + $td->FILTER => "perl filter-completion.pl $out"},
  129 + {$td->FILE => "$out", $td->EXIT_STATUS => 0},
  130 + $td->NORMALIZE_NEWLINES);
  131 +}
  132 +
103 133 show_ntests();
104 134 # ----------
105 135 $td->notify("--- Argument Parsing ---");
... ... @@ -3144,6 +3174,24 @@ sub show_ntests
3144 3174 }
3145 3175 }
3146 3176  
  3177 +sub bash_completion
  3178 +{
  3179 + my ($line, $point) = @_;
  3180 + if (! defined $point)
  3181 + {
  3182 + $point = length($line);
  3183 + }
  3184 + my $before_point = substr($line, 0, $point);
  3185 + $before_point =~ m/^(.*)([ =])([^= ]*)$/ or die;
  3186 + my ($first, $sep, $cur) = ($1, $2, $3);
  3187 + my $prev = ($sep eq '=' ? $sep : $first);
  3188 + $prev =~ s/.* (\S+)$/$1/;
  3189 + my $this = $first;
  3190 + $this =~ s/(\S+)\s.*/$1/;
  3191 + ['env', "COMP_LINE=$line", "COMP_POINT=$point",
  3192 + "qpdf", $this, $cur, $prev];
  3193 +}
  3194 +
3147 3195 sub check_pdf
3148 3196 {
3149 3197 my ($description, $command, $output, $status) = @_;
... ...
qpdf/qtest/qpdf/completion-decode-l.out 0 → 100644
  1 +--decode-level=
  2 +!--help
... ...
qpdf/qtest/qpdf/completion-decode-level.out 0 → 100644
  1 +all
  2 +generalized
  3 +none
  4 +!--help
... ...
qpdf/qtest/qpdf/completion-enc.out 0 → 100644
  1 +--encrypt
... ...
qpdf/qtest/qpdf/completion-encrypt-128.out 0 → 100644
  1 +--force-V4
  2 +!--annotate=
  3 +!--force-R5
... ...
qpdf/qtest/qpdf/completion-encrypt-256.out 0 → 100644
  1 +--force-R5
  2 +!--annotate=
  3 +!--force-V4
... ...
qpdf/qtest/qpdf/completion-encrypt-40.out 0 → 100644
  1 +--annotate=
  2 +!--force-R5
  3 +!--force-V4
... ...
qpdf/qtest/qpdf/completion-encrypt-bad.out 0 → 100644
  1 +!--help
  2 +!--print
... ...
qpdf/qtest/qpdf/completion-encrypt-u-o.out 0 → 100644
  1 +128
  2 +256
  3 +40
... ...
qpdf/qtest/qpdf/completion-encrypt-u.out 0 → 100644
  1 +owner-password
... ...
qpdf/qtest/qpdf/completion-encrypt.out 0 → 100644
  1 +user-password
  2 +!--print
... ...
qpdf/qtest/qpdf/completion-later-arg.out 0 → 100644
  1 +--check
  2 +--decode-level=
  3 +--encrypt
  4 +!--completion-bash
  5 +!--copyright
  6 +!--help
  7 +!--version
... ...
qpdf/qtest/qpdf/completion-split.out 0 → 100644
  1 +--split-pages
  2 +--split-pages=
... ...
qpdf/qtest/qpdf/completion-top-arg.out 0 → 100644
  1 +--check
  2 +--completion-bash
  3 +--copyright
  4 +--decode-level=
  5 +--encrypt
  6 +--help
  7 +--version
... ...
qpdf/qtest/qpdf/completion-top.out 0 → 100644
  1 +!--copyright
  2 +!--help
  3 +!--version
... ...
qpdf/qtest/qpdf/encrypt-u 0 → 100644
  1 +--encrypt
  2 +u
... ...
qpdf/qtest/qpdf/filter-completion.pl 0 → 100644
  1 +use warnings;
  2 +use strict;
  3 +
  4 +# Output every line from STDIN that appears in the file.
  5 +my %wanted = ();
  6 +my %notwanted = ();
  7 +my $f = $ARGV[0];
  8 +if (open(F, "<$f"))
  9 +{
  10 + while (<F>)
  11 + {
  12 + chomp;
  13 + if (s/^!//)
  14 + {
  15 + $notwanted{$_} = 1;
  16 + }
  17 + else
  18 + {
  19 + $wanted{$_} = 1;
  20 + }
  21 + }
  22 + close(F);
  23 +}
  24 +while (<STDIN>)
  25 +{
  26 + chomp;
  27 + if (exists $wanted{$_})
  28 + {
  29 + print $_, "\n";
  30 + }
  31 + elsif (exists $notwanted{$_})
  32 + {
  33 + delete $notwanted{$_};
  34 + }
  35 +}
  36 +foreach my $k (sort keys %notwanted)
  37 +{
  38 + print "!$k\n";
  39 +}
... ...