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>
==========================================