2
1
mirror of https://github.com/qpdf/qpdf.git synced 2024-06-03 19:00:51 +00:00

Support zsh completion

This commit is contained in:
Jay Berkenbilt 2018-12-23 10:45:24 -05:00
parent 2e306d3249
commit 64c1579544
8 changed files with 155 additions and 20 deletions

View File

@ -1,3 +1,8 @@
2018-12-23 Jay Berkenbilt <ejb@ql.org>
* Tweak completion so it works with zsh as well using
bashcompinit.
2018-12-22 Jay Berkenbilt <ejb@ql.org> 2018-12-22 Jay Berkenbilt <ejb@ql.org>
* Add new options --json, --json-key, and --json-object to * Add new options --json, --json-key, and --json-object to

View File

@ -303,11 +303,12 @@ make
<sect1 id="ref.shell-completion"> <sect1 id="ref.shell-completion">
<title>Shell Completion</title> <title>Shell Completion</title>
<para> <para>
Starting in qpdf version 8.3.0, qpdf provides its own bash Starting in qpdf version 8.3.0, qpdf provides its own completion
completion support. You can enable bash completion with support for zsh and bash. You can enable bash completion with
<command>eval $(qpdf --completion-bash)</command>. If <command>eval $(qpdf --completion-bash)</command> and zsh
<command>qpdf</command> is not in your path, you should invoke it completion with <command>eval $(qpdf --completion-zsh)</command>.
above with an absolute path. If you invoke it with a relative If <command>qpdf</command> is not in your path, you should invoke
it above with an absolute path. If you invoke it with a relative
path, it will warn you, and the completion won't work if you're in path, it will warn you, and the completion won't work if you're in
a different directory. a different directory.
</para> </para>
@ -342,6 +343,24 @@ make
</para> </para>
</listitem> </listitem>
</varlistentry> </varlistentry>
<varlistentry>
<term><option>--completion-bash</option></term>
<listitem>
<para>
Output a completion command you can eval to enable shell
completion from bash.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>--completion-zsh</option></term>
<listitem>
<para>
Output a completion command you can eval to enable shell
completion from zsh.
</para>
</listitem>
</varlistentry>
<varlistentry> <varlistentry>
<term><option>--password=password</option></term> <term><option>--password=password</option></term>
<listitem> <listitem>

View File

@ -446,6 +446,7 @@ class ArgParser
void argVersion(); void argVersion();
void argCopyright(); void argCopyright();
void argCompletionBash(); void argCompletionBash();
void argCompletionZsh();
void argJsonHelp(); void argJsonHelp();
void argPositional(char* arg); void argPositional(char* arg);
void argPassword(char* parameter); void argPassword(char* parameter);
@ -520,7 +521,7 @@ class ArgParser
void readArgsFromFile(char const* filename); void readArgsFromFile(char const* filename);
void doFinalChecks(); void doFinalChecks();
void addOptionsToCompletions(); void addOptionsToCompletions();
void addChoicesToCompletions(std::string const&); void addChoicesToCompletions(std::string const&, std::string const&);
void handleCompletion(); void handleCompletion();
std::vector<PageSpec> parsePagesOptions(); std::vector<PageSpec> parsePagesOptions();
void parseRotationParameter(std::string const&); void parseRotationParameter(std::string const&);
@ -534,6 +535,7 @@ class ArgParser
Options& o; Options& o;
int cur_arg; int cur_arg;
bool bash_completion; bool bash_completion;
bool zsh_completion;
std::string bash_prev; std::string bash_prev;
std::string bash_cur; std::string bash_cur;
std::string bash_line; std::string bash_line;
@ -556,7 +558,8 @@ ArgParser::ArgParser(int argc, char* argv[], Options& o) :
argv(argv), argv(argv),
o(o), o(o),
cur_arg(0), cur_arg(0),
bash_completion(false) bash_completion(false),
zsh_completion(false)
{ {
option_table = &main_option_table; option_table = &main_option_table;
initOptionTable(); initOptionTable();
@ -619,6 +622,7 @@ ArgParser::initOptionTable()
(*t)["version"] = oe_bare(&ArgParser::argVersion); (*t)["version"] = oe_bare(&ArgParser::argVersion);
(*t)["copyright"] = oe_bare(&ArgParser::argCopyright); (*t)["copyright"] = oe_bare(&ArgParser::argCopyright);
(*t)["completion-bash"] = oe_bare(&ArgParser::argCompletionBash); (*t)["completion-bash"] = oe_bare(&ArgParser::argCompletionBash);
(*t)["completion-zsh"] = oe_bare(&ArgParser::argCompletionZsh);
(*t)["json-help"] = oe_bare(&ArgParser::argJsonHelp); (*t)["json-help"] = oe_bare(&ArgParser::argJsonHelp);
t = &this->main_option_table; t = &this->main_option_table;
@ -809,6 +813,9 @@ ArgParser::argHelp()
void void
ArgParser::argCompletionBash() ArgParser::argCompletionBash()
{ {
std::cout << "complete -o bashdefault -o default -o nospace"
<< " -C " << argv[0] << " " << whoami << std::endl;
// Put output before error so calling from zsh works properly
std::string path = argv[0]; std::string path = argv[0];
size_t slash = path.find('/'); size_t slash = path.find('/');
if ((slash != 0) && (slash != std::string::npos)) if ((slash != 0) && (slash != std::string::npos))
@ -816,10 +823,14 @@ ArgParser::argCompletionBash()
std::cerr << "WARNING: qpdf completion enabled" std::cerr << "WARNING: qpdf completion enabled"
<< " using relative path to qpdf" << std::endl; << " using relative path to qpdf" << std::endl;
} }
std::cout << "complete -o bashdefault -o default -o nospace"
<< " -C " << argv[0] << " " << whoami << std::endl;
} }
void
ArgParser::argCompletionZsh()
{
std::cout << "autoload -U +X bashcompinit && bashcompinit && ";
argCompletionBash();
}
void void
ArgParser::argJsonHelp() ArgParser::argJsonHelp()
{ {
@ -1543,6 +1554,7 @@ Basic Options\n\
--copyright show qpdf's copyright and license information\n\ --copyright show qpdf's copyright and license information\n\
--help show command-line argument help\n\ --help show command-line argument help\n\
--completion-bash output a bash complete command you can eval\n\ --completion-bash output a bash complete command you can eval\n\
--completion-zsh output a zsh complete command you can eval\n\
--password=password specify a password for accessing encrypted files\n\ --password=password specify a password for accessing encrypted files\n\
--verbose provide additional informational output\n\ --verbose provide additional informational output\n\
--progress give progress indicators while writing output\n\ --progress give progress indicators while writing output\n\
@ -2198,13 +2210,61 @@ ArgParser::checkCompletion()
// cursor for completion purposes. // cursor for completion purposes.
bash_line = bash_line.substr(0, p); bash_line = bash_line.substr(0, p);
} }
if (argc >= 4) // Set bash_cur and bash_prev based on bash_line rather than
// relying on argv. This enables us to use bashcompinit to get
// completion in zsh too since bashcompinit sets COMP_LINE and
// COMP_POINT but doesn't invoke the command with options like
// bash does.
// p is equal to length of the string. Walk backwards looking
// for the first separator. bash_cur is everything after the
// last separator, possibly empty.
char sep(0);
while (--p > 0)
{ {
bash_cur = argv[2]; char ch = bash_line.at(p);
bash_prev = argv[3]; if ((ch == ' ') || (ch == '=') || (ch == ':'))
handleBashArguments(); {
bash_completion = true; sep = ch;
break;
}
} }
bash_cur = bash_line.substr(1+p, std::string::npos);
if ((sep == ':') || (sep == '='))
{
// Bash sets prev to the non-space separator if any.
// Actually, if there are multiple separators in a row,
// they are all included in prev, but that detail is not
// important to us and not worth coding.
bash_prev = bash_line.substr(p, 1);
}
else
{
// Go back to the last separator and set prev based on
// that.
int p1 = p;
while (--p1 > 0)
{
char ch = bash_line.at(p1);
if ((ch == ' ') || (ch == ':') || (ch == '='))
{
bash_prev = bash_line.substr(p1 + 1, p - p1 - 1);
break;
}
}
}
if (bash_prev.empty())
{
bash_prev = bash_line.substr(0, p);
}
if (argc == 1)
{
// This is probably zsh using bashcompinit. There are a
// few differences in the expected output.
zsh_completion = true;
}
handleBashArguments();
bash_completion = true;
} }
} }
@ -2377,7 +2437,8 @@ ArgParser::doFinalChecks()
} }
void void
ArgParser::addChoicesToCompletions(std::string const& option) ArgParser::addChoicesToCompletions(std::string const& option,
std::string const& extra_prefix)
{ {
if (this->option_table->count(option) != 0) if (this->option_table->count(option) != 0)
{ {
@ -2385,7 +2446,7 @@ ArgParser::addChoicesToCompletions(std::string const& option)
for (std::set<std::string>::iterator iter = oe.choices.begin(); for (std::set<std::string>::iterator iter = oe.choices.begin();
iter != oe.choices.end(); ++iter) iter != oe.choices.end(); ++iter)
{ {
completions.insert(*iter); completions.insert(extra_prefix + *iter);
} }
} }
} }
@ -2414,6 +2475,7 @@ ArgParser::addOptionsToCompletions()
void void
ArgParser::handleCompletion() ArgParser::handleCompletion()
{ {
std::string extra_prefix;
if (this->completions.empty()) if (this->completions.empty())
{ {
// Detect --option=... Bash treats the = as a word separator. // Detect --option=... Bash treats the = as a word separator.
@ -2450,7 +2512,12 @@ ArgParser::handleCompletion()
} }
if (! choice_option.empty()) if (! choice_option.empty())
{ {
addChoicesToCompletions(choice_option); if (zsh_completion)
{
// zsh wants --option=choice rather than just choice
extra_prefix = "--" + choice_option + "=";
}
addChoicesToCompletions(choice_option, extra_prefix);
} }
else if ((! bash_cur.empty()) && (bash_cur.at(0) == '-')) else if ((! bash_cur.empty()) && (bash_cur.at(0) == '-'))
{ {
@ -2467,11 +2534,12 @@ ArgParser::handleCompletion()
} }
} }
} }
std::string prefix = extra_prefix + bash_cur;
for (std::set<std::string>::iterator iter = completions.begin(); for (std::set<std::string>::iterator iter = completions.begin();
iter != completions.end(); ++iter) iter != completions.end(); ++iter)
{ {
if (this->bash_cur.empty() || if (prefix.empty() ||
((*iter).substr(0, bash_cur.length()) == bash_cur)) ((*iter).substr(0, prefix.length()) == prefix))
{ {
std::cout << *iter << std::endl; std::cout << *iter << std::endl;
} }

View File

@ -116,6 +116,7 @@ my @completion_tests = (
['qpdf --decode-l', undef, 'decode-l'], ['qpdf --decode-l', undef, 'decode-l'],
['qpdf --decode-lzzz', 15, 'decode-l'], ['qpdf --decode-lzzz', 15, 'decode-l'],
['qpdf --decode-level=', undef, 'decode-level'], ['qpdf --decode-level=', undef, 'decode-level'],
['qpdf --decode-level=g', undef, 'decode-level-g'],
['qpdf --check -', undef, 'later-arg'], ['qpdf --check -', undef, 'later-arg'],
['qpdf infile outfile oops --ch', undef, 'usage-empty'], ['qpdf infile outfile oops --ch', undef, 'usage-empty'],
['qpdf --encrypt \'user " password\' ', undef, 'quoting'], ['qpdf --encrypt \'user " password\' ', undef, 'quoting'],
@ -124,16 +125,26 @@ my @completion_tests = (
['qpdf --encrypt "user pass\'word" ', undef, 'quoting'], ['qpdf --encrypt "user pass\'word" ', undef, 'quoting'],
['qpdf --encrypt user\ password ', undef, 'quoting'], ['qpdf --encrypt user\ password ', undef, 'quoting'],
); );
$n_tests += scalar(@completion_tests); $n_tests += 2 * scalar(@completion_tests);
foreach my $c (@completion_tests) foreach my $c (@completion_tests)
{ {
my ($cmd, $point, $description) = @$c; my ($cmd, $point, $description) = @$c;
my $out = "completion-$description.out"; my $out = "completion-$description.out";
my $zout = "completion-$description-zsh.out";
if (! -f $zout)
{
$zout = $out;
}
$td->runtest("bash completion: $description", $td->runtest("bash completion: $description",
{$td->COMMAND => [@{bash_completion($cmd, $point)}], {$td->COMMAND => [@{bash_completion($cmd, $point)}],
$td->FILTER => "perl filter-completion.pl $out"}, $td->FILTER => "perl filter-completion.pl $out"},
{$td->FILE => "$out", $td->EXIT_STATUS => 0}, {$td->FILE => "$out", $td->EXIT_STATUS => 0},
$td->NORMALIZE_NEWLINES); $td->NORMALIZE_NEWLINES);
$td->runtest("zsh completion: $description",
{$td->COMMAND => [@{zsh_completion($cmd, $point)}],
$td->FILTER => "perl filter-completion.pl $zout"},
{$td->FILE => "$zout", $td->EXIT_STATUS => 0},
$td->NORMALIZE_NEWLINES);
} }
show_ntests(); show_ntests();
@ -3208,6 +3219,16 @@ sub bash_completion
"qpdf", $this, $cur, $prev]; "qpdf", $this, $cur, $prev];
} }
sub zsh_completion
{
my ($line, $point) = @_;
if (! defined $point)
{
$point = length($line);
}
['env', "COMP_LINE=$line", "COMP_POINT=$point", "qpdf"];
}
sub check_pdf sub check_pdf
{ {
my ($description, $command, $output, $status) = @_; my ($description, $command, $output, $status) = @_;

View File

@ -0,0 +1,6 @@
--decode-level=generalized
!--decode-level=all
!--decode-level=none
!all
!generalized
!none

View File

@ -0,0 +1,6 @@
generalized
!--decode-level=all
!--decode-level=generalized
!--decode-level=none
!all
!none

View File

@ -0,0 +1,7 @@
--decode-level=all
--decode-level=generalized
--decode-level=none
!--help
!all
!generalized
!none

View File

@ -1,4 +1,7 @@
all all
generalized generalized
none none
!--decode-level=all
!--decode-level=generalized
!--decode-level=none
!--help !--help