diff --git a/tests/tools/test_file_operations.py b/tests/tools/test_file_operations.py index eda5140b0163..ba3375f8b8ac 100644 --- a/tests/tools/test_file_operations.py +++ b/tests/tools/test_file_operations.py @@ -467,6 +467,44 @@ class TestShellFileOpsHelpers: # Should be safely escaped assert result.count("'") >= 4 # wrapping + escaping + def test_escape_shell_arg_rewrites_windows_drive_paths_to_msys(self, monkeypatch, file_ops): + # bash eats backslashes and MSYS mangles ``C:\...``; the Git Bash + # ``/c/...`` form is the reliable one (reuses _windows_to_msys_path). + import tools.environments.local as local_mod + + monkeypatch.setattr(local_mod, "_IS_WINDOWS", True) + assert file_ops._escape_shell_arg(r"C:\Users\alice\notes.txt") == "'/c/Users/alice/notes.txt'" + # Non-drive paths are untouched. + assert file_ops._escape_shell_arg("/tmp/foo") == "'/tmp/foo'" + + def test_read_file_uses_bash_safe_windows_paths(self, mock_env, monkeypatch): + import tools.environments.local as local_mod + + monkeypatch.setattr(local_mod, "_IS_WINDOWS", True) + commands = [] + + def side_effect(command, **kwargs): + commands.append(command) + if command.startswith("wc -c"): + return {"output": "5\n", "returncode": 0} + if command.startswith("head -c"): + return {"output": "hello", "returncode": 0} + if command.startswith("sed -n"): + return {"output": "hello\n", "returncode": 0} + if command.startswith("wc -l"): + return {"output": "1\n", "returncode": 0} + return {"output": "", "returncode": 0} + + mock_env.execute.side_effect = side_effect + ops = ShellFileOperations(mock_env) + result = ops.read_file(r"C:\Users\alice\notes.txt") + + assert result.error is None + assert commands[0] == "wc -c < '/c/Users/alice/notes.txt' 2>/dev/null" + assert commands[1] == "head -c 1000 '/c/Users/alice/notes.txt' 2>/dev/null" + assert commands[2] == "sed -n '1,500p' '/c/Users/alice/notes.txt'" + assert commands[3] == "wc -l < '/c/Users/alice/notes.txt'" + def test_is_likely_binary_by_extension(self, file_ops): assert file_ops._is_likely_binary("photo.png") is True assert file_ops._is_likely_binary("data.db") is True diff --git a/tools/file_operations.py b/tools/file_operations.py index 35b60b0b7417..1a0227a41dff 100644 --- a/tools/file_operations.py +++ b/tools/file_operations.py @@ -958,7 +958,18 @@ class ShellFileOperations(FileOperations): return path def _escape_shell_arg(self, arg: str) -> str: - """Escape a string for safe use in shell commands.""" + """Escape a string for safe use in shell commands. + + On Windows a native drive path (``C:\\Users\\x``) is first rewritten to + the Git Bash / MSYS ``/c/Users/x`` form: bash eats the backslashes and + MSYS otherwise mangles ``C:\\...`` (the "Directory \\drivers\\etc does not + exist" class of failures). Reuses the env-layer translator so shell file + ops and the terminal ``cd`` agree on the path form. No-op off Windows and + for non-drive-qualified paths. + """ + from tools.environments.local import _windows_to_msys_path + + arg = _windows_to_msys_path(arg) # Use single quotes and escape any single quotes in the string return "'" + arg.replace("'", "'\"'\"'") + "'"