Shell 框架
AxVisor 提供交互式 Shell 命令系统,用于虚拟机的全生命周期管理。本节介绍 Shell 框架的设计实现。
架构概述
Shell 框架采用分层架构设计,从用户输入到底层 VMM 操作共分为五个层次。每一层都有明确的职责,层与层之间通过清晰的接口进行交互,确保系统的可维护性和可扩展性。
下图展示了命令从用户输入到实际执行的完整数据流向。这种分层设计使得系统各部分职责清晰、易于测试和维护:
数据流说明:
- 用户输入层 → 命令解析层:用户通过交互式 Shell 或脚本输入命令字符串
- 命令解析层 → 命令树层:将字符串解析为结构化的命令和参数
- 命令树层 → 处理器层:根据命令路径找到对应的处理函数
- 处理器层 → VMM 核心层:调用底层 VMM 模块执行实际操作
- 结果沿相反方向返回,最终显示给用户
各层说明:
- 用户输入层:接收用户的交互式输入(REPL)或批量脚本执行请求
- 命令解析层:负责词法分析、参数解析和验证,将原始字符串转换为结构化命令
- 命令树层:组织命令的层次结构,实现命令的路由和分发
- 处理器层:各个命令的具体实现逻辑,处理业务请求
- VMM 核心层:底层虚拟机管理功能,提供 VM 列表管理、Vcpu 控制和配置管理等核心能力
设计原则
命令树架构
采用树状结构组织命令,支持子命令嵌套。这种设计具有以下优点:
- 层次清晰:命令按功能域分组(如
vm、vcpu),便于用户理解和记忆 - 易于扩展:添加新命令只需在相应节点挂载子节点,不影响其他部分
- 命名空间隔离:不同命令组可以有同名的子命令(如
vm list和vcpu list) - 自动帮助生成:基于树结构自动生成帮助信息
下图展示了当前实现的命令树结构。树的根节点包含顶级命令和命令组,每个命令组下又可以有多个子命令,形成清晰的层次结构:
命令树遍历示例:
- 用户输入
vm start 0:从 Root → vm → start,剩余参数[0] - 用户输入
help:直接从 Root → help,无剩余参数 - 用户输入
vcpu list:从 Root → vcpu → list,无剩余参数
构建器模式
命令注册采用构建器模式,提供流畅的链式 API。这种设计模式让命令定义代码简洁易读,同时保证了配置的完整性和正确性。
构建器模式的优势:
- 可读性强:代码自文档化,命令的配置一目了然
- 类型安全:编译期检查,避免运行时错误
- 灵活性高:可选配置项按需添加,不影响默认行为
- 易于维护:修改命令配置只需调整链式调用
下图展示了使用构建器模式注册命令的完整流程。每个方法调用都返回 self,允许方法链式调用,最终构建出完整的命令节点:
构建流程说明:
- new(description):创建命令节点,设置描述信息
- with_handler(fn):设置命令处理函数(可选,命令组可以没有处理函数)
- with_usage(str):设置使用说明字符串
- add_flag(def):添加布尔标志参数(如
--force) - add_option(def):添加键值对选项(如
--format json) - 每个方法返回
self,实现链式调用
命令解析流程
Shell 框架将用户输入的字符串转换为可执行命令需要经过多个阶段的处理。每个阶段都有特定的职责,确保命令的正确性和安全性。
完整流程
下图展示了从用户输入到命令执行的完整时序流程,以命令 "vm start 0 1 --detach" 为例。整个流程分为词法分析、命令树查找、参数解析、参数验证和命令执行五个阶段:
各阶段职责:
- 词法分析:将输入字符串按空格和引号规则分割成 token 数组
- 命令树查找:根据 token 在命令树中逐层查找,找到匹配的命令节点
- 参数解析:将剩余的 token 解析为位置参数、选项和标志
- 参数验证:检查必需参数是否提供,参数类型是否正确
- 命令执行:调用命令的处理函数,执行实际业务逻辑
错误处理:任何阶段出现错误都会中断流程,向用户显示错误信息,不会执行后续步骤。
词法分析
词法分析器(Tokenizer)负责将输入字符串按照 Shell 语法规则分割为 token 列表。这是命令解析的第一步,为后续处理提供结构化的输入。
分析器需要处理多种语法情况:
- 空白符分隔:普通参数通过空格、制表符等分隔
- 引号处理:支持单引号和双引号包围的字符串,允许参数中包含空格
- 转义字符:支持反斜杠转义,可以在字符串中包含特殊字符
下图展示了一个包含引号的复杂命令的分割过程。词法分析器识别出引号包围的部分作为一个完整的 token,即使其中包含空格:
分割规则应用示例:
- 空白符分隔:
vm start 0→["vm", "start", "0"] - 引号包围:
--name "my vm"→["--name", "my vm"](注意 "my vm" 是一个 token) - 转义字符:
echo \"test\"→["echo", "\"test\""]
特殊情况处理:
- 连续的空白符被视为单个分隔符
- 引号必须成对出现,否则报错
- 引号内的空白符被保留
- 反斜杠转义下一个字符
命令查找
命令树遍历器根据 token 列表在命令树中逐层查找匹配的节点。这个过程类似于在文件系统中根据路径查找文件。
查找过程:
- 从根节点开始
- 使用第一个 token 在当前节点的子命令中查找
- 如果找到匹配的子节点,移动到该节点并继续用下一个 token 查找
- 如果找不到匹配或节点是叶子命令,停止查找
- 将剩余的 token 作为命令参数
下图展示了命令 vm start 0 1 的查找过程。系统从根节点开始,依次匹配 "vm" 和 "start",最终到达 start 命令节点,剩余的 "0" 和 "1" 作为参数:
查找逻辑详解:
- 匹配成功:当前 token 在子命令映射表中找到对应键,移动到该子节点
- 匹配失败:当前节点没有该子命令,或者节点有处理函数(叶子命令),停止查找
- 剩余参数:未消耗的 token 传递给命令处理函数作为参数
错误情况处理:
- 如果中途匹配失败且当前节点没有处理函数,返回"命令不存在"错误
- 如果匹配到叶子命令但还有剩余 token,这些 token 会被解析为参数
参数解析
参数解析器将剩余的 token 列表解析为结构化的参数数据。它需要识别三种类型的参数:位置参数、选项(带值)和标志(布尔型)。
解析规则:
- 以
--或-开头的是选项或标志 - 选项后面跟随的 token 是其值(除非使用
--key=value格式) - 标志不需要值,解析为布尔
true - 不以
-开头的是位置参数,按顺序排列
下图展示了一个混合参数的解析结果。解析器将输入的 token 列表分类为三种不同类型的参数:
解析示例详解:
- 位置参数:"0" 和 "1" 不以
-开头,按出现顺序存储 - 标志:"--force" 是以
--开头但后面不跟值的参数,解析为force: true - 选项:"--format" 后跟 "json",解析为键值对
format: "json"
参数类型表:
| 类型 | 语法 | 示例 | 解析结果 |
|---|---|---|---|
| 位置参数 | 无前缀 | 0 1 | positional_args: ["0", "1"] |
| 长选项 | --key value | --format json | options: {format: "json"} |
| 长选项(等号) | --key=value | --format=json | options: {format: "json"} |
| 短选项 | -k value | -f json | options: {f: "json"} |
| 长标志 | --flag | --force | flags: {force: true} |
| 短标志 | -f | -f | flags: {f: true} |