dart-build-cli-app

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Building Dart CLI Applications

构建Dart CLI应用程序

Contents

目录

Project Setup & Architecture

项目设置与架构

Initialize new CLI projects using the official Dart template to ensure standard directory structures.
  • Run
    dart create -t cli <project_name>
    to scaffold a console application with basic argument parsing.
  • Place executable entry points (files containing
    main()
    ) exclusively in the
    bin/
    directory.
  • Place internal implementation logic in
    lib/src/
    and expose public APIs via
    lib/<project_name>.dart
    .
  • Enforce formatting in CI environments by running
    dart format . --set-exit-if-changed
    . This returns exit code 1 if formatting violations exist.
使用官方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。

Argument Parsing & Command Routing

参数解析与命令路由

Import the
args
package to manage command-line arguments, flags, and subcommands.
  • If building a simple script: Use
    ArgParser
    directly to define flags (
    addFlag
    ) and options (
    addOption
    ).
  • If building a complex, multi-command CLI (like
    git
    ): Implement
    CommandRunner
    and extend
    Command
    for each subcommand.
  • Define global arguments on the
    CommandRunner.argParser
    and command-specific arguments on the individual
    Command.argParser
    .
  • Catch
    UsageException
    to gracefully handle invalid arguments and display the automatically generated help text.
  • Validate Help Text Accuracy: Ensure the help text provides all necessary information to run the tool. If the help text references a compiled executable name, and the user needs to add it to their PATH to run it that way, provide clear instructions on how to do so in the help text or description.
导入
args
包来管理命令行参数、标志和子命令。
  • 如果构建简单脚本:直接使用
    ArgParser
    定义标志(
    addFlag
    )和选项(
    addOption
    )。
  • 如果构建复杂的多命令CLI(如
    git
    ):实现
    CommandRunner
    并为每个子命令扩展
    Command
    类。
  • CommandRunner.argParser
    上定义全局参数,在各个
    Command.argParser
    上定义命令专属参数。
  • 捕获
    UsageException
    以优雅处理无效参数,并显示自动生成的帮助文本。
  • 验证帮助文本准确性:确保帮助文本提供运行工具所需的所有信息。如果帮助文本引用了编译后的可执行文件名,且用户需要将其添加到PATH才能按该方式运行,请在帮助文本或描述中提供清晰的操作说明。

Execution & Error Handling

执行与错误处理

Leverage the
io
and
stack_trace
packages to build robust, production-ready CLI tools.
  • Use the
    io
    package's
    ExitCode
    enum to return standard POSIX exit codes (e.g.,
    ExitCode.success.code
    ,
    ExitCode.usage.code
    ).
  • Use
    sharedStdIn
    from the
    io
    package if multiple asynchronous listeners need sequential access to standard input.
  • Wrap the application execution in
    Chain.capture()
    from the
    stack_trace
    package to track asynchronous stack chains.
  • Format output stack traces using
    Trace.terse
    or
    Chain.terse
    to strip noisy core library frames and present readable errors to the user.
  • Do not swallow exceptions in lower-level logic or storage classes unless recovery is possible. Let them bubble up or rethrow them so higher-level commands know operations failed.
  • Fail fast and with non-zero exit codes: Ensure operation failures result in descriptive error messages to
    stderr
    and appropriate non-zero exit codes (e.g., using
    exit(1)
    or triggering a 64 exit code after a caught
    UsageException
    ).
利用
io
stack_trace
包构建健壮的生产级CLI工具。
  • 使用
    io
    包的
    ExitCode
    枚举返回标准POSIX退出码(例如
    ExitCode.success.code
    ExitCode.usage.code
    )。
  • 如果多个异步监听器需要顺序访问标准输入,请使用
    io
    包中的
    sharedStdIn
  • 使用
    stack_trace
    包的
    Chain.capture()
    包裹应用执行逻辑,以跟踪异步调用栈链。
  • 使用
    Trace.terse
    Chain.terse
    格式化输出栈跟踪,去除冗余的核心库帧,为用户呈现易读的错误信息。
  • 不要在底层逻辑或存储类中吞掉异常,除非可以恢复。让异常冒泡或重新抛出,以便上层命令知晓操作失败。
  • 快速失败并返回非零退出码:确保操作失败时向
    stderr
    输出描述性错误信息,并返回合适的非零退出码(例如使用
    exit(1)
    ,或捕获
    UsageException
    后触发64号退出码)。

Testing CLI Applications

CLI应用程序测试

[!IMPORTANT] All new commands and significant features must be covered by automated tests. Manual verification is not sufficient for testing logic. However, manual verification of help text and user experience (UX) is still required to ensure the interface is intuitive and correct.
Use
test_process
and
test_descriptor
to write high-fidelity integration tests for your CLI.
  • Define expected filesystem states using
    test_descriptor
    (
    d.dir
    ,
    d.file
    ).
  • Create the mock filesystem before execution using
    await d.Descriptor.create()
    .
  • Spawn the CLI process using
    TestProcess.start('dart', ['run', 'bin/cli.dart', ...args])
    .
  • Validate standard output and error streams using
    StreamQueue
    matchers (e.g.,
    emitsThrough
    ,
    emits
    ).
  • Assert the final exit code using
    await process.shouldExit(0)
    .
  • Validate resulting filesystem mutations using
    await d.Descriptor.validate()
    .
[!IMPORTANT] 所有新命令和重要功能必须覆盖自动化测试。 手动验证不足以测试逻辑,但仍需手动验证帮助文本和用户体验(UX),确保界面直观且正确。
使用
test_process
test_descriptor
为CLI编写高保真的集成测试。
  • 使用
    test_descriptor
    d.dir
    d.file
    )定义预期的文件系统状态。
  • 在执行前使用
    await d.Descriptor.create()
    创建模拟文件系统。
  • 使用
    TestProcess.start('dart', ['run', 'bin/cli.dart', ...args])
    启动CLI进程。
  • 使用
    StreamQueue
    匹配器(例如
    emitsThrough
    emits
    )验证标准输出和错误流。
  • 使用
    await process.shouldExit(0)
    断言最终退出码。
  • 使用
    await d.Descriptor.validate()
    验证文件系统的最终变更。

Compilation & Distribution

编译与分发

Select the appropriate compilation target based on your distribution requirements.
  • If testing locally during development: Use
    dart run bin/cli.dart
    . This uses the JIT compiler for rapid iteration.
  • If bundling code assets and dynamic libraries: Use
    dart build cli
    . This runs build hooks and outputs to
    build/cli/_/bundle/
    .
  • If distributing a standalone native executable: Use
    dart compile exe bin/cli.dart -o <output_path>
    . This bundles the Dart runtime and machine code into a single file.
  • If distributing multiple apps with strict disk space limits: Use
    dart compile aot-snapshot bin/cli.dart
    . Run the resulting
    .aot
    file using
    dartaotruntime
    .
<details> <summary>Cross-Compilation Targets (Linux Only)</summary>
Dart supports cross-compiling to Linux from macOS, Windows, or Linux hosts. Use the
--target-os
and
--target-arch
flags with
dart compile exe
or
dart compile aot-snapshot
.
  • --target-os=linux
    (Only Linux is currently supported as a cross-compilation target)
  • --target-arch=arm64
    (64-bit ARM)
  • --target-arch=x64
    (x86-64)
  • --target-arch=arm
    (32-bit ARM)
  • --target-arch=riscv64
    (64-bit RISC-V)
Example:
dart compile exe --target-os=linux --target-arch=arm64 bin/cli.dart
</details>
根据分发需求选择合适的编译目标。
  • 开发期间本地测试:使用
    dart run bin/cli.dart
    。该方式使用JIT编译器实现快速迭代。
  • 打包代码资源和动态库:使用
    dart build cli
    。该命令会运行构建钩子并输出到
    build/cli/_/bundle/
    目录。
  • 分发独立原生可执行文件:使用
    dart compile exe bin/cli.dart -o <output_path>
    。该命令会将Dart运行时和机器代码打包成单个文件。
  • 分发多个应用且严格限制磁盘空间:使用
    dart compile aot-snapshot bin/cli.dart
    。使用
    dartaotruntime
    运行生成的
    .aot
    文件。
<details> <summary>跨编译目标(仅Linux)</summary>
Dart支持从macOS、Windows或Linux主机交叉编译到Linux。 在
dart compile exe
dart 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>

Workflows

工作流程

Task Progress: Implement a New CLI Command

任务进度:实现新CLI命令

  • Create a new class extending
    Command
    in
    lib/src/commands/
    .
  • Define the
    name
    and
    description
    properties.
  • Register command-specific flags in the constructor using
    argParser.addFlag()
    or
    argParser.addOption()
    .
  • Implement the
    run()
    method with the core logic.
  • Register the new command in the
    CommandRunner
    instance in
    bin/cli.dart
    using
    addCommand()
    .
  • Create tests for the new command in the
    test/
    directory using
    test_process
    or standard tests.
  • Run validator -> Execute
    dart run bin/cli.dart help <command_name>
    to verify help text generation.
  • Verify final UX: Compile the application using
    dart compile exe
    and run the resulting executable to verify the target user experience (e.g.,
    ./bin/cli <command>
    ).
  • lib/src/commands/
    中创建一个继承自
    Command
    的新类。
  • 定义
    name
    description
    属性。
  • 在构造函数中使用
    argParser.addFlag()
    argParser.addOption()
    注册命令专属标志。
  • 实现包含核心逻辑的
    run()
    方法。
  • bin/cli.dart
    CommandRunner
    实例中使用
    addCommand()
    注册新命令。
  • test/
    目录中使用
    test_process
    或标准测试为新命令创建测试。
  • 运行验证:执行
    dart run bin/cli.dart help <command_name>
    验证帮助文本生成是否正确。
  • 验证最终UX:使用
    dart compile exe
    编译应用,运行生成的可执行文件验证目标用户体验(例如
    ./bin/cli <command>
    )。

Task Progress: Compile and Release Native Executable

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

  • Run validator -> Execute
    dart format . --set-exit-if-changed
    to ensure code formatting.
  • Run validator -> Execute
    dart analyze
    to ensure no static analysis errors.
  • Run validator -> Execute
    dart test
    to pass all integration tests.
  • Compile for host OS:
    dart compile exe bin/cli.dart -o build/cli-host
  • Compile for Linux (if host is macOS/Windows):
    dart compile exe --target-os=linux --target-arch=x64 bin/cli.dart -o build/cli-linux-x64
  • 运行验证:执行
    dart format . --set-exit-if-changed
    确保代码格式正确。
  • 运行验证:执行
    dart analyze
    确保无静态分析错误。
  • 运行验证:执行
    dart test
    确保所有集成测试通过。
  • 为宿主系统编译:
    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

Examples

示例

Example: CommandRunner Implementation

示例:CommandRunner实现

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

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

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

  
  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);
    }
  });
}
dart
import 'dart:io';
import 'package:args/command_runner.dart';
import 'package:stack_trace/stack_trace.dart';

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

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

  
  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);
    }
  });
}

Example: Integration Testing with Subprocesses

示例:使用子进程进行集成测试

dart
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();
  });
}
dart
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. 设置模拟文件系统
    await d.dir('project', [
      d.file('config.json', '{"key": "value"}')
    ]).create();

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

    // 3. 验证stdout流
    await expectLater(process.stdout, emitsThrough('Processing complete.'));

    // 4. 验证退出码
    await process.shouldExit(0);

    // 5. 验证文件系统变更
    await d.dir('project', [
      d.file('config.json', '{"key": "value"}'),
      d.file('output.log', 'Success')
    ]).validate();
  });
}