aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJay Berkenbilt <ejb@ql.org>2018-12-23 16:45:24 +0100
committerJay Berkenbilt <ejb@ql.org>2018-12-23 17:21:59 +0100
commit64c1579544daef83af1494aa16ee6bc081347d39 (patch)
tree7f14d2de61748107305f3ce40374591235fdb8db
parent2e306d32494bede29b995f34cacafd9d28b94500 (diff)
downloadqpdf-64c1579544daef83af1494aa16ee6bc081347d39.tar.zst
Support zsh completion
-rw-r--r--ChangeLog5
-rw-r--r--manual/qpdf-manual.xml29
-rw-r--r--qpdf/qpdf.cc96
-rw-r--r--qpdf/qtest/qpdf.test23
-rw-r--r--qpdf/qtest/qpdf/completion-decode-level-g-zsh.out6
-rw-r--r--qpdf/qtest/qpdf/completion-decode-level-g.out6
-rw-r--r--qpdf/qtest/qpdf/completion-decode-level-zsh.out7
-rw-r--r--qpdf/qtest/qpdf/completion-decode-level.out3
8 files changed, 155 insertions, 20 deletions
diff --git a/ChangeLog b/ChangeLog
index 2a6cba2e..710ac4da 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -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>
* Add new options --json, --json-key, and --json-object to
diff --git a/manual/qpdf-manual.xml b/manual/qpdf-manual.xml
index b4b07f4b..2ccaafd3 100644
--- a/manual/qpdf-manual.xml
+++ b/manual/qpdf-manual.xml
@@ -303,11 +303,12 @@ make
<sect1 id="ref.shell-completion">
<title>Shell Completion</title>
<para>
- Starting in qpdf version 8.3.0, qpdf provides its own bash
- completion support. You can enable bash completion with
- <command>eval $(qpdf --completion-bash)</command>. 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
+ Starting in qpdf version 8.3.0, qpdf provides its own completion
+ support for zsh and bash. You can enable bash completion with
+ <command>eval $(qpdf --completion-bash)</command> and zsh
+ completion with <command>eval $(qpdf --completion-zsh)</command>.
+ 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
a different directory.
</para>
@@ -343,6 +344,24 @@ make
</listitem>
</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>
<term><option>--password=password</option></term>
<listitem>
<para>
diff --git a/qpdf/qpdf.cc b/qpdf/qpdf.cc
index 94609bdc..3d1967d8 100644
--- a/qpdf/qpdf.cc
+++ b/qpdf/qpdf.cc
@@ -446,6 +446,7 @@ class ArgParser
void argVersion();
void argCopyright();
void argCompletionBash();
+ void argCompletionZsh();
void argJsonHelp();
void argPositional(char* arg);
void argPassword(char* parameter);
@@ -520,7 +521,7 @@ class ArgParser
void readArgsFromFile(char const* filename);
void doFinalChecks();
void addOptionsToCompletions();
- void addChoicesToCompletions(std::string const&);
+ void addChoicesToCompletions(std::string const&, std::string const&);
void handleCompletion();
std::vector<PageSpec> parsePagesOptions();
void parseRotationParameter(std::string const&);
@@ -534,6 +535,7 @@ class ArgParser
Options& o;
int cur_arg;
bool bash_completion;
+ bool zsh_completion;
std::string bash_prev;
std::string bash_cur;
std::string bash_line;
@@ -556,7 +558,8 @@ ArgParser::ArgParser(int argc, char* argv[], Options& o) :
argv(argv),
o(o),
cur_arg(0),
- bash_completion(false)
+ bash_completion(false),
+ zsh_completion(false)
{
option_table = &main_option_table;
initOptionTable();
@@ -619,6 +622,7 @@ ArgParser::initOptionTable()
(*t)["version"] = oe_bare(&ArgParser::argVersion);
(*t)["copyright"] = oe_bare(&ArgParser::argCopyright);
(*t)["completion-bash"] = oe_bare(&ArgParser::argCompletionBash);
+ (*t)["completion-zsh"] = oe_bare(&ArgParser::argCompletionZsh);
(*t)["json-help"] = oe_bare(&ArgParser::argJsonHelp);
t = &this->main_option_table;
@@ -809,6 +813,9 @@ ArgParser::argHelp()
void
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];
size_t slash = path.find('/');
if ((slash != 0) && (slash != std::string::npos))
@@ -816,11 +823,15 @@ ArgParser::argCompletionBash()
std::cerr << "WARNING: qpdf completion enabled"
<< " 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
ArgParser::argJsonHelp()
{
// Make sure the output looks right on an 80-column display.
@@ -1543,6 +1554,7 @@ Basic Options\n\
--copyright show qpdf's copyright and license information\n\
--help show command-line argument help\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\
--verbose provide additional informational output\n\
--progress give progress indicators while writing output\n\
@@ -2198,13 +2210,61 @@ ArgParser::checkCompletion()
// cursor for completion purposes.
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)
+ {
+ char ch = bash_line.at(p);
+ if ((ch == ' ') || (ch == '=') || (ch == ':'))
+ {
+ sep = ch;
+ break;
+ }
+ }
+ bash_cur = bash_line.substr(1+p, std::string::npos);
+ if ((sep == ':') || (sep == '='))
{
- bash_cur = argv[2];
- bash_prev = argv[3];
- handleBashArguments();
- bash_completion = true;
+ // 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
-ArgParser::addChoicesToCompletions(std::string const& option)
+ArgParser::addChoicesToCompletions(std::string const& option,
+ std::string const& extra_prefix)
{
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();
iter != oe.choices.end(); ++iter)
{
- completions.insert(*iter);
+ completions.insert(extra_prefix + *iter);
}
}
}
@@ -2414,6 +2475,7 @@ ArgParser::addOptionsToCompletions()
void
ArgParser::handleCompletion()
{
+ std::string extra_prefix;
if (this->completions.empty())
{
// Detect --option=... Bash treats the = as a word separator.
@@ -2450,7 +2512,12 @@ ArgParser::handleCompletion()
}
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) == '-'))
{
@@ -2467,11 +2534,12 @@ ArgParser::handleCompletion()
}
}
}
+ std::string prefix = extra_prefix + bash_cur;
for (std::set<std::string>::iterator iter = completions.begin();
iter != completions.end(); ++iter)
{
- if (this->bash_cur.empty() ||
- ((*iter).substr(0, bash_cur.length()) == bash_cur))
+ if (prefix.empty() ||
+ ((*iter).substr(0, prefix.length()) == prefix))
{
std::cout << *iter << std::endl;
}
diff --git a/qpdf/qtest/qpdf.test b/qpdf/qtest/qpdf.test
index f2f0579f..7fa6f18f 100644
--- a/qpdf/qtest/qpdf.test
+++ b/qpdf/qtest/qpdf.test
@@ -116,6 +116,7 @@ my @completion_tests = (
['qpdf --decode-l', undef, 'decode-l'],
['qpdf --decode-lzzz', 15, 'decode-l'],
['qpdf --decode-level=', undef, 'decode-level'],
+ ['qpdf --decode-level=g', undef, 'decode-level-g'],
['qpdf --check -', undef, 'later-arg'],
['qpdf infile outfile oops --ch', undef, 'usage-empty'],
['qpdf --encrypt \'user " password\' ', undef, 'quoting'],
@@ -124,16 +125,26 @@ my @completion_tests = (
['qpdf --encrypt "user pass\'word" ', undef, 'quoting'],
['qpdf --encrypt user\ password ', undef, 'quoting'],
);
-$n_tests += scalar(@completion_tests);
+$n_tests += 2 * scalar(@completion_tests);
foreach my $c (@completion_tests)
{
my ($cmd, $point, $description) = @$c;
my $out = "completion-$description.out";
+ my $zout = "completion-$description-zsh.out";
+ if (! -f $zout)
+ {
+ $zout = $out;
+ }
$td->runtest("bash completion: $description",
{$td->COMMAND => [@{bash_completion($cmd, $point)}],
$td->FILTER => "perl filter-completion.pl $out"},
{$td->FILE => "$out", $td->EXIT_STATUS => 0},
$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();
@@ -3208,6 +3219,16 @@ sub bash_completion
"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
{
my ($description, $command, $output, $status) = @_;
diff --git a/qpdf/qtest/qpdf/completion-decode-level-g-zsh.out b/qpdf/qtest/qpdf/completion-decode-level-g-zsh.out
new file mode 100644
index 00000000..47a40bba
--- /dev/null
+++ b/qpdf/qtest/qpdf/completion-decode-level-g-zsh.out
@@ -0,0 +1,6 @@
+--decode-level=generalized
+!--decode-level=all
+!--decode-level=none
+!all
+!generalized
+!none
diff --git a/qpdf/qtest/qpdf/completion-decode-level-g.out b/qpdf/qtest/qpdf/completion-decode-level-g.out
new file mode 100644
index 00000000..ab5d711d
--- /dev/null
+++ b/qpdf/qtest/qpdf/completion-decode-level-g.out
@@ -0,0 +1,6 @@
+generalized
+!--decode-level=all
+!--decode-level=generalized
+!--decode-level=none
+!all
+!none
diff --git a/qpdf/qtest/qpdf/completion-decode-level-zsh.out b/qpdf/qtest/qpdf/completion-decode-level-zsh.out
new file mode 100644
index 00000000..0d5d2e33
--- /dev/null
+++ b/qpdf/qtest/qpdf/completion-decode-level-zsh.out
@@ -0,0 +1,7 @@
+--decode-level=all
+--decode-level=generalized
+--decode-level=none
+!--help
+!all
+!generalized
+!none
diff --git a/qpdf/qtest/qpdf/completion-decode-level.out b/qpdf/qtest/qpdf/completion-decode-level.out
index 776f1e1f..3e48c118 100644
--- a/qpdf/qtest/qpdf/completion-decode-level.out
+++ b/qpdf/qtest/qpdf/completion-decode-level.out
@@ -1,4 +1,7 @@
all
generalized
none
+!--decode-level=all
+!--decode-level=generalized
+!--decode-level=none
!--help