diff --git a/invoke/runners.py b/invoke/runners.py index f1c888f44..2e10080fb 100644 --- a/invoke/runners.py +++ b/invoke/runners.py @@ -51,6 +51,7 @@ ready_for_reading, bytes_to_read, ) +from .shims import get_short_path_name from .util import has_fileno, isatty, ExceptionHandlingThread if TYPE_CHECKING: @@ -415,6 +416,7 @@ def _setup(self, command: str, kwargs: Any) -> None: # Echo running command (wants to be early to be included in dry-run) if self.opts["echo"]: self.echo(command) + self.opts["shell"] = get_short_path_name(self.opts["shell"]) # Prepare common result args. # TODO: I hate this. Needs a deeper separate think about tweaking # Runner.generate_result in a way that isn't literally just this same diff --git a/invoke/shims.py b/invoke/shims.py new file mode 100644 index 000000000..490f0342a --- /dev/null +++ b/invoke/shims.py @@ -0,0 +1,54 @@ +import sys + + +if sys.platform == "win32": + from ctypes import ( + windll, + wintypes, + create_unicode_buffer, + ) + + def _get_short_path_name(long_path: str) -> str: + # Access `GetShortPathNameW()` function from `kernel32.dll`. + GetShortPathNameW = windll.kernel32.GetShortPathNameW + GetShortPathNameW.argtypes = [ + wintypes.LPCWSTR, + wintypes.LPWSTR, + wintypes.DWORD, + ] + GetShortPathNameW.restype = wintypes.DWORD + # Call function to get short path form. + buffer_size = 0 + while True: + buffer_array = create_unicode_buffer(buffer_size) + required_size = GetShortPathNameW( + long_path, + buffer_array, + buffer_size, + ) + if required_size > buffer_size: + buffer_size = required_size + else: + return buffer_array.value + +else: + + def _get_short_path_name(long_path: str) -> str: + return long_path + + +def get_short_path_name(long_path: str) -> str: + """ + Get short path form for long path. + + Only applies to Windows-based systems; on Unix this is a pass-thru. + + .. note:: + This API converts path strings to the 8.3 format used by earlier + tools on the Windows platform. This format has no spaces. + + :param long_path: Long path such as `shutil.which()` results. + + :returns: `str` Short path form of the long path. + """ + return _get_short_path_name(long_path) diff --git a/tests/runners.py b/tests/runners.py index 94c63d8b3..91dc47993 100644 --- a/tests/runners.py +++ b/tests/runners.py @@ -231,6 +231,14 @@ def may_be_configured(self): runner = self._runner(run={"shell": "/bin/tcsh"}) assert runner.run(_).shell == "/bin/tcsh" + def may_be_configured_with_short_path_on_windows(self): + skip() + + @skip_if_windows + def may_be_configured_with_passthru_on_posix(self): + runner = self._runner(run={"shell": "/foo/bar/baz /bang"}) + assert runner.run(_).shell == "/foo/bar/baz /bang" + def kwarg_beats_config(self): runner = self._runner(run={"shell": "/bin/tcsh"}) assert runner.run(_, shell="/bin/zsh").shell == "/bin/zsh" diff --git a/tests/terminals.py b/tests/terminals.py index 96d095886..d1cb9cece 100644 --- a/tests/terminals.py +++ b/tests/terminals.py @@ -4,7 +4,12 @@ from unittest.mock import Mock, patch from pytest import skip, mark -from invoke.terminals import pty_size, bytes_to_read, WINDOWS +from invoke.shims import get_short_path_name +from invoke.terminals import ( + WINDOWS, + bytes_to_read, + pty_size, +) # Skip on Windows CI, it may blow up on one of these tests pytestmark = mark.skipif( @@ -76,3 +81,11 @@ def returns_FIONREAD_result_when_stream_is_a_tty(self): def returns_1_on_windows(self): skip() + + class get_short_path_name_: + def returns_passthru_on_posix(self): + short_path = "/foo/bar/baz" + assert get_short_path_name(short_path) == short_path + + def returns_shortpath_on_windows(self): + skip()