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
=== modified file 'NEWS'
--- NEWS 2010-05-04 22:02:05 +0000
+++ NEWS 2010-05-05 08:10:48 +0000
@@ -36,6 +36,11 @@
36 re-sign, unbind, unknowns.36 re-sign, unbind, unknowns.
37 (Martin von Gagern, #527878)37 (Martin von Gagern, #527878)
3838
39* The bash_completion plugin from the bzr-bash-completion project has
40 been merged into the tree. It provides a bash-completion command and
41 replaces the outdated ``contrib/bash/bzr`` script with a version
42 using the plugin. (Martin von Gagern, #560030)
43
39Bug Fixes44Bug Fixes
40*********45*********
4146
4247
=== added directory 'bzrlib/plugins/bash_completion'
=== added file 'bzrlib/plugins/bash_completion/README.txt'
--- bzrlib/plugins/bash_completion/README.txt 1970-01-01 00:00:00 +0000
+++ bzrlib/plugins/bash_completion/README.txt 2010-05-05 08:10:48 +0000
@@ -0,0 +1,201 @@
1.. comment
2
3 Copyright (C) 2010 Canonical Ltd
4
5 This file is part of bzr-bash-completion
6
7 bzr-bash-completion free software: you can redistribute it and/or
8 modify it under the terms of the GNU General Public License as
9 published by the Free Software Foundation, either version 2 of the
10 License, or (at your option) any later version.
11
12 bzr-bash-completion is distributed in the hope that it will be
13 useful, but WITHOUT ANY WARRANTY; without even the implied warranty
14 of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 General Public License for more details.
16
17 You should have received a copy of the GNU General Public License
18 along with this program. If not, see <http://www.gnu.org/licenses/>.
19
20=====================================
21bzr bash-completion script and plugin
22=====================================
23
24This script generates a shell function which can be used by bash to
25automatically complete the currently typed command when the user
26presses the completion key (usually tab).
27
28It is intended as a bzr plugin, but can be used to some extend as a
29standalone python script as well.
30
31| Copyright (C) 2009, 2010 Canonical Ltd
32
33.. contents::
34
35----------
36Installing
37----------
38
39You only need to do this if you want to use the script as a bzr
40plugin. Otherwise simply grab the bashcomp.py and place it wherever
41you want.
42
43Installing from bzr repository
44------------------------------
45
46To check out the current code from launchpad, use the following commands::
47
48 mkdir -p ~/.bazaar/plugins
49 cd ~/.bazaar/plugins
50 bzr checkout lp:bzr-bash-completion bash_completion
51
52To update such an installation, execute this command::
53
54 bzr update ~/.bazaar/plugins/bash_completion
55
56Installing using easy_install
57-----------------------------
58
59The following command should install the latest release of the plugin
60on your system::
61
62 easy_install bzr-bash-completion
63
64To use this method, you need to have `Easy Install`_ installed and
65also have write access to the required directories. So maybe you
66should execute this command as root or through sudo_. Or you want to
67`install to a different location`_.
68
69.. _Easy Install: http://peak.telecommunity.com/DevCenter/EasyInstall
70.. _sudo: http://linux.die.net/man/8/sudo
71.. _install to a different location:
72 http://peak.telecommunity.com/DevCenter/EasyInstall#non-root-installation
73
74Installing from tarball
75-----------------------
76
77If you have grabbed a source code tarball, or want to install from a
78bzr checkout in a different place than your bazaar plugins directory,
79then you should use the ``setup.py`` script shipped with the code::
80
81 ./setup.py install
82
83If you want to install the plugin only for your own user account, you
84might wish to pass the option ``--user`` or ``--home=$HOME`` to that
85command. For further information please read the manuals of distutils_
86as well as setuptools_ or distribute_, whatever is available on your
87system, or have a look at the command line help::
88
89 ./setup.py install --help
90
91.. _distutils: http://docs.python.org/install/index.html
92.. _setuptools: http://peak.telecommunity.com/DevCenter/setuptools#what-your-users-should-know
93.. _distribute: http://packages.python.org/distribute/setuptools.html#what-your-users-should-know
94
95-----
96Using
97-----
98
99Using as a plugin
100-----------------
101
102This is the preferred method of generating the completion function, as
103it will ensure proper bzr initialization.
104
105::
106
107 eval "`bzr bash-completion`"
108
109Lazy initialization
110-------------------
111
112Running the above command automatically from your ``~/.bashrc`` file
113or similar can cause annoying delays in the startup of your shell.
114To avoid this problem, you can delay the generation of the completion
115function until you actually need it.
116
117To do so, source the file ``lazy.sh`` shipped with this package from
118your ``~/.bashrc`` file or add it to your ``~/.bash_completion`` if
119your setup uses such a file. On a system-wide installation, the
120directory ``/usr/share/bash-completion/`` might contain such bash
121completion scripts.
122
123If you installed bzr-bash-completion from the repository or a source
124tarball, you find the ``lazy.sh`` script in the root of the source
125tree. If you installed the plugin using easy_install, you should grab
126the script manually from the bzr repository, e.g. through the bazaar
127web interface on launchpad.
128
129Note that the full completion function is generated only once per
130shell session. If you update your bzr installation or change the set
131of installed plugins, then you might wish to regenerate the completion
132function manually as described above in order for completion to take
133these changes into account.
134
135Using as a script
136-----------------
137
138As an alternative, if bzrlib is available to python scripts, the
139following invocation should yield the same results without requiring
140you to add a plugin::
141
142 eval "`./bashcomp.py`"
143
144This approach might have some issues, though, and provides less
145options than the bzr plugin. Therefore if you have the choice, go for
146the plugin setup.
147
148--------------
149Design concept
150--------------
151
152The plugin (or script) is designed to generate a completion function
153containing all the required information about the possible
154completions. This is usually only done once when bash
155initializes. After that, no more invocations of bzr are required. This
156makes the function much faster than a possible implementation talking
157to bzr for each and every completion. On the other hand, this has the
158effect that updates to bzr or its plugins won't show up in the
159completions immediately, but only after the completion function has
160been regenerated.
161
162-------
163License
164-------
165
166As this is built upon a bash completion script originally included in
167the bzr source tree, and as the bzr sources are covered by the GPL 2,
168this script here is licensed under these same terms.
169
170If you require a more liberal license, you'll have to contact all
171those who contributed code to this plugin, be it for bash or for
172python.
173
174.. cut long_description here
175
176-------
177History
178-------
179
180The plugin was created by Martin von Gagern in 2009, building on a
181static completion function of very limited scope distributed together
182with bzr.
183
184----------
185References
186----------
187
188Plugin homepages
189 | https://launchpad.net/bzr-bash-completion
190 | http://pypi.python.org/pypi/bzr-bash-completion
191Bazaar homepage
192 | http://bazaar.canonical.com/
193
194
195
196.. vim: ft=rst
197
198.. emacs
199 Local Variables:
200 mode: rst
201 End:
0202
=== added file 'bzrlib/plugins/bash_completion/__init__.py'
--- bzrlib/plugins/bash_completion/__init__.py 1970-01-01 00:00:00 +0000
+++ bzrlib/plugins/bash_completion/__init__.py 2010-05-05 08:10:48 +0000
@@ -0,0 +1,39 @@
1# Copyright (C) 2009, 2010 Canonical Ltd
2#
3# This program is free software; you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation; either version 2 of the License, or
6# (at your option) any later version.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program; if not, write to the Free Software
15# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
17__doc__ = """Generate a shell function for bash command line completion.
18
19This plugin provides a command called bash-completion that generates a
20bash completion function for bzr. See its documentation for details.
21"""
22
23from bzrlib import commands, version_info
24
25
26bzr_plugin_name = 'bash_completion'
27bzr_commands = [ 'bash-completion' ]
28
29commands.plugin_cmds.register_lazy('cmd_bash_completion', [],
30 'bzrlib.plugins.bash_completion.bashcomp')
31
32
33def load_tests(basic_tests, module, loader):
34 testmod_names = [
35 'tests',
36 ]
37 basic_tests.addTest(loader.loadTestsFromModuleNames(
38 ["%s.%s" % (__name__, tmn) for tmn in testmod_names]))
39 return basic_tests
040
=== added file 'bzrlib/plugins/bash_completion/bashcomp.py'
--- bzrlib/plugins/bash_completion/bashcomp.py 1970-01-01 00:00:00 +0000
+++ bzrlib/plugins/bash_completion/bashcomp.py 2010-05-05 08:10:48 +0000
@@ -0,0 +1,463 @@
1#!/usr/bin/env python
2
3# Copyright (C) 2009, 2010 Canonical Ltd
4#
5# This program is free software; you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by
7# the Free Software Foundation; either version 2 of the License, or
8# (at your option) any later version.
9#
10# This program is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with this program; if not, write to the Free Software
17# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
18
19from bzrlib import (
20 commands,
21 config,
22 help_topics,
23 option,
24 plugin,
25)
26import bzrlib
27import re
28
29
30class BashCodeGen(object):
31 """Generate a bash script for given completion data."""
32
33 def __init__(self, data, function_name='_bzr', debug=False):
34 self.data = data
35 self.function_name = function_name
36 self.debug = debug
37
38 def script(self):
39 return ("""\
40# Programmable completion for the Bazaar-NG bzr command under bash.
41# Known to work with bash 2.05a as well as bash 4.1.2, and probably
42# all versions in between as well.
43
44# Based originally on the svn bash completition script.
45# Customized by Sven Wilhelm/Icecrash.com
46# Adjusted for automatic generation by Martin von Gagern
47
48# Generated using the bash_completion plugin.
49# See https://launchpad.net/bzr-bash-completion for details.
50
51# Commands and options of bzr %(bzr_version)s
52
53shopt -s progcomp
54%(function)s
55complete -F %(function_name)s -o default bzr
56""" % {
57 "function_name": self.function_name,
58 "function": self.function(),
59 "bzr_version": self.bzr_version(),
60 })
61
62 def function(self):
63 return ("""\
64%(function_name)s ()
65{
66 local cur cmds cmdIdx cmd cmdOpts fixedWords i globalOpts
67 local curOpt optEnums
68
69 COMPREPLY=()
70 cur=${COMP_WORDS[COMP_CWORD]}
71
72 cmds='%(cmds)s'
73 globalOpts='%(global_options)s'
74
75 # do ordinary expansion if we are anywhere after a -- argument
76 for ((i = 1; i < COMP_CWORD; ++i)); do
77 [[ ${COMP_WORDS[i]} == "--" ]] && return 0
78 done
79
80 # find the command; it's the first word not starting in -
81 cmd=
82 for ((cmdIdx = 1; cmdIdx < ${#COMP_WORDS[@]}; ++cmdIdx)); do
83 if [[ ${COMP_WORDS[cmdIdx]} != -* ]]; then
84 cmd=${COMP_WORDS[cmdIdx]}
85 break
86 fi
87 done
88
89 # complete command name if we are not already past the command
90 if [[ $COMP_CWORD -le cmdIdx ]]; then
91 COMPREPLY=( $( compgen -W "$cmds $globalOpts" -- $cur ) )
92 return 0
93 fi
94
95 # find the option for which we want to complete a value
96 curOpt=
97 if [[ $cur != -* ]] && [[ $COMP_CWORD -gt 1 ]]; then
98 curOpt=${COMP_WORDS[COMP_CWORD - 1]}
99 if [[ $curOpt == = ]]; then
100 curOpt=${COMP_WORDS[COMP_CWORD - 2]}
101 elif [[ $cur == : ]]; then
102 cur=
103 curOpt="$curOpt:"
104 elif [[ $curOpt == : ]]; then
105 curOpt=${COMP_WORDS[COMP_CWORD - 2]}:
106 fi
107 fi
108%(debug)s
109 cmdOpts=
110 optEnums=
111 fixedWords=
112 case $cmd in
113%(cases)s\
114 *)
115 cmdOpts='--help -h'
116 ;;
117 esac
118
119 if [[ -z $fixedWords ]] && [[ -z $optEnums ]] && [[ $cur != -* ]]; then
120 case $curOpt in
121 tag:*)
122 fixedWords="$(bzr tags 2>/dev/null | sed 's/ *[^ ]*$//')"
123 ;;
124 esac
125 elif [[ $cur == = ]] && [[ -n $optEnums ]]; then
126 # complete directly after "--option=", list all enum values
127 COMPREPLY=( $optEnums )
128 return 0
129 else
130 fixedWords="$cmdOpts $globalOpts $optEnums $fixedWords"
131 fi
132
133 if [[ -n $fixedWords ]]; then
134 COMPREPLY=( $( compgen -W "$fixedWords" -- $cur ) )
135 fi
136
137 return 0
138}
139""" % {
140 "cmds": self.command_names(),
141 "function_name": self.function_name,
142 "cases": self.command_cases(),
143 "global_options": self.global_options(),
144 "debug": self.debug_output(),
145 })
146
147 def command_names(self):
148 return " ".join(self.data.all_command_aliases())
149
150 def debug_output(self):
151 if not self.debug:
152 return ''
153 else:
154 return (r"""
155 # Debugging code enabled using the --debug command line switch.
156 # Will dump some variables to the top portion of the terminal.
157 echo -ne '\e[s\e[H'
158 for (( i=0; i < ${#COMP_WORDS[@]}; ++i)); do
159 echo "\$COMP_WORDS[$i]='${COMP_WORDS[i]}'"$'\e[K'
160 done
161 for i in COMP_CWORD COMP_LINE COMP_POINT COMP_TYPE COMP_KEY cur curOpt; do
162 echo "\$${i}=\"${!i}\""$'\e[K'
163 done
164 echo -ne '---\e[K\e[u'
165""")
166
167 def bzr_version(self):
168 bzr_version = bzrlib.version_string
169 if not self.data.plugins:
170 bzr_version += "."
171 else:
172 bzr_version += " and the following plugins:"
173 for name, plugin in sorted(self.data.plugins.iteritems()):
174 bzr_version += "\n# %s" % plugin
175 return bzr_version
176
177 def global_options(self):
178 return " ".join(sorted(self.data.global_options))
179
180 def command_cases(self):
181 cases = ""
182 for command in self.data.commands:
183 cases += self.command_case(command)
184 return cases
185
186 def command_case(self, command):
187 case = "\t%s)\n" % "|".join(command.aliases)
188 if command.plugin:
189 case += "\t\t# plugin \"%s\"\n" % command.plugin
190 options = []
191 enums = []
192 for option in command.options:
193 for message in option.error_messages:
194 case += "\t\t# %s\n" % message
195 if option.registry_keys:
196 for key in option.registry_keys:
197 options.append("%s=%s" % (option, key))
198 enums.append("%s) optEnums='%s' ;;" %
199 (option, ' '.join(option.registry_keys)))
200 else:
201 options.append(str(option))
202 case += "\t\tcmdOpts='%s'\n" % " ".join(options)
203 if command.fixed_words:
204 fixed_words = command.fixed_words
205 if isinstance(fixed_words, list):
206 fixed_words = "'%s'" + ' '.join(fixed_words)
207 case += "\t\tfixedWords=%s\n" % fixed_words
208 if enums:
209 case += "\t\tcase $curOpt in\n\t\t\t"
210 case += "\n\t\t\t".join(enums)
211 case += "\n\t\tesac\n"
212 case += "\t\t;;\n"
213 return case
214
215
216class CompletionData(object):
217
218 def __init__(self):
219 self.plugins = {}
220 self.global_options = set()
221 self.commands = []
222
223 def all_command_aliases(self):
224 for c in self.commands:
225 for a in c.aliases:
226 yield a
227
228
229class CommandData(object):
230
231 def __init__(self, name):
232 self.name = name
233 self.aliases = [name]
234 self.plugin = None
235 self.options = []
236 self.fixed_words = None
237
238
239class PluginData(object):
240
241 def __init__(self, name, version=None):
242 if version is None:
243 version = bzrlib.plugin.plugins()[name].__version__
244 self.name = name
245 self.version = version
246
247 def __str__(self):
248 if self.version == 'unknown':
249 return self.name
250 return '%s %s' % (self.name, self.version)
251
252
253class OptionData(object):
254
255 def __init__(self, name):
256 self.name = name
257 self.registry_keys = None
258 self.error_messages = []
259
260 def __str__(self):
261 return self.name
262
263 def __cmp__(self, other):
264 return cmp(self.name, other.name)
265
266
267class DataCollector(object):
268
269 def __init__(self, no_plugins=False, selected_plugins=None):
270 self.data = CompletionData()
271 self.user_aliases = {}
272 if no_plugins:
273 self.selected_plugins = set()
274 elif selected_plugins is None:
275 self.selected_plugins = None
276 else:
277 self.selected_plugins = set([x.replace('-', '_')
278 for x in selected_plugins])
279
280 def collect(self):
281 self.global_options()
282 self.aliases()
283 self.commands()
284 return self.data
285
286 def global_options(self):
287 re_switch = re.compile(r'\n(--[A-Za-z0-9-_]+)(?:, (-\S))?\s')
288 help_text = help_topics.topic_registry.get_detail('global-options')
289 for long, short in re_switch.findall(help_text):
290 self.data.global_options.add(long)
291 if short:
292 self.data.global_options.add(short)
293
294 def aliases(self):
295 for alias, expansion in config.GlobalConfig().get_aliases().iteritems():
296 for token in commands.shlex_split_unicode(expansion):
297 if not token.startswith("-"):
298 self.user_aliases.setdefault(token, set()).add(alias)
299 break
300
301 def commands(self):
302 for name in sorted(commands.all_command_names()):
303 self.command(name)
304
305 def command(self, name):
306 cmd = commands.get_cmd_object(name)
307 cmd_data = CommandData(name)
308
309 plugin_name = cmd.plugin_name()
310 if plugin_name is not None:
311 if (self.selected_plugins is not None and
312 plugin not in self.selected_plugins):
313 return None
314 plugin_data = self.data.plugins.get(plugin_name)
315 if plugin_data is None:
316 plugin_data = PluginData(plugin_name)
317 self.data.plugins[plugin_name] = plugin_data
318 cmd_data.plugin = plugin_data
319 self.data.commands.append(cmd_data)
320
321 # Find all aliases to the command; both cmd-defined and user-defined.
322 # We assume a user won't override one command with a different one,
323 # but will choose completely new names or add options to existing
324 # ones while maintaining the actual command name unchanged.
325 cmd_data.aliases.extend(cmd.aliases)
326 cmd_data.aliases.extend(sorted([useralias
327 for cmdalias in cmd_data.aliases
328 if cmdalias in self.user_aliases
329 for useralias in self.user_aliases[cmdalias]
330 if useralias not in cmd_data.aliases]))
331
332 opts = cmd.options()
333 for optname, opt in sorted(opts.iteritems()):
334 cmd_data.options.extend(self.option(opt))
335
336 if 'help' == name or 'help' in cmd.aliases:
337 cmd_data.fixed_words = ('"$cmds %s"' %
338 " ".join(sorted(help_topics.topic_registry.keys())))
339
340 return cmd_data
341
342 def option(self, opt):
343 optswitches = {}
344 parser = option.get_optparser({opt.name: opt})
345 parser = self.wrap_parser(optswitches, parser)
346 optswitches.clear()
347 opt.add_option(parser, opt.short_name())
348 if isinstance(opt, option.RegistryOption) and opt.enum_switch:
349 enum_switch = '--%s' % opt.name
350 enum_data = optswitches.get(enum_switch)
351 if enum_data:
352 try:
353 enum_data.registry_keys = opt.registry.keys()
354 except ImportError, e:
355 enum_data.error_messages.append(
356 "ERROR getting registry keys for '--%s': %s"
357 % (opt.name, str(e).split('\n')[0]))
358 return sorted(optswitches.values())
359
360 def wrap_container(self, optswitches, parser):
361 def tweaked_add_option(*opts, **attrs):
362 for name in opts:
363 optswitches[name] = OptionData(name)
364 parser.add_option = tweaked_add_option
365 return parser
366
367 def wrap_parser(self, optswitches, parser):
368 orig_add_option_group = parser.add_option_group
369 def tweaked_add_option_group(*opts, **attrs):
370 return self.wrap_container(optswitches,
371 orig_add_option_group(*opts, **attrs))
372 parser.add_option_group = tweaked_add_option_group
373 return self.wrap_container(optswitches, parser)
374
375
376def bash_completion_function(out, function_name="_bzr", function_only=False,
377 debug=False,
378 no_plugins=False, selected_plugins=None):
379 dc = DataCollector(no_plugins=no_plugins, selected_plugins=selected_plugins)
380 data = dc.collect()
381 cg = BashCodeGen(data, function_name=function_name, debug=debug)
382 if function_only:
383 res = cg.function()
384 else:
385 res = cg.script()
386 out.write(res)
387
388
389class cmd_bash_completion(commands.Command):
390 __doc__ = """Generate a shell function for bash command line completion.
391
392 This command generates a shell function which can be used by bash to
393 automatically complete the currently typed command when the user presses
394 the completion key (usually tab).
395
396 Commonly used like this:
397 eval "`bzr bash-completion`"
398 """
399
400 takes_options = [
401 option.Option("function-name", short_name="f", type=str, argname="name",
402 help="Name of the generated function (default: _bzr)"),
403 option.Option("function-only", short_name="o", type=None,
404 help="Generate only the shell function, don't enable it"),
405 option.Option("debug", type=None, hidden=True,
406 help="Enable shell code useful for debugging"),
407 option.ListOption("plugin", type=str, argname="name",
408 # param_name="selected_plugins", # doesn't work, bug #387117
409 help="Enable completions for the selected plugin"
410 + " (default: all plugins)"),
411 ]
412
413 def run(self, **kwargs):
414 import sys
415 from bashcomp import bash_completion_function
416 if 'plugin' in kwargs:
417 # work around bug #387117 which prevents us from using param_name
418 if len(kwargs['plugin']) > 0:
419 kwargs['selected_plugins'] = kwargs['plugin']
420 del kwargs['plugin']
421 bash_completion_function(sys.stdout, **kwargs)
422
423
424if __name__ == '__main__':
425
426 import sys
427 import locale
428 import optparse
429
430 def plugin_callback(option, opt, value, parser):
431 values = parser.values.selected_plugins
432 if value == '-':
433 del values[:]
434 else:
435 values.append(value)
436
437 parser = optparse.OptionParser(usage="%prog [-f NAME] [-o]")
438 parser.add_option("--function-name", "-f", metavar="NAME",
439 help="Name of the generated function (default: _bzr)")
440 parser.add_option("--function-only", "-o", action="store_true",
441 help="Generate only the shell function, don't enable it")
442 parser.add_option("--debug", action="store_true",
443 help=optparse.SUPPRESS_HELP)
444 parser.add_option("--no-plugins", action="store_true",
445 help="Don't load any bzr plugins")
446 parser.add_option("--plugin", metavar="NAME", type="string",
447 dest="selected_plugins", default=[],
448 action="callback", callback=plugin_callback,
449 help="Enable completions for the selected plugin"
450 + " (default: all plugins)")
451 (opts, args) = parser.parse_args()
452 if args:
453 parser.error("script does not take positional arguments")
454 kwargs = dict()
455 for name, value in opts.__dict__.iteritems():
456 if value is not None:
457 kwargs[name] = value
458
459 locale.setlocale(locale.LC_ALL, '')
460 if not kwargs.get('no_plugins', False):
461 plugin.load_plugins()
462 commands.install_bzr_command_hooks()
463 bash_completion_function(sys.stdout, **kwargs)
0464
=== added directory 'bzrlib/plugins/bash_completion/tests'
=== added file 'bzrlib/plugins/bash_completion/tests/__init__.py'
--- bzrlib/plugins/bash_completion/tests/__init__.py 1970-01-01 00:00:00 +0000
+++ bzrlib/plugins/bash_completion/tests/__init__.py 2010-05-05 08:10:48 +0000
@@ -0,0 +1,23 @@
1# Copyright (C) 2010 by Canonical Ltd
2#
3# This program is free software; you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation; either version 2 of the License, or
6# (at your option) any later version.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program; if not, write to the Free Software
15# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
17def load_tests(basic_tests, module, loader):
18 testmod_names = [
19 'test_bashcomp',
20 ]
21 basic_tests.addTest(loader.loadTestsFromModuleNames(
22 ["%s.%s" % (__name__, tmn) for tmn in testmod_names]))
23 return basic_tests
024
=== added file 'bzrlib/plugins/bash_completion/tests/test_bashcomp.py'
--- bzrlib/plugins/bash_completion/tests/test_bashcomp.py 1970-01-01 00:00:00 +0000
+++ bzrlib/plugins/bash_completion/tests/test_bashcomp.py 2010-05-05 08:10:48 +0000
@@ -0,0 +1,318 @@
1# Copyright (C) 2010 by Canonical Ltd
2#
3# This program is free software; you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation; either version 2 of the License, or
6# (at your option) any later version.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program; if not, write to the Free Software
15# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
17import bzrlib
18from bzrlib import commands, tests
19from bzrlib.plugins.bash_completion.bashcomp import *
20
21import os
22import subprocess
23
24
25class _BashFeature(tests.Feature):
26 """Feature testing whether a bash executable is available."""
27
28 bash_paths = ['/bin/bash', '/usr/bin/bash']
29
30 def __init__(self):
31 super(_BashFeature, self).__init__()
32 self.bash_path = None
33
34 def available(self):
35 if self.bash_path is not None:
36 return self.bash_path is not False
37 for path in self.bash_paths:
38 if os.access(path, os.X_OK):
39 self.bash_path = path
40 return True
41 self.bash_path = False
42 return False
43
44 def feature_name(self):
45 return 'bash'
46
47BashFeature = _BashFeature()
48
49
50class BashCompletionMixin(object):
51 """Component for testing execution of a bash completion script."""
52
53 _test_needs_features = [BashFeature]
54
55 def complete(self, words, cword=-1):
56 """Perform a bash completion.
57
58 :param words: a list of words representing the current command.
59 :param cword: the current word to complete, defaults to the last one.
60 """
61 if self.script is None:
62 self.script = self.get_script()
63 proc = subprocess.Popen([BashFeature.bash_path, '--noprofile'],
64 stdin=subprocess.PIPE,
65 stdout=subprocess.PIPE,
66 stderr=subprocess.PIPE)
67 if cword < 0:
68 cword = len(words) + cword
69 input = '%s\n' % self.script
70 input += ('COMP_WORDS=( %s )\n' %
71 ' '.join(["'"+w.replace("'", "'\\''")+"'" for w in words]))
72 input += 'COMP_CWORD=%d\n' % cword
73 input += '%s\n' % getattr(self, 'script_name', '_bzr')
74 input += 'echo ${#COMPREPLY[*]}\n'
75 input += "IFS=$'\\n'\n"
76 input += 'echo "${COMPREPLY[*]}"\n'
77 (out, err) = proc.communicate(input)
78 if '' != err:
79 raise AssertionError('Unexpected error message:\n%s' % err)
80 self.assertEqual('', err, 'No messages to standard error')
81 #import sys
82 #print >>sys.stdout, '---\n%s\n---\n%s\n---\n' % (input, out)
83 lines = out.split('\n')
84 nlines = int(lines[0])
85 del lines[0]
86 self.assertEqual('', lines[-1], 'Newline at end')
87 del lines[-1]
88 if nlines == 0 and len(lines) == 1 and lines[0] == '':
89 del lines[0]
90 self.assertEqual(nlines, len(lines), 'No newlines in generated words')
91 self.completion_result = set(lines)
92 return self.completion_result
93
94 def assertCompletionEquals(self, *words):
95 self.assertEqual(set(words), self.completion_result)
96
97 def assertCompletionContains(self, *words):
98 missing = set(words) - self.completion_result
99 if missing:
100 raise AssertionError('Completion should contain %r but it has %r'
101 % (missing, self.completion_result))
102
103 def assertCompletionOmits(self, *words):
104 surplus = set(words) & self.completion_result
105 if surplus:
106 raise AssertionError('Completion should omit %r but it has %r'
107 % (surplus, res, self.completion_result))
108
109 def get_script(self):
110 commands.install_bzr_command_hooks()
111 dc = DataCollector()
112 data = dc.collect()
113 cg = BashCodeGen(data)
114 res = cg.function()
115 return res
116
117
118class TestBashCompletion(tests.TestCase, BashCompletionMixin):
119 """Test bash completions that don't execute bzr."""
120
121 def __init__(self, methodName='testMethod'):
122 super(TestBashCompletion, self).__init__(methodName)
123 self.script = None
124
125 def test_simple_scipt(self):
126 """Ensure that the test harness works as expected"""
127 self.script = """
128_bzr() {
129 COMPREPLY=()
130 # add all words in reverse order, with some markup around them
131 for ((i = ${#COMP_WORDS[@]}; i > 0; --i)); do
132 COMPREPLY+=( "-${COMP_WORDS[i-1]}+" )
133 done
134 # and append the current word
135 COMPREPLY+=( "+${COMP_WORDS[COMP_CWORD]}-" )
136}
137"""
138 self.complete(['foo', '"bar', "'baz"], cword=1)
139 self.assertCompletionEquals("-'baz+", '-"bar+', '-foo+', '+"bar-')
140
141 def test_cmd_ini(self):
142 self.complete(['bzr', 'ini'])
143 self.assertCompletionContains('init', 'init-repo', 'init-repository')
144 self.assertCompletionOmits('commit')
145
146 def test_init_opts(self):
147 self.complete(['bzr', 'init', '-'])
148 self.assertCompletionContains('-h', '--2a', '--format=2a')
149
150 def test_global_opts(self):
151 self.complete(['bzr', '-', 'init'], cword=1)
152 self.assertCompletionContains('--no-plugins', '--builtin')
153
154 def test_commit_dashm(self):
155 self.complete(['bzr', 'commit', '-m'])
156 self.assertCompletionEquals('-m')
157
158 def test_status_negated(self):
159 self.complete(['bzr', 'status', '--n'])
160 self.assertCompletionContains('--no-versioned', '--no-verbose')
161
162 def test_init_format_any(self):
163 self.complete(['bzr', 'init', '--format', '=', 'directory'], cword=3)
164 self.assertCompletionContains('1.9', '2a')
165
166 def test_init_format_2(self):
167 self.complete(['bzr', 'init', '--format', '=', '2', 'directory'],
168 cword=4)
169 self.assertCompletionContains('2a')
170 self.assertCompletionOmits('1.9')
171
172
173class TestBashCompletionInvoking(tests.TestCaseWithTransport,
174 BashCompletionMixin):
175 """Test bash completions that might execute bzr.
176
177 Only the syntax ``$(bzr ...`` is supported so far. The bzr command
178 will be replaced by the bzr instance running this selftest.
179 """
180
181 def __init__(self, methodName='testMethod'):
182 super(TestBashCompletionInvoking, self).__init__(methodName)
183 self.script = None
184
185 def get_script(self):
186 s = super(TestBashCompletionInvoking, self).get_script()
187 return s.replace("$(bzr ", "$('%s' " % self.get_bzr_path())
188
189 def test_revspec_tag_all(self):
190 wt = self.make_branch_and_tree('.', format='dirstate-tags')
191 wt.branch.tags.set_tag('tag1', 'null:')
192 wt.branch.tags.set_tag('tag2', 'null:')
193 wt.branch.tags.set_tag('3tag', 'null:')
194 self.complete(['bzr', 'log', '-r', 'tag', ':'])
195 self.assertCompletionEquals('tag1', 'tag2', '3tag')
196
197 def test_revspec_tag_prefix(self):
198 wt = self.make_branch_and_tree('.', format='dirstate-tags')
199 wt.branch.tags.set_tag('tag1', 'null:')
200 wt.branch.tags.set_tag('tag2', 'null:')
201 wt.branch.tags.set_tag('3tag', 'null:')
202 self.complete(['bzr', 'log', '-r', 'tag', ':', 't'])
203 self.assertCompletionEquals('tag1', 'tag2')
204
205
206class TestBashCodeGen(tests.TestCase):
207
208 def test_command_names(self):
209 data = CompletionData()
210 bar = CommandData('bar')
211 bar.aliases.append('baz')
212 data.commands.append(bar)
213 data.commands.append(CommandData('foo'))
214 cg = BashCodeGen(data)
215 self.assertEqual('bar baz foo', cg.command_names())
216
217 def test_debug_output(self):
218 data = CompletionData()
219 self.assertEqual('', BashCodeGen(data, debug=False).debug_output())
220 self.assertTrue(BashCodeGen(data, debug=True).debug_output())
221
222 def test_bzr_version(self):
223 data = CompletionData()
224 cg = BashCodeGen(data)
225 self.assertEqual('%s.' % bzrlib.version_string, cg.bzr_version())
226 data.plugins['foo'] = PluginData('foo', '1.0')
227 data.plugins['bar'] = PluginData('bar', '2.0')
228 cg = BashCodeGen(data)
229 self.assertEqual('''\
230%s and the following plugins:
231# bar 2.0
232# foo 1.0''' % bzrlib.version_string, cg.bzr_version())
233
234 def test_global_options(self):
235 data = CompletionData()
236 data.global_options.add('--foo')
237 data.global_options.add('--bar')
238 cg = BashCodeGen(data)
239 self.assertEqual('--bar --foo', cg.global_options())
240
241 def test_command_cases(self):
242 data = CompletionData()
243 bar = CommandData('bar')
244 bar.aliases.append('baz')
245 bar.options.append(OptionData('--opt'))
246 data.commands.append(bar)
247 data.commands.append(CommandData('foo'))
248 cg = BashCodeGen(data)
249 self.assertEqualDiff('''\
250\tbar|baz)
251\t\tcmdOpts='--opt'
252\t\t;;
253\tfoo)
254\t\tcmdOpts=''
255\t\t;;
256''', cg.command_cases())
257
258 def test_command_case(self):
259 cmd = CommandData('cmd')
260 cmd.plugin = PluginData('plugger', '1.0')
261 bar = OptionData('--bar')
262 bar.registry_keys = ['that', 'this']
263 bar.error_messages.append('Some error message')
264 cmd.options.append(bar)
265 cmd.options.append(OptionData('--foo'))
266 data = CompletionData()
267 data.commands.append(cmd)
268 cg = BashCodeGen(data)
269 self.assertEqualDiff('''\
270\tcmd)
271\t\t# plugin "plugger 1.0"
272\t\t# Some error message
273\t\tcmdOpts='--bar=that --bar=this --foo'
274\t\tcase $curOpt in
275\t\t\t--bar) optEnums='that this' ;;
276\t\tesac
277\t\t;;
278''', cg.command_case(cmd))
279
280
281class TestDataCollector(tests.TestCase):
282
283 def setUp(self):
284 super(TestDataCollector, self).setUp()
285 commands.install_bzr_command_hooks()
286
287 def test_global_options(self):
288 dc = DataCollector()
289 dc.global_options()
290 self.assertSubset(['--no-plugins', '--builtin'],
291 dc.data.global_options)
292
293 def test_commands(self):
294 dc = DataCollector()
295 dc.commands()
296 self.assertSubset(['init', 'init-repo', 'init-repository'],
297 dc.data.all_command_aliases())
298
299 def test_commit_dashm(self):
300 dc = DataCollector()
301 cmd = dc.command('commit')
302 self.assertSubset(['-m'],
303 [str(o) for o in cmd.options])
304
305 def test_status_negated(self):
306 dc = DataCollector()
307 cmd = dc.command('status')
308 self.assertSubset(['--no-versioned', '--no-verbose'],
309 [str(o) for o in cmd.options])
310
311 def test_init_format(self):
312 dc = DataCollector()
313 cmd = dc.command('init')
314 for opt in cmd.options:
315 if opt.name == '--format':
316 self.assertSubset(['2a'], opt.registry_keys)
317 return
318 raise AssertionError('Option --format not found')
0319
=== added file 'contrib/bash/bzr'
--- contrib/bash/bzr 1970-01-01 00:00:00 +0000
+++ contrib/bash/bzr 2010-05-05 08:10:48 +0000
@@ -0,0 +1,40 @@
1# Copyright (C) 2010 Canonical Ltd
2#
3# This program is free software; you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation; either version 2 of the License, or
6# (at your option) any later version.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program; if not, write to the Free Software
15# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
17# Programmable completion for the Bazaar-NG bzr command under bash.
18# Source this file (or add it to your ~/.bash_completion or ~/.bashrc
19# file, depending on your system configuration, and start a new shell)
20# and bash's completion mechanism will know all about bzr's options!
21#
22# This completion function assumes you have the bzr-bash-completion
23# plugin installed as a bzr plugin. It will generate the full
24# completion function at first invocation, thus avoiding long delays
25# for every shell you start.
26
27shopt -s progcomp
28_bzr_lazy ()
29{
30 unset _bzr
31 eval "$(bzr bash-completion)"
32 if [[ $(type -t _bzr) == function ]]; then
33 unset _bzr_lazy
34 _bzr
35 return $?
36 else
37 return 1
38 fi
39}
40complete -F _bzr_lazy -o default bzr
041
=== removed file 'contrib/bash/bzr'
--- contrib/bash/bzr 2005-09-19 06:05:19 +0000
+++ contrib/bash/bzr 1970-01-01 00:00:00 +0000
@@ -1,104 +0,0 @@
1# Programmable completion for the Bazaar-NG bzr command under bash. Source
2# this file (or on some systems add it to ~/.bash_completion and start a new
3# shell) and bash's completion mechanism will know all about bzr's options!
4
5# Known to work with bash 2.05a with programmable completion and extended
6# pattern matching enabled (use 'shopt -s extglob progcomp' to enable
7# these if they are not already enabled).
8
9# Based originally on the svn bash completition script.
10# Customized by Sven Wilhelm/Icecrash.com
11
12_bzr ()
13{
14 local cur cmds cmdOpts opt helpCmds optBase i
15
16 COMPREPLY=()
17 cur=${COMP_WORDS[COMP_CWORD]}
18
19 cmds='status diff commit ci checkin move remove log info check ignored'
20
21 if [[ $COMP_CWORD -eq 1 ]] ; then
22 COMPREPLY=( $( compgen -W "$cmds" -- $cur ) )
23 return 0
24 fi
25
26 # if not typing an option, or if the previous option required a
27 # parameter, then fallback on ordinary filename expansion
28 helpCmds='help|--help|h|\?'
29 if [[ ${COMP_WORDS[1]} != @($helpCmds) ]] && \
30 [[ "$cur" != -* ]] ; then
31 return 0
32 fi
33
34 cmdOpts=
35 case ${COMP_WORDS[1]} in
36 status)
37 cmdOpts="--all --show-ids"
38 ;;
39 diff)
40 cmdOpts="-r --revision --diff-options"
41 ;;
42 commit|ci|checkin)
43 cmdOpts="-r --message -F --file -v --verbose"
44 ;;
45 move)
46 cmdOpts=""
47 ;;
48 remove)
49 cmdOpts="-v --verbose"
50 ;;
51 log)
52 cmdOpts="--forward --timezone -v --verbose --show-ids -r --revision"
53 ;;
54 info)
55 cmdOpts=""
56 ;;
57 ignored)
58 cmdOpts=""
59 ;;
60 check)
61 cmdOpts=""
62 ;;
63 help|h|\?)
64 cmdOpts="$cmds $qOpts"
65 ;;
66 *)
67 ;;
68 esac
69
70 cmdOpts="$cmdOpts --help -h"
71
72 # take out options already given
73 for (( i=2; i<=$COMP_CWORD-1; ++i )) ; do
74 opt=${COMP_WORDS[$i]}
75
76 case $opt in
77 --*) optBase=${opt/=*/} ;;
78 -*) optBase=${opt:0:2} ;;
79 esac
80
81 cmdOpts=" $cmdOpts "
82 cmdOpts=${cmdOpts/ ${optBase} / }
83
84 # take out alternatives
85 case $optBase in
86 -v) cmdOpts=${cmdOpts/ --verbose / } ;;
87 --verbose) cmdOpts=${cmdOpts/ -v / } ;;
88 -h) cmdOpts=${cmdOpts/ --help / } ;;
89 --help) cmdOpts=${cmdOpts/ -h / } ;;
90 -r) cmdOpts=${cmdOpts/ --revision / } ;;
91 --revision) cmdOpts=${cmdOpts/ -r / } ;;
92 esac
93
94 # skip next option if this one requires a parameter
95 if [[ $opt == @($optsParam) ]] ; then
96 ((++i))
97 fi
98 done
99
100 COMPREPLY=( $( compgen -W "$cmdOpts" -- $cur ) )
101
102 return 0
103}
104complete -F _bzr -o default bzr
1050
=== removed file 'contrib/bash/bzr.simple'
--- contrib/bash/bzr.simple 2007-06-06 19:44:39 +0000
+++ contrib/bash/bzr.simple 1970-01-01 00:00:00 +0000
@@ -1,28 +0,0 @@
1# -*- shell-script -*-
2
3# experimental bzr bash completion
4
5# author: Martin Pool
6
7_bzr_commands()
8{
9 bzr help commands | sed -r 's/^([-[:alnum:]]*).*/\1/' | grep '^[[:alnum:]]'
10}
11
12_bzr()
13{
14 cur=${COMP_WORDS[COMP_CWORD]}
15 prev=${COMP_WORDS[COMP_CWORD-1]}
16 if [ $COMP_CWORD -eq 1 ]; then
17 COMPREPLY=( $( compgen -W "$(_bzr_commands)" $cur ) )
18 elif [ $COMP_CWORD -eq 2 ]; then
19 case "$prev" in
20 help)
21 COMPREPLY=( $( compgen -W "$(_bzr_commands) commands" $cur ) )
22 ;;
23 esac
24 fi
25}
26
27complete -F _bzr -o default bzr
28