Merge lp:~gagern/bzr/bug560030-include-bash-completion-plugin into lp:bzr
- bug560030-include-bash-completion-plugin
- Merge into bzr.dev
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | John A Meinel | ||||
Approved revision: | no longer in the source branch. | ||||
Merged at revision: | 5240 | ||||
Proposed branch: | lp:~gagern/bzr/bug560030-include-bash-completion-plugin | ||||
Merge into: | lp:bzr | ||||
Diff against target: |
1273 lines (+1049/-132) 8 files modified
NEWS (+5/-0) bzrlib/plugins/bash_completion/README.txt (+201/-0) bzrlib/plugins/bash_completion/__init__.py (+39/-0) bzrlib/plugins/bash_completion/bashcomp.py (+463/-0) bzrlib/plugins/bash_completion/tests/__init__.py (+23/-0) bzrlib/plugins/bash_completion/tests/test_bashcomp.py (+318/-0) contrib/bash/bzr (+0/-104) contrib/bash/bzr.simple (+0/-28) |
||||
To merge this branch: | bzr merge lp:~gagern/bzr/bug560030-include-bash-completion-plugin | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
John A Meinel | Approve | ||
Vincent Ladeuil | Approve | ||
Review via email: mp+23912@code.launchpad.net |
Commit message
Replace the unmaintained bzr completion script with gagern's new one.
Description of the change
According to bug #560030, distro packagers as well as users would like to see the bash completion plugin included in bzr core, instead of simply referred to as an optional plugin.
So this branch joins lp:bzr-bash-completion into lp:bzr and adjusts copyright information so the plugin is officially assigned to Canonical Ltd.
The outdated contrib/bash/bzr script is replaced by what used to be called lazy.sh in bzr-bash-
Martin Packman (gz) wrote : | # |
Martin von Gagern (gagern) wrote : | # |
Thanks for spotting the Python 2.4 thing. Testing it with Python 2.4 I also found another problem: the plugin used to depend on testtools for the registry of the --parallel option to the bzr selftest command.
Isn't there a large number of users that don't use launchpad, or that never merge NEWS files, or that don't store their credentials in ~/.netrc? Come to think of it, isn't there a large number of users that don't sign any commits, or never shelve stuff, or don't send merge directives via mail? Or, even more to the point, how many users are using the shell-completion builtin?
I bet you get my point: yes, there are certainly users that won't require the bash_completion plugin. But it's small enough that the extra space requirement shouldn't hurt, designed with the hope that the __init__.py executes pretty fast, and on the whole shouldn't hurt.
Seeing as most major GNU/Linux distros install the outdated completion script along with bzr despite its shortcomings seems to me an indication that there will be a large enough user base to warrant inclusion into core. There are some bzr builtins and core plugins that I assume have a smaller user base.
And if some platform is certain to never have any need for bash completion, and is concerned enough about memory requirements or whatever, they can simply delete the plugin in their installation.
Vincent Ladeuil (vila) wrote : | # |
I'm ok with merging this into core, but I'd like a few cleanups first.
Mostly, the plugin carries some excessive baggage from its birth as a standalone plugin that doesn't match the policies we have for the core plugins.
Regarding distribution, windows installers need to be told explicitly about new
plugins, so no worries there. For the other OSes, well, bash is at least available
if not already installed by default.
Regarding load time impact, you can go a step further and use CommandRegistry
We need tests. I don't clearly understand how the completion works and you may need to redesign a bit to be able to test at the python level. Don't hesitate
to ask for help.
I don't ask for a complete coverage here, but making sure we know how to
complete at least some basic commands and their args will be a minimum.
Having a design (with associated tests) ready for further enhancements
(prefix completion for known transport protocols or useful shortcuts like :parent, :push and the like) will be a nice bonus though.
The above are rather generic, here are some more concrete points:
- delete .bzrignore,
- use bzrlib version like the other plugins,
- watch for PEP8 compliance (head, fun, tail, etc in bashcomp.py miss double vertical spaces)
Martin von Gagern (gagern) wrote : | # |
Thanks for the review, Vincent! Probably won't have time to deal with it before 27 April or so, so I'm putting this on hold until then.
Testing is a real problem here: the things I'd be most concerned about or interested in is bash syntax and behaviour, and obviously testing these involves executing bash, which can't be done from within python only. Maybe I can write tests that check whether bash is available, and skip if it isn't.
I think this approach is the only thing that could really catch regressions, in particular the one I recently introduced and fixed on trunk. This is the only thing that can actually check the behaviour of the hardcoded template around the simple dynamically generated code.
Another thing I might do is refactor stuff so that data aquisition and code generation are separated. That way, we could ensure that the data aquisition does the correct thing, and hopefully keep the code generation simple enough so it will be easy to check. This might also open the door for other shells, zsh in particular, so it might be a good thing even without tests. But it's a major step, so it will take a bit of time.
Vincent Ladeuil (vila) wrote : | # |
>>>>> Martin von Gagern <email address hidden> writes:
> Thanks for the review, Vincent! Probably won't have time to deal
> with it before 27 April or so, so I'm putting this on hold until
> then.
> Testing is a real problem here: the things I'd be most concerned
> about or interested in is bash syntax and behaviour, and obviously
> testing these involves executing bash, which can't be done from
> within python only. Maybe I can write tests that check whether
> bash is available, and skip if it isn't.
Yup, that's the idea, define a feature in bzrlib.
that.
> I think this approach is the only thing that could really catch
> regressions, in particular the one I recently introduced and fixed
> on trunk. This is the only thing that can actually check the
> behaviour of the hardcoded template around the simple dynamically
> generated code.
Exactly. You may even want to add more functions in the template to have
smaller tests.
> Another thing I might do is refactor stuff so that data aquisition
> and code generation are separated. That way, we could ensure that
> the data aquisition does the correct thing, and hopefully keep the
> code generation simple enough so it will be easy to check.
... no comment :)
> This might also open the door for other shells, zsh in particular,
> so it might be a good thing even without tests. But it's a major
> step, so it will take a bit of time.
Yes, I didn't mention that in my first review and our experience is that
it's easier to review and land smaller proposals, so don't try to
address all of that in a single proposal.
But once you get there, have a look at
bzrlib.
Some more nits found while re-reading your mp:
- Don't use relative imports, they tend to break in obscure ways if you
happend to have a PYTHON_PATH that includes the current dir and you're
working in the directory of your plugin (or another plugin that use
the same package name or another python program, etc). Even if *you*
don't do that, we had bug reports in the past about that.
- try to use:
from bzrlib import commands
class cmd_foo(
instead of
from bzrlib.command import Command
class cmd_foo(Command)
We don't always respect this rule, but we're fixing such usages as we
encounter them.
Martin von Gagern (gagern) wrote : | # |
> delete .bzrignore,
Dropped.
> use bzrlib version like the other plugins,
Done, and also dropped meta.py along the way.
> watch for PEP8 compliance
Should be better now.
> check whether bash is available, and skip if it isn't.
> define a feature in bzrlib.
Have a test for bash, but it's in my own testing code, as I can't imagine other tests requiring bash as well, and as I wanted to include the test suite in the standalone distribution of the plugin as well.
Maybe some general feature class testing for the existence of arbitrary binaries, possibly taking the PATH environment variable and platform conventions into account might be a good idea as well. But that would be a different merge request, I think.
> You may even want to add more functions in the template
> to have smaller tests.
I don't feel like clobbering the bash function namespace with too many functions. I consider the completion tests to be pretty much black box tests: input a list of words, output a list of completions. Otherwise I'd have to write not only a number of smaller functions, but a suitable testing framework for all of these. Or express test input and assertions in bash syntax.
> bzrlib.
At least not by the zsh completion script shipped by either zsh or bzr. There are scripts out there making use of it, but none seem particularly "official". In any case, I'll investigate zsh in more detail once this got landed.
> Don't use relative imports, they tend to break in obscure ways
How so? I thought Python 2 does relative imports by default, and only falls back to absolute imports if the relative import fails. So I would have assumed that a relative import would be on the safe side, whereas an absolute one could break in cases where there was a relative import of the same name available.
Anyway, I simply take your word for it, and changed everything to absolute paths. Don't want to do the same for the standalone distribution, though, as there people might choose a different plugin name.
> try to use [module imports instead of class imports]
Done.
Vincent Ladeuil (vila) wrote : | # |
>>>>> Martin von Gagern <email address hidden> writes:
>> check whether bash is available, and skip if it isn't.
>> define a feature in bzrlib.
> Have a test for bash, but it's in my own testing code, as I can't
> imagine other tests requiring bash as well, and as I wanted to
> include the test suite in the standalone distribution of the
> plugin as well.
Perfect.
> Maybe some general feature class testing for the existence of
> arbitrary binaries, possibly taking the PATH environment variable
> and platform conventions into account might be a good idea as
> well. But that would be a different merge request, I think.
Very good idea, many plugins could certainly benefit from it.
>> You may even want to add more functions in the template
>> to have smaller tests.
> I don't feel like clobbering the bash function namespace with too
> many functions. I consider the completion tests to be pretty much
> black box tests: input a list of words, output a list of
> completions.
Hehe, yes, you got it right, I'm not a big fan of blackbox tests :)
For the sake of the discussion, imagine that we encounter a bug about an
option with a weird character like ' or `, whatever.
I'd like the ability to write a test for just the option related part
without having to rely on a command that will use this option.
But except for the remarks bwlow, I'm fine with the tests you've added.
> Otherwise I'd have to write not only a number of smaller
> functions, but a suitable testing framework for all of these. Or
> express test input and assertions in bash syntax.
Yeah, well, that's the point, I don't have strong opinions there as I
don't know the code well enough to decide whether or not you can write
the test in pythin or if bash is really required.
>> bzrlib.
> At least not by the zsh completion script shipped by either zsh or
> bzr.
Yes it is, look for shell-complete (not shell_complete) in contrib/zsh/_bzr.
> There are scripts out there making use of it, but none seem
> particularly "official". In any case, I'll investigate zsh in more
> detail once this got landed.
Yeah, no problem.
>> Don't use relative imports, they tend to break in obscure ways
> How so?
I don't remember the bug number off-hand, but having '.' in your
PYTHONPATH and '.' containing an unrelated python module with the same
name may wrongly trigger an import while using
'bzrlib.
<snip/>
> Don't want to do the same for the standalone distribution, though,
> as there people might choose a different plugin name.
Wow, interesting... There are so many cases where you *need* the plugin
name to be the same as its containing directory (BZR_PLUGINS_AT works
hard to remove this limitation) that I didn't think about this case... I
won't explore it myself :)
Now for some specifics:
19 --- bzrlib/
This still includes material related to the use of the plugin in its
non-core version...
Martin von Gagern (gagern) wrote : | # |
On 04.05.2010 12:32, Vincent Ladeuil wrote:
> For the sake of the discussion, imagine that we encounter a bug about an
> option with a weird character like ' or `, whatever.
The black-box tests are mostly about testing the fixed functionality
part of the template, not individual commands. But I've just adjusted
the get_script method to make it easier for future tests to provide
cooked completion data without actually having to provide commands for
these.
> Yeah, well, that's the point, I don't have strong opinions there as I
> don't know the code well enough to decide whether or not you can write
> the test in pythin or if bash is really required.
The bash completion stuff is advanced enough that a simplistic python
interpreter wouldn't suffice. And an afvanced one would be a project in
its own right, and require a full test suite to boot...
> Yes it is, look for shell-complete (not shell_complete) in contrib/zsh/_bzr.
OK, it does command name completion using that callback. But not option
completion, although that would be provided by the shell-completion
builtin as well, at least to some degree.
> 19 --- bzrlib/
>
> This still includes material related to the use of the plugin in its
> non-core version.
Once the plugin got merged, I'd adjust the README to state the fact, and
point out that there are distinct lines of development. That much I'd
write for the standalone plugin and merge into core in a separate merge
request. So I'd like the README to stay a while, but get updated.
> Depending on how long this plugin will continue to live outside of the
> bzr tree, we may want to clean this stuff. I wonder how it will will
> behave (maintaining it in both trees and merging from the plugin may
> trigger some spurious conflicts whatever choise we make)...
I guess I'll keep the plugin around for about a year or so, so people
can use it even without updating their bzr setup. Once most distros ship
bzr with the plugin in place, I'll probably phase out the standalone
distribution. And in any case I'll probably not keep the standalone
branch up to date in case third parties provide branches against the bzr
tree affecting the plugin. Dunno how likely this is.
> Splitting the above into three assertCompletio
> more explicit about what they are checking. Keeping the checks about the
> format returned may be left here though IMHO.
Done, and it does look better. Thanks!
> but 'Approved' with the tweaks mentioned above.
Why the plural? I'd consider the assertion stuff a single tweak, and the
rest of your comments I felt were suggestions, perhaps for a future
merge proposal, but in any case not requirements for this merge here.
Vincent Ladeuil (vila) wrote : | # |
> On 04.05.2010 12:32, Vincent Ladeuil wrote:
> > For the sake of the discussion, imagine that we encounter a bug about an
> > option with a weird character like ' or `, whatever.
>
> The black-box tests are mostly about testing the fixed functionality
> part of the template, not individual commands. But I've just adjusted
> the get_script method to make it easier for future tests to provide
> cooked completion data without actually having to provide commands for
> these.
Ok, good enough for now.
<snip/>
> Once the plugin got merged, I'd adjust the README to state the fact, and
> point out that there are distinct lines of development. That much I'd
> write for the standalone plugin and merge into core in a separate merge
> request. So I'd like the README to stay a while, but get updated.
Fine for me, thanks for sharing your thoughts on the subject.
> I guess I'll keep the plugin around for about a year or so,
Sounds reasonable.
> Done, and it does look better. Thanks!
Excellent.
>
> > but 'Approved' with the tweaks mentioned above.
>
> Why the plural?
Because my English is not precise enough I suspect :)
> I'd consider the assertion stuff a single tweak,
Yup, that's what I was referring to.
and the
> rest of your comments I felt were suggestions, perhaps for a future
> merge proposal, but in any case not requirements for this merge here.
Correct.
John A Meinel (jameinel) wrote : | # |
I'm going to assume that someone else has done a more thorough review of the bash side of things, as I don't really know how things integrate together.
The test infrastructure looks decent.
I do wonder about using "os.access(
subprocess.
Partially because I just tested it, and under Windows if you have a bash.exe in your path, this succeeds. And in theory, that means that you could run the tests on all platforms, rather than requiring a specific path to be available.
That can be updated/added later, though.
Note that if I hack in that 'bash' is the path that can be accessed, most of the tests pass on Windows except for a couple. However, they're really quite slow:
...lugins.
...plugins.
...ugins.
...ins.
...b.plugins.
...lugins.
...gins.
..._completion.
Text attachment: log
------------
15.616 creating repository in file://
15.632 creating branch <bzrlib.
15.725 opening working tree 'C:/users/
15.999 creating repository in file://
16.180 creating branch <bzrlib.
16.292 opening working tree 'C:/users/
------------
Text attachment: traceback
------------
Traceback (most recent call last):
File "c:\Python26\
return fn(*args)
File "c:\Python26\
testMethod()
File "C:\Users\
self.
File "C:\Users\
self.
AssertionError: not equal:
a = set(['3tag', 'tag1', 'tag2'])
b = set()
I'm cer...
John A Meinel (jameinel) wrote : | # |
In a VirtualBox guest on the same hardware, I can confirm that they run better, even if they are still a bit slow:
...letion.
...etion.
...tion.
...tion.
...etion.
...ion.
...pletion.
...on.tests.
...ion.
...n.tests.
...tests.
...etion.
...on.tests.
....tests.
...st_bashcomp.
...bashcomp.
...pletion.
...ion.
...n.tests.
...tion.
...n.tests.
-------
Ran 21 tests in 7.347s
However they:
a) pass
b) run in a reasonable speed
So I'm probably okay with this.
I'll mark this as ready, but give it a day or two in case people want to comment. (Especially on whether we want to try to run the tests on more platforms. Somebody should also probably try to test it on Mac...)
John Szakmeister (jszakmeister) wrote : | # |
Just for the record, I ran this on my MacBook. All the test passed, and it took about 11 seconds for all the tests to run.
Martin von Gagern (gagern) wrote : | # |
On 18.05.2010 22:48, John A Meinel wrote:
> I do wonder about using "os.access(
> subprocess.
> And in theory, that means that you could run the tests on all platforms, rather than requiring a specific path to be available.
> That can be updated/added later, though.
lp:~gagern/bzr/bash_completion-ExecutableFeature has the change from
hardcoded paths to generic PATH environment variable.
I'm avoiding Popen(shell=True) to avoid executing yet another shell
process just to do the path resolution.
> most of the tests pass on Windows except for a couple. However, they're really quite slow:
Ouch! Wouldn't have expected this.
One bad solution would be skipping these tests on windows, or depending
on some environment setting, or some such hack. Feels bad, as it reduces
test coverage for performance reasons only.
In the short run, keeping the number of test cases actually executing
bash is probably the best solution. In the long run, it might be
feasible to execute all of these tests from a single bash instance, but
ensuring proper isolation under these circumstances could be tricky.
> ..._completion.
That one deserves a closer look. Filed bug #582538 for this.
> I'm certainly hesitant to be adding tests that take multiple seconds to run. Though it may just be a win32 bash thing. Can someone else run "bzr selftest -s bp.bash" and let me know?
What alternatives do you have in mind? Not testing the handwritten bash
code seems like a poor alternative.
> The README clearly doesn't apply as-is anymore.
Will adjust that for both the in-tree and the standalone version at a
later point in time. Will submit a new merge proposal for that.
Thanks for the review, looking forward for the merge.
Robert Collins (lifeless) wrote : | # |
submitted to PQM by hand.
Preview Diff
1 | === modified file 'NEWS' |
2 | --- NEWS 2010-05-04 22:02:05 +0000 |
3 | +++ NEWS 2010-05-05 08:10:48 +0000 |
4 | @@ -36,6 +36,11 @@ |
5 | re-sign, unbind, unknowns. |
6 | (Martin von Gagern, #527878) |
7 | |
8 | +* The bash_completion plugin from the bzr-bash-completion project has |
9 | + been merged into the tree. It provides a bash-completion command and |
10 | + replaces the outdated ``contrib/bash/bzr`` script with a version |
11 | + using the plugin. (Martin von Gagern, #560030) |
12 | + |
13 | Bug Fixes |
14 | ********* |
15 | |
16 | |
17 | === added directory 'bzrlib/plugins/bash_completion' |
18 | === added file 'bzrlib/plugins/bash_completion/README.txt' |
19 | --- bzrlib/plugins/bash_completion/README.txt 1970-01-01 00:00:00 +0000 |
20 | +++ bzrlib/plugins/bash_completion/README.txt 2010-05-05 08:10:48 +0000 |
21 | @@ -0,0 +1,201 @@ |
22 | +.. comment |
23 | + |
24 | + Copyright (C) 2010 Canonical Ltd |
25 | + |
26 | + This file is part of bzr-bash-completion |
27 | + |
28 | + bzr-bash-completion free software: you can redistribute it and/or |
29 | + modify it under the terms of the GNU General Public License as |
30 | + published by the Free Software Foundation, either version 2 of the |
31 | + License, or (at your option) any later version. |
32 | + |
33 | + bzr-bash-completion is distributed in the hope that it will be |
34 | + useful, but WITHOUT ANY WARRANTY; without even the implied warranty |
35 | + of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
36 | + General Public License for more details. |
37 | + |
38 | + You should have received a copy of the GNU General Public License |
39 | + along with this program. If not, see <http://www.gnu.org/licenses/>. |
40 | + |
41 | +===================================== |
42 | +bzr bash-completion script and plugin |
43 | +===================================== |
44 | + |
45 | +This script generates a shell function which can be used by bash to |
46 | +automatically complete the currently typed command when the user |
47 | +presses the completion key (usually tab). |
48 | + |
49 | +It is intended as a bzr plugin, but can be used to some extend as a |
50 | +standalone python script as well. |
51 | + |
52 | +| Copyright (C) 2009, 2010 Canonical Ltd |
53 | + |
54 | +.. contents:: |
55 | + |
56 | +---------- |
57 | +Installing |
58 | +---------- |
59 | + |
60 | +You only need to do this if you want to use the script as a bzr |
61 | +plugin. Otherwise simply grab the bashcomp.py and place it wherever |
62 | +you want. |
63 | + |
64 | +Installing from bzr repository |
65 | +------------------------------ |
66 | + |
67 | +To check out the current code from launchpad, use the following commands:: |
68 | + |
69 | + mkdir -p ~/.bazaar/plugins |
70 | + cd ~/.bazaar/plugins |
71 | + bzr checkout lp:bzr-bash-completion bash_completion |
72 | + |
73 | +To update such an installation, execute this command:: |
74 | + |
75 | + bzr update ~/.bazaar/plugins/bash_completion |
76 | + |
77 | +Installing using easy_install |
78 | +----------------------------- |
79 | + |
80 | +The following command should install the latest release of the plugin |
81 | +on your system:: |
82 | + |
83 | + easy_install bzr-bash-completion |
84 | + |
85 | +To use this method, you need to have `Easy Install`_ installed and |
86 | +also have write access to the required directories. So maybe you |
87 | +should execute this command as root or through sudo_. Or you want to |
88 | +`install to a different location`_. |
89 | + |
90 | +.. _Easy Install: http://peak.telecommunity.com/DevCenter/EasyInstall |
91 | +.. _sudo: http://linux.die.net/man/8/sudo |
92 | +.. _install to a different location: |
93 | + http://peak.telecommunity.com/DevCenter/EasyInstall#non-root-installation |
94 | + |
95 | +Installing from tarball |
96 | +----------------------- |
97 | + |
98 | +If you have grabbed a source code tarball, or want to install from a |
99 | +bzr checkout in a different place than your bazaar plugins directory, |
100 | +then you should use the ``setup.py`` script shipped with the code:: |
101 | + |
102 | + ./setup.py install |
103 | + |
104 | +If you want to install the plugin only for your own user account, you |
105 | +might wish to pass the option ``--user`` or ``--home=$HOME`` to that |
106 | +command. For further information please read the manuals of distutils_ |
107 | +as well as setuptools_ or distribute_, whatever is available on your |
108 | +system, or have a look at the command line help:: |
109 | + |
110 | + ./setup.py install --help |
111 | + |
112 | +.. _distutils: http://docs.python.org/install/index.html |
113 | +.. _setuptools: http://peak.telecommunity.com/DevCenter/setuptools#what-your-users-should-know |
114 | +.. _distribute: http://packages.python.org/distribute/setuptools.html#what-your-users-should-know |
115 | + |
116 | +----- |
117 | +Using |
118 | +----- |
119 | + |
120 | +Using as a plugin |
121 | +----------------- |
122 | + |
123 | +This is the preferred method of generating the completion function, as |
124 | +it will ensure proper bzr initialization. |
125 | + |
126 | +:: |
127 | + |
128 | + eval "`bzr bash-completion`" |
129 | + |
130 | +Lazy initialization |
131 | +------------------- |
132 | + |
133 | +Running the above command automatically from your ``~/.bashrc`` file |
134 | +or similar can cause annoying delays in the startup of your shell. |
135 | +To avoid this problem, you can delay the generation of the completion |
136 | +function until you actually need it. |
137 | + |
138 | +To do so, source the file ``lazy.sh`` shipped with this package from |
139 | +your ``~/.bashrc`` file or add it to your ``~/.bash_completion`` if |
140 | +your setup uses such a file. On a system-wide installation, the |
141 | +directory ``/usr/share/bash-completion/`` might contain such bash |
142 | +completion scripts. |
143 | + |
144 | +If you installed bzr-bash-completion from the repository or a source |
145 | +tarball, you find the ``lazy.sh`` script in the root of the source |
146 | +tree. If you installed the plugin using easy_install, you should grab |
147 | +the script manually from the bzr repository, e.g. through the bazaar |
148 | +web interface on launchpad. |
149 | + |
150 | +Note that the full completion function is generated only once per |
151 | +shell session. If you update your bzr installation or change the set |
152 | +of installed plugins, then you might wish to regenerate the completion |
153 | +function manually as described above in order for completion to take |
154 | +these changes into account. |
155 | + |
156 | +Using as a script |
157 | +----------------- |
158 | + |
159 | +As an alternative, if bzrlib is available to python scripts, the |
160 | +following invocation should yield the same results without requiring |
161 | +you to add a plugin:: |
162 | + |
163 | + eval "`./bashcomp.py`" |
164 | + |
165 | +This approach might have some issues, though, and provides less |
166 | +options than the bzr plugin. Therefore if you have the choice, go for |
167 | +the plugin setup. |
168 | + |
169 | +-------------- |
170 | +Design concept |
171 | +-------------- |
172 | + |
173 | +The plugin (or script) is designed to generate a completion function |
174 | +containing all the required information about the possible |
175 | +completions. This is usually only done once when bash |
176 | +initializes. After that, no more invocations of bzr are required. This |
177 | +makes the function much faster than a possible implementation talking |
178 | +to bzr for each and every completion. On the other hand, this has the |
179 | +effect that updates to bzr or its plugins won't show up in the |
180 | +completions immediately, but only after the completion function has |
181 | +been regenerated. |
182 | + |
183 | +------- |
184 | +License |
185 | +------- |
186 | + |
187 | +As this is built upon a bash completion script originally included in |
188 | +the bzr source tree, and as the bzr sources are covered by the GPL 2, |
189 | +this script here is licensed under these same terms. |
190 | + |
191 | +If you require a more liberal license, you'll have to contact all |
192 | +those who contributed code to this plugin, be it for bash or for |
193 | +python. |
194 | + |
195 | +.. cut long_description here |
196 | + |
197 | +------- |
198 | +History |
199 | +------- |
200 | + |
201 | +The plugin was created by Martin von Gagern in 2009, building on a |
202 | +static completion function of very limited scope distributed together |
203 | +with bzr. |
204 | + |
205 | +---------- |
206 | +References |
207 | +---------- |
208 | + |
209 | +Plugin homepages |
210 | + | https://launchpad.net/bzr-bash-completion |
211 | + | http://pypi.python.org/pypi/bzr-bash-completion |
212 | +Bazaar homepage |
213 | + | http://bazaar.canonical.com/ |
214 | + |
215 | + |
216 | + |
217 | +.. vim: ft=rst |
218 | + |
219 | +.. emacs |
220 | + Local Variables: |
221 | + mode: rst |
222 | + End: |
223 | |
224 | === added file 'bzrlib/plugins/bash_completion/__init__.py' |
225 | --- bzrlib/plugins/bash_completion/__init__.py 1970-01-01 00:00:00 +0000 |
226 | +++ bzrlib/plugins/bash_completion/__init__.py 2010-05-05 08:10:48 +0000 |
227 | @@ -0,0 +1,39 @@ |
228 | +# Copyright (C) 2009, 2010 Canonical Ltd |
229 | +# |
230 | +# This program is free software; you can redistribute it and/or modify |
231 | +# it under the terms of the GNU General Public License as published by |
232 | +# the Free Software Foundation; either version 2 of the License, or |
233 | +# (at your option) any later version. |
234 | +# |
235 | +# This program is distributed in the hope that it will be useful, |
236 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
237 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
238 | +# GNU General Public License for more details. |
239 | +# |
240 | +# You should have received a copy of the GNU General Public License |
241 | +# along with this program; if not, write to the Free Software |
242 | +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
243 | + |
244 | +__doc__ = """Generate a shell function for bash command line completion. |
245 | + |
246 | +This plugin provides a command called bash-completion that generates a |
247 | +bash completion function for bzr. See its documentation for details. |
248 | +""" |
249 | + |
250 | +from bzrlib import commands, version_info |
251 | + |
252 | + |
253 | +bzr_plugin_name = 'bash_completion' |
254 | +bzr_commands = [ 'bash-completion' ] |
255 | + |
256 | +commands.plugin_cmds.register_lazy('cmd_bash_completion', [], |
257 | + 'bzrlib.plugins.bash_completion.bashcomp') |
258 | + |
259 | + |
260 | +def load_tests(basic_tests, module, loader): |
261 | + testmod_names = [ |
262 | + 'tests', |
263 | + ] |
264 | + basic_tests.addTest(loader.loadTestsFromModuleNames( |
265 | + ["%s.%s" % (__name__, tmn) for tmn in testmod_names])) |
266 | + return basic_tests |
267 | |
268 | === added file 'bzrlib/plugins/bash_completion/bashcomp.py' |
269 | --- bzrlib/plugins/bash_completion/bashcomp.py 1970-01-01 00:00:00 +0000 |
270 | +++ bzrlib/plugins/bash_completion/bashcomp.py 2010-05-05 08:10:48 +0000 |
271 | @@ -0,0 +1,463 @@ |
272 | +#!/usr/bin/env python |
273 | + |
274 | +# Copyright (C) 2009, 2010 Canonical Ltd |
275 | +# |
276 | +# This program is free software; you can redistribute it and/or modify |
277 | +# it under the terms of the GNU General Public License as published by |
278 | +# the Free Software Foundation; either version 2 of the License, or |
279 | +# (at your option) any later version. |
280 | +# |
281 | +# This program is distributed in the hope that it will be useful, |
282 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
283 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
284 | +# GNU General Public License for more details. |
285 | +# |
286 | +# You should have received a copy of the GNU General Public License |
287 | +# along with this program; if not, write to the Free Software |
288 | +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
289 | + |
290 | +from bzrlib import ( |
291 | + commands, |
292 | + config, |
293 | + help_topics, |
294 | + option, |
295 | + plugin, |
296 | +) |
297 | +import bzrlib |
298 | +import re |
299 | + |
300 | + |
301 | +class BashCodeGen(object): |
302 | + """Generate a bash script for given completion data.""" |
303 | + |
304 | + def __init__(self, data, function_name='_bzr', debug=False): |
305 | + self.data = data |
306 | + self.function_name = function_name |
307 | + self.debug = debug |
308 | + |
309 | + def script(self): |
310 | + return ("""\ |
311 | +# Programmable completion for the Bazaar-NG bzr command under bash. |
312 | +# Known to work with bash 2.05a as well as bash 4.1.2, and probably |
313 | +# all versions in between as well. |
314 | + |
315 | +# Based originally on the svn bash completition script. |
316 | +# Customized by Sven Wilhelm/Icecrash.com |
317 | +# Adjusted for automatic generation by Martin von Gagern |
318 | + |
319 | +# Generated using the bash_completion plugin. |
320 | +# See https://launchpad.net/bzr-bash-completion for details. |
321 | + |
322 | +# Commands and options of bzr %(bzr_version)s |
323 | + |
324 | +shopt -s progcomp |
325 | +%(function)s |
326 | +complete -F %(function_name)s -o default bzr |
327 | +""" % { |
328 | + "function_name": self.function_name, |
329 | + "function": self.function(), |
330 | + "bzr_version": self.bzr_version(), |
331 | + }) |
332 | + |
333 | + def function(self): |
334 | + return ("""\ |
335 | +%(function_name)s () |
336 | +{ |
337 | + local cur cmds cmdIdx cmd cmdOpts fixedWords i globalOpts |
338 | + local curOpt optEnums |
339 | + |
340 | + COMPREPLY=() |
341 | + cur=${COMP_WORDS[COMP_CWORD]} |
342 | + |
343 | + cmds='%(cmds)s' |
344 | + globalOpts='%(global_options)s' |
345 | + |
346 | + # do ordinary expansion if we are anywhere after a -- argument |
347 | + for ((i = 1; i < COMP_CWORD; ++i)); do |
348 | + [[ ${COMP_WORDS[i]} == "--" ]] && return 0 |
349 | + done |
350 | + |
351 | + # find the command; it's the first word not starting in - |
352 | + cmd= |
353 | + for ((cmdIdx = 1; cmdIdx < ${#COMP_WORDS[@]}; ++cmdIdx)); do |
354 | + if [[ ${COMP_WORDS[cmdIdx]} != -* ]]; then |
355 | + cmd=${COMP_WORDS[cmdIdx]} |
356 | + break |
357 | + fi |
358 | + done |
359 | + |
360 | + # complete command name if we are not already past the command |
361 | + if [[ $COMP_CWORD -le cmdIdx ]]; then |
362 | + COMPREPLY=( $( compgen -W "$cmds $globalOpts" -- $cur ) ) |
363 | + return 0 |
364 | + fi |
365 | + |
366 | + # find the option for which we want to complete a value |
367 | + curOpt= |
368 | + if [[ $cur != -* ]] && [[ $COMP_CWORD -gt 1 ]]; then |
369 | + curOpt=${COMP_WORDS[COMP_CWORD - 1]} |
370 | + if [[ $curOpt == = ]]; then |
371 | + curOpt=${COMP_WORDS[COMP_CWORD - 2]} |
372 | + elif [[ $cur == : ]]; then |
373 | + cur= |
374 | + curOpt="$curOpt:" |
375 | + elif [[ $curOpt == : ]]; then |
376 | + curOpt=${COMP_WORDS[COMP_CWORD - 2]}: |
377 | + fi |
378 | + fi |
379 | +%(debug)s |
380 | + cmdOpts= |
381 | + optEnums= |
382 | + fixedWords= |
383 | + case $cmd in |
384 | +%(cases)s\ |
385 | + *) |
386 | + cmdOpts='--help -h' |
387 | + ;; |
388 | + esac |
389 | + |
390 | + if [[ -z $fixedWords ]] && [[ -z $optEnums ]] && [[ $cur != -* ]]; then |
391 | + case $curOpt in |
392 | + tag:*) |
393 | + fixedWords="$(bzr tags 2>/dev/null | sed 's/ *[^ ]*$//')" |
394 | + ;; |
395 | + esac |
396 | + elif [[ $cur == = ]] && [[ -n $optEnums ]]; then |
397 | + # complete directly after "--option=", list all enum values |
398 | + COMPREPLY=( $optEnums ) |
399 | + return 0 |
400 | + else |
401 | + fixedWords="$cmdOpts $globalOpts $optEnums $fixedWords" |
402 | + fi |
403 | + |
404 | + if [[ -n $fixedWords ]]; then |
405 | + COMPREPLY=( $( compgen -W "$fixedWords" -- $cur ) ) |
406 | + fi |
407 | + |
408 | + return 0 |
409 | +} |
410 | +""" % { |
411 | + "cmds": self.command_names(), |
412 | + "function_name": self.function_name, |
413 | + "cases": self.command_cases(), |
414 | + "global_options": self.global_options(), |
415 | + "debug": self.debug_output(), |
416 | + }) |
417 | + |
418 | + def command_names(self): |
419 | + return " ".join(self.data.all_command_aliases()) |
420 | + |
421 | + def debug_output(self): |
422 | + if not self.debug: |
423 | + return '' |
424 | + else: |
425 | + return (r""" |
426 | + # Debugging code enabled using the --debug command line switch. |
427 | + # Will dump some variables to the top portion of the terminal. |
428 | + echo -ne '\e[s\e[H' |
429 | + for (( i=0; i < ${#COMP_WORDS[@]}; ++i)); do |
430 | + echo "\$COMP_WORDS[$i]='${COMP_WORDS[i]}'"$'\e[K' |
431 | + done |
432 | + for i in COMP_CWORD COMP_LINE COMP_POINT COMP_TYPE COMP_KEY cur curOpt; do |
433 | + echo "\$${i}=\"${!i}\""$'\e[K' |
434 | + done |
435 | + echo -ne '---\e[K\e[u' |
436 | +""") |
437 | + |
438 | + def bzr_version(self): |
439 | + bzr_version = bzrlib.version_string |
440 | + if not self.data.plugins: |
441 | + bzr_version += "." |
442 | + else: |
443 | + bzr_version += " and the following plugins:" |
444 | + for name, plugin in sorted(self.data.plugins.iteritems()): |
445 | + bzr_version += "\n# %s" % plugin |
446 | + return bzr_version |
447 | + |
448 | + def global_options(self): |
449 | + return " ".join(sorted(self.data.global_options)) |
450 | + |
451 | + def command_cases(self): |
452 | + cases = "" |
453 | + for command in self.data.commands: |
454 | + cases += self.command_case(command) |
455 | + return cases |
456 | + |
457 | + def command_case(self, command): |
458 | + case = "\t%s)\n" % "|".join(command.aliases) |
459 | + if command.plugin: |
460 | + case += "\t\t# plugin \"%s\"\n" % command.plugin |
461 | + options = [] |
462 | + enums = [] |
463 | + for option in command.options: |
464 | + for message in option.error_messages: |
465 | + case += "\t\t# %s\n" % message |
466 | + if option.registry_keys: |
467 | + for key in option.registry_keys: |
468 | + options.append("%s=%s" % (option, key)) |
469 | + enums.append("%s) optEnums='%s' ;;" % |
470 | + (option, ' '.join(option.registry_keys))) |
471 | + else: |
472 | + options.append(str(option)) |
473 | + case += "\t\tcmdOpts='%s'\n" % " ".join(options) |
474 | + if command.fixed_words: |
475 | + fixed_words = command.fixed_words |
476 | + if isinstance(fixed_words, list): |
477 | + fixed_words = "'%s'" + ' '.join(fixed_words) |
478 | + case += "\t\tfixedWords=%s\n" % fixed_words |
479 | + if enums: |
480 | + case += "\t\tcase $curOpt in\n\t\t\t" |
481 | + case += "\n\t\t\t".join(enums) |
482 | + case += "\n\t\tesac\n" |
483 | + case += "\t\t;;\n" |
484 | + return case |
485 | + |
486 | + |
487 | +class CompletionData(object): |
488 | + |
489 | + def __init__(self): |
490 | + self.plugins = {} |
491 | + self.global_options = set() |
492 | + self.commands = [] |
493 | + |
494 | + def all_command_aliases(self): |
495 | + for c in self.commands: |
496 | + for a in c.aliases: |
497 | + yield a |
498 | + |
499 | + |
500 | +class CommandData(object): |
501 | + |
502 | + def __init__(self, name): |
503 | + self.name = name |
504 | + self.aliases = [name] |
505 | + self.plugin = None |
506 | + self.options = [] |
507 | + self.fixed_words = None |
508 | + |
509 | + |
510 | +class PluginData(object): |
511 | + |
512 | + def __init__(self, name, version=None): |
513 | + if version is None: |
514 | + version = bzrlib.plugin.plugins()[name].__version__ |
515 | + self.name = name |
516 | + self.version = version |
517 | + |
518 | + def __str__(self): |
519 | + if self.version == 'unknown': |
520 | + return self.name |
521 | + return '%s %s' % (self.name, self.version) |
522 | + |
523 | + |
524 | +class OptionData(object): |
525 | + |
526 | + def __init__(self, name): |
527 | + self.name = name |
528 | + self.registry_keys = None |
529 | + self.error_messages = [] |
530 | + |
531 | + def __str__(self): |
532 | + return self.name |
533 | + |
534 | + def __cmp__(self, other): |
535 | + return cmp(self.name, other.name) |
536 | + |
537 | + |
538 | +class DataCollector(object): |
539 | + |
540 | + def __init__(self, no_plugins=False, selected_plugins=None): |
541 | + self.data = CompletionData() |
542 | + self.user_aliases = {} |
543 | + if no_plugins: |
544 | + self.selected_plugins = set() |
545 | + elif selected_plugins is None: |
546 | + self.selected_plugins = None |
547 | + else: |
548 | + self.selected_plugins = set([x.replace('-', '_') |
549 | + for x in selected_plugins]) |
550 | + |
551 | + def collect(self): |
552 | + self.global_options() |
553 | + self.aliases() |
554 | + self.commands() |
555 | + return self.data |
556 | + |
557 | + def global_options(self): |
558 | + re_switch = re.compile(r'\n(--[A-Za-z0-9-_]+)(?:, (-\S))?\s') |
559 | + help_text = help_topics.topic_registry.get_detail('global-options') |
560 | + for long, short in re_switch.findall(help_text): |
561 | + self.data.global_options.add(long) |
562 | + if short: |
563 | + self.data.global_options.add(short) |
564 | + |
565 | + def aliases(self): |
566 | + for alias, expansion in config.GlobalConfig().get_aliases().iteritems(): |
567 | + for token in commands.shlex_split_unicode(expansion): |
568 | + if not token.startswith("-"): |
569 | + self.user_aliases.setdefault(token, set()).add(alias) |
570 | + break |
571 | + |
572 | + def commands(self): |
573 | + for name in sorted(commands.all_command_names()): |
574 | + self.command(name) |
575 | + |
576 | + def command(self, name): |
577 | + cmd = commands.get_cmd_object(name) |
578 | + cmd_data = CommandData(name) |
579 | + |
580 | + plugin_name = cmd.plugin_name() |
581 | + if plugin_name is not None: |
582 | + if (self.selected_plugins is not None and |
583 | + plugin not in self.selected_plugins): |
584 | + return None |
585 | + plugin_data = self.data.plugins.get(plugin_name) |
586 | + if plugin_data is None: |
587 | + plugin_data = PluginData(plugin_name) |
588 | + self.data.plugins[plugin_name] = plugin_data |
589 | + cmd_data.plugin = plugin_data |
590 | + self.data.commands.append(cmd_data) |
591 | + |
592 | + # Find all aliases to the command; both cmd-defined and user-defined. |
593 | + # We assume a user won't override one command with a different one, |
594 | + # but will choose completely new names or add options to existing |
595 | + # ones while maintaining the actual command name unchanged. |
596 | + cmd_data.aliases.extend(cmd.aliases) |
597 | + cmd_data.aliases.extend(sorted([useralias |
598 | + for cmdalias in cmd_data.aliases |
599 | + if cmdalias in self.user_aliases |
600 | + for useralias in self.user_aliases[cmdalias] |
601 | + if useralias not in cmd_data.aliases])) |
602 | + |
603 | + opts = cmd.options() |
604 | + for optname, opt in sorted(opts.iteritems()): |
605 | + cmd_data.options.extend(self.option(opt)) |
606 | + |
607 | + if 'help' == name or 'help' in cmd.aliases: |
608 | + cmd_data.fixed_words = ('"$cmds %s"' % |
609 | + " ".join(sorted(help_topics.topic_registry.keys()))) |
610 | + |
611 | + return cmd_data |
612 | + |
613 | + def option(self, opt): |
614 | + optswitches = {} |
615 | + parser = option.get_optparser({opt.name: opt}) |
616 | + parser = self.wrap_parser(optswitches, parser) |
617 | + optswitches.clear() |
618 | + opt.add_option(parser, opt.short_name()) |
619 | + if isinstance(opt, option.RegistryOption) and opt.enum_switch: |
620 | + enum_switch = '--%s' % opt.name |
621 | + enum_data = optswitches.get(enum_switch) |
622 | + if enum_data: |
623 | + try: |
624 | + enum_data.registry_keys = opt.registry.keys() |
625 | + except ImportError, e: |
626 | + enum_data.error_messages.append( |
627 | + "ERROR getting registry keys for '--%s': %s" |
628 | + % (opt.name, str(e).split('\n')[0])) |
629 | + return sorted(optswitches.values()) |
630 | + |
631 | + def wrap_container(self, optswitches, parser): |
632 | + def tweaked_add_option(*opts, **attrs): |
633 | + for name in opts: |
634 | + optswitches[name] = OptionData(name) |
635 | + parser.add_option = tweaked_add_option |
636 | + return parser |
637 | + |
638 | + def wrap_parser(self, optswitches, parser): |
639 | + orig_add_option_group = parser.add_option_group |
640 | + def tweaked_add_option_group(*opts, **attrs): |
641 | + return self.wrap_container(optswitches, |
642 | + orig_add_option_group(*opts, **attrs)) |
643 | + parser.add_option_group = tweaked_add_option_group |
644 | + return self.wrap_container(optswitches, parser) |
645 | + |
646 | + |
647 | +def bash_completion_function(out, function_name="_bzr", function_only=False, |
648 | + debug=False, |
649 | + no_plugins=False, selected_plugins=None): |
650 | + dc = DataCollector(no_plugins=no_plugins, selected_plugins=selected_plugins) |
651 | + data = dc.collect() |
652 | + cg = BashCodeGen(data, function_name=function_name, debug=debug) |
653 | + if function_only: |
654 | + res = cg.function() |
655 | + else: |
656 | + res = cg.script() |
657 | + out.write(res) |
658 | + |
659 | + |
660 | +class cmd_bash_completion(commands.Command): |
661 | + __doc__ = """Generate a shell function for bash command line completion. |
662 | + |
663 | + This command generates a shell function which can be used by bash to |
664 | + automatically complete the currently typed command when the user presses |
665 | + the completion key (usually tab). |
666 | + |
667 | + Commonly used like this: |
668 | + eval "`bzr bash-completion`" |
669 | + """ |
670 | + |
671 | + takes_options = [ |
672 | + option.Option("function-name", short_name="f", type=str, argname="name", |
673 | + help="Name of the generated function (default: _bzr)"), |
674 | + option.Option("function-only", short_name="o", type=None, |
675 | + help="Generate only the shell function, don't enable it"), |
676 | + option.Option("debug", type=None, hidden=True, |
677 | + help="Enable shell code useful for debugging"), |
678 | + option.ListOption("plugin", type=str, argname="name", |
679 | + # param_name="selected_plugins", # doesn't work, bug #387117 |
680 | + help="Enable completions for the selected plugin" |
681 | + + " (default: all plugins)"), |
682 | + ] |
683 | + |
684 | + def run(self, **kwargs): |
685 | + import sys |
686 | + from bashcomp import bash_completion_function |
687 | + if 'plugin' in kwargs: |
688 | + # work around bug #387117 which prevents us from using param_name |
689 | + if len(kwargs['plugin']) > 0: |
690 | + kwargs['selected_plugins'] = kwargs['plugin'] |
691 | + del kwargs['plugin'] |
692 | + bash_completion_function(sys.stdout, **kwargs) |
693 | + |
694 | + |
695 | +if __name__ == '__main__': |
696 | + |
697 | + import sys |
698 | + import locale |
699 | + import optparse |
700 | + |
701 | + def plugin_callback(option, opt, value, parser): |
702 | + values = parser.values.selected_plugins |
703 | + if value == '-': |
704 | + del values[:] |
705 | + else: |
706 | + values.append(value) |
707 | + |
708 | + parser = optparse.OptionParser(usage="%prog [-f NAME] [-o]") |
709 | + parser.add_option("--function-name", "-f", metavar="NAME", |
710 | + help="Name of the generated function (default: _bzr)") |
711 | + parser.add_option("--function-only", "-o", action="store_true", |
712 | + help="Generate only the shell function, don't enable it") |
713 | + parser.add_option("--debug", action="store_true", |
714 | + help=optparse.SUPPRESS_HELP) |
715 | + parser.add_option("--no-plugins", action="store_true", |
716 | + help="Don't load any bzr plugins") |
717 | + parser.add_option("--plugin", metavar="NAME", type="string", |
718 | + dest="selected_plugins", default=[], |
719 | + action="callback", callback=plugin_callback, |
720 | + help="Enable completions for the selected plugin" |
721 | + + " (default: all plugins)") |
722 | + (opts, args) = parser.parse_args() |
723 | + if args: |
724 | + parser.error("script does not take positional arguments") |
725 | + kwargs = dict() |
726 | + for name, value in opts.__dict__.iteritems(): |
727 | + if value is not None: |
728 | + kwargs[name] = value |
729 | + |
730 | + locale.setlocale(locale.LC_ALL, '') |
731 | + if not kwargs.get('no_plugins', False): |
732 | + plugin.load_plugins() |
733 | + commands.install_bzr_command_hooks() |
734 | + bash_completion_function(sys.stdout, **kwargs) |
735 | |
736 | === added directory 'bzrlib/plugins/bash_completion/tests' |
737 | === added file 'bzrlib/plugins/bash_completion/tests/__init__.py' |
738 | --- bzrlib/plugins/bash_completion/tests/__init__.py 1970-01-01 00:00:00 +0000 |
739 | +++ bzrlib/plugins/bash_completion/tests/__init__.py 2010-05-05 08:10:48 +0000 |
740 | @@ -0,0 +1,23 @@ |
741 | +# Copyright (C) 2010 by Canonical Ltd |
742 | +# |
743 | +# This program is free software; you can redistribute it and/or modify |
744 | +# it under the terms of the GNU General Public License as published by |
745 | +# the Free Software Foundation; either version 2 of the License, or |
746 | +# (at your option) any later version. |
747 | +# |
748 | +# This program is distributed in the hope that it will be useful, |
749 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
750 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
751 | +# GNU General Public License for more details. |
752 | +# |
753 | +# You should have received a copy of the GNU General Public License |
754 | +# along with this program; if not, write to the Free Software |
755 | +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
756 | + |
757 | +def load_tests(basic_tests, module, loader): |
758 | + testmod_names = [ |
759 | + 'test_bashcomp', |
760 | + ] |
761 | + basic_tests.addTest(loader.loadTestsFromModuleNames( |
762 | + ["%s.%s" % (__name__, tmn) for tmn in testmod_names])) |
763 | + return basic_tests |
764 | |
765 | === added file 'bzrlib/plugins/bash_completion/tests/test_bashcomp.py' |
766 | --- bzrlib/plugins/bash_completion/tests/test_bashcomp.py 1970-01-01 00:00:00 +0000 |
767 | +++ bzrlib/plugins/bash_completion/tests/test_bashcomp.py 2010-05-05 08:10:48 +0000 |
768 | @@ -0,0 +1,318 @@ |
769 | +# Copyright (C) 2010 by Canonical Ltd |
770 | +# |
771 | +# This program is free software; you can redistribute it and/or modify |
772 | +# it under the terms of the GNU General Public License as published by |
773 | +# the Free Software Foundation; either version 2 of the License, or |
774 | +# (at your option) any later version. |
775 | +# |
776 | +# This program is distributed in the hope that it will be useful, |
777 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
778 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
779 | +# GNU General Public License for more details. |
780 | +# |
781 | +# You should have received a copy of the GNU General Public License |
782 | +# along with this program; if not, write to the Free Software |
783 | +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
784 | + |
785 | +import bzrlib |
786 | +from bzrlib import commands, tests |
787 | +from bzrlib.plugins.bash_completion.bashcomp import * |
788 | + |
789 | +import os |
790 | +import subprocess |
791 | + |
792 | + |
793 | +class _BashFeature(tests.Feature): |
794 | + """Feature testing whether a bash executable is available.""" |
795 | + |
796 | + bash_paths = ['/bin/bash', '/usr/bin/bash'] |
797 | + |
798 | + def __init__(self): |
799 | + super(_BashFeature, self).__init__() |
800 | + self.bash_path = None |
801 | + |
802 | + def available(self): |
803 | + if self.bash_path is not None: |
804 | + return self.bash_path is not False |
805 | + for path in self.bash_paths: |
806 | + if os.access(path, os.X_OK): |
807 | + self.bash_path = path |
808 | + return True |
809 | + self.bash_path = False |
810 | + return False |
811 | + |
812 | + def feature_name(self): |
813 | + return 'bash' |
814 | + |
815 | +BashFeature = _BashFeature() |
816 | + |
817 | + |
818 | +class BashCompletionMixin(object): |
819 | + """Component for testing execution of a bash completion script.""" |
820 | + |
821 | + _test_needs_features = [BashFeature] |
822 | + |
823 | + def complete(self, words, cword=-1): |
824 | + """Perform a bash completion. |
825 | + |
826 | + :param words: a list of words representing the current command. |
827 | + :param cword: the current word to complete, defaults to the last one. |
828 | + """ |
829 | + if self.script is None: |
830 | + self.script = self.get_script() |
831 | + proc = subprocess.Popen([BashFeature.bash_path, '--noprofile'], |
832 | + stdin=subprocess.PIPE, |
833 | + stdout=subprocess.PIPE, |
834 | + stderr=subprocess.PIPE) |
835 | + if cword < 0: |
836 | + cword = len(words) + cword |
837 | + input = '%s\n' % self.script |
838 | + input += ('COMP_WORDS=( %s )\n' % |
839 | + ' '.join(["'"+w.replace("'", "'\\''")+"'" for w in words])) |
840 | + input += 'COMP_CWORD=%d\n' % cword |
841 | + input += '%s\n' % getattr(self, 'script_name', '_bzr') |
842 | + input += 'echo ${#COMPREPLY[*]}\n' |
843 | + input += "IFS=$'\\n'\n" |
844 | + input += 'echo "${COMPREPLY[*]}"\n' |
845 | + (out, err) = proc.communicate(input) |
846 | + if '' != err: |
847 | + raise AssertionError('Unexpected error message:\n%s' % err) |
848 | + self.assertEqual('', err, 'No messages to standard error') |
849 | + #import sys |
850 | + #print >>sys.stdout, '---\n%s\n---\n%s\n---\n' % (input, out) |
851 | + lines = out.split('\n') |
852 | + nlines = int(lines[0]) |
853 | + del lines[0] |
854 | + self.assertEqual('', lines[-1], 'Newline at end') |
855 | + del lines[-1] |
856 | + if nlines == 0 and len(lines) == 1 and lines[0] == '': |
857 | + del lines[0] |
858 | + self.assertEqual(nlines, len(lines), 'No newlines in generated words') |
859 | + self.completion_result = set(lines) |
860 | + return self.completion_result |
861 | + |
862 | + def assertCompletionEquals(self, *words): |
863 | + self.assertEqual(set(words), self.completion_result) |
864 | + |
865 | + def assertCompletionContains(self, *words): |
866 | + missing = set(words) - self.completion_result |
867 | + if missing: |
868 | + raise AssertionError('Completion should contain %r but it has %r' |
869 | + % (missing, self.completion_result)) |
870 | + |
871 | + def assertCompletionOmits(self, *words): |
872 | + surplus = set(words) & self.completion_result |
873 | + if surplus: |
874 | + raise AssertionError('Completion should omit %r but it has %r' |
875 | + % (surplus, res, self.completion_result)) |
876 | + |
877 | + def get_script(self): |
878 | + commands.install_bzr_command_hooks() |
879 | + dc = DataCollector() |
880 | + data = dc.collect() |
881 | + cg = BashCodeGen(data) |
882 | + res = cg.function() |
883 | + return res |
884 | + |
885 | + |
886 | +class TestBashCompletion(tests.TestCase, BashCompletionMixin): |
887 | + """Test bash completions that don't execute bzr.""" |
888 | + |
889 | + def __init__(self, methodName='testMethod'): |
890 | + super(TestBashCompletion, self).__init__(methodName) |
891 | + self.script = None |
892 | + |
893 | + def test_simple_scipt(self): |
894 | + """Ensure that the test harness works as expected""" |
895 | + self.script = """ |
896 | +_bzr() { |
897 | + COMPREPLY=() |
898 | + # add all words in reverse order, with some markup around them |
899 | + for ((i = ${#COMP_WORDS[@]}; i > 0; --i)); do |
900 | + COMPREPLY+=( "-${COMP_WORDS[i-1]}+" ) |
901 | + done |
902 | + # and append the current word |
903 | + COMPREPLY+=( "+${COMP_WORDS[COMP_CWORD]}-" ) |
904 | +} |
905 | +""" |
906 | + self.complete(['foo', '"bar', "'baz"], cword=1) |
907 | + self.assertCompletionEquals("-'baz+", '-"bar+', '-foo+', '+"bar-') |
908 | + |
909 | + def test_cmd_ini(self): |
910 | + self.complete(['bzr', 'ini']) |
911 | + self.assertCompletionContains('init', 'init-repo', 'init-repository') |
912 | + self.assertCompletionOmits('commit') |
913 | + |
914 | + def test_init_opts(self): |
915 | + self.complete(['bzr', 'init', '-']) |
916 | + self.assertCompletionContains('-h', '--2a', '--format=2a') |
917 | + |
918 | + def test_global_opts(self): |
919 | + self.complete(['bzr', '-', 'init'], cword=1) |
920 | + self.assertCompletionContains('--no-plugins', '--builtin') |
921 | + |
922 | + def test_commit_dashm(self): |
923 | + self.complete(['bzr', 'commit', '-m']) |
924 | + self.assertCompletionEquals('-m') |
925 | + |
926 | + def test_status_negated(self): |
927 | + self.complete(['bzr', 'status', '--n']) |
928 | + self.assertCompletionContains('--no-versioned', '--no-verbose') |
929 | + |
930 | + def test_init_format_any(self): |
931 | + self.complete(['bzr', 'init', '--format', '=', 'directory'], cword=3) |
932 | + self.assertCompletionContains('1.9', '2a') |
933 | + |
934 | + def test_init_format_2(self): |
935 | + self.complete(['bzr', 'init', '--format', '=', '2', 'directory'], |
936 | + cword=4) |
937 | + self.assertCompletionContains('2a') |
938 | + self.assertCompletionOmits('1.9') |
939 | + |
940 | + |
941 | +class TestBashCompletionInvoking(tests.TestCaseWithTransport, |
942 | + BashCompletionMixin): |
943 | + """Test bash completions that might execute bzr. |
944 | + |
945 | + Only the syntax ``$(bzr ...`` is supported so far. The bzr command |
946 | + will be replaced by the bzr instance running this selftest. |
947 | + """ |
948 | + |
949 | + def __init__(self, methodName='testMethod'): |
950 | + super(TestBashCompletionInvoking, self).__init__(methodName) |
951 | + self.script = None |
952 | + |
953 | + def get_script(self): |
954 | + s = super(TestBashCompletionInvoking, self).get_script() |
955 | + return s.replace("$(bzr ", "$('%s' " % self.get_bzr_path()) |
956 | + |
957 | + def test_revspec_tag_all(self): |
958 | + wt = self.make_branch_and_tree('.', format='dirstate-tags') |
959 | + wt.branch.tags.set_tag('tag1', 'null:') |
960 | + wt.branch.tags.set_tag('tag2', 'null:') |
961 | + wt.branch.tags.set_tag('3tag', 'null:') |
962 | + self.complete(['bzr', 'log', '-r', 'tag', ':']) |
963 | + self.assertCompletionEquals('tag1', 'tag2', '3tag') |
964 | + |
965 | + def test_revspec_tag_prefix(self): |
966 | + wt = self.make_branch_and_tree('.', format='dirstate-tags') |
967 | + wt.branch.tags.set_tag('tag1', 'null:') |
968 | + wt.branch.tags.set_tag('tag2', 'null:') |
969 | + wt.branch.tags.set_tag('3tag', 'null:') |
970 | + self.complete(['bzr', 'log', '-r', 'tag', ':', 't']) |
971 | + self.assertCompletionEquals('tag1', 'tag2') |
972 | + |
973 | + |
974 | +class TestBashCodeGen(tests.TestCase): |
975 | + |
976 | + def test_command_names(self): |
977 | + data = CompletionData() |
978 | + bar = CommandData('bar') |
979 | + bar.aliases.append('baz') |
980 | + data.commands.append(bar) |
981 | + data.commands.append(CommandData('foo')) |
982 | + cg = BashCodeGen(data) |
983 | + self.assertEqual('bar baz foo', cg.command_names()) |
984 | + |
985 | + def test_debug_output(self): |
986 | + data = CompletionData() |
987 | + self.assertEqual('', BashCodeGen(data, debug=False).debug_output()) |
988 | + self.assertTrue(BashCodeGen(data, debug=True).debug_output()) |
989 | + |
990 | + def test_bzr_version(self): |
991 | + data = CompletionData() |
992 | + cg = BashCodeGen(data) |
993 | + self.assertEqual('%s.' % bzrlib.version_string, cg.bzr_version()) |
994 | + data.plugins['foo'] = PluginData('foo', '1.0') |
995 | + data.plugins['bar'] = PluginData('bar', '2.0') |
996 | + cg = BashCodeGen(data) |
997 | + self.assertEqual('''\ |
998 | +%s and the following plugins: |
999 | +# bar 2.0 |
1000 | +# foo 1.0''' % bzrlib.version_string, cg.bzr_version()) |
1001 | + |
1002 | + def test_global_options(self): |
1003 | + data = CompletionData() |
1004 | + data.global_options.add('--foo') |
1005 | + data.global_options.add('--bar') |
1006 | + cg = BashCodeGen(data) |
1007 | + self.assertEqual('--bar --foo', cg.global_options()) |
1008 | + |
1009 | + def test_command_cases(self): |
1010 | + data = CompletionData() |
1011 | + bar = CommandData('bar') |
1012 | + bar.aliases.append('baz') |
1013 | + bar.options.append(OptionData('--opt')) |
1014 | + data.commands.append(bar) |
1015 | + data.commands.append(CommandData('foo')) |
1016 | + cg = BashCodeGen(data) |
1017 | + self.assertEqualDiff('''\ |
1018 | +\tbar|baz) |
1019 | +\t\tcmdOpts='--opt' |
1020 | +\t\t;; |
1021 | +\tfoo) |
1022 | +\t\tcmdOpts='' |
1023 | +\t\t;; |
1024 | +''', cg.command_cases()) |
1025 | + |
1026 | + def test_command_case(self): |
1027 | + cmd = CommandData('cmd') |
1028 | + cmd.plugin = PluginData('plugger', '1.0') |
1029 | + bar = OptionData('--bar') |
1030 | + bar.registry_keys = ['that', 'this'] |
1031 | + bar.error_messages.append('Some error message') |
1032 | + cmd.options.append(bar) |
1033 | + cmd.options.append(OptionData('--foo')) |
1034 | + data = CompletionData() |
1035 | + data.commands.append(cmd) |
1036 | + cg = BashCodeGen(data) |
1037 | + self.assertEqualDiff('''\ |
1038 | +\tcmd) |
1039 | +\t\t# plugin "plugger 1.0" |
1040 | +\t\t# Some error message |
1041 | +\t\tcmdOpts='--bar=that --bar=this --foo' |
1042 | +\t\tcase $curOpt in |
1043 | +\t\t\t--bar) optEnums='that this' ;; |
1044 | +\t\tesac |
1045 | +\t\t;; |
1046 | +''', cg.command_case(cmd)) |
1047 | + |
1048 | + |
1049 | +class TestDataCollector(tests.TestCase): |
1050 | + |
1051 | + def setUp(self): |
1052 | + super(TestDataCollector, self).setUp() |
1053 | + commands.install_bzr_command_hooks() |
1054 | + |
1055 | + def test_global_options(self): |
1056 | + dc = DataCollector() |
1057 | + dc.global_options() |
1058 | + self.assertSubset(['--no-plugins', '--builtin'], |
1059 | + dc.data.global_options) |
1060 | + |
1061 | + def test_commands(self): |
1062 | + dc = DataCollector() |
1063 | + dc.commands() |
1064 | + self.assertSubset(['init', 'init-repo', 'init-repository'], |
1065 | + dc.data.all_command_aliases()) |
1066 | + |
1067 | + def test_commit_dashm(self): |
1068 | + dc = DataCollector() |
1069 | + cmd = dc.command('commit') |
1070 | + self.assertSubset(['-m'], |
1071 | + [str(o) for o in cmd.options]) |
1072 | + |
1073 | + def test_status_negated(self): |
1074 | + dc = DataCollector() |
1075 | + cmd = dc.command('status') |
1076 | + self.assertSubset(['--no-versioned', '--no-verbose'], |
1077 | + [str(o) for o in cmd.options]) |
1078 | + |
1079 | + def test_init_format(self): |
1080 | + dc = DataCollector() |
1081 | + cmd = dc.command('init') |
1082 | + for opt in cmd.options: |
1083 | + if opt.name == '--format': |
1084 | + self.assertSubset(['2a'], opt.registry_keys) |
1085 | + return |
1086 | + raise AssertionError('Option --format not found') |
1087 | |
1088 | === added file 'contrib/bash/bzr' |
1089 | --- contrib/bash/bzr 1970-01-01 00:00:00 +0000 |
1090 | +++ contrib/bash/bzr 2010-05-05 08:10:48 +0000 |
1091 | @@ -0,0 +1,40 @@ |
1092 | +# Copyright (C) 2010 Canonical Ltd |
1093 | +# |
1094 | +# This program is free software; you can redistribute it and/or modify |
1095 | +# it under the terms of the GNU General Public License as published by |
1096 | +# the Free Software Foundation; either version 2 of the License, or |
1097 | +# (at your option) any later version. |
1098 | +# |
1099 | +# This program is distributed in the hope that it will be useful, |
1100 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
1101 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1102 | +# GNU General Public License for more details. |
1103 | +# |
1104 | +# You should have received a copy of the GNU General Public License |
1105 | +# along with this program; if not, write to the Free Software |
1106 | +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
1107 | + |
1108 | +# Programmable completion for the Bazaar-NG bzr command under bash. |
1109 | +# Source this file (or add it to your ~/.bash_completion or ~/.bashrc |
1110 | +# file, depending on your system configuration, and start a new shell) |
1111 | +# and bash's completion mechanism will know all about bzr's options! |
1112 | +# |
1113 | +# This completion function assumes you have the bzr-bash-completion |
1114 | +# plugin installed as a bzr plugin. It will generate the full |
1115 | +# completion function at first invocation, thus avoiding long delays |
1116 | +# for every shell you start. |
1117 | + |
1118 | +shopt -s progcomp |
1119 | +_bzr_lazy () |
1120 | +{ |
1121 | + unset _bzr |
1122 | + eval "$(bzr bash-completion)" |
1123 | + if [[ $(type -t _bzr) == function ]]; then |
1124 | + unset _bzr_lazy |
1125 | + _bzr |
1126 | + return $? |
1127 | + else |
1128 | + return 1 |
1129 | + fi |
1130 | +} |
1131 | +complete -F _bzr_lazy -o default bzr |
1132 | |
1133 | === removed file 'contrib/bash/bzr' |
1134 | --- contrib/bash/bzr 2005-09-19 06:05:19 +0000 |
1135 | +++ contrib/bash/bzr 1970-01-01 00:00:00 +0000 |
1136 | @@ -1,104 +0,0 @@ |
1137 | -# Programmable completion for the Bazaar-NG bzr command under bash. Source |
1138 | -# this file (or on some systems add it to ~/.bash_completion and start a new |
1139 | -# shell) and bash's completion mechanism will know all about bzr's options! |
1140 | - |
1141 | -# Known to work with bash 2.05a with programmable completion and extended |
1142 | -# pattern matching enabled (use 'shopt -s extglob progcomp' to enable |
1143 | -# these if they are not already enabled). |
1144 | - |
1145 | -# Based originally on the svn bash completition script. |
1146 | -# Customized by Sven Wilhelm/Icecrash.com |
1147 | - |
1148 | -_bzr () |
1149 | -{ |
1150 | - local cur cmds cmdOpts opt helpCmds optBase i |
1151 | - |
1152 | - COMPREPLY=() |
1153 | - cur=${COMP_WORDS[COMP_CWORD]} |
1154 | - |
1155 | - cmds='status diff commit ci checkin move remove log info check ignored' |
1156 | - |
1157 | - if [[ $COMP_CWORD -eq 1 ]] ; then |
1158 | - COMPREPLY=( $( compgen -W "$cmds" -- $cur ) ) |
1159 | - return 0 |
1160 | - fi |
1161 | - |
1162 | - # if not typing an option, or if the previous option required a |
1163 | - # parameter, then fallback on ordinary filename expansion |
1164 | - helpCmds='help|--help|h|\?' |
1165 | - if [[ ${COMP_WORDS[1]} != @($helpCmds) ]] && \ |
1166 | - [[ "$cur" != -* ]] ; then |
1167 | - return 0 |
1168 | - fi |
1169 | - |
1170 | - cmdOpts= |
1171 | - case ${COMP_WORDS[1]} in |
1172 | - status) |
1173 | - cmdOpts="--all --show-ids" |
1174 | - ;; |
1175 | - diff) |
1176 | - cmdOpts="-r --revision --diff-options" |
1177 | - ;; |
1178 | - commit|ci|checkin) |
1179 | - cmdOpts="-r --message -F --file -v --verbose" |
1180 | - ;; |
1181 | - move) |
1182 | - cmdOpts="" |
1183 | - ;; |
1184 | - remove) |
1185 | - cmdOpts="-v --verbose" |
1186 | - ;; |
1187 | - log) |
1188 | - cmdOpts="--forward --timezone -v --verbose --show-ids -r --revision" |
1189 | - ;; |
1190 | - info) |
1191 | - cmdOpts="" |
1192 | - ;; |
1193 | - ignored) |
1194 | - cmdOpts="" |
1195 | - ;; |
1196 | - check) |
1197 | - cmdOpts="" |
1198 | - ;; |
1199 | - help|h|\?) |
1200 | - cmdOpts="$cmds $qOpts" |
1201 | - ;; |
1202 | - *) |
1203 | - ;; |
1204 | - esac |
1205 | - |
1206 | - cmdOpts="$cmdOpts --help -h" |
1207 | - |
1208 | - # take out options already given |
1209 | - for (( i=2; i<=$COMP_CWORD-1; ++i )) ; do |
1210 | - opt=${COMP_WORDS[$i]} |
1211 | - |
1212 | - case $opt in |
1213 | - --*) optBase=${opt/=*/} ;; |
1214 | - -*) optBase=${opt:0:2} ;; |
1215 | - esac |
1216 | - |
1217 | - cmdOpts=" $cmdOpts " |
1218 | - cmdOpts=${cmdOpts/ ${optBase} / } |
1219 | - |
1220 | - # take out alternatives |
1221 | - case $optBase in |
1222 | - -v) cmdOpts=${cmdOpts/ --verbose / } ;; |
1223 | - --verbose) cmdOpts=${cmdOpts/ -v / } ;; |
1224 | - -h) cmdOpts=${cmdOpts/ --help / } ;; |
1225 | - --help) cmdOpts=${cmdOpts/ -h / } ;; |
1226 | - -r) cmdOpts=${cmdOpts/ --revision / } ;; |
1227 | - --revision) cmdOpts=${cmdOpts/ -r / } ;; |
1228 | - esac |
1229 | - |
1230 | - # skip next option if this one requires a parameter |
1231 | - if [[ $opt == @($optsParam) ]] ; then |
1232 | - ((++i)) |
1233 | - fi |
1234 | - done |
1235 | - |
1236 | - COMPREPLY=( $( compgen -W "$cmdOpts" -- $cur ) ) |
1237 | - |
1238 | - return 0 |
1239 | -} |
1240 | -complete -F _bzr -o default bzr |
1241 | |
1242 | === removed file 'contrib/bash/bzr.simple' |
1243 | --- contrib/bash/bzr.simple 2007-06-06 19:44:39 +0000 |
1244 | +++ contrib/bash/bzr.simple 1970-01-01 00:00:00 +0000 |
1245 | @@ -1,28 +0,0 @@ |
1246 | -# -*- shell-script -*- |
1247 | - |
1248 | -# experimental bzr bash completion |
1249 | - |
1250 | -# author: Martin Pool |
1251 | - |
1252 | -_bzr_commands() |
1253 | -{ |
1254 | - bzr help commands | sed -r 's/^([-[:alnum:]]*).*/\1/' | grep '^[[:alnum:]]' |
1255 | -} |
1256 | - |
1257 | -_bzr() |
1258 | -{ |
1259 | - cur=${COMP_WORDS[COMP_CWORD]} |
1260 | - prev=${COMP_WORDS[COMP_CWORD-1]} |
1261 | - if [ $COMP_CWORD -eq 1 ]; then |
1262 | - COMPREPLY=( $( compgen -W "$(_bzr_commands)" $cur ) ) |
1263 | - elif [ $COMP_CWORD -eq 2 ]; then |
1264 | - case "$prev" in |
1265 | - help) |
1266 | - COMPREPLY=( $( compgen -W "$(_bzr_commands) commands" $cur ) ) |
1267 | - ;; |
1268 | - esac |
1269 | - fi |
1270 | -} |
1271 | - |
1272 | -complete -F _bzr -o default bzr |
1273 | - |
Source is not Python 2.4 compatible:
+ "debug": debug_output if debug else "",
Should the plugin really be unconditionally installed when a number of platforms don't use bash?