Commit 64c1579544daef83af1494aa16ee6bc081347d39
1 parent
2e306d32
Support zsh completion
Showing
8 changed files
with
155 additions
and
20 deletions
ChangeLog
manual/qpdf-manual.xml
| ... | ... | @@ -303,11 +303,12 @@ make |
| 303 | 303 | <sect1 id="ref.shell-completion"> |
| 304 | 304 | <title>Shell Completion</title> |
| 305 | 305 | <para> |
| 306 | - Starting in qpdf version 8.3.0, qpdf provides its own bash | |
| 307 | - completion support. You can enable bash completion with | |
| 308 | - <command>eval $(qpdf --completion-bash)</command>. If | |
| 309 | - <command>qpdf</command> is not in your path, you should invoke it | |
| 310 | - above with an absolute path. If you invoke it with a relative | |
| 306 | + Starting in qpdf version 8.3.0, qpdf provides its own completion | |
| 307 | + support for zsh and bash. You can enable bash completion with | |
| 308 | + <command>eval $(qpdf --completion-bash)</command> and zsh | |
| 309 | + completion with <command>eval $(qpdf --completion-zsh)</command>. | |
| 310 | + If <command>qpdf</command> is not in your path, you should invoke | |
| 311 | + it above with an absolute path. If you invoke it with a relative | |
| 311 | 312 | path, it will warn you, and the completion won't work if you're in |
| 312 | 313 | a different directory. |
| 313 | 314 | </para> |
| ... | ... | @@ -343,6 +344,24 @@ make |
| 343 | 344 | </listitem> |
| 344 | 345 | </varlistentry> |
| 345 | 346 | <varlistentry> |
| 347 | + <term><option>--completion-bash</option></term> | |
| 348 | + <listitem> | |
| 349 | + <para> | |
| 350 | + Output a completion command you can eval to enable shell | |
| 351 | + completion from bash. | |
| 352 | + </para> | |
| 353 | + </listitem> | |
| 354 | + </varlistentry> | |
| 355 | + <varlistentry> | |
| 356 | + <term><option>--completion-zsh</option></term> | |
| 357 | + <listitem> | |
| 358 | + <para> | |
| 359 | + Output a completion command you can eval to enable shell | |
| 360 | + completion from zsh. | |
| 361 | + </para> | |
| 362 | + </listitem> | |
| 363 | + </varlistentry> | |
| 364 | + <varlistentry> | |
| 346 | 365 | <term><option>--password=password</option></term> |
| 347 | 366 | <listitem> |
| 348 | 367 | <para> | ... | ... |
qpdf/qpdf.cc
| ... | ... | @@ -446,6 +446,7 @@ class ArgParser |
| 446 | 446 | void argVersion(); |
| 447 | 447 | void argCopyright(); |
| 448 | 448 | void argCompletionBash(); |
| 449 | + void argCompletionZsh(); | |
| 449 | 450 | void argJsonHelp(); |
| 450 | 451 | void argPositional(char* arg); |
| 451 | 452 | void argPassword(char* parameter); |
| ... | ... | @@ -520,7 +521,7 @@ class ArgParser |
| 520 | 521 | void readArgsFromFile(char const* filename); |
| 521 | 522 | void doFinalChecks(); |
| 522 | 523 | void addOptionsToCompletions(); |
| 523 | - void addChoicesToCompletions(std::string const&); | |
| 524 | + void addChoicesToCompletions(std::string const&, std::string const&); | |
| 524 | 525 | void handleCompletion(); |
| 525 | 526 | std::vector<PageSpec> parsePagesOptions(); |
| 526 | 527 | void parseRotationParameter(std::string const&); |
| ... | ... | @@ -534,6 +535,7 @@ class ArgParser |
| 534 | 535 | Options& o; |
| 535 | 536 | int cur_arg; |
| 536 | 537 | bool bash_completion; |
| 538 | + bool zsh_completion; | |
| 537 | 539 | std::string bash_prev; |
| 538 | 540 | std::string bash_cur; |
| 539 | 541 | std::string bash_line; |
| ... | ... | @@ -556,7 +558,8 @@ ArgParser::ArgParser(int argc, char* argv[], Options& o) : |
| 556 | 558 | argv(argv), |
| 557 | 559 | o(o), |
| 558 | 560 | cur_arg(0), |
| 559 | - bash_completion(false) | |
| 561 | + bash_completion(false), | |
| 562 | + zsh_completion(false) | |
| 560 | 563 | { |
| 561 | 564 | option_table = &main_option_table; |
| 562 | 565 | initOptionTable(); |
| ... | ... | @@ -619,6 +622,7 @@ ArgParser::initOptionTable() |
| 619 | 622 | (*t)["version"] = oe_bare(&ArgParser::argVersion); |
| 620 | 623 | (*t)["copyright"] = oe_bare(&ArgParser::argCopyright); |
| 621 | 624 | (*t)["completion-bash"] = oe_bare(&ArgParser::argCompletionBash); |
| 625 | + (*t)["completion-zsh"] = oe_bare(&ArgParser::argCompletionZsh); | |
| 622 | 626 | (*t)["json-help"] = oe_bare(&ArgParser::argJsonHelp); |
| 623 | 627 | |
| 624 | 628 | t = &this->main_option_table; |
| ... | ... | @@ -809,6 +813,9 @@ ArgParser::argHelp() |
| 809 | 813 | void |
| 810 | 814 | ArgParser::argCompletionBash() |
| 811 | 815 | { |
| 816 | + std::cout << "complete -o bashdefault -o default -o nospace" | |
| 817 | + << " -C " << argv[0] << " " << whoami << std::endl; | |
| 818 | + // Put output before error so calling from zsh works properly | |
| 812 | 819 | std::string path = argv[0]; |
| 813 | 820 | size_t slash = path.find('/'); |
| 814 | 821 | if ((slash != 0) && (slash != std::string::npos)) |
| ... | ... | @@ -816,11 +823,15 @@ ArgParser::argCompletionBash() |
| 816 | 823 | std::cerr << "WARNING: qpdf completion enabled" |
| 817 | 824 | << " using relative path to qpdf" << std::endl; |
| 818 | 825 | } |
| 819 | - std::cout << "complete -o bashdefault -o default -o nospace" | |
| 820 | - << " -C " << argv[0] << " " << whoami << std::endl; | |
| 821 | 826 | } |
| 822 | 827 | |
| 823 | 828 | void |
| 829 | +ArgParser::argCompletionZsh() | |
| 830 | +{ | |
| 831 | + std::cout << "autoload -U +X bashcompinit && bashcompinit && "; | |
| 832 | + argCompletionBash(); | |
| 833 | +} | |
| 834 | +void | |
| 824 | 835 | ArgParser::argJsonHelp() |
| 825 | 836 | { |
| 826 | 837 | // Make sure the output looks right on an 80-column display. |
| ... | ... | @@ -1543,6 +1554,7 @@ Basic Options\n\ |
| 1543 | 1554 | --copyright show qpdf's copyright and license information\n\ |
| 1544 | 1555 | --help show command-line argument help\n\ |
| 1545 | 1556 | --completion-bash output a bash complete command you can eval\n\ |
| 1557 | +--completion-zsh output a zsh complete command you can eval\n\ | |
| 1546 | 1558 | --password=password specify a password for accessing encrypted files\n\ |
| 1547 | 1559 | --verbose provide additional informational output\n\ |
| 1548 | 1560 | --progress give progress indicators while writing output\n\ |
| ... | ... | @@ -2198,13 +2210,61 @@ ArgParser::checkCompletion() |
| 2198 | 2210 | // cursor for completion purposes. |
| 2199 | 2211 | bash_line = bash_line.substr(0, p); |
| 2200 | 2212 | } |
| 2201 | - if (argc >= 4) | |
| 2213 | + // Set bash_cur and bash_prev based on bash_line rather than | |
| 2214 | + // relying on argv. This enables us to use bashcompinit to get | |
| 2215 | + // completion in zsh too since bashcompinit sets COMP_LINE and | |
| 2216 | + // COMP_POINT but doesn't invoke the command with options like | |
| 2217 | + // bash does. | |
| 2218 | + | |
| 2219 | + // p is equal to length of the string. Walk backwards looking | |
| 2220 | + // for the first separator. bash_cur is everything after the | |
| 2221 | + // last separator, possibly empty. | |
| 2222 | + char sep(0); | |
| 2223 | + while (--p > 0) | |
| 2224 | + { | |
| 2225 | + char ch = bash_line.at(p); | |
| 2226 | + if ((ch == ' ') || (ch == '=') || (ch == ':')) | |
| 2227 | + { | |
| 2228 | + sep = ch; | |
| 2229 | + break; | |
| 2230 | + } | |
| 2231 | + } | |
| 2232 | + bash_cur = bash_line.substr(1+p, std::string::npos); | |
| 2233 | + if ((sep == ':') || (sep == '=')) | |
| 2202 | 2234 | { |
| 2203 | - bash_cur = argv[2]; | |
| 2204 | - bash_prev = argv[3]; | |
| 2205 | - handleBashArguments(); | |
| 2206 | - bash_completion = true; | |
| 2235 | + // Bash sets prev to the non-space separator if any. | |
| 2236 | + // Actually, if there are multiple separators in a row, | |
| 2237 | + // they are all included in prev, but that detail is not | |
| 2238 | + // important to us and not worth coding. | |
| 2239 | + bash_prev = bash_line.substr(p, 1); | |
| 2207 | 2240 | } |
| 2241 | + else | |
| 2242 | + { | |
| 2243 | + // Go back to the last separator and set prev based on | |
| 2244 | + // that. | |
| 2245 | + int p1 = p; | |
| 2246 | + while (--p1 > 0) | |
| 2247 | + { | |
| 2248 | + char ch = bash_line.at(p1); | |
| 2249 | + if ((ch == ' ') || (ch == ':') || (ch == '=')) | |
| 2250 | + { | |
| 2251 | + bash_prev = bash_line.substr(p1 + 1, p - p1 - 1); | |
| 2252 | + break; | |
| 2253 | + } | |
| 2254 | + } | |
| 2255 | + } | |
| 2256 | + if (bash_prev.empty()) | |
| 2257 | + { | |
| 2258 | + bash_prev = bash_line.substr(0, p); | |
| 2259 | + } | |
| 2260 | + if (argc == 1) | |
| 2261 | + { | |
| 2262 | + // This is probably zsh using bashcompinit. There are a | |
| 2263 | + // few differences in the expected output. | |
| 2264 | + zsh_completion = true; | |
| 2265 | + } | |
| 2266 | + handleBashArguments(); | |
| 2267 | + bash_completion = true; | |
| 2208 | 2268 | } |
| 2209 | 2269 | } |
| 2210 | 2270 | |
| ... | ... | @@ -2377,7 +2437,8 @@ ArgParser::doFinalChecks() |
| 2377 | 2437 | } |
| 2378 | 2438 | |
| 2379 | 2439 | void |
| 2380 | -ArgParser::addChoicesToCompletions(std::string const& option) | |
| 2440 | +ArgParser::addChoicesToCompletions(std::string const& option, | |
| 2441 | + std::string const& extra_prefix) | |
| 2381 | 2442 | { |
| 2382 | 2443 | if (this->option_table->count(option) != 0) |
| 2383 | 2444 | { |
| ... | ... | @@ -2385,7 +2446,7 @@ ArgParser::addChoicesToCompletions(std::string const& option) |
| 2385 | 2446 | for (std::set<std::string>::iterator iter = oe.choices.begin(); |
| 2386 | 2447 | iter != oe.choices.end(); ++iter) |
| 2387 | 2448 | { |
| 2388 | - completions.insert(*iter); | |
| 2449 | + completions.insert(extra_prefix + *iter); | |
| 2389 | 2450 | } |
| 2390 | 2451 | } |
| 2391 | 2452 | } |
| ... | ... | @@ -2414,6 +2475,7 @@ ArgParser::addOptionsToCompletions() |
| 2414 | 2475 | void |
| 2415 | 2476 | ArgParser::handleCompletion() |
| 2416 | 2477 | { |
| 2478 | + std::string extra_prefix; | |
| 2417 | 2479 | if (this->completions.empty()) |
| 2418 | 2480 | { |
| 2419 | 2481 | // Detect --option=... Bash treats the = as a word separator. |
| ... | ... | @@ -2450,7 +2512,12 @@ ArgParser::handleCompletion() |
| 2450 | 2512 | } |
| 2451 | 2513 | if (! choice_option.empty()) |
| 2452 | 2514 | { |
| 2453 | - addChoicesToCompletions(choice_option); | |
| 2515 | + if (zsh_completion) | |
| 2516 | + { | |
| 2517 | + // zsh wants --option=choice rather than just choice | |
| 2518 | + extra_prefix = "--" + choice_option + "="; | |
| 2519 | + } | |
| 2520 | + addChoicesToCompletions(choice_option, extra_prefix); | |
| 2454 | 2521 | } |
| 2455 | 2522 | else if ((! bash_cur.empty()) && (bash_cur.at(0) == '-')) |
| 2456 | 2523 | { |
| ... | ... | @@ -2467,11 +2534,12 @@ ArgParser::handleCompletion() |
| 2467 | 2534 | } |
| 2468 | 2535 | } |
| 2469 | 2536 | } |
| 2537 | + std::string prefix = extra_prefix + bash_cur; | |
| 2470 | 2538 | for (std::set<std::string>::iterator iter = completions.begin(); |
| 2471 | 2539 | iter != completions.end(); ++iter) |
| 2472 | 2540 | { |
| 2473 | - if (this->bash_cur.empty() || | |
| 2474 | - ((*iter).substr(0, bash_cur.length()) == bash_cur)) | |
| 2541 | + if (prefix.empty() || | |
| 2542 | + ((*iter).substr(0, prefix.length()) == prefix)) | |
| 2475 | 2543 | { |
| 2476 | 2544 | std::cout << *iter << std::endl; |
| 2477 | 2545 | } | ... | ... |
qpdf/qtest/qpdf.test
| ... | ... | @@ -116,6 +116,7 @@ my @completion_tests = ( |
| 116 | 116 | ['qpdf --decode-l', undef, 'decode-l'], |
| 117 | 117 | ['qpdf --decode-lzzz', 15, 'decode-l'], |
| 118 | 118 | ['qpdf --decode-level=', undef, 'decode-level'], |
| 119 | + ['qpdf --decode-level=g', undef, 'decode-level-g'], | |
| 119 | 120 | ['qpdf --check -', undef, 'later-arg'], |
| 120 | 121 | ['qpdf infile outfile oops --ch', undef, 'usage-empty'], |
| 121 | 122 | ['qpdf --encrypt \'user " password\' ', undef, 'quoting'], |
| ... | ... | @@ -124,16 +125,26 @@ my @completion_tests = ( |
| 124 | 125 | ['qpdf --encrypt "user pass\'word" ', undef, 'quoting'], |
| 125 | 126 | ['qpdf --encrypt user\ password ', undef, 'quoting'], |
| 126 | 127 | ); |
| 127 | -$n_tests += scalar(@completion_tests); | |
| 128 | +$n_tests += 2 * scalar(@completion_tests); | |
| 128 | 129 | foreach my $c (@completion_tests) |
| 129 | 130 | { |
| 130 | 131 | my ($cmd, $point, $description) = @$c; |
| 131 | 132 | my $out = "completion-$description.out"; |
| 133 | + my $zout = "completion-$description-zsh.out"; | |
| 134 | + if (! -f $zout) | |
| 135 | + { | |
| 136 | + $zout = $out; | |
| 137 | + } | |
| 132 | 138 | $td->runtest("bash completion: $description", |
| 133 | 139 | {$td->COMMAND => [@{bash_completion($cmd, $point)}], |
| 134 | 140 | $td->FILTER => "perl filter-completion.pl $out"}, |
| 135 | 141 | {$td->FILE => "$out", $td->EXIT_STATUS => 0}, |
| 136 | 142 | $td->NORMALIZE_NEWLINES); |
| 143 | + $td->runtest("zsh completion: $description", | |
| 144 | + {$td->COMMAND => [@{zsh_completion($cmd, $point)}], | |
| 145 | + $td->FILTER => "perl filter-completion.pl $zout"}, | |
| 146 | + {$td->FILE => "$zout", $td->EXIT_STATUS => 0}, | |
| 147 | + $td->NORMALIZE_NEWLINES); | |
| 137 | 148 | } |
| 138 | 149 | |
| 139 | 150 | show_ntests(); |
| ... | ... | @@ -3208,6 +3219,16 @@ sub bash_completion |
| 3208 | 3219 | "qpdf", $this, $cur, $prev]; |
| 3209 | 3220 | } |
| 3210 | 3221 | |
| 3222 | +sub zsh_completion | |
| 3223 | +{ | |
| 3224 | + my ($line, $point) = @_; | |
| 3225 | + if (! defined $point) | |
| 3226 | + { | |
| 3227 | + $point = length($line); | |
| 3228 | + } | |
| 3229 | + ['env', "COMP_LINE=$line", "COMP_POINT=$point", "qpdf"]; | |
| 3230 | +} | |
| 3231 | + | |
| 3211 | 3232 | sub check_pdf |
| 3212 | 3233 | { |
| 3213 | 3234 | my ($description, $command, $output, $status) = @_; | ... | ... |
qpdf/qtest/qpdf/completion-decode-level-g-zsh.out
0 โ 100644
qpdf/qtest/qpdf/completion-decode-level-g.out
0 โ 100644
qpdf/qtest/qpdf/completion-decode-level-zsh.out
0 โ 100644