在实际工程中,凡是需要向用户或调用方暴露“可输入指令”的模块,无论是命令行工具、运维机器人、自动化平台,还是游戏服务器管理后台,本质上都在做同一件事:把一段连续文本变成一次可执行的方法调用。泰坦天降系列自定义指令指南 v1.0 整理的就是这样一套通用实现思路,核心不是某个具体框架的 API,而是指令注册、参数解析、校验、执行、帮助生成和错误恢复这一整条链路。读完这篇内容,你能独立实现一个最小可运行的指令框架,知道每条指令从输入到执行经过了哪些环节,也能在出现“指令不生效”“参数解析报错”“执行器抛异常”时按顺序找到问题。
这套方案在本地学习环境中使用 Python 标准库即可跑通,不依赖第三方包。进入生产环境后,再在协议接入、权限控制、日志审计和配置外置化等方向逐层补强。下面先从最关键的几个设计问题开始。
1. 先理解自定义指令系统的核心设计问题
1.1 一条指令从输入到执行的完整链路
自定义指令系统的输入通常是一行字符串。比如:
bash复制greet --name "Alice" --times 3
这行字符串不能直接传给执行函数,必须经过拆词、识别指令名、参数解析、类型转换和参数校验。完整链路如下:
- 拆词:根据空格和引号规则,把整行字符串拆成 token 列表。
- 识别指令名:取第一个 token,去注册表里查找对应的指令规格。
- 解析参数:根据指令规格定义的参数列表,把剩余 token 映射成函数所需的字典。
- 类型转换:把字符串形式的参数值转换为 int、float、bool 等类型。
- 参数校验:检查必填参数是否齐全,类型转换是否成功。
- 执行指令:调用指令真正的 handler 函数,得到返回结果。
- 结果输出:把返回值格式化后交给上层打印或发送。
- 错误恢复:任何一步失败都要转换成统一的错误信息,不能直接让进程崩溃。
很多自建指令系统只实现了第 1、6 两步,遇到引号参数、重复执行失败、未知指令等问题就开始在调用方补零散判断。更稳妥的做法是从一开始就把每一步拆成独立函数,这样每一层都可以单独测试,也方便后续扩展。
1.2 先定义最小指令模型,再讨论扩展
指令模型是注册表、解析器和执行器共同依赖的数据结构。最小模型至少包含指令名、描述、参数列表和执行函数。
python复制# cmd_framework/model.py
from dataclasses import dataclass, field
from typing import Any, Callable, List, Optional
@dataclass
class ParamSpec:
name: str
type: type = str
default: Any = None
required: bool = False
position: Optional[int] = None
help: str = ""
@dataclass
class CommandSpec:
name: str
description: str
params: List[ParamSpec] = field(default_factory=list)
handler: Optional[Callable[..., Any]] = None
aliases: List[str] = field(default_factory=list)
ParamSpec 中的 position 用来标记位置参数。例如 math-add 2 5 中,2 是第 0 个位置参数,5 是第 1 个位置参数。default 表示可选参数的默认值,required 表示必填。参数若不是位置参数,就通过 --name value 或 --name=value 传入。
这个模型的宽度刚好覆盖大多数自定义指令需求。后续如果要支持子命令、标签标记、权限等级,可以在 CommandSpec 上继续加字段,而不需要推翻解析器。
1.3 为什么不能用简单的 split 处理参数
最常见的错误是用 raw.split(" ") 拆参数。遇到 --name "Alice Smith" 时,Alice Smith 会被拆成两个 token,执行函数的 name 参数变成 Alice,多余参数又可能报错。
标准库 shlex 是更好的选择,它能把带引号的内容作为一个整体处理,也支持基本的转义。以下对比可以说明问题:
python复制import shlex
line = 'greet --name "Alice Smith" --times 3'
print(line.split(" "))
# ['greet', '--name', '"Alice', 'Smith"', '--times', '3'],明显错误
print(shlex.split(line))
# ['greet', '--name', 'Alice Smith', '--times', '3']
空参数、中文参数、带空格路径、带反斜杠内容,都能通过 shlex 获得稳定拆词结果。自定义指令框架的解析器应该以 token 列表为输入,不要在解析器内部再去切分原始字符串。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备与项目结构
2.1 Python 环境与版本建议
本文示例基于 Python 3.8+,因为用到了 dataclasses 中的 field(default_factory=list),该特性在 Python 3.7 开始可用。开发环境建议使用 Python 3.10 或更高版本,只是在运行示范时需要确认 python3 指向的版本。
| 环境项 | 学习环境建议 | 生产环境建议 |
|---|---|---|
| Python 版本 | 3.8 以上即可 | 3.10 或 3.11 LTS 版本 |
| 第三方依赖 | 无,仅标准库 | 根据接入方式确定,如 FastAPI、Redis 等 |
| 操作系统 | Windows / Linux / macOS 均可 | 优先 Linux 容器或云主机 |
| 测试工具 | unittest 或 pytest |
推荐 pytest,结合 CI 流水线 |
| 日志 | 直接 print 演示 | 接入 logging,输出到文件或集中日志平台 |
如果原始项目没有指定 Python 版本,落地前需要先确认环境里实际存在哪个版本。命令行工具最容易出问题是“本地能跑,服务器跑不起来”,多数原因是版本差异或路径差异。
2.2 项目目录结构要按职责分层
指令框架不建议把所有代码放一个文件。目录结构按职责分成模型、解析器、注册表、执行器和应用入口。
text复制cmd_demo/
├── cmd_framework/
│ ├── __init__.py
│ ├── model.py
│ ├── parser.py
│ ├── registry.py
│ └── executor.py
├── commands/
│ └── builtin.py
├── app.py
└── tests/
├── test_parser.py
└── test_commands.py
cmd_framework:指令框架本身,与具体业务指令无关。commands:注册具体业务指令,例如greet、math-add。app.py:启动入口,支持交互模式和单次执行模式。tests:负责验证解析器和执行器的行为,避免后续改动破坏已有功能。
这种分层最大的好处是新增指令时不需要改动框架代码,只要在 commands 下新增注册函数即可。框架层一旦稳定,就可以作为共享基础库被多个项目复用。
2.3 最小依赖原则
自定义指令系统最怕依赖膨胀。每引一个第三方库,就多一个版本冲突来源。本文的示例完全使用标准库完成,学习者可以先把核心机制跑通,不急着引入 argparse 或 click。
argparse、click 这些工具本身很成熟,但它们适合“参数结构固定”的 CLI 工具,而自定义指令场景往往需要运行时动态注册指令,并允许调用方自行扩展。自行实现解析器虽然多写一些代码,却可以让解析逻辑完全受控,也能更清楚地展示参数如何在指令规格、解析器和执行器之间流动。
3. 实现一个可扩展的自定义指令框架
3.1 注册表:让指令可动态登记和查找
注册表负责保存指令规格,并提供按名字查找、遍历全部指令的能力。为了避免注册重复指令导致后续行为不可预期,注册表默认拒绝覆盖同名指令。
python复制# cmd_framework/registry.py
from typing import Dict, List, Optional
from .model import CommandSpec
class CommandRegistry:
def __init__(self, allow_overwrite: bool = False):
self._commands: Dict[str, CommandSpec] = {}
self._aliases: Dict[str, str] = {}
self.allow_overwrite = allow_overwrite
def register(self, spec: CommandSpec) -> None:
if spec.name in self._commands and not self.allow_overwrite:
raise RuntimeError(f"指令已存在:{spec.name}")
self._commands[spec.name] = spec
for alias in spec.aliases:
if alias in self._commands:
raise RuntimeError(f"别名与指令名冲突:{alias}")
self._aliases[alias] = spec.name
def lookup(self, name: str) -> Optional[CommandSpec]:
if name in self._commands:
return self._commands[name]
real_name = self._aliases.get(name)
if real_name:
return self._commands.get(real_name)
return None
def all_commands(self) -> List[CommandSpec]:
return list(self._commands.values())
注册表需要同时维护指令名和别名。别名只做查询映射,不参与遍历。allow_overwrite 用于测试环境或热更新场景,生产环境建议保持默认 False,避免巡检时发现有两个模块注册同一个指令。
3.2 解析器:支持长选项、位置参数和布尔标志
解析器的输入是 token 列表,输出是符合 CommandSpec 定义的参数字典。这里只支持 --key value、--key=value、位置参数和 -- 分隔符,不实现短选项,因为短选项更容易引起歧义。
python复制# cmd_framework/parser.py
from typing import Any, Dict, List
from .model import CommandSpec, ParamSpec
_TRUE_SET = {"true", "1", "yes", "on"}
_FALSE_SET = {"false", "0", "no", "off"}
def find_param(spec: CommandSpec, key: str) -> ParamSpec:
for param in spec.params:
if param.name == key:
return param
raise ValueError(f"未知参数:--{key}")
def convert_value(param: ParamSpec, raw: str) -> Any:
if param.type is bool:
normalized = raw.lower()
if normalized in _TRUE_SET:
return True
if normalized in _FALSE_SET:
return False
raise ValueError(f"{param.name} 需要布尔值(true/false/1/0/yes/no/on/off)")
try:
return param.type(raw)
except (TypeError, ValueError) as exc:
raise ValueError(f"{param.name} 需要 {param.type.__name__} 类型,但收到 {raw!r}") from exc
def parse_command(spec: CommandSpec, tokens: List[str]) -> Dict[str, Any]:
options: Dict[str, str] = {}
positional: List[str] = []
index = 0
while index < len(tokens):
token = tokens[index]
if token == "--":
positional.extend(tokens[index + 1:])
break
if token.startswith("--"):
body = token[2:]
if "=" in body:
key, value = body.split("=", 1)
find_param(spec, key)
options[key] = value
else:
param = find_param(spec, body)
if param.type is bool:
options[body] = "true"
else:
index += 1
if index >= len(tokens):
raise ValueError(f"参数 --{body} 需要值")
options[body] = tokens[index]
else:
positional.append(token)
index += 1
result: Dict[str, Any] = {}
position_count = 0
for param in spec.params:
if param.name in options:
result[param.name] = convert_value(param, options[param.name])
elif param.position is not None:
position_count += 1
if param.position < len(positional):
result[param.name] = convert_value(param, positional[param.position])
elif param.required:
raise ValueError(f"缺少位置参数 {param.name}")
elif param.default is not None:
result[param.name] = param.default
elif param.required:
raise ValueError(f"缺少必填参数 --{param.name}")
elif param.default is not None:
result[param.name] = param.default
if len(positional) > position_count:
raise ValueError("存在多余的位置参数")
return result
这里有几个刻意考虑的细节。布尔标志 --verbose 可以不用显式给值,出现即视为 true。非布尔参数必须带值。-- 之后的内容全部视为位置参数,专门用来处理“负数开头的参数”或“必须以字符串形式传入的复杂值”。find_param 会在参数名前先做合法性检查,避免执行器收到规格中没有的 key。
3.3 执行器:统一异常和结果结构
执行器负责把指令名解析成指令规格,再调用解析器,最后调用 handler。为了让上层调用方稳定处理结果,执行器返回一个统一结构;出现错误时抛出统一异常。
python复制# cmd_framework/executor.py
import shlex
from dataclasses import dataclass
from typing import Any, Optional
from .model import CommandSpec
from .parser import parse_command
from .registry import CommandRegistry
class CommandError(Exception):
pass
@dataclass
class ExecuteResult:
exit_code: int = 0
output: str = ""
command: Optional[str] = None
class CommandExecutor:
def __init__(self, registry: CommandRegistry):
self.registry = registry
def execute(self, raw_line: str) -> ExecuteResult:
if not raw_line.strip():
return ExecuteResult()
tokens = shlex.split(raw_line)
name = tokens[0]
spec = self.registry.lookup(name)
if spec is None:
raise CommandError(f"未知指令:{name},可用 help 查看指令列表。")
args_tokens = tokens[1:]
try:
kwargs = parse_command(spec, args_tokens)
output = spec.handler(**kwargs)
return ExecuteResult(output=output if output else "", command=name)
except CommandError:
raise
except Exception as exc:
raise CommandError(f"执行指令 {name} 失败:{exc}") from exc
所有业务 handler 内部出现的异常,最终都会被包装成 CommandError。这样上层只需要捕获一个类型就能统一处理。注意这里只是抛给顶层捕获,并不是把异常吞掉,原始异常通过 raise ... from exc 保留了 traceback,排查时可以定位到真正的问题。
3.4 内置 help 指令:让系统自解释
有注册表之后,help 指令可以自动生成。它遍历所有已注册指令,格式化输出指令名、描述和参数说明。
python复制# commands/builtin.py
from cmd_framework.model import CommandSpec
from cmd_framework.registry import CommandRegistry
def build_help_handler(registry: CommandRegistry):
def handler():
lines = ["可用指令:"]
for spec in sorted(registry.all_commands(), key=lambda item: item.name):
params = " ".join(_format_params(spec))
lines.append(f" {spec.name} {params}")
lines.append(f" 描述:{spec.description}")
return "\n".join(lines)
return handler
def _format_params(spec):
for param in spec.params:
if param.position is not None:
yield f"<{param.name}>"
else:
yield f"--{param.name}"
在 build_registry() 中把 help 指令注册进去,避免每次新增业务指令时都去改帮助代码。帮助指令本身也是一个普通指令,它只依赖注册表,不依赖具体业务。
3.5 指令命名规则和别名设计
指令名建议统一使用小写字母、数字和连字符,例如 math-add。不要使用空格作为指令名的一部分,否则执行器难以判断指令边界。若业务需要“父指令 子指令”的形态,更合适的方式是把第一次出现的 token 作为父指令,将剩余 token 作为父指令的参数,由父指令的 handler 再路由到子逻辑。
别名适合给用户提供便捷入口,例如 greet 的别名可以注册为 hello。但要防止别名与已有指令名冲突,注册表在 register 时会做检查。别名数量不宜过多,别名过多会让帮助系统变得冗长。
4. 运行验证与结果分析
4.1 注册三条业务指令并启动入口
以实际可运行的完整注册过程为例:
python复制# app.py
from cmd_framework.model import CommandSpec, ParamSpec
from cmd_framework.registry import CommandRegistry
from cmd_framework.executor import CommandExecutor, CommandError
from commands.builtin import build_help_handler
def build_registry() -> CommandRegistry:
registry = CommandRegistry()
def handler_greet(name: str, times: int = 1) -> str:
lines = []
for index in range(times):
lines.append(f"{index + 1}. hello, {name}")
return "\n".join(lines)
registry.register(CommandSpec(
name="greet",
description="向指定姓名输出问候语,可控制重复次数",
params=[
ParamSpec(name="name", type=str, required=True, help="被问候的人名"),
ParamSpec(name="times", type=int, default=1, help="重复次数"),
],
handler=handler_greet,
aliases=["hello"],
))
def handler_add(a: float, b: float) -> str:
return f"{a} + {b} = {a + b}"
registry.register(CommandSpec(
name="math-add",
description="计算两个数字之和",
params=[
ParamSpec(name="a", type=float, position=0, required=True, help="第一个数"),
ParamSpec(name="b", type=float, position=1, required=True, help="第二个数"),
],
handler=handler_add,
))
registry.register(CommandSpec(
name="help",
description="列出所有可用指令",
params=[],
handler=build_help_handler(registry),
))
return registry
def main() -> int:
registry = build_registry()
executor = CommandExecutor(registry)
import sys
if len(sys.argv) > 1:
raw_line = " ".join(sys.argv[1:])
try:
result = executor.execute(raw_line)
if result.output:
print(result.output)
return result.exit_code
except CommandError as exc:
print(f"[ERROR] {exc}")
return 1
print("进入交互模式,输入 exit 或 quit 退出。")
while True:
try:
raw = input("cmd> ").strip()
except (EOFError, KeyboardInterrupt):
break
if raw in {"exit", "quit"}:
break
if not raw:
continue
try:
result = executor.execute(raw)
if result.output:
print(result.output)
except CommandError as exc:
print(f"[ERROR] {exc}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
main() 支持两种模式:有命令行参数时执行一次并返回退出码;没有参数时进入交互模式。生产环境里,单次执行模式更适合被 Cron、监控探活或上游系统调用。
4.2 正常输入下的预期输出
先验证正常的指令执行:
bash复制python app.py greet --name "Alice Smith" --times 3
预期输出:
text复制1. hello, Alice Smith
2. hello, Alice Smith
3. hello, Alice Smith
再验证位置参数:
bash复制python app.py math-add 2 5
预期输出:
text复制2.0 + 5.0 = 7.0
最后验证帮助指令:
bash复制python app.py help
预期输出:
text复制可用指令:
greet --name --times
描述:向指定姓名输出问候语,可控制重复次数
help
描述:列出所有可用指令
math-add <a> <b>
描述:计算两个数字之和
这里可以看到,位置参数在帮助信息中用 <a> <b> 展示,选项参数用 --name 展示,说明解析器、注册表和帮助信息遵守同一套参数模型。
4.3 错误输入下的行为与退出码
错误路径同样要验证,否则系统只会“看起来正常”。
bash复制python app.py unknown-command
python app.py greet
python app.py math-add 2 not-a-number
三种情况分别对应未知指令、缺少必填参数、类型转换失败,预期都会输出 [ERROR] 并返回退出码 1。类型转换失败的异常信息应该包含参数名和原始值,否则用户不知道是自己传错还是指令缺陷。
退出码约定可以固定为:0 表示成功,1 表示用户输入或执行错误,2 表示框架自身异常。框架内部随机错误建议统一归为 1,避免调用方维护过多状态。
4.4 用单元测试守护解析器和执行器
指令框架越往后扩展越容易在参数解析时引入回归。建议至少在 tests 中覆盖解析器转换和 help 指令。
python复制# tests/test_parser.py
from cmd_framework.model import CommandSpec, ParamSpec
from cmd_framework.parser import parse_command
def _make_spec():
return CommandSpec(
name="test",
description="test",
params=[
ParamSpec(name="name", type=str, required=True),
ParamSpec(name="times", type=int, default=1),
ParamSpec(name="verbose", type=bool, default=False),
],
handler=lambda **kwargs: kwargs,
)
def test_parse_long_option_with_space():
spec = _make_spec()
result = parse_command(spec, ["--name", "Alice Smith", "--times", "3"])
assert result["name"] == "Alice Smith"
assert result["times"] == 3
def test_parse_boolean_flag():
spec = _make_spec()
result = parse_command(spec, ["--name", "alice", "--verbose"])
assert result["verbose"] is True
def test_parse_unknown_option():
spec = _make_spec()
try:
parse_command(spec, ["--name", "alice", "--unknown", "1"])
except ValueError:
pass
else:
raise AssertionError("应当抛出未知参数错误")
测试的价值不仅是验证功能,更是把“错误参数必须被拒绝”固化成行为。后续任何人修改解析器,只要测试还在,就能第一时间发现破坏。
4.5 检查确认清单
一个指令实现在发布前应当确认以下内容:
| 检查项 | 确认方式 |
|---|---|
| 指令是否注册成功 | help 是否能列出新指令 |
| 参数默认值是否符合预期 | 不传可选参数执行一次 |
| 必填参数缺失是否有提示 | 只传部分参数观察 [ERROR] |
| 类型错误是否可读 | 传非法类型观察提示中的参数名 |
| 别名是否生效 | 使用别名执行一次 |
| 异常执行后进程是否存活 | 故意让 handler 抛异常,观察进程退出码 |
5. 常见问题与排查路径
5.1 指令不生效
现象:执行新注册的指令时提示“未知指令”。
排查顺序:
- 确认指令真的调用了
registry.register(...),而不是只定义了 handler 函数。 - 确认
build_registry()返回的注册表最终被CommandExecutor使用。 - 确认应用改代码后重新启动了进程。交互模式下长期运行会让人忘记旧进程仍在使用老注册表。
- 确认指令名拼写没有大小写差异。本框架默认严格区分大小写,
Greet和greet是两个不同 token。
缓存机制、热更新、模块加载路径错误也可能导致注册未发生。最好的排查方式是加一条启动日志,打印当前注册的指令名列表。
5.2 参数解析失败
现象:带空格的参数被拆开、布尔参数被误判、负数位置参数报错。
排查顺序:
- 先看输入字符串本身有没有配对齐的引号。
- 在
execute方法中打印tokens = shlex.split(raw_line),确认拆词结果。 - 确认参数在
CommandSpec.params中有定义。 - 如果负数作为位置参数传入,在负数前加
--分隔,例如math-add -- -3 5。
常见坑是用户在脚本里拼接命令时把带空格参数去掉引号:
python复制# 错误示例
raw = f"greet --name {user_input}"
# 正确示例
raw = f"greet --name {shlex.quote(user_input)}"
这条建议在生成远程指令时尤其重要,否则输入内容会被按空格拆开。
5.3 执行器抛出异常后状态无法恢复
现象:某个指令 handler 抛了 RuntimeError,之后启动入口也退出。
原因:顶层没有捕获 CommandError,或者 handler 内部直接使用 sys.exit()。
解决方式:框架层只捕获 CommandError,业务层不要调用 sys.exit(),而是抛异常或返回错误结果。启动入口在最外层捕获 CommandError 并打印错误,保证进程不会因为单条指令失败直接崩溃。
5.4 重复注册与覆盖顺序
现象:两个模块注册了同名指令,后注册的覆盖了先注册的,行为因启动顺序而变化。
原因:默认 CommandRegistry.allow_overwrite=False,但某些模块在初始化时绕过该限制,或手动清空注册表。
建议:
- 生产环境保持不覆盖原则。
- 新增兼容性别名时明确命名空间。
- 当确实需要热更新时,增加“先移除旧指令再注册新指令”的显式操作,不要静默覆盖。
5.5 环境差异:路径、编码与权限
| 问题现象 | 常见原因 | 检查方式 | 处理建议 |
|---|---|---|---|
| Windows 下引号解析不同 | CMD 参数传递规则与 shlex 默认行为不同 | 打印 sys.argv |
交互模式下直接输入原始字符串,避免经过 Shell 二次处理 |
| 中文参数乱码 | 终端编码不一致 | 检查 sys.stdout.encoding |
统一 stdout 编码为 UTF-8 |
| 脚本读取权限失败 | 指令执行目录与日志目录不同 | 打印 os.getcwd() |
路径全部使用绝对路径或相对路径统一收口 |
| 命令带换行符 | 参数中混入 \n |
打印 repr(tokens) |
解析前移除首尾空白,或对显式换行参数单独控制 |
5.6 标准排查链路
在处理所有指令系统问题时,建议遵循固定顺序:输入是否正确,指令是否注册,文件路径与入口是否匹配,依赖版本是否匹配,代码改动是否加载,日志有无明确异常,再考虑框架版本限制。不要一上来就去查框架源码,多数问题发生在调用侧。
6. 生产环境扩展方向
6.1 从单行指令到批处理脚本
交互模式适合学习,生产环境往往需要一次执行多条指令。可设计 run-script 指令,从文件按行读取指令并顺序执行。每行视为一次独立调用,异常行需要记录行号和错误信息。
python复制def execute_script(executor, path):
executed, failed = 0, 0
with open(path, "r", encoding="utf-8") as file:
for line_number, line in enumerate(file, start=1):
raw = line.strip()
if not raw or raw.startswith("#"):
continue
try:
executor.execute(raw)
executed += 1
except CommandError as exc:
failed += 1
print(f"第 {line_number} 行失败:{exc}")
return executed, failed
批处理场景中,至少要支持注释行、空行、失败后继续或暂停的开关。生产环境还要考虑执行日志和断点记录,否则脚本执行到一半失败时,很难定位已经执行了哪些行。
6.2 远程协议接入
指令框架的核心是“文本输入-结构化输出”,这本身体系与协议解耦。可以按以下方式接入远程调用:
- 在请求侧传
command字段,例如greet --name "alice"。 - 在服务侧调用
executor.execute(command)。 - 返回 JSON 中的
exit_code和output字段。 - 由上层协议负责鉴权、限流和重试。
HTTP、WebSocket、消息队列都可以做同样转换。尽量不要把指令拼接代码散落在各个接口,而是提供统一的 execute_api 函数。
6.3 权限、审计与配置外置化
生产环境不能无条件执行任何指令。建议引入权限标记,在 CommandSpec 中增加 required_role 字段,执行前检查调用方身份。审计记录至少包含指令名、参数、调用方、时间、执行结果和退出码。敏感指令的参数不能原样写入日志。
配置外置化要解决“每台机器指令行为不同”的问题。参数默认值、允许注册的指令列表、日志开关都应从环境变量或配置中心读取,而不是硬编码在代码里。指令框架只负责执行逻辑,不负责携带环境相关配置,两者分离后部署才灵活。
6.4 自定义指令框架最佳实践清单
每一条都可以直接落地到日常开发:
- 每个指令 handler 只做一件事,返回字符串或结构化数据,不要直接打印输出。
- 参数类型转换统一放在解析器,不要在 handler 里各自转换。
- 错误信息必须包含指令名、参数名和原始输入,方便调用方自查。
- 所有异常统一包装为
CommandError,最外层只捕获一种异常类型。 - 帮助信息由注册表自动生成,不要手工维护一份容易过期的文档。
- 注册表默认禁止覆盖,显式注册流程能避免模块加载顺序影响行为。
- 单元测试至少覆盖参数解析、必填校验、类型转换和未知指令分支。
- 不要在高频路径上动态构造指令字符串,如果参数包含用户输入,使用
shlex.quote()生成参数片段。
6.5 后续可以继续做的练习
建议从以下三个方向继续扩展:
- 把
greet改为支持--name多次出现的列表参数,观察解析器和执行器需要改动多少。 - 增加
role字段,并实现require_role校验,让help只能看到当前角色有权限执行的指令。 - 实现“历史记录”功能,用
history指令展示最近 50 条输入,再实现简单的上下键回放。
自定义指令系统本身不是一个“写完就结束”的模块。当它承担的业务指令越来越多时,解析规则、权限控制、审计和帮助生成都会逐渐成为核心能力。本文所建立的最小框架价值在于:它先把数据模型、注册、解析、执行和错误恢复这五层拆开,后续无论接入协议还是增加权限,都可以在某一层单独扩展,而不需要推倒整个架构。
