feat(shell-tool): honor user $SHELL on POSIX

This commit is contained in:
matevip 2026-05-02 15:40:38 +08:00
parent 2cd51743d1
commit 82ed239b5b

View File

@ -11,9 +11,11 @@ import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
/**
* 内置工具本地命令执行跨平台
@ -130,7 +132,10 @@ public class ShellExecuteTool {
* Windows: cmd.exe /D /S /C "command"
* /D 禁用 AutoRun 注册表项避免副作用
* /S 保留引号原样传递给命令
* Unix: /bin/sh -c command
* Unix: $SHELL -c command (RFC-03 Lane A1, fixes QwenPaw #3767)
* honors the user's interactive shell so alias resolution / PATH
* from the calling environment still apply; falls back to /bin/sh
* when $SHELL is unset or points at a non-executable path.
*/
private static ProcessBuilder buildShellProcess(String command) {
ProcessBuilder pb;
@ -138,7 +143,8 @@ public class ShellExecuteTool {
String winCommand = sanitizeWindowsCommand(command);
pb = new ProcessBuilder("cmd.exe", "/D", "/S", "/C", winCommand);
} else {
pb = new ProcessBuilder("/bin/sh", "-c", command);
String shell = selectPosixShell(System.getenv("SHELL"));
pb = new ProcessBuilder(shell, "-c", command);
}
// 设置工作区活动目录
@ -181,6 +187,49 @@ public class ShellExecuteTool {
return command.replace("\r\n", " ").replace("\n", " ");
}
/**
* RFC-03 Lane A1 pick the POSIX shell binary to invoke for a non-Windows
* tool call (fixes QwenPaw #3767).
*
* <p>Returns {@code userShellEnv} verbatim when:
* <ul>
* <li>the value is non-blank,</li>
* <li>parses as a valid path,</li>
* <li>and the resolved binary is executable.</li>
* </ul>
*
* <p>Otherwise falls back to {@code /bin/sh} the legacy hardcoded
* default. Important: {@code /bin/sh} on Debian/Ubuntu is dash, which
* does NOT honor users' bash-isms; the whole point of this method is
* to prefer the user's actual interactive shell when one is configured.
*/
static String selectPosixShell(String userShellEnv) {
return selectPosixShell(userShellEnv, Files::isExecutable);
}
/**
* Test seam same logic as {@link #selectPosixShell(String)} but with
* an injectable executable check so unit tests can drive every branch
* without depending on which shells actually exist on the test runner
* (Windows CI has no {@code /bin/sh}, POSIX dev hosts have varying
* shells installed). Production callers go through the single-arg
* overload above.
*/
static String selectPosixShell(String userShellEnv, Predicate<Path> executableCheck) {
if (userShellEnv == null || userShellEnv.isBlank()) {
return "/bin/sh";
}
try {
Path candidate = Path.of(userShellEnv);
if (executableCheck.test(candidate)) {
return userShellEnv;
}
} catch (InvalidPathException ignored) {
// Some exotic $SHELL value that isn't a path fall through to default.
}
return "/bin/sh";
}
/**
* 修复 LLM 常见的 Windows 命令转义问题
* LLM 有时会产生 bash 风格的反斜杠转义引号 (\"