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 | 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& 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& 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->runtest("UTF-16 encoding errors", |
| 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
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 | +} | ... | ... |