6

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?

2 Answers 2

8

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.
Sign up to request clarification or add additional context in comments.

4 Comments

Thanks again for the great feedback! I'll clarify because I didn't explain very well. I'm wondering how to add completion for the names of the objects created. Another example: for Kubernetes there's kubectl. I can type kubectl get pods and then hit <TAB> a few times. I'll then get a list of pods deployed. That information can only come from querying the Kubernetes API since pod names are always changing. This means the completion script for kubectl will query the API for a list of pod names, and return that as a completion. I'm wondering if that's possible in Click.
I understood you. The example above does everything needed except the querying. You will need to generate that list of strings via the k8s api, and then pass it to click.Choice().
Ah, I see, thank you for the clarification. Does that mean instead of passing 'obj1 obj2 obj3'.split(), I would pass a function that returns a list of objects I want to be autocompleted? To use my simple example, if I used click.Choice(os.listdir("~/.cli-tool/objects/foo")), the files inside of ~/.cli-tool/objects/foo would then be listed as an autocomplete if I typed cli-tool object get <TAB><TAB> ?
Yes that is correct. A list of strings passed in will be the valid choices. Those valid choices are what the command completion uses.
2

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]}")

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.