dart-build-cli-app

dart-build-cli-app

热门

入口结构、退出码规范及跨平台脚本实现。适用于开发 Dart 命令行工具(CLI)、脚本或终端应用。

2792Star
164Fork
更新于 2026/8/5
SKILL.md
只读
名称
dart-build-cli-app
描述

入口结构、退出码规范及跨平台脚本实现。适用于开发 Dart 命令行工具(CLI)、脚本或终端应用。

开发 Dart CLI 命令行应用

目录

项目搭建与架构设计

使用 Dart 官方模板初始化新的 CLI 项目,以确保符合标准的目录结构。

  • 运行 dart create -t cli <project_name> 快速生成自带基础参数解析能力的控制台应用骨架。
  • 可执行入口文件(包含 main() 函数的文件)必须统一放在 bin/ 目录下。
  • 内部实现逻辑统一存放在 lib/src/ 中,并通过 lib/<project_name>.dart 导出对外公开的 API。
  • 在 CI 环境中运行 dart format . --set-exit-if-changed 强制校验代码格式。如果存在不符合规范的代码,该命令会返回退出码 1。

参数解析与命令路由

引入 args 依赖包,用于统一管理命令行参数、标志(flags)及子命令。

  • 开发简单脚本:直接使用 ArgParser 定义标志(addFlag)与选项(addOption)。
  • 开发复杂的多命令 CLI 工具(类似 git):实现 CommandRunner 并在每个子命令中继承 Command 类。
  • CommandRunner.argParser 上定义全局参数,在各个 Command.argParser 上定义子命令专有的参数。
  • 捕获 UsageException 以优雅地处理无效参数,并展示自动生成的帮助文档。
  • 校验帮助文本准确性:确保帮助信息覆盖运行工具所需的全部关键信息。若帮助文本中引用了编译后的可执行文件名,且需要用户将其添加到 PATH 环境变量中才能直接运行,请在帮助文本或描述中明确提示添加步骤。

执行逻辑与异常处理

利用 iostack_trace 依赖包构建稳健、生产可用的 CLI 工具。

  • 使用 io 包提供的 ExitCode 枚举返回标准 POSIX 退出码(如 ExitCode.success.codeExitCode.usage.code)。
  • 当存在多个异步监听器需要按顺序读取标准输入(stdin)时,请使用 io 包中的 sharedStdIn
  • 使用 stack_trace 包的 Chain.capture() 包裹应用执行逻辑,以精准追踪异步调用堆栈。
  • 通过 Trace.terseChain.terse 格式化输出堆栈信息,过滤掉核心库等杂音堆栈,呈现易读的错误报告。
  • 切勿在底层逻辑或存储类中静默吞掉异常(除非可以自动恢复)。应让异常向上抛出或重新抛出(rethrow),以便上层命令知晓操作已失败。
  • 快速失败并返回非零退出码:确保在操作失败时向 stderr 输出明确的错误提示,并返回对应的非零退出码(例如使用 exit(1),或在捕获 UsageException 后触发 64 退出码)。

CLI 应用自动化测试

[!IMPORTANT]
所有新命令和核心功能必须覆盖自动化测试。 仅靠手动验证无法保障逻辑的严谨性。不过,针对帮助文本和用户体验(UX)的手动验证依然是必要的,这能确保交互逻辑符合直觉且表述准确。

使用 test_processtest_descriptor 为 CLI 编写高保真的集成测试。

  • 使用 test_descriptord.dird.file)定义预期的文件系统状态。
  • 在测试执行前运行 await d.Descriptor.create() 来创建 Mock 文件系统环境。
  • 调用 TestProcess.start('dart', ['run', 'bin/cli.dart', ...args]) 启动 CLI 测试子进程。
  • 通过 StreamQueue 匹配器(如 emitsThroughemits)校验标准输出(stdout)与标准错误(stderr)流。
  • 使用 await process.shouldExit(0) 断言最终的退出码。
  • 运行 await d.Descriptor.validate() 验证最终的文件系统变更是否符合预期。

编译打包与分发部署

根据分发需求选择合适的编译目标产物。

  • 开发阶段本地调试: 使用 dart run bin/cli.dart。该方式基于 JIT 编译器,可实现快速迭代。
  • 需要打包代码资源与动态库: 使用 dart build cli。这会触发构建 Hook 并将产物输出到 build/cli/_/bundle/
  • 分发独立原生可执行文件: 使用 dart compile exe bin/cli.dart -o <output_path>。这会将 Dart 运行时与机器码打包为单一可执行文件。
  • 分发多个应用且对磁盘空间有严格限制: 使用 dart compile aot-snapshot bin/cli.dart。生成的 .aot 文件需通过 dartaotruntime 运行。

<details>
<summary>跨平台编译目标(仅限 Linux)</summary>

Dart 支持在 macOS、Windows 或 Linux 宿主机上跨平台编译至 Linux 系统。
在运行 dart compile exedart compile aot-snapshot 时搭配 --target-os--target-arch 标志即可。

  • --target-os=linux(目前仅支持将 Linux 作为跨平台编译的目标系统)
  • --target-arch=arm64(64 位 ARM)
  • --target-arch=x64(x86-64)
  • --target-arch=arm(32 位 ARM)
  • --target-arch=riscv64(64 位 RISC-V)

示例:dart compile exe --target-os=linux --target-arch=arm64 bin/cli.dart
</details>

标准工作流

任务进度:新增 CLI 子命令

  • [ ] 在 lib/src/commands/ 目录下新建继承自 Command 的类。
  • [ ] 定义 namedescription 属性。
  • [ ] 在构造函数中使用 argParser.addFlag()argParser.addOption() 注册该命令专有的标志/选项。
  • [ ] 在 run() 方法中实现核心业务逻辑。
  • [ ] 在 bin/cli.dartCommandRunner 实例中通过 addCommand() 注册新命令。
  • [ ] 在 test/ 目录下编写对应新命令的测试用例(可使用 test_process 或标准测试)。
  • [ ] 执行校验 -> 运行 dart run bin/cli.dart help <command_name>,检查自动生成的帮助文本是否正确。
  • [ ] 验证最终 UX -> 使用 dart compile exe 编译应用并运行生成的可执行文件,确认终端交互体验无误(例如 ./bin/cli <command>)。

任务进度:编译并发布原生可执行文件

  • [ ] 执行校验 -> 运行 dart format . --set-exit-if-changed 确认代码格式规范。
  • [ ] 执行校验 -> 运行 dart analyze 确保无任何静态分析错误。
  • [ ] 执行校验 -> 运行 dart test 确保所有集成测试顺利通过。
  • [ ] 编译当前宿主机 OS 产物:dart compile exe bin/cli.dart -o build/cli-host
  • [ ] 编译 Linux 产物(若宿主机为 macOS/Windows):dart compile exe --target-os=linux --target-arch=x64 bin/cli.dart -o build/cli-linux-x64

代码示例

示例:实现 CommandRunner

import 'dart:io';
import 'package:args/command_runner.dart';
import 'package:stack_trace/stack_trace.dart';

class CommitCommand extends Command {
  @override
  final String name = 'commit';
  @override
  final String description = 'Record changes to the repository.';

  CommitCommand() {
    argParser.addFlag('all', abbr: 'a', help: 'Commit all changed files.');
  }

  @override
  Future<void> run() async {
    final commitAll = argResults?['all'] as bool? ?? false;
    print('Committing... (All: $commitAll)');
  }
}

void main(List<String> args) {
  Chain.capture(() async {
    final runner = CommandRunner('dgit', 'Distributed version control.')
      ..addCommand(CommitCommand());

    await runner.run(args);
  }, onError: (error, chain) {
    if (error is UsageException) {
      stderr.writeln(error.message);
      stderr.writeln(error.usage);
      exit(64); // ExitCode.usage.code
    } else {
      stderr.writeln('Fatal error: $error');
      stderr.writeln(chain.terse);
      exit(1);
    }
  });
}

示例:基于子进程的集成测试

import 'package:test/test.dart';
import 'package:test_process/test_process.dart';
import 'package:test_descriptor/test_descriptor.dart' as d;

void main() {
  test('CLI formats output correctly and modifies filesystem', () async {
    // 1. Setup mock filesystem
    await d.dir('project', [
      d.file('config.json', '{"key": "value"}')
    ]).create();

    // 2. Spawn the CLI process
    final process = await TestProcess.start(
      'dart',
      ['run', 'bin/cli.dart', 'process', '--path', '${d.sandbox}/project']
    );

    // 3. Validate stdout stream
    await expectLater(process.stdout, emitsThrough('Processing complete.'));

    // 4. Validate exit code
    await process.shouldExit(0);

    // 5. Validate filesystem mutations
    await d.dir('project', [
      d.file('config.json', '{"key": "value"}'),
      d.file('output.log', 'Success')
    ]).validate();
  });
}