pyinvoke / invoke

Pythonic task management & command execution.
http://pyinvoke.org
BSD 2-Clause "Simplified" License
4.31k stars 365 forks source link

Config run should handle shell paths with spaces #988

Open achekery opened 3 months ago

achekery commented 3 months ago

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.

achekery commented 3 months ago

Created tasks.py for demo:

```python # 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'
git status --porcelain
 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'
git status --porcelain
 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'
git status --porcelain

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'
git status --porcelain
 M projects/bce-patch-builder/tasks.py
** Demo Success!: res_=<Result cmd='git status --porcelain' exited=0>

==========================================
achekery commented 3 months ago

Added this qol change here

https://github.com/pyinvoke/invoke/pull/989