首页
/ Gemini CLI 自定义命令实战指南:用 TOML 打造带参数注入与 Shell 执行的提示词快捷键

Gemini CLI 自定义命令实战指南:用 TOML 打造带参数注入与 Shell 执行的提示词快捷键

2026-09-06 09:53:23作者:沈韬淼Beryl

本文基于 gemini-cli 官方文档 custom-commands.md 展开,系统讲解 Gemini CLI 自定义命令(Custom commands)的完整机制:命令文件的存放位置与优先级、命名空间规则、TOML 文件格式,以及 {{args}} 参数注入、!{...} Shell 命令执行、@{...} 文件内容注入三种动态化手段。读完本文,你既能按步骤创建、热加载可复用的项目级/全局命令,也能从 FileCommandLoader 与 prompt-processors 源码层理解每条注入语法的实际执行链路和安全边界。

核心概念:把高频提示词变成可复用命令

自定义命令让你把最喜欢、最常用的提示词保存为个人快捷键。命令可以只服务于某一个项目,也可以全局可用并跨所有项目生效,从而简化工作流、保证团队内行为一致。

命令的最终形态是一个斜杠命令:输入 /命令名 后,CLI 会把你在 TOML 文件中定义的提示词发送给 Gemini 模型。整个机制由 packages/cli/src/services/FileCommandLoader.ts 中的 FileCommandLoader 类驱动,它负责递归扫描命令目录、解析并校验 TOML 文件,并将合法的定义适配为可执行的 SlashCommand 对象。

文件位置与优先级

Gemini CLI 从以下位置发现命令,并按固定顺序加载:

  1. 用户命令(全局):位于 ~/.gemini/commands/,在你工作的任何项目中都可用;
  2. 项目命令(本地):位于 <your-project-root>/.gemini/commands/,仅对当前项目生效,可以提交到版本控制与团队共享。

如果项目目录下的命令与用户目录下的命令同名,项目命令始终生效。这允许项目用本地版本覆盖全局命令。

这两个路径在源码中有明确定义:packages/core/src/config/storage.tsStorage.getUserCommandsDir()(L97)与 getProjectCommandsDir()(L297)分别返回用户与项目命令目录;对应的单元测试位于 packages/core/src/config/storage.test.ts

从源码结构看,优先级的实现方式是"加载顺序 + 后者胜出":FileCommandLoader.tsgetCommandDirectories()(L209-L248)按 用户目录 → 项目目录 → 扩展目录 的顺序返回命令目录,加载方法 loadCommands() 的注释也写明 user/project 命令(无 extensionName 的)采用 "last wins" 策略,而扩展命令在冲突时会被改名以避免覆盖。名称冲突的协调工作最终委托给 packages/cli/src/services/SlashCommandResolver.tsCommandService。此外还有两个源码细节值得注意:

  • 若当前工作目录就是用户主目录(storage.isWorkspaceHomeDir()),会跳过项目命令目录,避免与用户命令目录重合产生虚假的冲突告警(见 FileCommandLoader.ts L221-L228);
  • 若开启了文件夹信任(folder trust)且当前目录不受信,loadCommands() 会直接返回空列表,即不受信目录下不加载任何自定义命令(L96-L98)。

命名与命名空间

命令名由文件路径相对于其 commands 目录的相对路径决定。子目录用于创建带命名空间的命令,路径分隔符(/\)会被转换为冒号(:):

  • ~/.gemini/commands/test.toml 中的文件 → 命令 /test
  • <project>/.gemini/commands/git/commit.toml 中的文件 → 命名空间命令 /git:commit

源码中这段转换逻辑在 parseAndAdaptFile()FileCommandLoader.ts L302-L320)中,官方文档没有细说的规则有两点:

  • 每个路径段会先做清洗:segment.replace(/[^a-zA-Z0-9_\-.]/g, '_'),即字母、数字、_-. 之外的字符一律替换为下划线。由于 : 是命名空间分隔符,这一步保证了文件名中的冒号不会引发命名冲突;
  • 单段清洗后超过 50 个字符会被截断为前 47 个字符加 ...,防止 UI 溢出。

创建或修改 .toml 命令文件后,运行 /commands reload 可在不重启 CLI 的情况下重新加载;运行 /commands list 可查看全部可用命令文件。这两个内置子命令的实现位于 packages/cli/src/ui/commands/commandsCommand.ts/commands list 调用 FileCommandLoader.listAvailableFiles() 按 User / Project / Extension 分组列出所有 .toml 文件;/commands reload 调用 context.ui.reloadCommands() 触发完整的命令再发现(包括用户/项目 TOML、MCP prompts 与扩展命令),reload 还有别名 refresh

TOML 文件格式(v1)

命令定义文件必须使用 TOML 格式、以 .toml 为扩展名。加载器用 @iarna/toml 解析,并用 Zod 做校验,其 Schema 就是字段定义的唯一事实来源(FileCommandLoader.ts L55-L61):

const TomlCommandDefSchema = z.object({
  prompt: z.string({
    required_error: "The 'prompt' field is required.",
    invalid_type_error: "The 'prompt' field must be a string.",
  }),
  description: z.string().optional(),
});

必填字段

  • prompt(String):命令执行时发送给 Gemini 模型的提示词,可以是单行或多行字符串。

可选字段

  • description(String):一行简短描述命令的作用,会显示在 /help 菜单中你的命令旁边。如果省略该字段,会基于文件名生成一段通用描述——源码中的兜底文案是 `Custom command from ${path.basename(filePath)}`FileCommandLoader.ts L323-L326),并且最终描述会被 sanitizeForDisplay() 截断到 100 字符以内;来自扩展的命令还会在描述前加上 [扩展名] 前缀。

校验不通过的文件会被跳过,同时通过 coreEvents.emitFeedback 输出一条 [FileCommandLoader] Skipping invalid command file 的错误反馈,方便排查(L289-L298)。解析失败的单测覆盖可参见 packages/cli/src/services/FileCommandLoader.test.ts

参数处理:三种注入语法

自定义命令支持强大的参数处理机制。CLI 会根据 prompt 的内容自动选择正确的处理方式。在 packages/cli/src/services/prompt-processors/types.ts 中,三种语法的触发字符串被集中定义为常量:SHORTHAND_ARGS_PLACEHOLDER = '{{args}}'SHELL_INJECTION_TRIGGER = '!{'AT_FILE_INJECTION_TRIGGER = '@{'

FileCommandLoader 在适配每个命令时,会按固定顺序组装一条"提示词处理流水线"(prompt processor pipeline,L332-L358):

  1. 若 prompt 含 @{...},先加 AtFileProcessor(注释写明 "Security First":先注入文件内容,避免执行可能动态生成恶意 @ 路径的 shell 命令);
  2. 若含 !{...}{{args}},加 ShellProcessor
  3. 不含 {{args}},最后加 DefaultArgumentProcessor

这个顺序与下文"文件注入先于 shell 执行、shell 执行先于参数追加"的处理次序一致。

1. {{args}} 的上下文感知注入

prompt 包含占位符 {{args}} 时,CLI 会把用户在命令名后输入的文本替换到该占位符处。替换行为取决于使用位置:

A. 原始注入(shell 命令之外)

用在提示词主体中时,参数按用户原样注入。

示例(git/fix.toml):

# Invoked via: /git:fix "Button is misaligned"

description = "Generates a fix for a given issue."
prompt = "Please provide a code fix for the issue described here: {{args}}."

模型最终收到: Please provide a code fix for the issue described here: "Button is misaligned".

B. 在 shell 命令中使用参数(!{...} 块内)

在 shell 注入块(!{...})内使用 {{args}} 时,参数会被自动 shell 转义后再替换。这样可以把参数安全地传给 shell 命令,保证生成的命令语法正确、安全,防止命令注入漏洞。

示例(/grep-code.toml):

prompt = """
Please summarize the findings for the pattern `{{args}}`.

Search Results:
!{grep -r {{args}} .}
"""

执行 /grep-code It's complicated 时的流程:

  1. CLI 看到 {{args}} 同时出现在 !{...} 之外和之内;
  2. 外面:第一个 {{args}} 被原始替换为 It's complicated
  3. 里面:第二个 {{args}} 被替换为转义后的版本(例如 Linux 下为 "It\'s complicated");
  4. 实际执行的命令是 grep -r "It's complicated" .
  5. CLI 会弹出确认框,让你确认这条精确且安全的命令;
  6. 最终提示词发出。

这一行为在 packages/cli/src/services/prompt-processors/shellProcessor.ts 中逐段可见:

  • 块外文本用 segment.replaceAll(SHORTHAND_ARGS_PLACEHOLDER, userArgsRaw) 做原始替换(L155-L161、L208-L213);
  • 块内命令用 escapeShellArg(userArgsRaw, shell) 生成转义参数(L97-L98),再 command.replaceAll(..., userArgsEscaped) 完成替换(L108-L111);
  • 若命令中没有 shell 触发符或没有闭合的注入块,则退化为纯 {{args}} 原始替换(L71-L75、L90-L95)。相关行为有专门测试覆盖,见 shellProcessor.test.ts

2. 默认参数处理(不含 {{args}} 时)

如果 prompt 没有 {{args}} 占位符,CLI 使用默认行为:

  • 提供了参数(如 /mycommand arg1):CLI 把完整输入的命令追加到提示词末尾,中间以两个换行分隔,让模型同时看到原始指令和你刚提供的参数;
  • 未提供任何参数(如 /mycommand):提示词原样发出,不做任何追加。

对应实现是 packages/cli/src/services/prompt-processors/argumentProcessor.ts 中的 DefaultArgumentProcessor:当 context.invocation?.args 存在时,调用 appendToLastTextPart(prompt, context.invocation.raw) 把原始调用文本追加到最后一段文本上,并明确注释"该处理器仅在 prompt 不含 {{args}} 时使用"。

示例(changelog.toml):下面这个例子展示了如何定义模型角色、说明用户输入的位置,并规定期望的格式与行为,从而构建一个健壮命令。

# In: <project>/.gemini/commands/changelog.toml
# Invoked via: /changelog 1.2.0 added "Support for default argument parsing."

description = "Adds a new entry to the project's CHANGELOG.md file."
prompt = """
# Task: Update Changelog

You are an expert maintainer of this software project. A user has invoked a command to add a new entry to the changelog.

**The user's raw command is appended below your instructions.**

Your task is to parse the `<version>`, `<change_type>`, and `<message>` from their input and use the `write_file` tool to correctly update the `CHANGELOG.md` file.

## Expected Format
The command follows this format: `/changelog <version> <type> <message>`
- `<type>` must be one of: "added", "changed", "fixed", "removed".

## Behavior
1. Read the `CHANGELOG.md` file.
2. Find the section for the specified `<version>`.
3. Add the `<message>` under the correct `<type>` heading.
4. If the version or type section doesn't exist, create it.
5. Adhere strictly to the "Keep a Changelog" format.
"""

执行 /changelog 1.2.0 added "New feature" 时,发给模型的最终文本就是上面的原始 prompt,加上两个换行,再加上你输入的完整命令。

3. 用 !{...} 执行 shell 命令

你可以在 prompt 中直接执行 shell 命令并注入其输出,让命令变得动态化,非常适合从本地环境收集上下文——例如读取文件内容、检查 Git 状态。

当自定义命令尝试执行 shell 命令时,Gemini CLI 会先征求你的确认。这是安全措施,确保只有预期内的命令才能被执行。

工作原理:

  1. 注入命令:使用 !{...} 语法;
  2. 参数替换:块内如有 {{args}},自动 shell 转义(见上文第 1 节 B 部分);
  3. 健壮解析:解析器能正确处理包含嵌套花括号的复杂 shell 命令,例如 JSON 载荷。!{...} 内的内容必须花括号配对({})。要执行含未配对花括号的命令,建议把命令包进外部脚本文件,再在 !{...} 块内调用该脚本;
  4. 安全检查与确认:CLI 对最终解析出的命令(参数转义替换之后)执行安全检查,弹窗展示将要执行的精确命令;
  5. 执行与错误报告:命令被执行。若命令失败,注入提示词的内容会包含错误信息(stderr)加一行状态,例如 [Shell command exited with code 1],帮助模型理解失败上下文。

与上述步骤一一对应的源码逻辑在 shellProcessor.ts

  • 嵌套花括号由 injectionParser.ts 中的 extractInjections() 处理,它用简单的花括号计数匹配闭合位置,不支持转义;遇到未闭合的注入块会抛出 Unclosed injection 错误;
  • 安全检查通过策略引擎完成:config.getPolicyEngine().check({ name: 'run_shell_command', args: { command } })(L127-L133)。结果为 DENY 时直接抛错 Blocked command ... Blocked by policy;结果为 ASK_USER 时把命令收集起来,最终抛出 ConfirmationRequiredError 交由 UI 层弹确认框(L135-L150);
  • 已被本次会话 allowlist 放行过的命令会跳过确认(L122-L124);
  • 执行走 ShellExecutionService.execute(),失败时按情况追加 [Shell command '...' exited with code N][... aborted][... terminated by signal ...] 状态行(L180-L202)。

示例(git/commit.toml):该命令取暂存的 git diff,让模型据此写提交信息。

# In: <project>/.gemini/commands/git/commit.toml
# Invoked via: /git:commit

description = "Generates a Git commit message based on staged changes."

# The prompt uses !{...} to execute the command and inject its output.
prompt = """
Please generate a Conventional Commit message based on the following git diff:

```diff
!{git diff --staged}
```

"""

执行 /git:commit 时,CLI 先运行 git diff --staged,再在把最终完整提示词发给模型之前,用该命令的输出替换 !{git diff --staged}

4. 用 @{...} 注入文件内容

可以使用 @{...} 语法把文件或目录列表的内容直接嵌入提示词,非常适合编写针对特定文件操作的命令。

工作原理:

  • 文件注入@{path/to/file.txt} 被替换为 file.txt 的内容;
  • 多模态支持:路径指向受支持的图片(如 PNG、JPEG)、PDF、音频或视频文件时,会正确编码并以多模态输入注入。其他二进制文件会被妥善处理并跳过;
  • 目录列表@{path/to/dir} 会遍历目录,把该目录及所有子目录中的文件插入提示词,并遵循启用时的 .gitignore.geminiignore 规则;
  • 工作区感知:命令会在当前目录及其他工作区目录中查找路径。工作区内的绝对路径是允许的;
  • 处理顺序@{...} 的文件注入在 shell 命令(!{...})和参数替换({{args}}之前处理;
  • 解析:解析器要求 @{...} 内的路径花括号配对({})。

实现位于 packages/cli/src/services/prompt-processors/atFileProcessor.ts:每个注入块通过 readPathFromWorkspace() 从工作区解析路径并返回多模态 parts;若文件被 .gitignore/.geminiignore 忽略而未包含,UI 会提示一条 info 消息;若注入失败,会在 UI 中报 error,并保留原始占位符在提示词中(而不是静默丢弃),该行为有 atFileProcessor.test.ts 的测试佐证。

示例(review.toml):该命令注入一份固定的最佳实践文件(docs/best-practices.md),并用用户参数提供评审上下文。

# In: <project>/.gemini/commands/review.toml
# Invoked via: /review FileCommandLoader.ts

description = "Reviews the provided context using a best practice guide."
prompt = """
You are an expert code reviewer.

Your task is to review {{args}}.

Use the following best practices when providing your review:

@{docs/best-practices.md}
"""

执行 /review FileCommandLoader.ts 时,@{docs/best-practices.md} 占位符先被该文件内容替换,{{args}} 再被你的输入替换,最终提示词才发给模型。

完整示例:一个"纯函数"重构命令

下面创建一个全局命令,让模型把代码重构成纯函数。

1. 创建目录和文件

先确保用户命令目录存在,再创建 refactor 子目录用于组织,最后创建 TOML 文件。

macOS / Linux:

mkdir -p ~/.gemini/commands/refactor
touch ~/.gemini/commands/refactor/pure.toml

Windows(PowerShell):

New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\.gemini\commands\refactor"
New-Item -ItemType File -Force -Path "$env:USERPROFILE\.gemini\commands\refactor\pure.toml"

2. 写入文件内容

用编辑器打开 ~/.gemini/commands/refactor/pure.toml,加入以下内容。最佳实践是包含可选的 description 字段。

# In: ~/.gemini/commands/refactor/pure.toml
# This command will be invoked via: /refactor:pure

description = "Asks the model to refactor the current context into a pure function."

prompt = """
Please analyze the code I've provided in the current context.
Refactor it into a pure function.

Your response should include:
1. The refactored, pure function code block.
2. A brief explanation of the key changes you made and why they contribute to purity.
"""

3. 运行命令

完成。现在可以在 CLI 中运行该命令了。先往上下文里添加一个文件,再调用命令:

> @my-messy-function.js
> /refactor:pure

Gemini CLI 随后就会执行你在 TOML 文件中定义的多行提示词。如果创建或修改文件后命令没有出现,输入 /commands list 确认文件已被扫描到,再执行 /commands reload 热加载。

小结

主题 关键点 源码位置
命令发现 用户 ~/.gemini/commands/ → 项目 .gemini/commands/ → 扩展,glob 递归扫描 **/*.toml FileCommandLoader.ts
优先级 同名时项目命令覆盖用户命令(last wins,由 resolver 协调) SlashCommandResolver.tsCommandService.ts
字段校验 Zod Schema:prompt 必填字符串、description 可选 FileCommandLoader.ts L55-L61
命名清洗 非法字符替换为 _,单段超 50 字符截断,分隔符转 : FileCommandLoader.ts L307-L320
{{args}} 块外原始注入、!{...} 内 shell 转义注入 shellProcessor.ts
默认参数 {{args}} 时把完整原始调用追加到提示词末尾 argumentProcessor.ts
!{...} 嵌套花括号解析、策略引擎检查、用户确认、退出码状态行 injectionParser.tsshellProcessor.ts
@{...} 文件/目录/多模态注入,工作区感知,先于 shell 与参数处理 atFileProcessor.ts
管理命令 /commands list/commands reload(别名 refresh commandsCommand.ts

掌握以上内容后,你可以把团队的高频工作流(写 changelog、生成提交信息、按规范评审代码等)沉淀为可版本控制、可热加载、且参数经过安全转义的斜杠命令,显著降低重复提示词的成本并统一团队的使用方式。

登录后查看全文
热门项目推荐
相关项目推荐