Skip to content

Commit 6fb1c5a

Browse files
authored
Merge pull request #281 from alex-feel/alex-feel-dev
Prevent MSYS path conversion in run_bash_command
2 parents c1f39d0 + e62da7c commit 6fb1c5a

File tree

2 files changed

+169
-1
lines changed

2 files changed

+169
-1
lines changed

scripts/setup_environment.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -695,8 +695,14 @@ def run_bash_command(
695695

696696
debug_log(f'Executing: {args}')
697697

698+
# Disable MSYS path conversion on Windows to preserve /c flags and other arguments
699+
# that would otherwise be incorrectly converted to Windows drive paths (e.g., /c -> C:/)
700+
env = os.environ.copy()
701+
if sys.platform == 'win32':
702+
env['MSYS_NO_PATHCONV'] = '1'
703+
698704
try:
699-
result = subprocess.run(args, capture_output=capture_output, text=True)
705+
result = subprocess.run(args, capture_output=capture_output, text=True, env=env)
700706
debug_log(f'Exit code: {result.returncode}')
701707
if capture_output:
702708
stdout_preview = result.stdout[:500] if result.stdout else '(empty)'

tests/test_setup_environment.py

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3854,3 +3854,165 @@ def test_parallel_workers_env_parsing_logic(self) -> None:
38543854
# Test fallback to default
38553855
value = int(os.environ.get('CLAUDE_PARALLEL_WORKERS', '3'))
38563856
assert value == 3
3857+
3858+
3859+
class TestRunBashCommandMsysPathConversion:
3860+
"""Tests for MSYS path conversion prevention in run_bash_command().
3861+
3862+
Git Bash (MSYS2) automatically converts POSIX-style paths like /c to
3863+
Windows drive paths like C:/. This breaks cmd.exe's /c flag which is
3864+
used to run commands. These tests verify that MSYS_NO_PATHCONV=1 is
3865+
set correctly to disable this conversion.
3866+
"""
3867+
3868+
@patch('scripts.setup_environment.sys.platform', 'win32')
3869+
@patch('setup_environment.find_bash_windows')
3870+
@patch('subprocess.run')
3871+
def test_msys_no_pathconv_set_on_windows(
3872+
self, mock_run: MagicMock, mock_find_bash: MagicMock,
3873+
) -> None:
3874+
"""Verify MSYS_NO_PATHCONV=1 is set in env on Windows."""
3875+
mock_find_bash.return_value = r'C:\Program Files\Git\bin\bash.exe'
3876+
mock_run.return_value = subprocess.CompletedProcess([], 0, '', '')
3877+
3878+
setup_environment.run_bash_command('echo test')
3879+
3880+
# Verify subprocess.run was called with env parameter containing MSYS_NO_PATHCONV
3881+
assert mock_run.called
3882+
call_kwargs = mock_run.call_args[1]
3883+
assert 'env' in call_kwargs
3884+
assert call_kwargs['env'].get('MSYS_NO_PATHCONV') == '1'
3885+
3886+
@patch('scripts.setup_environment.sys.platform', 'linux')
3887+
@patch('shutil.which')
3888+
@patch('subprocess.run')
3889+
def test_msys_no_pathconv_not_set_on_linux(
3890+
self, mock_run: MagicMock, mock_which: MagicMock,
3891+
) -> None:
3892+
"""Verify MSYS_NO_PATHCONV is NOT set in env on Linux."""
3893+
mock_which.return_value = '/usr/bin/bash'
3894+
mock_run.return_value = subprocess.CompletedProcess([], 0, '', '')
3895+
3896+
setup_environment.run_bash_command('echo test')
3897+
3898+
assert mock_run.called
3899+
call_kwargs = mock_run.call_args[1]
3900+
# On Linux, env should not contain MSYS_NO_PATHCONV
3901+
env = call_kwargs.get('env')
3902+
assert env is not None
3903+
assert 'MSYS_NO_PATHCONV' not in env
3904+
3905+
@patch('scripts.setup_environment.sys.platform', 'darwin')
3906+
@patch('shutil.which')
3907+
@patch('subprocess.run')
3908+
def test_msys_no_pathconv_not_set_on_macos(
3909+
self, mock_run: MagicMock, mock_which: MagicMock,
3910+
) -> None:
3911+
"""Verify MSYS_NO_PATHCONV is NOT set in env on macOS."""
3912+
mock_which.return_value = '/bin/bash'
3913+
mock_run.return_value = subprocess.CompletedProcess([], 0, '', '')
3914+
3915+
setup_environment.run_bash_command('echo test')
3916+
3917+
assert mock_run.called
3918+
call_kwargs = mock_run.call_args[1]
3919+
# On macOS, env should not contain MSYS_NO_PATHCONV
3920+
env = call_kwargs.get('env')
3921+
assert env is not None
3922+
assert 'MSYS_NO_PATHCONV' not in env
3923+
3924+
@patch('scripts.setup_environment.sys.platform', 'win32')
3925+
@patch('setup_environment.find_bash_windows')
3926+
@patch('subprocess.run')
3927+
def test_c_flag_preserved_in_command(
3928+
self, mock_run: MagicMock, mock_find_bash: MagicMock,
3929+
) -> None:
3930+
"""Verify /c flag is preserved and not converted to C:/."""
3931+
mock_find_bash.return_value = r'C:\Program Files\Git\bin\bash.exe'
3932+
mock_run.return_value = subprocess.CompletedProcess([], 0, '', '')
3933+
3934+
# Command that includes /c flag (typical Windows cmd wrapper)
3935+
setup_environment.run_bash_command('cmd /c npx test-package')
3936+
3937+
# Verify the command string passed to subprocess contains /c not C:/
3938+
assert mock_run.called
3939+
call_args = mock_run.call_args[0][0]
3940+
command_arg = call_args[-1] # Last element is the command string
3941+
assert '/c' in command_arg
3942+
assert 'C:/' not in command_arg
3943+
3944+
@patch('scripts.setup_environment.sys.platform', 'win32')
3945+
@patch('setup_environment.find_bash_windows')
3946+
@patch('subprocess.run')
3947+
@patch.dict('os.environ', {'EXISTING_VAR': 'existing_value', 'PATH': '/usr/bin'})
3948+
def test_existing_env_vars_preserved(
3949+
self, mock_run: MagicMock, mock_find_bash: MagicMock,
3950+
) -> None:
3951+
"""Verify existing environment variables are preserved when adding MSYS_NO_PATHCONV."""
3952+
mock_find_bash.return_value = r'C:\Program Files\Git\bin\bash.exe'
3953+
mock_run.return_value = subprocess.CompletedProcess([], 0, '', '')
3954+
3955+
setup_environment.run_bash_command('echo test')
3956+
3957+
call_kwargs = mock_run.call_args[1]
3958+
env = call_kwargs.get('env')
3959+
assert env is not None
3960+
# MSYS_NO_PATHCONV should be set
3961+
assert env.get('MSYS_NO_PATHCONV') == '1'
3962+
# Existing environment variables should be preserved
3963+
assert env.get('EXISTING_VAR') == 'existing_value'
3964+
assert env.get('PATH') == '/usr/bin'
3965+
3966+
@patch('scripts.setup_environment.sys.platform', 'win32')
3967+
@patch('setup_environment.find_bash_windows')
3968+
@patch('subprocess.run')
3969+
def test_tilde_expansion_still_works(
3970+
self, mock_run: MagicMock, mock_find_bash: MagicMock,
3971+
) -> None:
3972+
"""Verify that disabling path conversion does not break tilde expansion.
3973+
3974+
Tilde expansion (~) is handled by bash itself, not MSYS path conversion,
3975+
so it should continue to work when MSYS_NO_PATHCONV=1 is set.
3976+
"""
3977+
mock_find_bash.return_value = r'C:\Program Files\Git\bin\bash.exe'
3978+
mock_run.return_value = subprocess.CompletedProcess([], 0, '/c/Users/test', '')
3979+
3980+
result = setup_environment.run_bash_command('echo ~')
3981+
3982+
# The command should execute successfully
3983+
assert mock_run.called
3984+
assert result.returncode == 0
3985+
3986+
@patch('scripts.setup_environment.sys.platform', 'win32')
3987+
@patch('setup_environment.find_bash_windows')
3988+
@patch('subprocess.run')
3989+
def test_login_shell_with_msys_no_pathconv(
3990+
self, mock_run: MagicMock, mock_find_bash: MagicMock,
3991+
) -> None:
3992+
"""Verify MSYS_NO_PATHCONV is set even with login_shell=True."""
3993+
mock_find_bash.return_value = r'C:\Program Files\Git\bin\bash.exe'
3994+
mock_run.return_value = subprocess.CompletedProcess([], 0, '', '')
3995+
3996+
setup_environment.run_bash_command('echo test', login_shell=True)
3997+
3998+
call_kwargs = mock_run.call_args[1]
3999+
env = call_kwargs.get('env')
4000+
assert env is not None
4001+
assert env.get('MSYS_NO_PATHCONV') == '1'
4002+
4003+
@patch('scripts.setup_environment.sys.platform', 'win32')
4004+
@patch('setup_environment.find_bash_windows')
4005+
@patch('subprocess.run')
4006+
def test_capture_output_with_msys_no_pathconv(
4007+
self, mock_run: MagicMock, mock_find_bash: MagicMock,
4008+
) -> None:
4009+
"""Verify MSYS_NO_PATHCONV is set with capture_output=False."""
4010+
mock_find_bash.return_value = r'C:\Program Files\Git\bin\bash.exe'
4011+
mock_run.return_value = subprocess.CompletedProcess([], 0, '', '')
4012+
4013+
setup_environment.run_bash_command('echo test', capture_output=False)
4014+
4015+
call_kwargs = mock_run.call_args[1]
4016+
env = call_kwargs.get('env')
4017+
assert env is not None
4018+
assert env.get('MSYS_NO_PATHCONV') == '1'

0 commit comments

Comments
 (0)