Skip to content

Commit

Permalink
Support tab completion in bash
Browse files Browse the repository at this point in the history
This includes dynamic URI completion of known URI's and aliases.
Note: Alias complete doesn't work for env/activate. It requires working
around click's default parsing implementation.
  • Loading branch information
MHendricks committed Jul 30, 2024
1 parent 44f4094 commit 5e44ff2
Show file tree
Hide file tree
Showing 5 changed files with 91 additions and 2 deletions.
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,21 @@ Finally they are exiting the current URI so they can enable another URI. The use
doesn't need to worry about which version of the Maya or Houdini applications they
should launch, that is configured for them by the URI they pass to `hab env`.

#### Tab Completion

You can enable tab completion including known URI's and aliases when using bash.
This requires using bash 4.4 or newer. To do this you need to source the
`.hab-complete.bash` file. This is installed next to the `hab` script (This makes
its path consistent even with editable installs). Use `which .hab-complete.bash`
to locate the file. If that doesn't work it should be next to the file returned
by `which hab`.

Examples of what to add to .bashrc
- Windows: `. /c/Program\ Files/Python39/Scripts/.hab-complete.bash`
- Linux: `. /usr/local/bin/.hab-complete.bash`

See https://click.palletsprojects.com/en/8.1.x/shell-completion/ for more details.

### Looking up aliases

In the previous section the use knew that they could run maya and houdini. You can
Expand Down
37 changes: 37 additions & 0 deletions bin/.hab-complete.bash
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# This script enables tab completion for hab in bash. It requires bash 4.4+.
# https://click.palletsprojects.com/en/8.1.x/shell-completion/

# To enable tab completion source this script in your .bashrc script.
# `. /c/Program\ Files/Python39/Scripts/.hab-complete.bash`
# `. /usr/local/bin/.hab-complete.bash`

# This script below was generated by running `_HAB_COMPLETE=bash_source hab > .hab-complete.bash`

_hab_completion() {
local IFS=$'\n'
local response

response=$(env COMP_WORDS="${COMP_WORDS[*]}" COMP_CWORD=$COMP_CWORD _HAB_COMPLETE=bash_complete $1)

for completion in $response; do
IFS=',' read type value <<< "$completion"

if [[ $type == 'dir' ]]; then
COMPREPLY=()
compopt -o dirnames
elif [[ $type == 'file' ]]; then
COMPREPLY=()
compopt -o default
elif [[ $type == 'plain' ]]; then
COMPREPLY+=($value)
fi
done

return 0
}

_hab_completion_setup() {
complete -o nosort -F _hab_completion hab
}

_hab_completion_setup;
2 changes: 1 addition & 1 deletion hab/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@
# prog_name prevents __main__.py from being shown as the command name in the help
# text. We don't know the exact command the user passed so we provide a generic
# `python -m hab` command.
sys.exit(hab.cli.cli(prog_name="python -m hab"))
sys.exit(hab.cli.cli(prog_name="hab"))
38 changes: 37 additions & 1 deletion hab/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from pathlib import Path

import click
from click.shell_completion import CompletionItem
from colorama import Fore

from . import Resolver, Site, __version__, utils
Expand All @@ -18,6 +19,21 @@
CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])


def complete_alias(ctx, param, incomplete):
"""Dynamic tab completion for shell_complete generating available aliases"""
resolver = ctx.obj.resolver
if not ctx.obj.uri:
# The URI hasn't been resolved, so don't have any aliases to return.
return []
# Resolve the config from the URI stored in `UriArgument.type_cast_value`.
cfg = resolver.resolve(ctx.obj.uri)
return [
CompletionItem(alias)
for alias in cfg.aliases
if alias.strip().startswith(incomplete)
]


class UriArgument(click.Argument):
"""Accepts a URI string, frozen string or path to frozen json file.
Expand Down Expand Up @@ -54,6 +70,8 @@ class UriArgument(click.Argument):

def __init__(self, *args, prompt=True, **kwargs):
self.prompt = prompt
# Override shell_complete with our implementation by default
kwargs.setdefault("shell_complete", self.complete_uri)
super().__init__(*args, **kwargs)

def __uri_prompt(self, uri=None):
Expand All @@ -75,6 +93,15 @@ def __uri_prompt(self, uri=None):
)
return response

def complete_uri(self, ctx, param, incomplete):
"""Dynamic tab completion for shell_complete generating available URI's"""
resolver = ctx.obj.resolver
return [
CompletionItem(uri.strip())
for uri in resolver.dump_forest(resolver.configs)
if uri.strip().startswith(incomplete)
]

def type_cast_value(self, ctx, value):
"""Convert and validate the uri value. This override handles saving the
uri to user prefs if enabled by the cli.
Expand Down Expand Up @@ -150,6 +177,9 @@ def type_cast_value(self, ctx, value):
if ctx.obj.enable_user_prefs_save:
ctx.obj.resolver.user_prefs().uri = value

# Store the URI so it can be used by other shell_complete functions.
ctx.obj.uri = value

return value


Expand Down Expand Up @@ -541,6 +571,9 @@ def set_uri(settings, uri):
"--launch",
default=None,
help="Run this alias after activating. This leaves the new shell active.",
# TODO: This requires resolving the uri argument before this option. Click
# doesn't support that by default. Figure out how to get this working.
# shell_complete=complete_alias,
)
@click.pass_obj
def env(settings, uri, launch):
Expand Down Expand Up @@ -682,6 +715,9 @@ def echo_line(line):
"--launch",
default=None,
help="Run this alias after activating. This leaves the new shell activated.",
# TODO: This requires resolving the uri argument before this option. Click
# doesn't support that by default. Figure out how to get this working.
# shell_complete=complete_alias,
)
@click.pass_obj
def activate(settings, uri, launch):
Expand Down Expand Up @@ -717,7 +753,7 @@ def activate(settings, uri, launch):
cls=UriHelpClass,
)
@click.argument("uri", cls=UriArgument)
@click.argument("alias")
@click.argument("alias", shell_complete=complete_alias)
# Pass all remaining arguments to the requested alias
@click.argument("args", nargs=-1, type=click.UNPROCESSED)
@click.pass_obj
Expand Down
1 change: 1 addition & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ install_requires =
python_requires = >=3.6
include_package_data = True
scripts =
bin/.hab-complete.bash
bin/hab.bat
bin/hab.ps1
bin/hab
Expand Down

0 comments on commit 5e44ff2

Please sign in to comment.