diff --git a/HISTORY.rst b/HISTORY.rst index 7b124df6..82091206 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -9,6 +9,7 @@ PlatformIO Core 4 4.3.4 (2020-??-??) ~~~~~~~~~~~~~~~~~~ +* Added `PlatformIO CLI Shell Completion `__ for Fish, Zsh, Bash, and PowerShell (`issue #3435 `_) * Automatically build ``contrib-pysite`` package on a target machine when pre-built package is not compatible (`issue #3482 `_) 4.3.3 (2020-04-28) diff --git a/docs b/docs index 2fd23581..695be976 160000 --- a/docs +++ b/docs @@ -1 +1 @@ -Subproject commit 2fd2358162db56ddbc344f0a874953ac96ea441f +Subproject commit 695be97646509b2ee01616b3e18303bec35567d2 diff --git a/platformio/__main__.py b/platformio/__main__.py index 6679d52e..8420544b 100644 --- a/platformio/__main__.py +++ b/platformio/__main__.py @@ -22,6 +22,13 @@ from platformio import __version__, exception, maintenance, util from platformio.commands import PlatformioCLI from platformio.compat import CYGWIN +try: + import click_completion # pylint: disable=import-error + + click_completion.init() +except: # pylint: disable=bare-except + pass + @click.command( cls=PlatformioCLI, context_settings=dict(help_option_names=["-h", "--help"]) diff --git a/platformio/commands/home/rpc/handlers/piocore.py b/platformio/commands/home/rpc/handlers/piocore.py index 9bcef9e5..cea7cb5b 100644 --- a/platformio/commands/home/rpc/handlers/piocore.py +++ b/platformio/commands/home/rpc/handlers/piocore.py @@ -59,8 +59,8 @@ class MultiThreadingStdStream(object): result = "" try: result = self.getvalue() - self.truncate(0) self.seek(0) + self.truncate(0) except AttributeError: pass return result diff --git a/platformio/commands/misc/__init__.py b/platformio/commands/misc/__init__.py new file mode 100644 index 00000000..b0514903 --- /dev/null +++ b/platformio/commands/misc/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2014-present PlatformIO +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/platformio/commands/misc/command.py b/platformio/commands/misc/command.py new file mode 100644 index 00000000..46fc0c4e --- /dev/null +++ b/platformio/commands/misc/command.py @@ -0,0 +1,96 @@ +# Copyright (c) 2014-present PlatformIO +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import subprocess + +import click + +from platformio import proc +from platformio.commands.misc.completion import ( + get_completion_install_path, + install_completion_code, + uninstall_completion_code, +) + + +@click.group("misc", short_help="Miscellaneous commands") +def cli(): + pass + + +@cli.group("completion", short_help="Shell completion support") +def completion(): + # pylint: disable=import-outside-toplevel + try: + import click_completion # pylint: disable=import-error,unused-import + except ImportError: + click.echo("Installing dependent packages...") + subprocess.check_call( + [proc.get_pythonexe_path(), "-m", "pip", "install", "click-completion"], + ) + + +@completion.command("install", short_help="Install shell completion files/code") +@click.option( + "--shell", + default=None, + type=click.Choice(["fish", "bash", "zsh", "powershell", "auto"]), + help="The shell type, default=auto", +) +@click.option( + "--path", + type=click.Path(file_okay=True, dir_okay=False, readable=True, resolve_path=True), + help="Custom installation path of the code to be evaluated by the shell. " + "The standard installation path is used by default.", +) +def completion_install(shell, path): + + import click_completion # pylint: disable=import-outside-toplevel,import-error + + shell = shell or click_completion.get_auto_shell() + path = path or get_completion_install_path(shell) + install_completion_code(shell, path) + click.echo( + "PlatformIO CLI completion has been installed for %s shell to %s \n" + "Please restart a current shell session." + % (click.style(shell, fg="cyan"), click.style(path, fg="blue")) + ) + + +@completion.command("uninstall", short_help="Uninstall shell completion files/code") +@click.option( + "--shell", + default=None, + type=click.Choice(["fish", "bash", "zsh", "powershell", "auto"]), + help="The shell type, default=auto", +) +@click.option( + "--path", + type=click.Path(file_okay=True, dir_okay=False, readable=True, resolve_path=True), + help="Custom installation path of the code to be evaluated by the shell. " + "The standard installation path is used by default.", +) +def completion_uninstall(shell, path): + + import click_completion # pylint: disable=import-outside-toplevel,import-error + + shell = shell or click_completion.get_auto_shell() + path = path or get_completion_install_path(shell) + uninstall_completion_code(shell, path) + click.echo( + "PlatformIO CLI completion has been uninstalled for %s shell from %s \n" + "Please restart a current shell session." + % (click.style(shell, fg="cyan"), click.style(path, fg="blue")) + ) diff --git a/platformio/commands/misc/completion.py b/platformio/commands/misc/completion.py new file mode 100644 index 00000000..032225c4 --- /dev/null +++ b/platformio/commands/misc/completion.py @@ -0,0 +1,73 @@ +# Copyright (c) 2014-present PlatformIO +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import subprocess + +import click + + +def get_completion_install_path(shell): + home_dir = os.path.expanduser("~") + prog_name = click.get_current_context().find_root().info_name + if shell == "fish": + return os.path.join( + home_dir, ".config", "fish", "completions", "%s.fish" % prog_name + ) + if shell == "bash": + return os.path.join(home_dir, ".bash_completion") + if shell == "zsh": + return os.path.join(home_dir, ".zshrc") + if shell == "powershell": + return subprocess.check_output( + ["powershell", "-NoProfile", "echo $profile"] + ).strip() + raise click.ClickException("%s is not supported." % shell) + + +def is_completion_code_installed(shell, path): + if shell == "fish" or not os.path.exists(path): + return False + + import click_completion # pylint: disable=import-error,import-outside-toplevel + + with open(path) as fp: + return click_completion.get_code(shell=shell) in fp.read() + + +def install_completion_code(shell, path): + import click_completion # pylint: disable=import-error,import-outside-toplevel + + if is_completion_code_installed(shell, path): + return None + + return click_completion.install(shell=shell, path=path) + + +def uninstall_completion_code(shell, path): + if not os.path.exists(path): + return True + if shell == "fish": + os.remove(path) + return True + + import click_completion # pylint: disable=import-error,import-outside-toplevel + + with open(path, "r+") as fp: + contents = fp.read() + fp.seek(0) + fp.truncate() + fp.write(contents.replace(click_completion.get_code(shell=shell), "")) + + return True diff --git a/platformio/managers/core.py b/platformio/managers/core.py index 677a1c81..53f435fd 100644 --- a/platformio/managers/core.py +++ b/platformio/managers/core.py @@ -138,7 +138,7 @@ def build_contrib_pysite_deps(target_dir): pythonexe = get_pythonexe_path() for dep in get_contrib_pysite_deps(): - subprocess.call( + subprocess.check_call( [ pythonexe, "-m",