Commit dd1aca552c912e1675d8927d9c945d6642cb8241
1 parent
3c075fc0
Support bash completion using complete -C
Showing
19 changed files
with
424 additions
and
28 deletions
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 | 2018-12-19 Jay Berkenbilt <ejb@ql.org> | 6 | 2018-12-19 Jay Berkenbilt <ejb@ql.org> |
| 2 | 7 | ||
| 3 | * When splitting pages using --split-pages, the outlines | 8 | * When splitting pages using --split-pages, the outlines |
qpdf/qpdf.cc
| @@ -322,7 +322,7 @@ class ArgParser | @@ -322,7 +322,7 @@ class ArgParser | ||
| 322 | void argShowLinearization(); | 322 | void argShowLinearization(); |
| 323 | void argShowXref(); | 323 | void argShowXref(); |
| 324 | void argShowObject(char* parameter); | 324 | void argShowObject(char* parameter); |
| 325 | - void argShowObject(); | 325 | + void argRawStreamData(); |
| 326 | void argFilteredStreamData(); | 326 | void argFilteredStreamData(); |
| 327 | void argShowNpages(); | 327 | void argShowNpages(); |
| 328 | void argShowPages(); | 328 | void argShowPages(); |
| @@ -344,10 +344,16 @@ class ArgParser | @@ -344,10 +344,16 @@ class ArgParser | ||
| 344 | void argEndEncrypt(); | 344 | void argEndEncrypt(); |
| 345 | 345 | ||
| 346 | void usage(std::string const& message); | 346 | void usage(std::string const& message); |
| 347 | + void checkCompletion(); | ||
| 347 | void initOptionTable(); | 348 | void initOptionTable(); |
| 348 | - void handleHelpVersion(); | 349 | + void handleHelpArgs(); |
| 349 | void handleArgFileArguments(); | 350 | void handleArgFileArguments(); |
| 351 | + void handleBashArguments(); | ||
| 350 | void readArgsFromFile(char const* filename); | 352 | void readArgsFromFile(char const* filename); |
| 353 | + void doFinalChecks(); | ||
| 354 | + void addOptionsToCompletions(); | ||
| 355 | + void addChoicesToCompletions(std::string const&); | ||
| 356 | + void handleCompletion(); | ||
| 351 | std::vector<PageSpec> parsePagesOptions(); | 357 | std::vector<PageSpec> parsePagesOptions(); |
| 352 | void parseRotationParameter(std::string const&); | 358 | void parseRotationParameter(std::string const&); |
| 353 | std::vector<int> parseNumrange(char const* range, int max, | 359 | std::vector<int> parseNumrange(char const* range, int max, |
| @@ -359,6 +365,12 @@ class ArgParser | @@ -359,6 +365,12 @@ class ArgParser | ||
| 359 | char** argv; | 365 | char** argv; |
| 360 | Options& o; | 366 | Options& o; |
| 361 | int cur_arg; | 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 | std::map<std::string, OptionEntry>* option_table; | 375 | std::map<std::string, OptionEntry>* option_table; |
| 364 | std::map<std::string, OptionEntry> main_option_table; | 376 | std::map<std::string, OptionEntry> main_option_table; |
| @@ -366,14 +378,18 @@ class ArgParser | @@ -366,14 +378,18 @@ class ArgParser | ||
| 366 | std::map<std::string, OptionEntry> encrypt128_option_table; | 378 | std::map<std::string, OptionEntry> encrypt128_option_table; |
| 367 | std::map<std::string, OptionEntry> encrypt256_option_table; | 379 | std::map<std::string, OptionEntry> encrypt256_option_table; |
| 368 | std::vector<PointerHolder<char> > new_argv; | 380 | std::vector<PointerHolder<char> > new_argv; |
| 381 | + std::vector<PointerHolder<char> > bash_argv; | ||
| 369 | PointerHolder<char*> argv_ph; | 382 | PointerHolder<char*> argv_ph; |
| 383 | + PointerHolder<char*> bash_argv_ph; | ||
| 370 | }; | 384 | }; |
| 371 | 385 | ||
| 372 | ArgParser::ArgParser(int argc, char* argv[], Options& o) : | 386 | ArgParser::ArgParser(int argc, char* argv[], Options& o) : |
| 373 | argc(argc), | 387 | argc(argc), |
| 374 | argv(argv), | 388 | argv(argv), |
| 375 | o(o), | 389 | o(o), |
| 376 | - cur_arg(0) | 390 | + cur_arg(0), |
| 391 | + bash_completion(false), | ||
| 392 | + bash_point(0) | ||
| 377 | { | 393 | { |
| 378 | option_table = &main_option_table; | 394 | option_table = &main_option_table; |
| 379 | initOptionTable(); | 395 | initOptionTable(); |
| @@ -496,7 +512,7 @@ ArgParser::initOptionTable() | @@ -496,7 +512,7 @@ ArgParser::initOptionTable() | ||
| 496 | (*t)["show-xref"] = oe_bare(&ArgParser::argShowXref); | 512 | (*t)["show-xref"] = oe_bare(&ArgParser::argShowXref); |
| 497 | (*t)["show-object"] = oe_requiredParameter( | 513 | (*t)["show-object"] = oe_requiredParameter( |
| 498 | &ArgParser::argShowObject, "obj[,gen]"); | 514 | &ArgParser::argShowObject, "obj[,gen]"); |
| 499 | - (*t)["raw-stream-data"] = oe_bare(&ArgParser::argShowObject); | 515 | + (*t)["raw-stream-data"] = oe_bare(&ArgParser::argRawStreamData); |
| 500 | (*t)["filtered-stream-data"] = oe_bare(&ArgParser::argFilteredStreamData); | 516 | (*t)["filtered-stream-data"] = oe_bare(&ArgParser::argFilteredStreamData); |
| 501 | (*t)["show-npages"] = oe_bare(&ArgParser::argShowNpages); | 517 | (*t)["show-npages"] = oe_bare(&ArgParser::argShowNpages); |
| 502 | (*t)["show-pages"] = oe_bare(&ArgParser::argShowPages); | 518 | (*t)["show-pages"] = oe_bare(&ArgParser::argShowPages); |
| @@ -573,9 +589,30 @@ void | @@ -573,9 +589,30 @@ void | ||
| 573 | ArgParser::argEncrypt() | 589 | ArgParser::argEncrypt() |
| 574 | { | 590 | { |
| 575 | ++cur_arg; | 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 | o.user_password = argv[cur_arg++]; | 617 | o.user_password = argv[cur_arg++]; |
| 581 | o.owner_password = argv[cur_arg++]; | 618 | o.owner_password = argv[cur_arg++]; |
| @@ -904,7 +941,7 @@ ArgParser::argShowObject(char* parameter) | @@ -904,7 +941,7 @@ ArgParser::argShowObject(char* parameter) | ||
| 904 | } | 941 | } |
| 905 | 942 | ||
| 906 | void | 943 | void |
| 907 | -ArgParser::argShowObject() | 944 | +ArgParser::argRawStreamData() |
| 908 | { | 945 | { |
| 909 | o.show_raw_stream_data = true; | 946 | o.show_raw_stream_data = true; |
| 910 | } | 947 | } |
| @@ -1098,14 +1135,55 @@ ArgParser::handleArgFileArguments() | @@ -1098,14 +1135,55 @@ ArgParser::handleArgFileArguments() | ||
| 1098 | argv[argc] = 0; | 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 | char const* ArgParser::help = "\ | 1188 | char const* ArgParser::help = "\ |
| 1111 | \n\ | 1189 | \n\ |
| @@ -1127,6 +1205,7 @@ Basic Options\n\ | @@ -1127,6 +1205,7 @@ Basic Options\n\ | ||
| 1127 | --version show version of qpdf\n\ | 1205 | --version show version of qpdf\n\ |
| 1128 | --copyright show qpdf's copyright and license information\n\ | 1206 | --copyright show qpdf's copyright and license information\n\ |
| 1129 | --help show command-line argument help\n\ | 1207 | --help show command-line argument help\n\ |
| 1208 | +--completion-bash output a bash complete command you can eval\n\ | ||
| 1130 | --password=password specify a password for accessing encrypted files\n\ | 1209 | --password=password specify a password for accessing encrypted files\n\ |
| 1131 | --verbose provide additional informational output\n\ | 1210 | --verbose provide additional informational output\n\ |
| 1132 | --progress give progress indicators while writing output\n\ | 1211 | --progress give progress indicators while writing output\n\ |
| @@ -1412,7 +1491,15 @@ void usageExit(std::string const& msg) | @@ -1412,7 +1491,15 @@ void usageExit(std::string const& msg) | ||
| 1412 | void | 1491 | void |
| 1413 | ArgParser::usage(std::string const& message) | 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 | static JSON json_schema() | 1505 | static JSON json_schema() |
| @@ -1718,13 +1805,33 @@ ArgParser::readArgsFromFile(char const* filename) | @@ -1718,13 +1805,33 @@ ArgParser::readArgsFromFile(char const* filename) | ||
| 1718 | } | 1805 | } |
| 1719 | 1806 | ||
| 1720 | void | 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 | std::cout | 1836 | std::cout |
| 1730 | << whoami << " version " << QPDF::QPDFVersion() << std::endl | 1837 | << whoami << " version " << QPDF::QPDFVersion() << std::endl |
| @@ -1733,10 +1840,9 @@ ArgParser::handleHelpVersion() | @@ -1733,10 +1840,9 @@ ArgParser::handleHelpVersion() | ||
| 1733 | exit(0); | 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 | // 1 2 3 4 5 6 7 8 | 1846 | // 1 2 3 4 5 6 7 8 |
| 1741 | // 12345678901234567890123456789012345678901234567890123456789012345678901234567890 | 1847 | // 12345678901234567890123456789012345678901234567890123456789012345678901234567890 |
| 1742 | std::cout | 1848 | std::cout |
| @@ -1776,13 +1882,25 @@ ArgParser::handleHelpVersion() | @@ -1776,13 +1882,25 @@ ArgParser::handleHelpVersion() | ||
| 1776 | exit(0); | 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 | std::cout << help; | 1887 | std::cout << help; |
| 1784 | exit(0); | 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 | void | 1906 | void |
| @@ -1851,9 +1969,37 @@ ArgParser::parseRotationParameter(std::string const& parameter) | @@ -1851,9 +1969,37 @@ ArgParser::parseRotationParameter(std::string const& parameter) | ||
| 1851 | } | 1969 | } |
| 1852 | 1970 | ||
| 1853 | void | 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 | ArgParser::parseOptions() | 1996 | ArgParser::parseOptions() |
| 1855 | { | 1997 | { |
| 1856 | - handleHelpVersion(); // QXXXQ calls std::cout | 1998 | + checkCompletion(); |
| 1999 | + if (! this->bash_completion) | ||
| 2000 | + { | ||
| 2001 | + handleHelpArgs(); | ||
| 2002 | + } | ||
| 1857 | handleArgFileArguments(); | 2003 | handleArgFileArguments(); |
| 1858 | for (cur_arg = 1; cur_arg < argc; ++cur_arg) | 2004 | for (cur_arg = 1; cur_arg < argc; ++cur_arg) |
| 1859 | { | 2005 | { |
| @@ -1957,7 +2103,19 @@ ArgParser::parseOptions() | @@ -1957,7 +2103,19 @@ ArgParser::parseOptions() | ||
| 1957 | usage(std::string("unknown argument ") + arg); | 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 | if (this->option_table != &(this->main_option_table)) | 2119 | if (this->option_table != &(this->main_option_table)) |
| 1962 | { | 2120 | { |
| 1963 | usage("missing -- at end of options"); | 2121 | usage("missing -- at end of options"); |
| @@ -2002,6 +2160,107 @@ ArgParser::parseOptions() | @@ -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 | static void set_qpdf_options(QPDF& pdf, Options& o) | 2264 | static void set_qpdf_options(QPDF& pdf, Options& o) |
| 2006 | { | 2265 | { |
| 2007 | if (o.ignore_xref_streams) | 2266 | if (o.ignore_xref_streams) |
qpdf/qtest/qpdf.test
| @@ -100,6 +100,36 @@ $td->runtest("UTF-16 encoding errors", | @@ -100,6 +100,36 @@ $td->runtest("UTF-16 encoding errors", | ||
| 100 | {$td->FILE => "unicode-errors.out", $td->EXIT_STATUS => 0}, | 100 | {$td->FILE => "unicode-errors.out", $td->EXIT_STATUS => 0}, |
| 101 | $td->NORMALIZE_NEWLINES); | 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 | show_ntests(); | 133 | show_ntests(); |
| 104 | # ---------- | 134 | # ---------- |
| 105 | $td->notify("--- Argument Parsing ---"); | 135 | $td->notify("--- Argument Parsing ---"); |
| @@ -3144,6 +3174,24 @@ sub show_ntests | @@ -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 | sub check_pdf | 3195 | sub check_pdf |
| 3148 | { | 3196 | { |
| 3149 | my ($description, $command, $output, $status) = @_; | 3197 | my ($description, $command, $output, $status) = @_; |
qpdf/qtest/qpdf/completion-decode-l.out
0 → 100644
qpdf/qtest/qpdf/completion-decode-level.out
0 → 100644
qpdf/qtest/qpdf/completion-enc.out
0 → 100644
| 1 | +--encrypt |
qpdf/qtest/qpdf/completion-encrypt-128.out
0 → 100644
qpdf/qtest/qpdf/completion-encrypt-256.out
0 → 100644
qpdf/qtest/qpdf/completion-encrypt-40.out
0 → 100644
qpdf/qtest/qpdf/completion-encrypt-bad.out
0 → 100644
qpdf/qtest/qpdf/completion-encrypt-u-o.out
0 → 100644
qpdf/qtest/qpdf/completion-encrypt-u.out
0 → 100644
| 1 | +owner-password |
qpdf/qtest/qpdf/completion-encrypt.out
0 → 100644
qpdf/qtest/qpdf/completion-later-arg.out
0 → 100644
qpdf/qtest/qpdf/completion-split.out
0 → 100644
qpdf/qtest/qpdf/completion-top-arg.out
0 → 100644
qpdf/qtest/qpdf/completion-top.out
0 → 100644
qpdf/qtest/qpdf/encrypt-u
0 → 100644
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 | +} |