Commit 64c1579544daef83af1494aa16ee6bc081347d39

Authored by Jay Berkenbilt
1 parent 2e306d32

Support zsh completion

ChangeLog
  1 +2018-12-23 Jay Berkenbilt <ejb@ql.org>
  2 +
  3 + * Tweak completion so it works with zsh as well using
  4 + bashcompinit.
  5 +
1 6 2018-12-22 Jay Berkenbilt <ejb@ql.org>
2 7  
3 8 * Add new options --json, --json-key, and --json-object to
... ...
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&amp; 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&amp; 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
  1 +--decode-level=generalized
  2 +!--decode-level=all
  3 +!--decode-level=none
  4 +!all
  5 +!generalized
  6 +!none
... ...
qpdf/qtest/qpdf/completion-decode-level-g.out 0 โ†’ 100644
  1 +generalized
  2 +!--decode-level=all
  3 +!--decode-level=generalized
  4 +!--decode-level=none
  5 +!all
  6 +!none
... ...
qpdf/qtest/qpdf/completion-decode-level-zsh.out 0 โ†’ 100644
  1 +--decode-level=all
  2 +--decode-level=generalized
  3 +--decode-level=none
  4 +!--help
  5 +!all
  6 +!generalized
  7 +!none
... ...
qpdf/qtest/qpdf/completion-decode-level.out
1 1 all
2 2 generalized
3 3 none
  4 +!--decode-level=all
  5 +!--decode-level=generalized
  6 +!--decode-level=none
4 7 !--help
... ...