Scott Crooks
Scott Crooks

Reputation: 1663

Extending BASH completion with Python Click

I'm wanting to customize the BASH completion functionality of my Python Click CLI program to include not only the commands / subcommands of the script, but also objects that the script creates.

Let's say my program is called cli-tool, and it can create object foo using the command:

cli-tool object create foo

For simplicity, let's say the command simply concatenates the argument string (foo in this case) to a text file of the same name located in ~/.cli-tool/objects/foo. Doing cat ~/.cli-tool/objects/foo would then print foo in your terminal.

What I would like for the tool to do is when I then type:

cli-tool object get <TAB><TAB>

The terminal would then list foo and any other files that live inside ~/.cli-tool/objects.

For the record, I have read the Python Click 6.x stable documentation, which clearly states:

Currently, Bash completion is an internal feature that is not customizable. This might be relaxed in future versions.

What I was hoping is that there would be a way to extract the full BASH completion script from the following command:

eval "$(_CLI_TOOL_COMPLETE=source cli-tool)"

And then customize it myself. I've also seen the click-completion project, but I'm not sure what it does beyond extending the completion for Zsh and Fish shells.

Has anyone achieved the type of completion I mention above?

Upvotes: 6

Views: 6565

Answers (2)

user17297103
user17297103

Reputation:

For Click 8.1.3, you can use either of Overriding Value Completion or Custom Type Completion.

Here's an example from the docs where you can use a custom shell_complete function. It must return a list of CompletionItem objects, or as a shortcut it can return a list of strings.

def complete_env_vars(ctx, param, incomplete):
    return [k for k in os.environ if k.startswith(incomplete)]

@click.command()
@click.argument("name", shell_complete=complete_env_vars)
def cli(name):
    click.echo(f"Name: {name}")
    click.echo(f"Value: {os.environ[name]}")

Upvotes: 2

Stephen Rauch
Stephen Rauch

Reputation: 49812

Using click-completion, this is quite straight forward.

Code:

Import and init() Click Completion:

import click
import click_completion

click_completion.init()

Then instantiate a click.Choice object:

option_type = click.Choice('obj1 obj2 obj3'.split())

In the case of your option directory, pass in a list of the appropriate items instead of the example obj1-obj3.

Then pass the option type to the click.argument() decorator like:

@click.argument('option', type=option_type)

And don't forget to activate your completion with your shell. The click variation for bash is here:

Test Code:

import click
import click_completion

click_completion.init()

option_type = click.Choice('obj1 obj2 obj3'.split())

@click.group()
def cli():
    """My Cool Tool"""

@cli.group(name='object')
def object_group():
    """Object subcommand"""

@object_group.command()
@click.argument('option', type=option_type)
def get(option):
    click.echo('option: {}'.format(option))


commands = (
    ('"" object get ""', 1),
    ('"" object get ""', 2),
    ('"" object get ""', 3),
    'object get obj1',
    '--help',
    'object --help',
    'object get --help',
)

os.environ['BASH_COMP'] = 'complete'

import sys, time
time.sleep(1)
print('Click Version: {}'.format(click.__version__))
print('Click Completion Version: {}'.format(click_completion.__version__))
print('Python Version: {}'.format(sys.version))
for cmd in commands:
    try:
        time.sleep(0.1)
        print('\n-----------')
        print('> ' + str(cmd))
        time.sleep(0.1)

        if len(cmd) == 2:
            os.environ['COMP_WORDS'] = cmd[0]
            os.environ['COMP_CWORD'] = str(cmd[1])
            cli(complete_var='BASH_COMP')
        else:
            try:
                del os.environ['COMP_WORDS']
                del os.environ['COMP_CWORD']
            except:
                pass
            cli(cmd.split())


    except BaseException as exc:
        if str(exc) != '0' and \
                not isinstance(exc, (click.ClickException, SystemExit)):
            raise

Results:

Click Version: 6.7
Click Completion Version: 0.4.1
Python Version: 3.6.3 (v3.6.3:2c5fed8, Oct  3 2017, 18:11:49) [MSC v.1900 64 bit (AMD64)]

-----------
> ('"" object get ""', 1)
object
-----------
> ('"" object get ""', 2)
get
-----------
> ('"" object get ""', 3)
obj1    obj2    obj3
-----------
> object get obj1
option: obj1

-----------
> --help
Usage: test.py [OPTIONS] COMMAND [ARGS]...

  My Cool Tool

Options:
  --help  Show this message and exit.

Commands:
  object  Object subcommand

-----------
> object --help
Usage: test.py object [OPTIONS] COMMAND [ARGS]...

  Object subcommand

Options:
  --help  Show this message and exit.

Commands:
  get

-----------
> object get --help
Usage: test.py object get [OPTIONS] OPTION

Options:
  --help  Show this message and exit.

Upvotes: 8

Related Questions