From b9fa0a8affce0519a617075d052ea452c4697ee4 Mon Sep 17 00:00:00 2001 From: woutdenolf Date: Fri, 16 Oct 2026 14:15:59 +0200 Subject: [PATCH 1/2] ewoks install: print execute and remove commands that work from any terminal Co-Authored-By: Claude Opus 5.5 --- doc/tutorials/install.rst | 8 +-- src/ewoks/__main__.py | 65 +++++++++++++++---- .../tests/requirements/test_install_cli.py | 14 ++++ 3 files changed, 70 insertions(+), 17 deletions(-) diff --git a/doc/tutorials/install.rst b/doc/tutorials/install.rst index dd3cbbd..e3d5c8d 100644 --- a/doc/tutorials/install.rst +++ b/doc/tutorials/install.rst @@ -71,14 +71,14 @@ Create the environment ``ewoks install`` creates a python environment for the :term:`workflow`. It prints the python interpreter of the environment it created, the command to execute the :term:`workflow` in it -and the command to remove it again +and the command to remove it again. These commands work from any terminal and directory .. code-block:: text Installed requirements for demo.json - Python : ewoks_envs/demo/bin/python - Execute: ewoks execute --env ewoks_envs/demo demo.json - Remove : rm -rf ewoks_envs/demo + Python : /home/user/ewoks_envs/demo/bin/python + Execute: /home/user/ewoks_envs/demo/bin/python -m ewoks execute /home/user/demo.json + Remove : rm -rf /home/user/ewoks_envs/demo Without ``--yes`` you are asked to confirm after the packages have been listed. The walk-throughs use ``--env-root`` to create the environment in the working directory. Without diff --git a/src/ewoks/__main__.py b/src/ewoks/__main__.py index 1383a56..b136400 100644 --- a/src/ewoks/__main__.py +++ b/src/ewoks/__main__.py @@ -1,3 +1,6 @@ +import os +import re +import shlex import subprocess import sys import traceback @@ -207,27 +210,63 @@ def command_install( print(f"Install aborted for {workflow}") else: print(f"Installed requirements for {workflow}") + print(f" Python : {environment.python}") + if environment.distribution_version("ewoks"): + execute = _shell_command( + [ + str(environment.python), + "-m", + "ewoks", + "execute", + _absolute_workflow(workflow), + ] + ) + print(f" Execute: {execute}") + else: + print( + " Execute: the environment has no ewoks to execute the " + "workflow with (use '--with-ewoks')" + ) if not cli_args.in_place: - location = environment.location - print(f" Python : {environment.python}") - if environment.distribution_version("ewoks"): - print(f" Execute: ewoks execute --env {location} {workflow}") - else: - print( - " Execute: the environment has no ewoks to execute the " - "workflow with (use '--with-ewoks')" - ) - print(f" Remove : {_remove_command(location)}") + print(f" Remove : {_remove_command(environment.location)}") if shell: return 0 return None def _remove_command(location: Path) -> str: - """Shell command that removes a python environment.""" + """Shell command that removes a python environment: PowerShell on Windows.""" + if sys.platform == "win32": + return f"Remove-Item -Recurse -Force {_powershell_quote(str(location))}" + return f"rm -rf {shlex.quote(str(location))}" + + +def _shell_command(arguments: List[str]) -> str: + """Command line for the shell of the platform: PowerShell on Windows.""" if sys.platform == "win32": - return f'rmdir /s /q "{location}"' - return f"rm -rf {location}" + command = " ".join(_powershell_quote(argument) for argument in arguments) + if command.startswith("'"): + # A quoted string is not a command in PowerShell + return f"& {command}" + return command + return shlex.join(arguments) + + +_POWERSHELL_SAFE = re.compile(r"[\w./\\:=+-]+") + + +def _powershell_quote(argument: str) -> str: + """Argument quoted for PowerShell when needed.""" + if _POWERSHELL_SAFE.fullmatch(argument): + return argument + return "'" + argument.replace("'", "''") + "'" + + +def _absolute_workflow(workflow: str) -> str: + """Workflow that does not depend on the working directory.""" + if os.path.isfile(workflow): + return os.path.abspath(workflow) + return workflow def command_in_environment( diff --git a/src/ewoks/tests/requirements/test_install_cli.py b/src/ewoks/tests/requirements/test_install_cli.py index a0b4342..3305b46 100644 --- a/src/ewoks/tests/requirements/test_install_cli.py +++ b/src/ewoks/tests/requirements/test_install_cli.py @@ -12,6 +12,8 @@ import pytest +from ...__main__ import _remove_command +from ...__main__ import _shell_command from ..._requirements import environment_name from ..._requirements.utils.environment import Environment from ...bindings import load_graph @@ -27,6 +29,18 @@ environment created for them.""" +def test_printed_commands_with_spaces(tmp_path): + """The printed commands work in the shell of the platform.""" + location = tmp_path / "ewoks envs" + python = str(location / "python") + if sys.platform == "win32": + assert _shell_command([python, "-m", "ewoks"]) == f"& '{python}' -m ewoks" + assert _remove_command(location) == f"Remove-Item -Recurse -Force '{location}'" + else: + assert _shell_command([python, "-m", "ewoks"]) == f"'{python}' -m ewoks" + assert _remove_command(location) == f"rm -rf '{location}'" + + def test_install_with_files(fast_manager_case, env_root): files = fast_manager_case.native_files(DISTRIBUTIONS, PYTHON_VERSION) if not files: From f572bcf956908f8e9ed2432d31c7441a73c4e909 Mon Sep 17 00:00:00 2001 From: Wout De Nolf Date: Tue, 3 Nov 2026 10:31:36 +0100 Subject: [PATCH 2/2] document regex purpose --- src/ewoks/__main__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ewoks/__main__.py b/src/ewoks/__main__.py index b136400..93afe08 100644 --- a/src/ewoks/__main__.py +++ b/src/ewoks/__main__.py @@ -253,6 +253,7 @@ def _shell_command(arguments: List[str]) -> str: _POWERSHELL_SAFE = re.compile(r"[\w./\\:=+-]+") +# Characters allowed in PowerShell arguments without quoting def _powershell_quote(argument: str) -> str: