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

参数解析与命令路由

Implement 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.
使用
args
包来管理命令行参数、标志和子命令。
  • 如果构建简单脚本:直接使用
    ArgParser
    定义标志(
    addFlag
    )和选项(
    addOption
    )。
  • 如果构建复杂的多命令CLI(如
    git
    ):实现
    CommandRunner
    并为每个子命令扩展
    Command
    类。
  • CommandRunner.argParser
    上定义全局参数,在各个
    Command.argParser
    上定义命令专属参数。
  • 捕获
    UsageException
    以优雅处理无效参数,并显示自动生成的帮助文本。

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.
利用
io
stack_trace
包构建健壮的生产级CLI工具。
  • 使用
    io
    包的
    ExitCode
    枚举返回标准POSIX退出码(例如
    ExitCode.success.code
    ExitCode.usage.code
    )。
  • 如果多个异步监听器需要顺序访问标准输入,使用
    io
    包中的
    sharedStdIn
  • 使用
    stack_trace
    包中的
    Chain.capture()
    包裹应用执行过程,以跟踪异步调用栈链。
  • 使用
    Trace.terse
    Chain.terse
    格式化输出栈跟踪,去除冗余的核心库帧,为用户呈现易读的错误信息。

Testing CLI Applications

CLI应用程序测试

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()
    .
使用
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()
    .
  • Run validator -> Execute
    dart run bin/cli.dart help <command_name>
    to verify help text generation.
  • lib/src/commands/
    中创建一个继承自
    Command
    的新类。
  • 定义
    name
    description
    属性。
  • 在构造函数中使用
    argParser.addFlag()
    argParser.addOption()
    注册命令专属标志。
  • 实现包含核心逻辑的
    run()
    方法。
  • bin/cli.dart
    CommandRunner
    实例中使用
    addCommand()
    注册新命令。
  • 运行验证器 -> 执行
    dart run bin/cli.dart help <command_name>
    以验证帮助文本生成是否正确。

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