diff --git a/CHANGES.rst b/CHANGES.rst index 8a1a7814..d4461b82 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,9 @@ +v23.13.0 +-------- + +* #608: Added support for tab completion on the ``keyring`` command + if the ``completion`` extra is installed (``keyring[completion]``). + v23.12.1 -------- diff --git a/README.rst b/README.rst index 5e0f8acf..0bbffb16 100644 --- a/README.rst +++ b/README.rst @@ -93,6 +93,41 @@ package, suitable for invoking from Python like so:: $ python -m keyring get system username password +Tab Completion +-------------- + +If installed via a package manager (apt, pacman, nix, homebrew, etc), +these shell completions may already have been distributed with the package +(no action required). + +Keyring provides tab completion if the ``completion`` extra is installed:: + + $ pip install 'keyring[completion]' + +Then, generate shell completions, something like:: + + $ keyring --print-completion bash | sudo tee /usr/share/bash-completion/completions/keyring + $ keyring --print-completion zsh | sudo tee /usr/share/zsh/site-functions/_keyring + $ keyring --print-completion tcsh | sudo tee /etc/profile.d/keyring.csh + +**Note**: the path of `/usr/share` is mainly for GNU/Linux. For other OSs, +consider: + +- macOS (Homebrew x86): /usr/local/share +- macOS (Homebrew ARM): /opt/homebrew/share +- Android (Termux): /data/data/com.termux/files/usr/share +- Windows (mingw64 of msys2): /mingw64/share +- ... + +After installing the shell completions, enable them following your shell's +recommended instructions. e.g.: + +- bash: install [bash-completion](https://github.com/scop/bash-completion), + and ensure ``. /usr/share/bash-completion/bash_completion`` in ``~/.bashrc``. +- zsh: ensure ``autoload -Uz compinit && compinit`` appears in ``~/.zshrc``, + then ``grep -w keyring ~/.zcompdump`` to verify keyring appears, indicating + it was installed correctly. + Configuring =========== diff --git a/keyring/backend_complete.zsh b/keyring/backend_complete.zsh new file mode 100644 index 00000000..eba76c6b --- /dev/null +++ b/keyring/backend_complete.zsh @@ -0,0 +1,14 @@ +# Complete keyring backends for `keyring -b` from `keyring --list-backends` +# % keyring -b +# keyring priority +# keyring.backends.chainer.ChainerBackend 10 +# keyring.backends.fail.Keyring 0 +# ... ... + +backend_complete() { + local line + while read -r line; do + choices+=(${${line/ \(priority: /\\\\:}/)/}) + done <<< "$($words[1] --list-backends)" + _arguments "*:keyring priority:(($choices))" +} diff --git a/keyring/cli.py b/keyring/cli.py index e4a4c78f..02cb65b0 100755 --- a/keyring/cli.py +++ b/keyring/cli.py @@ -7,6 +7,7 @@ from . import core from . import backend +from . import completion from . import set_keyring, get_password, set_password, delete_password @@ -48,6 +49,7 @@ def __init__(self): 'username', nargs="?", ) + completion.install(self.parser) def run(self, argv): args = self.parser.parse_args(argv) diff --git a/keyring/completion.py b/keyring/completion.py new file mode 100644 index 00000000..37c9c053 --- /dev/null +++ b/keyring/completion.py @@ -0,0 +1,51 @@ +import argparse +import sys + +try: + import shtab +except ImportError: + pass + +if sys.version_info < (3, 9): + from importlib_resources import files +else: + from importlib.resources import files + + +class _MissingCompletionAction(argparse.Action): + def __call__(self, parser, namespace, values, option_string): + print("Install keyring[completion] for completion support.") + parser.exit(0) + + +def add_completion_notice(parser): + """Add completion argument to parser.""" + parser.add_argument( + "--print-completion", + choices=["bash", "zsh", "tcsh"], + action=_MissingCompletionAction, + help="print shell completion script", + ) + return parser + + +def get_action(parser, option): + (match,) = (action for action in parser._actions if option in action.option_strings) + return match + + +def install_completion(parser): + preamble = dict( + zsh=files(__package__).joinpath('backend_complete.zsh').read_text(), + ) + shtab.add_argument_to(parser, preamble=preamble) + get_action(parser, '--keyring-path').completion = shtab.DIR + get_action(parser, '--keyring-backend').completion = dict(zsh='backend_complete') + return parser + + +def install(parser): + try: + install_completion(parser) + except NameError: + add_completion_notice(parser) diff --git a/setup.cfg b/setup.cfg index e7db40f2..e5df7efc 100644 --- a/setup.cfg +++ b/setup.cfg @@ -24,6 +24,7 @@ install_requires = jeepney>=0.4.2; sys_platform=="linux" importlib_metadata >= 4.11.4; python_version < "3.12" jaraco.classes + importlib_resources; python_version < "3.9" [options.packages.find] exclude = @@ -63,6 +64,9 @@ docs = # local +completion = + shtab + [options.entry_points] console_scripts = keyring=keyring.cli:main