дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-08 19:02:17 +02:00
fix(terminal): keep Windows shell paths intact
The Shell path setting was split with POSIX rules, where a backslash escapes the next character, so C:\Windows\system32\cmd.exe reached the backend as C:Windowssystem32cmd.exe and every new terminal failed to start. On Windows a backslash is now a path separator, and an unquoted path up to its .exe/.cmd/.bat/.com is the program even with spaces, as the Installed Shells menu and Browse set it. Paths picked there are quoted when they contain whitespace. Fixes #1327
Цей коміт міститься в:
1 джерело
266e4eeb03
коміт
4eb48de1e4
4 files changed
+82
-5
No files matched your search
@@ -7,6 +7,7 @@
|
||||
import { FontPicker } from "../components/FontPicker";
|
||||
import { useEffect, useId, useRef, useState, type ReactNode } from "react";
|
||||
import clsx from "clsx";
|
||||
import { quotePath } from "@/platform/commandLine";
|
||||
import { commands, isTauri } from "@/platform/ipc";
|
||||
import {
|
||||
DEFAULT_EXTERNAL_TOOL,
|
||||
@@ -110,7 +111,7 @@ function ShellPathRow() {
|
||||
const { open: pick } = await import("@tauri-apps/plugin-dialog");
|
||||
const picked = await pick({ multiple: false, title: "Shell Path" });
|
||||
const path = Array.isArray(picked) ? picked[0] : picked;
|
||||
if (path) set(path);
|
||||
if (path) set(quotePath(path));
|
||||
};
|
||||
return (
|
||||
<Row
|
||||
@@ -142,7 +143,7 @@ function ShellPathRow() {
|
||||
anchor={menuRef}
|
||||
placement="bottom-end"
|
||||
aria-label="Installed shells"
|
||||
items={shells.map((p) => ({ id: p, label: p, checked: p === value, run: () => set(p) }))}
|
||||
items={shells.map((p) => ({ id: p, label: p, checked: quotePath(p) === value, run: () => set(quotePath(p)) }))}
|
||||
/>
|
||||
<IconButton
|
||||
icon="folder-open"
|
||||
|
||||
@@ -16,9 +16,9 @@ import { installTerminalPaste, pasteTerminalClipboard } from "./paste";
|
||||
import { ImeCommitGate } from "./imeCommit";
|
||||
import { ownTerminalInput } from "./imeInput";
|
||||
import { findTerminalLinks, resolvePath, type TerminalLink } from "./links";
|
||||
import { splitCommandLine } from "@/platform/commandLine";
|
||||
import { parseShellCommand } from "@/platform/commandLine";
|
||||
import { useProject } from "@/platform/project";
|
||||
import { useRemote } from "@/platform/remote";
|
||||
import { hostOs, useRemote } from "@/platform/remote";
|
||||
import { usePlatform } from "@/platform/platformInfo";
|
||||
import { workflowSettings, type TerminalSettingsValid } from "@/platform/workflowSettings";
|
||||
import { editorPrefs } from "@/platform/editorSettings";
|
||||
@@ -530,7 +530,7 @@ export class TerminalSession {
|
||||
};
|
||||
try {
|
||||
const t = workflowSettings().terminal;
|
||||
const [shellPath, ...shellArgs] = splitCommandLine(t.shellPath);
|
||||
const [shellPath, ...shellArgs] = parseShellCommand(t.shellPath, hostOs() === "windows");
|
||||
const shell = custom ? custom.program : shellPath;
|
||||
const args = custom ? custom.args : shellArgs;
|
||||
const info = await call(
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { parseShellCommand, quotePath } from "./commandLine";
|
||||
|
||||
describe("parseShellCommand", () => {
|
||||
it("keeps Windows paths as typed", () => {
|
||||
expect(parseShellCommand(String.raw`C:\Windows\system32\cmd.exe`, true)).toEqual([
|
||||
String.raw`C:\Windows\system32\cmd.exe`,
|
||||
]);
|
||||
expect(parseShellCommand(String.raw`C:\Windows\system32\cmd.exe /k C:\tools\init.bat`, true)).toEqual([
|
||||
String.raw`C:\Windows\system32\cmd.exe`,
|
||||
"/k",
|
||||
String.raw`C:\tools\init.bat`,
|
||||
]);
|
||||
});
|
||||
|
||||
it("takes an unquoted Windows path with spaces up to its executable", () => {
|
||||
expect(parseShellCommand(String.raw`C:\Program Files\PowerShell\7\pwsh.exe`, true)).toEqual([
|
||||
String.raw`C:\Program Files\PowerShell\7\pwsh.exe`,
|
||||
]);
|
||||
expect(parseShellCommand(String.raw`C:\Program Files\Git\bin\bash.EXE --login -i`, true)).toEqual([
|
||||
String.raw`C:\Program Files\Git\bin\bash.EXE`,
|
||||
"--login",
|
||||
"-i",
|
||||
]);
|
||||
});
|
||||
|
||||
it("honours quotes", () => {
|
||||
expect(parseShellCommand(String.raw`"C:\Program Files\Git\bin\bash.exe" --login`, true)).toEqual([
|
||||
String.raw`C:\Program Files\Git\bin\bash.exe`,
|
||||
"--login",
|
||||
]);
|
||||
expect(parseShellCommand(`/bin/zsh --login`, false)).toEqual(["/bin/zsh", "--login"]);
|
||||
expect(parseShellCommand(`"/Applications/My Shell/bin/fish" -l`, false)).toEqual([
|
||||
"/Applications/My Shell/bin/fish",
|
||||
"-l",
|
||||
]);
|
||||
expect(parseShellCommand(String.raw`/bin/sh -c a\ b`, false)).toEqual(["/bin/sh", "-c", "a b"]);
|
||||
});
|
||||
|
||||
it("is empty for an empty setting", () => {
|
||||
expect(parseShellCommand(" ", true)).toEqual([]);
|
||||
expect(parseShellCommand("", false)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("quotePath", () => {
|
||||
it("quotes only paths with whitespace", () => {
|
||||
expect(quotePath(String.raw`C:\Windows\system32\cmd.exe`)).toBe(String.raw`C:\Windows\system32\cmd.exe`);
|
||||
expect(quotePath(String.raw`C:\Program Files\PowerShell\7\pwsh.exe`)).toBe(
|
||||
String.raw`"C:\Program Files\PowerShell\7\pwsh.exe"`,
|
||||
);
|
||||
expect(parseShellCommand(quotePath("/Applications/My Shell/fish"), false)).toEqual(["/Applications/My Shell/fish"]);
|
||||
});
|
||||
});
|
||||
@@ -67,6 +67,28 @@ export function parseArgs(input: string, windowsPaths = false): string[] {
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* The program and arguments of a shell command line (Settings › Terminal › Shell path). On
|
||||
* Windows a backslash is a path separator, and an unquoted path up to its `.exe` / `.cmd` /
|
||||
* `.bat` / `.com` is the program even with spaces in it (`C:\Program Files\…\pwsh.exe -NoLogo`).
|
||||
*/
|
||||
export function parseShellCommand(line: string, windows: boolean): string[] {
|
||||
const text = line.trim();
|
||||
if (windows && !/["']/.test(text)) {
|
||||
const end = /\.(?:exe|cmd|bat|com)(?=\s|$)/i.exec(text);
|
||||
if (end) {
|
||||
const stop = end.index + end[0].length;
|
||||
return [text.slice(0, stop), ...parseArgs(text.slice(stop), true)];
|
||||
}
|
||||
}
|
||||
return parseArgs(text, windows);
|
||||
}
|
||||
|
||||
/** `path` as one word of a command line: quoted when it has whitespace. */
|
||||
export function quotePath(path: string): string {
|
||||
return /\s/.test(path) ? `"${path}"` : path;
|
||||
}
|
||||
|
||||
/** Inverse of `parseArgs` (quotes only when needed). */
|
||||
export function formatArgs(args: readonly string[]): string {
|
||||
return args.map((a) => (a === "" ? "''" : /[\s'"\\$`]/.test(a) ? `'${a.replace(/'/g, `'\\''`)}'` : a)).join(" ");
|
||||
|
||||
Reference in new issue
Заблокувати користувача