diff --git a/poetry.lock b/poetry.lock index 18621739..76742c1c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -11,6 +11,23 @@ files = [ {file = "appnope-0.1.3.tar.gz", hash = "sha256:02bd91c4de869fbb1e1c50aafc4098827a7a54ab2f39d9dcba6c9547ed920e24"}, ] +[[package]] +name = "argcomplete" +version = "3.1.2" +description = "Bash tab completion for argparse" +optional = false +python-versions = ">=3.6" +files = [ + {file = "argcomplete-3.1.2-py3-none-any.whl", hash = "sha256:d97c036d12a752d1079f190bc1521c545b941fda89ad85d15afa909b4d1b9a99"}, + {file = "argcomplete-3.1.2.tar.gz", hash = "sha256:d5d1e5efd41435260b8f85673b74ea2e883affcbec9f4230c582689e8e78251b"}, +] + +[package.dependencies] +importlib-metadata = {version = ">=0.23,<7", markers = "python_version < \"3.8\""} + +[package.extras] +test = ["coverage", "mypy", "pexpect", "ruff", "wheel"] + [[package]] name = "backcall" version = "0.2.0" @@ -1299,4 +1316,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more [metadata] lock-version = "2.0" python-versions = "^3.7" -content-hash = "fe1d858464c4989bb015b34398e296877c3e0aee1a4d1ce3c3dd19979ec9e71f" +content-hash = "c6bfb696d49ef0fd7eea1b47a2ee91123f4a1f6ae7c88836c01d0533a9377668" diff --git a/pyproject.toml b/pyproject.toml index 057ab487..7357e360 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,6 +44,7 @@ packaging = ">=22.0" psutil = ">= 5.8.0" pyjwt = "^2.4.0" requests = ">= 2.25" +argcomplete = "^3.1.2" [tool.poetry.group.dev.dependencies] black = "*" diff --git a/src/codemagic/cli/argument/argument_parser_builder.py b/src/codemagic/cli/argument/argument_parser_builder.py index d5720273..1f2e8d9b 100644 --- a/src/codemagic/cli/argument/argument_parser_builder.py +++ b/src/codemagic/cli/argument/argument_parser_builder.py @@ -1,9 +1,11 @@ from __future__ import annotations import argparse +import textwrap from typing import TYPE_CHECKING from typing import Dict from typing import Type +from typing import cast from codemagic.cli.cli_help_formatter import CliHelpFormatter from codemagic.cli.colors import Colors @@ -52,7 +54,7 @@ def _create_action_parser(self, parent_parser: SubParsersAction, for_deprecated_ self._cli_action.action_name, formatter_class=CliHelpFormatter, description=Colors.BOLD(self._cli_action.__doc__), - help=self._cli_action.__doc__, + help=textwrap.dedent(cast(str, self._cli_action.__doc__)).strip().replace("\n", " "), ) @property diff --git a/src/codemagic/cli/cli_app.py b/src/codemagic/cli/cli_app.py index edf07c31..782764c8 100644 --- a/src/codemagic/cli/cli_app.py +++ b/src/codemagic/cli/cli_app.py @@ -11,6 +11,7 @@ import shlex import shutil import sys +import textwrap import time from functools import wraps from itertools import chain @@ -28,6 +29,8 @@ from typing import TypeVar from typing import Union +import argcomplete + from codemagic import __version__ from codemagic.utilities import auditing from codemagic.utilities import log @@ -300,7 +303,7 @@ def _add_action_group(cls, action_group, parent_parser): group_parser = parent_parser.add_parser( action_group.name, formatter_class=CliHelpFormatter, - help=action_group.description, + help=textwrap.dedent(action_group.description).strip().replace("\n", " "), description=action_group.description, ) ArgumentParserBuilder.set_default_cli_options(group_parser) @@ -337,6 +340,8 @@ def _setup_cli_options(cls) -> argparse.ArgumentParser: main_action: ActionCallable = action_or_group cls._register_cli_action(main_action, action_parsers, action_parsers) + argcomplete.autocomplete(main_parser) + return main_parser @classmethod diff --git a/src/codemagic/tools/codemagic_cli_tools.py b/src/codemagic/tools/codemagic_cli_tools.py index 5301029d..bfaebd70 100644 --- a/src/codemagic/tools/codemagic_cli_tools.py +++ b/src/codemagic/tools/codemagic_cli_tools.py @@ -1,7 +1,12 @@ +import pathlib import shutil +import textwrap + +import argcomplete from codemagic import __version__ from codemagic import cli +from codemagic.cli import Colors class CodemagicCliTools(cli.CliApp): @@ -27,6 +32,52 @@ def installed_tools(self): executable = tool_class.get_executable_name() self.echo(f"{executable} installed at {shutil.which(executable) or executable}") + @cli.action("enable-shell-autocompletion") + def enable_shell_autocompletion(self): + """ + Enable tab autocompletion for Codemagic CLI tools in your shell + """ + + # TODO: Add required shell argument + # TODO: Add optional argument for completion script location + + executables = [tool_class.get_executable_name() for tool_class in cli.CliApp.__subclasses__()] + + completion_scripts_dir = pathlib.Path("~/.codemagic-cli-tools/completions").expanduser() + completion_scripts_dir.mkdir(parents=True, exist_ok=True) + + zsh_completion_path = completion_scripts_dir / "zsh_autocomplete.sh" + zsh_completion_path.write_text(argcomplete.shellcode(executables, shell="zsh")) + + bash_completion_path = completion_scripts_dir / "bash_autocomplete.sh" + bash_completion_path.write_text(argcomplete.shellcode(executables, shell="bash")) + + completion_path = completion_scripts_dir / "autocomplete.sh" + completion_path.write_text( + textwrap.dedent( + f"""\ + #!/bin/sh + + if [ -n "$BASH_VERSION" ]; then + source {bash_completion_path} + elif [ -n "$ZSH_VERSION" ]; then + source {zsh_completion_path} + fi + + """, + ), + ) + + self.echo(Colors.GREEN(f"Shell autocompletion instructions were saved to {completion_path}")) + self.echo( + "To use autocomplete for Codemagic CLI tools, add the following " + "line to your shell profile (~/.zshrc, ~/.bashrc, etc):", + ) + self.echo("") + self.echo(Colors.WHITE(f" source {completion_path}")) + self.echo("") + self.echo("Don't forget to source the completion script in your current shell.") + if __name__ == "__main__": CodemagicCliTools.invoke_cli()