EN

运行时执行模型

了解 func 如何装配应用,并依次完成命令作用域选择、选项解析、处理器分派、实例与参数准备、校验、执行和错误路由。

更新于 2周前

func 的运行时分为两个时期。应用启动时,它先把模块声明聚合成一份完整的命令行模型;每次调用到来后,再根据当前激活的命令找到唯一的处理器调用。装饰器提供声明与元数据,func 运行时会把这些声明的数据组合与隔离,同时也确保这些命令之间共享服务、错误、验证等全局规则。

例如 ship project member add alice --role owner --force 会依次得到 project 命令作用域、roleforce 选项、member add 处理器路径,以及剩余的 alice 位置输入。各阶段只消费自己负责的部分,并把结果交给下一阶段。

func 运行时管线
  1. 装配应用

    展开模块,收集命令与服务,并验证整份命令行模型。

  2. 选择命令

    根据 argv 的第一个 token 确定具名、主命令或缺失命令。

  3. 解析作用域

    使用所选命令的 option 规则解析完整参数,并识别未知选项。

  4. 分派处理器

    依次检查最长 handler path、handler option 和默认处理器。

  5. 准备执行

    创建所选命令及其服务,写入字段,执行校验并生成方法参数。

  6. 调用或失败

    等待唯一处理器完成;失败按发生阶段进入局部或应用级错误路由。

装配应用

@FuncModule 是运行时的装配边界。func 递归展开 imports,将各模块注册的 commands 和 services 合并为应用级模型。模块描述结构,但不会因为装配过程而创建所有命令与服务;实例只在某次调用实际需要时产生。

命令在这里按角色分组:@Command 形成具名命令集合,@CommandMajor 提供没有具名命令时的顶层作用域,@CommandMissing 接收未知 command,错误处理器则独立参与失败路由。服务列表形成后续依赖注入可使用的 provider 集合。

装配阶段还会检查整份模型是否可判定:command name 和 alias 不能重复;应用最多有一个主命令和一个缺失命令;同一命令内的 option token 不能冲突;除兼容型缺失命令外,可分派命令必须有处理器,且最多有一个默认处理器。这些不是 POSIX 规则,而是 func 的注册约束。它们保证运行时不会依赖声明顺序或偶然的覆盖关系完成分派。

选择命令作用域

shell 已经完成引号、转义和变量展开后,才把 argv 交给程序。func 不重新拆分字符串,而是直接检查可执行文件之后的第一个 token。这个阶段只决定由哪个命令类解释本次调用,还不解析 option。

argv 开头命令作用域处理方式
[] / [--version]主命令没有具名 command token;由应用的顶层命令处理。
[project, ...]具名命令 project第一个 token 匹配 command name 或 alias。
[unknown, ...]缺失命令第一个普通 token 未匹配具名命令,并且应用注册了缺失命令。

以连字符开头的 token 按 Unix 命令行惯例表示 option,因此不会被当作 command name;option-first 调用属于主命令。把第一个普通 token 作为 command 则是 func 对常见 command/subcommand 模型的约定,不是 POSIX 对所有 utility 的要求。

如果未知 command 没有对应的缺失命令,运行时不会把它回退到主命令。作用域是严格选择,不是逐个命令尝试。这个性质使后续 option 解析只面对一份明确的语法。

解析当前命令的 option

命令作用域确定后,func 才能建立本次调用的 option 规则。规则由该命令的字段 option、handler option 和 sub-options 共同组成。其他命令的字段不会加入,因此主命令 option 也不会自然成为具名命令的 global option。

这里沿用 POSIX Utility Argument Syntax 的术语:option 是具名开关,option-argument 是选项携带的值,operand 是位置输入。func 同时支持 GNU 风格的 --long-option,并允许 option 出现在 operand 前后;这种交错方式属于 GNU command-line convention,不是严格 POSIX 的排列方式。

已声明的 option 在这一阶段完成 alias 归一化和 Boolean、String、Number、重复字符串等类型转换。未识别且仍以连字符开头的 token 被归类为 unknown option;普通 token 则保留为 operand,交给处理器分派。具名命令的 command token 会在分派前移除,主命令和缺失命令没有这一步。

主命令、具名命令和带处理器的缺失命令使用同一套 option 解析。旧式的构造函数型缺失命令是兼容分支:它直接接收原始输入,不进入字段和处理器管线。

分派唯一处理器

option 解析完成后,运行时使用剩余 operand 选择处理器。主命令、具名命令和现代缺失命令执行相同的三段分派:

  1. 查找以当前 operand 开头的 handler path,并选择最长匹配。
  2. 没有 path 命中时,检查显式出现的 handler option。
  3. 前两者都没有选中处理器时,使用默认处理器。

最长 path 优先是 longest-prefix matching,也就是路由与语法分析常用的“更具体者优先”规则。若同时声明 profileprofile get,输入 profile get work 会选择后者,并留下 work 作为业务 operand。

path、handler option、default 的先后顺序是 func 的运行时契约,不属于 POSIX 或 GNU option 规范。path 描述命令的结构分支,因此优先于用于切换动作的 option;进入 handler option 分支后,一次调用最多只能显式选择一个动作,否则产生互斥冲突。

准备并执行所选命令

处理器确定后,运行时才创建命令实例和该调用需要的服务。服务按构造函数类型解析,并在本次命令的依赖图中复用;未选中的命令不会实例化。@Args@Regs 等参数则由当前命令、处理器、path、剩余 inputs 和 option 结果生成。

命令构造完成后,func 把显式 option 写入字段,未提供的字段保留类属性默认值,然后依次执行 required 检查、字段 validator 和跨字段 constraint。校验通过后,唯一处理器才会被调用;同步返回和 Promise 使用相同的完成语义。

由于字段赋值发生在命令构造之后,构造函数可以接收服务或调用上下文,但不应依赖字段已经被 argv 覆盖。处理器和局部 catch 看到的才是完成字段注入与默认值合并后的命令状态。

按阶段路由错误

装配阶段发现的重复 token、缺失处理器或无效命令形状属于注册错误。它们发生在应用运行边界建立之前,直接阻止应用启动,也不会进入命令自己的 catch。

作用域选择、option 解析、处理器分派以及命令构造发生在局部 catch 之外;这些阶段的输入错误或构造错误进入应用级错误处理。字段注入、校验、参数准备和处理器调用已经拥有命令实例,因此会先进入当前命令的局部 catch,未处理的失败再进入应用级路由。

system error 始终表示框架声明或注册缺陷,会直接抛出。runtime error 可以交给局部或全局处理;runtime-print error 还带有 func 的默认 stderr 输出。完整错误等级与打印控制见 错误处理

Q&A

问题运行时答案
主命令上的 option 是全局 option 吗?不是。命令作用域先确定,随后只解析该作用域声明的 option。
path 和 handler option 同时匹配时执行谁?执行 path 处理器;结构路径优先于动作选项。
一次调用会执行多个处理器吗?不会。路径、动作选项和默认处理器是互斥分支,最终只产生一个调用目标。
匹配 path 后,那些 token 去哪里了?它们记录为当前 path,并从最终位置 inputs 中移除。
构造函数中能读取解析后的字段吗?不能依赖。命令先完成构造,随后才写入 option 字段和默认值。
局部 catch 能处理未知 option 吗?不能。选项解析早于命令实例创建,解析错误直接进入应用级错误路由。