Communitygithub.com

photoeven2-netizen/astronusa

Dart 命令行界面(CLI)应用的架构模式、入口点结构、退出码、流路由和子进程生成。适用于构建CLI工具、控制台实用程序、脚本、使用`package:args`(ArgParser或CommandRunner)进行参数解析、处理退出码、在pubspec.yaml中配置可执行文件、生成Dart子进程或编译原生CLI二进制文件。不适用于Flutter UI小部件、Web应用程序或独立HTTP后端服务器。

astronusa 是什么?

这个技能为构建专业的 Dart CLI 应用程序提供了全面的架构指南和最佳实践。它涵盖了从核心进程生命周期管理到测试和分发的完整流程,特别强调了避免使用破坏性的 `exit(N)` 调用,转而推荐通过设置 `exitCode` 或返回整数退出码来实现优雅的进程终止。该技能详细阐述了如何利用“薄入口点”模式(`bin/` 目录仅作为跳板,逻辑代码放在 `lib/src/` 下)提高代码的可测试性。它提供了关于 `package:args` 进行参数解析的标准模式,包括 `ArgParser`、`CommandRunner` 以及类型安全的数据访问器(如 `results.flag()`、`results.option()`)。此外,还涉及输出格式化(stdout 用于数据,stderr 用于诊断)、终端能力检测、信号处理(包括 Windows 下的特殊处理)以及子进程生成(强调使用 `cli_util` 而非依赖 `Platform.resolvedExecutable`)等高级主题。最后,该技能还包含了使用 `package:test_process` 进行单元和集成测试的标准方法,以及基于 Dart 3.12+ 的现代编译与分发策略。

兼容平台~Claude Code~Codex CLI✓Cursor
npx skills add https://github.com/photoeven2-netizen/astronusa/tree/HEAD/.agents/skills/dart-build-cli-app

在你喜欢的 AI 中提问

打开一个已预加载此 Agent Skill 的新对话。

文档

构建 Dart CLI 应用

目录


1. 核心架构与进程生命周期

避免破坏性退出 (exit(N))

调用 dart:io 的 exit(int code) 会调用 C++ 运行时中的 Platform::Exit(code)。它会立即终止操作系统进程,而不展开 Dart 堆栈:

  • 调试器断开:当使用 --pause-isolates-on-exit 启动时,VM Service 会在关闭前暂停隔离区以允许 IDE 检查。exit() 在 VM Service 可以暂停或检查状态之前终止了操作系统进程。
  • 覆盖丢失:package:coverage 在暂停退出状态下通过 VM Service RPC 查询执行行。exit() 在 RPC 提取之前销毁进程,导致 0% 的覆盖率。
  • 缓冲区截断:stdout 和 stderr 是缓冲的异步 IOSink 流。exit() 会丢弃未刷新的字节。
  • 资源泄漏:finally 块(关闭锁、删除临时目录)被绕过。

规则:避免在正常执行期间直接调用 exit(code);设置 exitCode = code 或从 CommandRunner<int>(来自 package:args)返回一个整数退出码,并允许异步 main() 函数自然返回。不要在未处理的错误上调用 exit();抛出一个未处理的 Error 或异常,以便运行时干净地展开并以非零状态退出。

标准的 POSIX 退出码 (/usr/include/sysexits.h):

  • 0: 成功 (EX_OK / ExitCode.success.code)
  • 64: 命令行使用错误 (EX_USAGE / ExitCode.usage.code)
  • 65: 数据格式错误 (EX_DATAERR / ExitCode.data.code)
  • 70: 内部软件崩溃 (EX_SOFTWARE / ExitCode.software.code)
  • 78: 配置错误 (EX_CONFIG / ExitCode.config.code)

注意:优先使用导入 package:io/io.dart 并使用 ExitCode 常量(例如,ExitCode.usage.code,ExitCode.software.code),而不是使用魔数整数常量。对于没有包依赖的最小独立脚本,可以使用标准 POSIX 整数文字(0,64,70)。

import 'dart:io';
import 'package:args/command_runner.dart';
import 'package:io/io.dart' show ExitCode; // 提供标准的 POSIX ExitCode 常量

Future<void> main(List<String> args) async {
  final runner = CommandRunner<int>('tool', 'CLI tool description.');
  try {
    final status = await runner.run(args);
    exitCode = status ?? ExitCode.success.code;
  } on UsageException catch (e) {
    stderr
      ..writeln(e.message)
      ..writeln(e.usage);
    exitCode = ExitCode.usage.code;
  }
}

薄入口点模式 (bin/ 与 lib/src/)

保持 bin/*.dart 文件严格作为最小入口点跳板(实例化 runner,传递 args,等待退出码)。将所有命令定义、参数解析器、格式化程序和业务逻辑放在 lib/src/ 中。

  • 理由:bin/ 中的代码不能通过 package: URI 干净地导入。将逻辑移入 lib/src/ 允许整个命令运行程序、子命令层次结构和业务逻辑在内存中进行单元测试,时间在毫秒级(< 2ms),而无需生成操作系统子进程。
// bin/my_cli.dart — 薄入口点跳板
import 'dart:io';
import 'package:my_cli/src/cli.dart';

Future<void> main(List<String> args) async {
  exitCode = await runCli(args);
}

2. 输出、诊断与格式化

  • 数据与诊断:将预期的程序结果和机器可读数据专门写入 stdout。将警告、错误消息和调试日志专门写入 stderr。
  • 错误使用规则:当参数解析或强制选项错误发生时(FormatException、UsageException 或访问缺失的 mandatory: true 选项时通过 results.option(...) 抛出的 ArgumentError),错误消息和使用文本必须写入 stderr,并且必须返回退出码 64(EX_USAGE / ExitCode.usage.code)。stdout 只应在用户通过 --help 或 -h 明确请求时接收使用帮助。
  • 错误处理器中不要使用 print():print() 路由到 stdout。对所有失败通知使用 stderr.writeln()。对于标准输出,优先使用 stdout.writeln() 而不是 print(),以符合 avoid_print lint 规则(除非 analysis_options.yaml 明确配置了 avoid_print: false)。
  • 终端能力检测与 NO_COLOR:在发出 ANSI 颜色或光标转义码之前,验证 stdout.hasTerminal、stdout.supportsAnsiEscapes 和 !Platform.environment.containsKey('NO_COLOR'):
    bool get useAnsi =>
        stdout.hasTerminal &&
        stdout.supportsAnsiEscapes &&
        !Platform.environment.containsKey('NO_COLOR');
    
  • 机器可读模式:当传入 --json 或 --machine 标志时,将数据格式化为 JSON 输出到 stdout,并将日志路由到 stderr。

3. 项目配置与打包

脚手架与 Pubspec 可执行文件映射 (executables:)

使用 dart create -t console <package_name> 搭建新的命令行项目,它将初始化标准的 bin/ 和 lib/ 布局。始终在 pubspec.yaml 的 executables: 下声明可执行文件,将命令名称映射到 bin/ 中的脚本,从而允许通过 dart run <command> 进行干净的调用(无需指定 bin/...dart),并为 dart install 配置全局二进制符号链接:

name: my_cli
description: High-performance CLI tool.
version: 1.0.0

executables:
  my_cli: # 映射到 bin/my_cli.dart
  secondary_cmd: helper # 映射到 bin/helper.dart

单一来源版本控制 (package:build_version)

避免在 bin/*.dart 中硬编码 --version 字符串或手动同步常量文件。使用 package:build_version 在构建期间直接从 pubspec.yaml 生成包含 const packageVersion = 'x.y.z'; 的 lib/src/version.dart。

缓存约定

将瞬态缓存文件存储在 .dart_tool/<package_name>/ 中。切勿将持久缓存文件直接写入项目根目录。


4. 参数解析与命令路由

导入 package:args 来管理命令行参数:

  • 简单脚本:直接使用 ArgParser,配合 addFlag() 和 addOption()。
  • 多命令工具:为每个子命令实现 CommandRunner<int> 并扩展 Command<int>,直接返回 POSIX 退出码。
  • 类型安全访问器:使用 results.flag('name')、results.option('name') 和 results.multiOption('name')(在 package:args 2.5+ 中可用),而不是使用映射索引 operator [],以消除手动类型转换(as bool、as String?)。
  • 复杂选项模型:对于具有大量标志的应用程序,使用 package:build_cli 生成强类型的选项类。利用命名的默认覆盖(例如 {String? hostDefaultOverride})来干净地合并配置文件与 CLI 标志。

5. 原生异步与现代堆栈跟踪

  • 避免 Chain.capture():Dart VM 原生地在 await 挂起点之间保留异步堆栈帧。Chain.capture 在自定义 Zone 中包装事件循环,导致大量分配开销并将错误捕获到 Zone 边界之外。
  • 使用 Trace.from(st).terse 进行清理:在不捕获 Zone 的情况下,对未捕获的错误使用 package:stack_trace 中的静态工具:
import 'dart:io';
import 'package:io/io.dart' show ExitCode;
import 'package:stack_trace/stack_trace.dart';

Future<void> runMain(List<String> args) async {
  try {
    await executeLogic(args);
    exitCode = ExitCode.success.code;
  } catch (e, st) {
    stderr.writeln('Fatal error: $e');
    if (args.contains('-v') || args.contains('--verbose')) {
      stderr.writeln(Trace.from(st).terse);
    }
    exitCode = ExitCode.software.code;
  }
}

6. 子进程生成与 AOT 韧性

当生成 Dart SDK 子进程或执行其他 Dart 工具(例如 dart format、dart test、build_runner)时:

  • 不要假设 Platform.resolvedExecutable 或 Platform.executable 指向 dart 命令行可执行文件:在独立的 AOT 编译二进制文件中(dart install / dart compile exe),resolvedExecutable 指向编译后的应用程序二进制文件本身,导致递归的自调用循环或标志拒绝崩溃。
  • 使用 package:cli_util:使用 cli_util.dartExecutable 或 cli_util.sdkPath 来解析 Dart SDK 可执行文件,而不是编写自定义的 PATH 或目录抓取器。
  • 查看版本要求和详细的技术指南,请参阅 references/aot_sdk_discovery.md。

7. 信号处理与终端清理

如果你的 CLI 更改了终端模式、显示旋转器或打开了监听套接字:

  • Windows 信号防护:在 Windows 上,ProcessSignal.sigterm.watch() 会抛出 UnsupportedError。使用 if (!Platform.isWindows) 防护 sigterm。
  • 回显与行模式清理:如果设置了 stdin.echoMode = false 或 stdin.lineMode = false,首先检查 if (!stdin.hasTerminal) return;,并安装一个 SIGINT 监听器和 finally 块来恢复它们,以便用户按键在退出后仍然可见。
  • 光标可见性:如果发出了 ANSI 隐藏光标 (\x1B[?25l),始终在退出或取消时恢复光标可见性 (\x1B[?25h)。
  • 套接字清理:在终止信号上显式关闭监听的 HttpServer 或 ServerSocket 实例(server.close(force: true)),以立即释放操作系统端口。
  • 查看详细模式,请参阅 references/signals_and_terminal.md。

8. 测试 CLI 应用

在两个不同的层中进行结构化测试:

  1. 单元测试(内存中,< 5ms):通过在 test/ 中导入 package:<pkg>/src/... 直接测试命令类、选项解析和业务逻辑。
  2. 集成测试(子进程):使用 package:test_process 和 package:test_descriptor 验证端到端的二进制执行、进程 I/O 流和操作系统退出码:
import 'package:test/test.dart';
import 'package:test_descriptor/test_descriptor.dart' as d;
import 'package:test_process/test_process.dart';

void main() {
  test('CLI processes input and exits cleanly', () async {
    await d.file('input.txt', 'hello').create();

    final process = await TestProcess.start('dart', [
      'run',
      'bin/my_cli.dart',
      '--input',
      d.path('input.txt'),
    ]);

    await expectLater(process.stdout, emitsThrough('Processing complete.'));
    await process.shouldExit(0);
  });
}

9. 现代编译与分发

Dart 3.12+ 围绕 dart run 和 dart install 标准化了 CLI 分发(远离 dart pub global activate):

  • 临时执行 (JIT):dart run <package>@<version> [args] 按需下载并运行 CLI。
  • 全局安装 (原生 AOT):dart install <package> 将包入口点编译为位于 ~/.dart/install/bin/ 中的快速原生独立二进制文件。
  • 本地开发:使用 dart run <command>(通过 pubspec.yaml 中的 executables: 解析)或 dart run bin/cli.dart。
  • 打包动态库和代码资产:使用 dart build cli。将输出打包到 build/cli/_/bundle/。
  • 独立可执行文件编译:使用 dart compile exe bin/cli.dart -o <output_path>。

10. 工作流与审核清单

实现工作流

  • 在 pubspec.yaml 的 executables: 下声明入口点。
  • 保持 bin/*.dart 作为薄入口点;将命令逻辑放在 lib/src/ 中。
  • 返回整数退出码或设置 exitCode = N;避免原始 exit(N)。
  • 将解析失败时的错误、警告和使用说明定向到 stderr。
  • 使用 results.flag()、results.option() 和 results.multiOption() 保证类型安全。
  • 在发出 ANSI 代码之前验证 useAnsi(检查 stdout.hasTerminal、supportsAnsiEscapes 和 NO_COLOR)。
  • 使用 cli_util.dartExecutable 生成子工具,绝不使用 Platform.resolvedExecutable。
  • 在内存中对命令运行程序进行单元测试;使用 test_process 测试端到端的二进制执行。

参考与示例

Individual skills in this repo

This repo contains 3 individual skills — each has its own dedicated page.

相关技能