pyinvoke / invoke

Pythonic task management & command execution.

Home Page:http://pyinvoke.org

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Config run should handle shell paths with spaces

achekery opened this issue · comments

When setting shell to shutil.which('pwsh.exe') on my Windows system, I noticed invoke couldn't handle the path with spaces. So I am using a workaround that converts the path to 8.3 format. It would be nice if this were handled by invoke automatically.

Created tasks.py for demo:

# pylint: disable-next=E0401:import-error
from invoke import task  # type: ignore

@task(default=True)
def demo_qol_shellpathspaces(ctx):
    """Config run shell should handle paths with spaces."""
    # pylint: disable=C0415:import-outside-toplevel
    import shutil

    # pylint: disable-next=E0401:import-error
    from invoke.exceptions import UnexpectedExit  # type: ignore


    class Kernel32DllExt:
        """Namespace for `kernel32.dll` extensions."""

        # Disable pylint error message when importing from namespaces.
        # pylint: disable=C0415:import-outside-toplevel

        # Useful for any `invoke` tasks that set `config.run.shell` value
        # on Windows platform because `invoke` does not handle shell
        # executable files with paths containing spaces.  Note the default
        # shell for `invoke` is `cmd`, which has been superceded by `pwsh`.

        # It would be nice to have this handled by `invoke` automatically.

        @staticmethod
        def get_short_path_name(long_path):
            """Get short path form for long path.

            This API converts path strings from the posix format used by
            `pathlib.Path` to the 8.3 format used by earlier tools on the
            Windows platform.
            """
            import ctypes
            from ctypes import wintypes

            # Access `GetShortPathNameW()` function from `kernel32.dll`.
            kernel32_func = ctypes.windll.kernel32.GetShortPathNameW
            kernel32_func.argtypes = [
                wintypes.LPCWSTR,
                wintypes.LPWSTR,
                wintypes.DWORD,
            ]
            kernel32_func.restype = wintypes.DWORD
            # Call function to get short path form.
            buffer_size = 0
            while True:
                buffer_array = ctypes.create_unicode_buffer(buffer_size)
                required_size = kernel32_func(long_path, buffer_array, buffer_size)
                if required_size > buffer_size:
                    buffer_size = required_size
                else:
                    return buffer_array.value


    def _demo(ctx_, index_, shell_path_, command_):
        ctx.config.run.shell = shell_path_
        print(f"** Demo #{index_}: with {shell_path_=} run {command_=}")
        try:
            res_ = ctx_.run(command)
        except (OSError, FileNotFoundError, TypeError, UnexpectedExit) as _exc:
            print(f"** Demo Error: {_exc=}\n")
        else:
            print(f"** Demo Success!: {res_=}\n")


    ctx.config.run.echo = True
    command = "git status --porcelain"
    for index, shell_name in enumerate([
        "cmd.exe",
        "pwsh.exe",
    ], start=1):
        shell_which = shutil.which(shell_name)
        print(f"** Setup Start: {shell_name=}, {shell_which=}")
        try:
            _demo(ctx, f"{index}-whichformat", shell_which, command)
            _demo(ctx, f"{index}-83format", Kernel32DllExt.get_short_path_name(shell_which), command)
        except TypeError as exc:
            print(f"** Setup Error: {exc=}")
        finally:
            print("==========================================")

Results with invoke main:

** Setup Start: shell_name='cmd.exe', shell_which='C:\\WINDOWS\\system32\\cmd.exe'
** Demo #1-whichformat: with shell_path_='C:\\WINDOWS\\system32\\cmd.exe' run command_='git status --porcelain'
�[1;37mgit status --porcelain�[0m
 M projects/bce-patch-builder/tasks.py
** Demo Success!: res_=<Result cmd='git status --porcelain' exited=0>

** Demo #1-83format: with shell_path_='C:\\WINDOWS\\system32\\cmd.exe' run command_='git status --porcelain'
�[1;37mgit status --porcelain�[0m
 M projects/bce-patch-builder/tasks.py
The argument 'Files\PowerShell\7\pwsh.exe' is not recognized as the name of a script file. Check the spelling of the name, or if a path was included, verify that the path is correct and try again.

** Demo Success!: res_=<Result cmd='git status --porcelain' exited=0>

==========================================
** Setup Start: shell_name='pwsh.exe', shell_which='C:\\Program Files\\PowerShell\\7\\pwsh.exe'
** Demo #2-whichformat: with shell_path_='C:\\Program Files\\PowerShell\\7\\pwsh.exe' run command_='git status --porcelain'
�[1;37mgit status --porcelain�[0m


Usage: pwsh[.exe] [-Login] [[-File] <filePath> [args]]

                  [-Command { - | <script-block> [-args <arg-array>]

                                | <string> [<CommandParameters>] } ]

                  [-CommandWithArgs <string> [<CommandParameters>]

                  [-ConfigurationName <string>] [-ConfigurationFile <filePath>]

                  [-CustomPipeName <string>] [-EncodedCommand <Base64EncodedCommand>]

                  [-ExecutionPolicy <ExecutionPolicy>] [-InputFormat {Text | XML}]

                  [-Interactive] [-MTA] [-NoExit] [-NoLogo] [-NonInteractive] [-NoProfile]

                  [-NoProfileLoadTime] [-OutputFormat {Text | XML}] 

                  [-SettingsFile <filePath>] [-SSHServerMode] [-STA] 

                  [-Version] [-WindowStyle <style>] 

                  [-WorkingDirectory <directoryPath>]



       pwsh[.exe] -h | -Help | -? | /?



PowerShell Online Help https://aka.ms/powershell-docs



All parameters are case-insensitive.

** Demo Error: _exc=<UnexpectedExit: cmd='git status --porcelain' exited=64>

** Demo #2-83format: with shell_path_='C:\\PROGRA~1\\POWERS~1\\7\\pwsh.exe' run command_='git status --porcelain'
�[1;37mgit status --porcelain�[0m
 M projects/bce-patch-builder/tasks.py
** Demo Success!: res_=<Result cmd='git status --porcelain' exited=0>

==========================================

Added this qol change here

#989