命令参数引号处理
本文档说明了 git-workflow 如何正确处理带引号和特殊字符的命令参数。
问题背景
在早期版本中,使用简单的字符串分割方式处理命令参数:
typescript
// ❌ 错误的方式
const [cmd, ...args] = command.split(" ");
spawn(cmd, args);这种方式无法正确处理引号:
javascript
'git tag -a "v1.5.3" -m "Release v1.5.3"'.split(" ");
// 结果: ["git", "tag", "-a", '"v1.5.3"', "-m", '"Release', 'v1.5.3"']
// 引号被当作参数的一部分!导致的问题:
- Tag 名称包含引号:
"v1.5.3"而不是v1.5.3 - Git 报错:
fatal: Failed to resolve '"v1.5.3"' as a valid ref.
解决方案
使用 shell: true 选项让 spawn 通过 shell 执行命令:
typescript
// ✅ 正确的方式
spawn(command, {
stdio: spinner ? "pipe" : "inherit",
shell: true, // 使用 shell 模式
});优势
- 正确处理引号:Shell 会自动解析和移除引号
- 支持特殊字符:emoji、中文、空格等都能正确处理
- 支持转义:
\"等转义字符正常工作
支持的场景
1. Tag 命令
bash
# 基本版本号
gw tag # 创建 v1.5.3
# 预发布版本
gw tag # 创建 v1.0.0-beta.1
# 带特殊字符
git tag -a "v1.0.0-🎉" -m "Release 🎉"2. Branch 命令
bash
# 带日期和描述的分支
gw b feature # 创建 feature/20240120-123-add-feature
# 删除带特殊字符的分支
git branch -D "feature/20240120-123-add-feature"
git push origin --delete "feature/20240120-123-add-feature"3. Stash 命令
bash
# 带中文的 stash 消息
git stash push -m "临时保存:修复登录bug"
# 带引号的消息
git stash push -m "WIP: 添加\"新功能\""
# 从 stash 创建分支
git stash branch "feature/from-stash" stash@{0}4. Commit 命令
bash
# 带特殊字符的提交消息
git commit -m "feat: add \"quotes\" support"
git commit -m "fix: 修复登录问题 🐛"测试覆盖
我们创建了全面的测试用例来确保引号处理的正确性:
测试场景
基本引号处理
- 带引号的 tag 名称
- 带空格的分支名称
- 带特殊字符的 commit message
特殊字符支持
- Emoji:
v1.0.0-🎉 - 中文:
临时保存:修复bug - 转义引号:
feat: add \"quotes\" support
- Emoji:
错误处理
- 捕获 stderr 错误信息
- 显示详细的失败原因
- 提供解决建议
运行测试
bash
# 运行引号处理测试
npm test -- tests/command-with-quotes.test.ts
# 运行所有测试
npm test错误信息改进
现在当命令失败时,会显示详细的错误信息:
Tag 已存在
✗ Tag v1.5.3 已存在
提示: 如需重新创建,请先删除旧 tag:
git tag -d v1.5.3
git push origin --delete v1.5.3没有提交
✗ 当前仓库没有任何提交
提示: 需要先创建至少一个提交才能打 tag:
git add .
git commit -m "Initial commit"
gw tagGit 命令错误
✗ tag 创建失败
fatal: Failed to resolve 'HEAD' as a valid ref.实现细节
execAsync 函数
typescript
export function execAsync(
command: string,
spinner?: Ora,
): Promise<{ success: boolean; error?: string }> {
return new Promise((resolve) => {
const process = spawn(command, {
stdio: spinner ? "pipe" : "inherit",
shell: true, // 关键:使用 shell 模式
});
let errorOutput = "";
// 捕获错误输出
if (process.stderr) {
process.stderr.on("data", (data) => {
errorOutput += data.toString();
});
}
process.on("close", (code) => {
if (code === 0) {
resolve({ success: true });
} else {
resolve({ success: false, error: errorOutput.trim() });
}
});
process.on("error", (err) => {
resolve({ success: false, error: err.message });
});
});
}execWithSpinner 函数
typescript
export async function execWithSpinner(
command: string,
spinner: Ora,
successMessage?: string,
errorMessage?: string,
): Promise<boolean> {
const result = await execAsync(command, spinner);
if (result.success) {
if (successMessage) {
spinner.succeed(successMessage);
} else {
spinner.succeed();
}
} else {
if (errorMessage) {
spinner.fail(errorMessage);
} else {
spinner.fail();
}
// 显示具体的错误信息
if (result.error) {
console.log(colors.dim(` ${result.error}`));
}
}
return result.success;
}最佳实践
1. 始终使用引号包裹可能包含特殊字符的参数
typescript
// ✅ 推荐
await execAsync(`git tag -a "${tagName}" -m "Release ${tagName}"`);
// ❌ 不推荐(如果 tagName 包含空格会失败)
await execAsync(`git tag -a ${tagName} -m Release ${tagName}`);2. 转义用户输入中的引号
typescript
// ✅ 正确处理用户输入
const message = userInput.replace(/"/g, '\\"');
await execAsync(`git commit -m "${message}"`);3. 使用 execWithSpinner 显示进度
typescript
// ✅ 推荐:显示进度和错误信息
const spinner = ora("正在创建 tag...").start();
const success = await execWithSpinner(
`git tag -a "${tagName}" -m "Release ${tagName}"`,
spinner,
"Tag 创建成功",
"Tag 创建失败",
);
if (!success) {
// 错误信息已自动显示
return;
}相关文件
src/utils.ts- execAsync 和 execWithSpinner 实现tests/command-with-quotes.test.ts- 引号处理测试src/commands/tag.ts- Tag 命令实现src/commands/branch.ts- Branch 命令实现src/commands/stash.ts- Stash 命令实现
版本历史
- v0.4.5 - 修复引号处理问题,添加详细错误信息
- v0.4.4 - 修复 spinner 阻塞问题
- v0.4.3 - 添加 execAsync 和 execWithSpinner 函数