Files
Netcatty/domain/shellArgs.ts
陈大猫 008890a688 feat(terminal): 自定义本地 Shell 支持启动参数 (#1221) (#1225)
* feat(terminal): 支持为自定义本地 Shell 配置启动参数 (#1221)

自定义本地 Shell 此前只能填可执行文件路径,无法指定启动参数,
导致接入 msys2 bash 时缺少 `--login -i`,shell 不经 profile 初始化、
环境变量缺失而无法使用。

- TerminalSettings 新增 localShellArgs(string[],默认 []),随设置自动迁移
- 新增 domain/shellArgs:引号感知的命令行分词/回显格式化
- resolveShellSetting 透传自定义参数;参数为空时回退到 bridge 默认参数,
  命中已发现 shell(WSL/Git Bash)时忽略自定义参数,避免串味
- 自定义 Shell 弹窗新增「启动参数」输入,设置行展示完整命令(en/zh-CN/ru)

参数复用已有管线 session.localShellArgs → startLocalSession → pty.spawn,
不涉及云同步(与机器相关的 localShell 一致,均不同步)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(terminal): 修复自定义 Shell 参数的两处 review 问题

- 切换到默认/已发现 shell 时清空 localShellArgs:自定义参数仅对自定义
  路径有意义,清空可避免在发现列表尚未加载(启动竞态)或所选 shell ID
  在本机不可用时,旧参数被当作该 shell 的启动参数而泄漏导致启动失败
- formatShellArgs 对含双引号的参数改用单引号包裹,修复 `-c "..."`
  这类参数重新打开弹窗保存时被破坏的往返问题

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(terminal): shellArgs 量化处理含两种引号的参数往返

采用单引号优先的引号策略:单引号内全字面(Windows 路径、双引号原样保留,
无需转义);仅当 token 自身含单引号时改用双引号包裹并转义 \ 与 "。
解析端仅在双引号区内将 \" / \\ 视为转义,其余反斜杠保持字面,
确保未加引号的 Windows 路径不被破坏。修复 `echo "it's ok"` 这类同时含
单双引号的参数在弹窗重新保存时被破坏的问题。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(terminal): 自定义 Shell 已选中后可重新打开编辑参数

自定义 shell 选中时下拉框值已是 __custom__,再次点选「自定义…」不会触发
onValueChange,导致无法重新打开弹窗修改参数/路径。改为把自定义 shell 概要
做成可点击的编辑入口(铅笔图标),点击即重新填充草稿并打开弹窗;
打开逻辑抽到 openCustomShellModal 复用。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(terminal): shellArgs 改用 POSIX 单引号方案,修复尾部反斜杠与空参数

- 解析:两种引号区内均全字面(双引号不再把 \" 当转义),保留 Windows
  路径尾部反斜杠等手输入;引号外仅 \' 转义为字面单引号(支撑 '\'' 习语),
  其余反斜杠保持字面,未加引号的 Windows 路径不受影响
- 格式化:统一单引号包裹(内容全字面),内嵌单引号用 POSIX '\'' 习语;
  显式空参数输出为 '' 以免重新保存时被丢弃

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(terminal): customArgs take precedence over discovered shell defaults when user explicitly sets them

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 11:07:13 +08:00

73 lines
2.3 KiB
TypeScript

/**
* Tokenize a command-line argument string into discrete args, and format an
* arg array back into an editable string.
*
* Used by the custom local-shell config (#1221): the user types launch args
* like `--login -i` in a single field; we store them as a string[] that flows
* into `pty.spawn(shell, args)`.
*
* Quoting model (POSIX single-quote style, so format ⇄ parse round-trips):
* - Both quote types are fully literal inside their span — nothing is escaped.
* This keeps Windows paths (`C:\msys64\…`, even a trailing `\`) and embedded
* double quotes intact.
* - Outside quotes a backslash is literal too, EXCEPT `\'` which yields a
* literal single quote. That is the only escape, and it exists solely to
* support the POSIX `'\''` idiom that `formatShellArgs` emits for tokens that
* themselves contain a single quote. Every other backslash stays literal, so
* unquoted Windows paths survive.
*/
export function parseShellArgs(input: string): string[] {
const args: string[] = [];
let current = "";
let inToken = false;
let quote: '"' | "'" | null = null;
for (let i = 0; i < input.length; i++) {
const ch = input[i];
if (quote) {
if (ch === quote) quote = null;
else current += ch;
continue;
}
if (ch === "\\" && input[i + 1] === "'") {
current += "'";
inToken = true;
i++;
continue;
}
if (ch === '"' || ch === "'") {
quote = ch;
inToken = true;
continue;
}
if (/\s/.test(ch)) {
if (inToken) {
args.push(current);
current = "";
inToken = false;
}
continue;
}
current += ch;
inToken = true;
}
if (inToken) args.push(current);
return args;
}
/**
* Inverse of {@link parseShellArgs} for re-display in the editor. Single-quote
* quoting keeps the contents literal (Windows paths and double quotes need no
* escaping); an embedded single quote uses the POSIX `'\''` idiom. An explicit
* empty arg is emitted as `''` so it is not dropped on the next save.
*/
export function formatShellArgs(args: string[]): string {
return args
.map((arg) => {
if (arg === "") return "''";
if (!/[\s"']/.test(arg)) return arg;
return `'${arg.replace(/'/g, "'\\''")}'`;
})
.join(" ");
}