Merge lp:~gagern/bzr/bug560030-include-bash-completion-plugin into lp:bzr

Proposed by Martin von Gagern
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
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-completion: a script that generates the full bzr completion function upon first invocation, keeping bash startup times low and still avoiding the overhead of a bzr invocation for every completion except the first.

To post a comment you must log in.
Revision history for this message
Martin Packman (gz) wrote :

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?

Revision history for this message
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.

Revision history for this message
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.register_lazy().

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)

review: Needs Fixing
Revision history for this message
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.

Revision history for this message
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.tests.features for
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.builtins.cmd_shell_complete which is used by zsh (I think).

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(commands.Command)

  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.

Revision history for this message
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.tests.features for that.
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.builtins.cmd_shell_complete which is used by zsh (I think).
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.

Revision history for this message
Vincent Ladeuil (vila) wrote :
Download full text (4.2 KiB)

>>>>> Martin von Gagern <email address hidden> writes:

    >> check whether bash is available, and skip if it isn't.
    >> define a feature in bzrlib.tests.features for that.

    > 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.builtins.cmd_shell_complete which is used by zsh (I think).

    > 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.plugins.<plugin_name>' ensures we get the right one.

<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/plugins/bash_completion/README.txt 1970-01-01 00:00:00 +0000

This still includes material related to the use of the plugin in its
non-core version...

Read more...

review: Needs Fixing
Revision history for this message
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/plugins/bash_completion/README.txt 1970-01-01 00:00:00 +0000
>
> 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 assertCompletionXXXX will make the tests
> 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.

Revision history for this message
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.

review: Approve
Revision history for this message
John A Meinel (jameinel) wrote :
Download full text (3.5 KiB)

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('/bin/bash', X_OK)" rather than just:

subprocess.Popen(['bash', '-c', 'echo hello'], shell=True)

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.bash_completion.tests.test_bashcomp.TestBashCompletion.test_commit_dashm OK 2163ms
...plugins.bash_completion.tests.test_bashcomp.TestBashCompletion.test_global_opts OK 3039ms
...ugins.bash_completion.tests.test_bashcomp.TestBashCompletion.test_init_format_2 OK 1719ms
...ins.bash_completion.tests.test_bashcomp.TestBashCompletion.test_init_format_any OK 1786ms
...b.plugins.bash_completion.tests.test_bashcomp.TestBashCompletion.test_init_opts OK 1808ms
...lugins.bash_completion.tests.test_bashcomp.TestBashCompletion.test_simple_scipt OK 65ms
...gins.bash_completion.tests.test_bashcomp.TestBashCompletion.test_status_negated OK 1822ms
..._completion.tests.test_bashcomp.TestBashCompletionInvoking.test_revspec_tag_all FAIL 3407ms
    Text attachment: log
------------
15.616 creating repository in file:///C:/users/jameinel/appdata/local/temp/testbzr-06_rhc.tmp/.bzr/.
15.632 creating branch <bzrlib.branch.BzrBranchFormat7 object at 0x01FA9110> in file:///C:/users/jameinel/appdata/local/temp/testbzr-06_rhc.tmp/
15.725 opening working tree 'C:/users/jameinel/appdata/local/temp/testbzr-06_rhc.tmp'
15.999 creating repository in file:///C:/users/jameinel/appdata/local/temp/testbzr-06_rhc.tmp/nInvoking.test_revspec_tag_all/work/.bzr/.
16.180 creating branch <bzrlib.branch.BzrBranchFormat6 object at 0x033F5E50> in file:///C:/users/jameinel/appdata/local/temp/testbzr-06_rhc.tmp/nInvoking.test_revspec_tag_all/work/
16.292 opening working tree 'C:/users/jameinel/appdata/local/temp/testbzr-06_rhc.tmp/nInvoking.test_revspec_tag_all/work'
------------
Text attachment: traceback
------------
Traceback (most recent call last):
  File "c:\Python26\lib\site-packages\testtools-0.9.2-py2.6.egg\testtools\runtest.py", line 128, in _run_user
    return fn(*args)
  File "c:\Python26\lib\site-packages\testtools-0.9.2-py2.6.egg\testtools\testcase.py", line 368, in _run_test_method
    testMethod()
  File "C:\Users\jameinel\dev\bzr\jam-integration\bzrlib\plugins\bash_completion\tests\test_bashcomp.py", line 197, in test_revspec_tag_all
    self.assertCompletionEquals('tag1', 'tag2', '3tag')
  File "C:\Users\jameinel\dev\bzr\jam-integration\bzrlib\plugins\bash_completion\tests\test_bashcomp.py", line 97, in assertCompletionEquals
    self.assertEqual(set(words), self.completion_result)
AssertionError: not equal:
a = set(['3tag', 'tag1', 'tag2'])
b = set()

I'm cer...

Read more...

review: Needs Information
Revision history for this message
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.tests.test_bashcomp.TestBashCodeGen.test_bzr_version OK 32ms
...etion.tests.test_bashcomp.TestBashCodeGen.test_command_case OK 8ms
...tion.tests.test_bashcomp.TestBashCodeGen.test_command_cases OK 1ms
...tion.tests.test_bashcomp.TestBashCodeGen.test_command_names OK 2ms
...etion.tests.test_bashcomp.TestBashCodeGen.test_debug_output OK 1ms
...ion.tests.test_bashcomp.TestBashCodeGen.test_global_options OK 3ms
...pletion.tests.test_bashcomp.TestBashCompletion.test_cmd_ini OK 685ms
...on.tests.test_bashcomp.TestBashCompletion.test_commit_dashm OK 680ms
...ion.tests.test_bashcomp.TestBashCompletion.test_global_opts OK 682ms
...n.tests.test_bashcomp.TestBashCompletion.test_init_format_2 OK 649ms
...tests.test_bashcomp.TestBashCompletion.test_init_format_any OK 676ms
...etion.tests.test_bashcomp.TestBashCompletion.test_init_opts OK 664ms
...on.tests.test_bashcomp.TestBashCompletion.test_simple_scipt OK 38ms
....tests.test_bashcomp.TestBashCompletion.test_status_negated OK 649ms
...st_bashcomp.TestBashCompletionInvoking.test_revspec_tag_all OK 985ms
...bashcomp.TestBashCompletionInvoking.test_revspec_tag_prefix OK 894ms
...pletion.tests.test_bashcomp.TestDataCollector.test_commands OK 623ms
...ion.tests.test_bashcomp.TestDataCollector.test_commit_dashm OK 8ms
...n.tests.test_bashcomp.TestDataCollector.test_global_options OK 1ms
...tion.tests.test_bashcomp.TestDataCollector.test_init_format OK 9ms
...n.tests.test_bashcomp.TestDataCollector.test_status_negated OK 6ms

----------------------------------------------------------------------
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...)

review: Approve
Revision history for this message
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.

Revision history for this message
Martin von Gagern (gagern) wrote :

On 18.05.2010 22:48, John A Meinel wrote:
> I do wonder about using "os.access('/bin/bash', X_OK)" rather than just:
> subprocess.Popen(['bash', '-c', 'echo hello'], shell=True)
> 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.tests.test_bashcomp.TestBashCompletionInvoking.test_revspec_tag_all FAIL 3407ms

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.

Revision history for this message
Robert Collins (lifeless) wrote :

submitted to PQM by hand.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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-