Skip to content

Commit

Permalink
Add --install-tab-completion option for installing shell completion
Browse files Browse the repository at this point in the history
The user gets four options to choose from bash, zsh, fish and
auto (auto detects shell using `shellingham`). To activate
shell completion run:

        kart config --install-completion auto

Replace `auto` with desired shell eg:

        kart config --install-completion zsh

Completion will take effect once you restart the terminal.
  • Loading branch information
amalthundiyil committed Jun 16, 2022
1 parent 0724ed4 commit 5b3145a
Show file tree
Hide file tree
Showing 10 changed files with 256 additions and 21 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ _When adding new entries to the changelog, please include issue/PR numbers where

## 0.11.4 (UNRELEASED)

- Add `--install-tab-completion` option for installing shell completion. [#643](https://github.com/koordinates/kart/issues/643)
- Changed format of feature IDs in GeoJSON output to be more informative and consistent. [#135](https://github.com/koordinates/kart/issues/135)
- Fixed primary key issues for shapefile import - now generates an `auto_pk` by default, but uses an existing field if specified (doesn't use the FID). [#646](https://github.com/koordinates/kart/pull/646)
- Add `--with-dataset-types` option to `kart meta get` which is informative now that there is more than one type of dataset. [#649](https://github.com/koordinates/kart/pull/649)
- Add `--with-dataset-types` option to `kart meta get` which is informative now that there is more than one type of dataset. [#649](https://github.com/koordinates/kart/pull/649)

## 0.11.3

Expand All @@ -26,6 +27,7 @@ _When adding new entries to the changelog, please include issue/PR numbers where

- Improve performance when creating a working copy in a spatially filtered repository (this was previously slower than in a non-filtered repository; now it is much faster) [#561](https://github.com/koordinates/kart/issues/561)
- Added Sphinx [documentation](https://docs.kartproject.org/en/latest/), built from the `docs` directory. [#220](https://github.com/koordinates/kart/issues/220)

## 0.11.0

### Major changes
Expand Down
8 changes: 8 additions & 0 deletions kart/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from .cli_util import add_help_subcommand, call_and_exit_flag, tool_environment
from .context import Context
from .exec import execvp
from kart.completion import Shells, install_callback

MODULE_COMMANDS = {
"annotations.cli": {"build-annotations"},
Expand Down Expand Up @@ -280,6 +281,13 @@ def reflog(ctx, args):

@cli.command(context_settings=dict(ignore_unknown_options=True))
@click.pass_context
@click.option(
"--install-tab-completion",
type=click.Choice([s.value for s in Shells] + ["auto"]),
callback=install_callback,
expose_value=False,
help="Install tab completion for the specific or current shell",
)
@click.argument("args", nargs=-1, type=click.UNPROCESSED)
def config(ctx, args):
"""Get and set repository or global options"""
Expand Down
157 changes: 157 additions & 0 deletions kart/completion.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import os
import re
import sys
from enum import Enum
from pathlib import Path
from typing import Optional, Tuple, Any, Optional

import click
from click.shell_completion import _SOURCE_BASH, _SOURCE_ZSH, _SOURCE_FISH

try:
import shellingham
except ImportError:
shellingham = None


class Shells(str, Enum):
bash = "bash"
zsh = "zsh"
fish = "fish"


_completion_scripts = {
"bash": _SOURCE_BASH,
"zsh": _SOURCE_ZSH,
"fish": _SOURCE_FISH,
}

_invalid_ident_char_re = re.compile(r"[^a-zA-Z0-9_]")


def get_completion_script(*, prog_name: str, complete_var: str, shell: str) -> str:
cf_name = _invalid_ident_char_re.sub("", prog_name.replace("-", "_"))
script = _completion_scripts.get(shell)
if script is None:
click.echo(f"Shell {shell} not supported.", err=True)
sys.exit(1)
return (
script
% dict(
complete_func="_{}_completion".format(cf_name),
complete_var=complete_var,
prog_name=prog_name,
)
).strip()


def install_helper(
prog_name, complete_var, shell, completion_path, rc_path, completion_init_lines
):
rc_path.parent.mkdir(parents=True, exist_ok=True)
rc_content = ""
if rc_path.is_file():
rc_content = rc_path.read_text()
for line in completion_init_lines:
if line not in rc_content:
rc_content += f"\n{line}"
rc_content += "\n"
rc_path.write_text(rc_content)
# Install completion
completion_path.parent.mkdir(parents=True, exist_ok=True)
script_content = get_completion_script(
prog_name=prog_name, complete_var=complete_var, shell=shell
)
completion_path.write_text(script_content)
return completion_path


def install_bash(*, prog_name: str, complete_var: str, shell: str) -> Path:
# Ref: https://github.com/scop/bash-completion#faq
# It seems bash-completion is the official completion system for bash:
# Ref: https://www.gnu.org/software/bash/manual/html_node/A-Programmable-Completion-Example.html
# But installing in the locations from the docs doesn't seem to have effect
completion_path = Path.home() / f".bash_completions/{prog_name}.sh"
bashrc_path = Path.home() / ".bashrc"
completion_init_lines = [f"source {completion_path}"]
return install_helper(
prog_name,
complete_var,
shell,
completion_path,
bashrc_path,
completion_init_lines,
)


def install_zsh(*, prog_name: str, complete_var: str, shell: str) -> Path:
# Setup Zsh and load ~/.zfunc
completion_path = Path.home() / f".zfunc/_{prog_name}"
zshrc_path = Path.home() / ".zshrc"
completion_init_lines = [
"autoload -Uz compinit",
"zstyle ':completion:*' menu select",
"fpath+=~/.zfunc; compinit",
]
return install_helper(
prog_name,
complete_var,
shell,
completion_path,
zshrc_path,
completion_init_lines,
)


def install_fish(*, prog_name: str, complete_var: str, shell: str) -> Path:
path_obj = Path.home() / f".config/fish/completions/{prog_name}.fish"
parent_dir: Path = path_obj.parent
parent_dir.mkdir(parents=True, exist_ok=True)
script_content = get_completion_script(
prog_name=prog_name, complete_var=complete_var, shell=shell
)
path_obj.write_text(f"{script_content}\n")
return path_obj


def install(
shell: Optional[str] = None,
prog_name: Optional[str] = None,
complete_var: Optional[str] = None,
) -> Tuple[str, Path]:
prog_name = prog_name or click.get_current_context().find_root().info_name
assert prog_name
if complete_var is None:
complete_var = "_{}_COMPLETE".format(prog_name.replace("-", "_").upper())
if shell is None and shellingham is not None:
shell, _ = shellingham.detect_shell()
if shell == "bash":
installed_path = install_bash(
prog_name=prog_name, complete_var=complete_var, shell=shell
)
return shell, installed_path
elif shell == "zsh":
installed_path = install_zsh(
prog_name=prog_name, complete_var=complete_var, shell=shell
)
return shell, installed_path
elif shell == "fish":
installed_path = install_fish(
prog_name=prog_name, complete_var=complete_var, shell=shell
)
return shell, installed_path
else:
click.echo(f"Shell {shell} is not supported.")
raise click.exceptions.Exit(1)


def install_callback(ctx: click.Context, param: click.Parameter, value: Any) -> Any:
if not value or ctx.resilient_parsing:
return value
if value == "auto":
shell, path = install()
else:
shell, path = install(shell=value)
click.secho(f"{shell} completion installed in {path}", fg="green")
click.echo("Completion will take effect once you restart the terminal")
sys.exit(0)
11 changes: 7 additions & 4 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,21 @@ attrs==21.4.0
# via jsonschema
cached-property==1.5.2
# via pygit2
certifi==2021.10.8
certifi==2022.5.18.1
# via -r requirements/requirements.in
cffi==1.15.0
# via
# cryptography
# pygit2
click==7.1.2
click==8.1.3
# via -r requirements/requirements.in
cryptography==37.0.2
# via -r requirements/requirements.in
greenlet==1.1.2
# via sqlalchemy
importlib-metadata==4.11.3
importlib-metadata==4.11.4
# via
# click
# jsonschema
# sqlalchemy
jsonschema==4.1.2
Expand All @@ -44,7 +45,9 @@ pyrsistent==0.18.1
# via jsonschema
rtree==0.9.7
# via -r requirements/requirements.in
sqlalchemy==1.4.36
shellingham==1.4.0
# via -r requirements/requirements.in
sqlalchemy==1.4.37
# via -r requirements/requirements.in
typing-extensions==4.2.0
# via importlib-metadata
Expand Down
14 changes: 7 additions & 7 deletions requirements/dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ decorator==5.1.1
# ipython
execnet==1.9.0
# via pytest-xdist
importlib-metadata==4.11.3
importlib-metadata==4.11.4
# via
# -c requirements/../requirements.txt
# -c requirements/docs.txt
Expand Down Expand Up @@ -76,27 +76,27 @@ pygments==2.12.0
# -c requirements/../requirements.txt
# -c requirements/docs.txt
# ipython
pyparsing==3.0.8
pyparsing==3.0.9
# via
# -c requirements/docs.txt
# -c requirements/test.txt
# packaging
pytest-forked==1.4.0
# via pytest-xdist
pytest-xdist==2.5.0
# via -r requirements/dev.in
pytest==7.1.2
# via
# -c requirements/test.txt
# pytest-forked
# pytest-xdist
pytest-forked==1.4.0
# via pytest-xdist
pytest-xdist==2.5.0
# via -r requirements/dev.in
toml==0.10.2
# via ipdb
tomli==2.0.1
# via
# -c requirements/test.txt
# pytest
traitlets==5.1.1
traitlets==5.2.2.post1
# via
# ipython
# matplotlib-inline
Expand Down
10 changes: 5 additions & 5 deletions requirements/docs.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ alabaster==0.7.12
# via sphinx
babel==2.10.1
# via sphinx
certifi==2021.10.8
certifi==2022.5.18.1
# via
# -c requirements/../requirements.txt
# requests
Expand All @@ -26,7 +26,7 @@ idna==3.3
# via requests
imagesize==1.3.0
# via sphinx
importlib-metadata==4.11.3
importlib-metadata==4.11.4
# via
# -c requirements/../requirements.txt
# -c requirements/test.txt
Expand All @@ -45,13 +45,13 @@ pygments==2.12.0
# via
# -c requirements/../requirements.txt
# sphinx
pyparsing==3.0.8
pyparsing==3.0.9
# via
# -c requirements/test.txt
# packaging
pytz==2022.1
# via babel
requests==2.27.1
requests==2.28.0
# via sphinx
six==1.16.0
# via
Expand All @@ -63,7 +63,7 @@ sphinx-autobuild==2021.3.14
# via -r requirements/docs.in
sphinx-rtd-theme==1.0.0
# via -r requirements/docs.in
sphinx==4.5.0
sphinx==5.0.1
# via
# sphinx-autobuild
# sphinx-rtd-theme
Expand Down
3 changes: 2 additions & 1 deletion requirements/requirements.in
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
certifi
Click~=7.0
Click~=8.1
cryptography
msgpack~=0.6.1
pymysql
Pygments
Rtree~=0.9.4
sqlalchemy
pyodbc ; platform_system!="Linux"
shellingham

# jsonschema>=4.2 pulls in importlib_resources, which breaks PyInstaller<4.8
# https://github.com/pyinstaller/pyinstaller/pull/6195
Expand Down
1 change: 1 addition & 0 deletions requirements/test.in
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ pytest-helpers-namespace
pytest-benchmark[aspect]
pytest-profiling
pytest-shard
pytest-mock
html5lib
9 changes: 6 additions & 3 deletions requirements/test.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@ attrs==21.4.0
# pytest
colorama==0.4.4
# via -r requirements/test.in
coverage[toml]==6.3.2
coverage[toml]==6.4.1
# via pytest-cov
fields==5.0.0
# via aspectlib
gprof2dot==2021.2.21
# via pytest-profiling
html5lib==1.1
# via -r requirements/test.in
importlib-metadata==4.11.3
importlib-metadata==4.11.4
# via
# -c requirements/../requirements.txt
# pluggy
Expand All @@ -39,14 +39,16 @@ py-cpuinfo==8.0.0
# via pytest-benchmark
py==1.11.0
# via pytest
pyparsing==3.0.8
pyparsing==3.0.9
# via packaging
pytest-benchmark[aspect]==3.4.1
# via -r requirements/test.in
pytest-cov==3.0.0
# via -r requirements/test.in
pytest-helpers-namespace==2021.12.29
# via -r requirements/test.in
pytest-mock==3.7.0
# via -r requirements/test.in
pytest-profiling==1.7.0
# via -r requirements/test.in
pytest-shard==0.1.2
Expand All @@ -59,6 +61,7 @@ pytest==7.1.2
# pytest-benchmark
# pytest-cov
# pytest-helpers-namespace
# pytest-mock
# pytest-profiling
# pytest-shard
# pytest-sugar
Expand Down
Loading

0 comments on commit 5b3145a

Please sign in to comment.