git:20260909.998370c to git:20261001.6c38d68

197 added, 131 removed. Audit A to A.

---
name: dart-build-cli-app
- description: Entrypoint structure, exit codes, cross-platform scripts. Use when building command line utilities, scripts, or applications.
- metadata:
- model: models/gemini-3.1-pro-preview
- last_modified: Fri, 04 May 2026 17:41:00 GMT
+ description: 命令行工具的入口结构、退出码与跨平台脚本编写规范。
+ en_description: Entrypoint structure, exit codes, cross-platform scripts. Use when building command line utilities, scripts, or applications.
+ zh_displayName: Dart CLI 应用构建
+ category: 移动端开发
+ en_category: Mobile Dev
---
# Building Dart CLI Applications
## Contents
- - [Project Setup & Architecture](#project-setup--architecture)
- - [Argument Parsing & Command Routing](#argument-parsing--command-routing)
- - [Execution & Error Handling](#execution--error-handling)
- - [Testing CLI Applications](#testing-cli-applications)
- - [Compilation & Distribution](#compilation--distribution)
- - [Workflows](#workflows)
- - [Examples](#examples)
+ * [1. Core Architecture & Process Lifecycle](#1-core-architecture--process-lifecycle)
+ * [2. Streams, Diagnostics & Formatting](#2-streams-diagnostics--formatting)
+ * [3. Project Configuration & Packaging](#3-project-configuration--packaging)
+ * [4. Argument Parsing & Command Routing](#4-argument-parsing--command-routing)
+ * [5. Native Async & Modern Stack Traces](#5-native-async--modern-stack-traces)
+ * [6. Subprocess Spawning & AOT Resilience](#6-subprocess-spawning--aot-resilience)
+ * [7. Signal Handling & Terminal Teardown](#7-signal-handling--terminal-teardown)
+ * [8. Testing CLI Applications](#8-testing-cli-applications)
+ * [9. Modern Compilation & Distribution](#9-modern-compilation--distribution)
+ * [10. Workflows & Audit Checklist](#10-workflows--audit-checklist)
+ * [References & Examples](#references--examples)
- ## Project Setup & Architecture
+ ---
- Initialize new CLI projects using the official Dart template to ensure standard directory structures.
+ ## 1. Core Architecture & Process Lifecycle
- * 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.
+ ### Avoid Destructive Exits (`exit(N)`)
+ Calling `dart:io`'s `exit(int code)` invokes `Platform::Exit(code)` in the C++ runtime. It immediately terminates the OS process without unwinding the Dart stack:
+ * **Debugger Disconnect**: When launched with `--pause-isolates-on-exit`, the VM Service pauses isolates before shutdown to allow IDE inspection. `exit()` terminates the OS process before the VM Service can pause or inspect state.
+ * **Coverage Loss**: `package:coverage` queries execution lines over VM Service RPCs during the paused-on-exit state. `exit()` destroys the process before RPC extraction, yielding 0% coverage.
+ * **Buffer Truncation**: `stdout` and `stderr` are buffered asynchronous `IOSink` streams. `exit()` drops unflushed bytes.
+ * **Resource Leaks**: `finally` blocks (closing locks, deleting temp directories) are bypassed.
- ## Argument Parsing & Command Routing
+ **Rule**: Avoid calling `exit(code)` directly during normal execution; set `exitCode = code` or return an integer exit code from `CommandRunner<int>` (from `package:args`) and allow the asynchronous `main()` function to return naturally. Do not call `exit()` on unhandled errors; throw an unhandled `Error` or exception so the runtime unwinds cleanly and exits with a non-zero status.
- Import the `args` package to manage command-line arguments, flags, and subcommands.
+ Standard POSIX exit codes (`/usr/include/sysexits.h`):
+ * `0`: Success (`EX_OK` / `ExitCode.success.code`)
+ * `64`: Command-line usage error (`EX_USAGE` / `ExitCode.usage.code`)
+ * `65`: Data format error (`EX_DATAERR` / `ExitCode.data.code`)
+ * `70`: Internal software crash (`EX_SOFTWARE` / `ExitCode.software.code`)
+ * `78`: Configuration error (`EX_CONFIG` / `ExitCode.config.code`)
- * 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.
+ *Note*: Prefer importing `package:io/io.dart` and using `ExitCode` constants
+ (e.g., `ExitCode.usage.code`, `ExitCode.software.code`) rather than magic
+ integer literals. For minimal standalone scripts without package dependencies,
+ standard POSIX integer literals (`0`, `64`, `70`) may be used.
- ## Execution & Error Handling
+ ```dart
+ import 'dart:io';
+ import 'package:args/command_runner.dart';
+ import 'package:io/io.dart' show ExitCode; // Provides standard POSIX ExitCode constants
- Leverage the `io` and `stack_trace` packages to build robust, production-ready CLI tools.
+ 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;
+ }
+ }
+ ```
- * 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`).
+ ### The Thin Entrypoint Pattern (`bin/` vs. `lib/src/`)
+ Keep `bin/*.dart` files strictly as minimal entrypoint trampolines (instantiate runner, pass `args`, await exit code). Place all command definitions, argument parsers, formatters, and business logic inside `lib/src/`.
- ## Testing CLI Applications
+ * **Rationale**: Code in `bin/` cannot be cleanly imported via `package:` URIs. Moving logic into `lib/src/` allows the entire command runner, subcommand hierarchy, and business logic to be unit-tested in-memory in milliseconds (`< 2ms`) without spawning OS subprocesses.
- > [!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.
+ ```dart
+ // bin/my_cli.dart — Thin entrypoint trampoline
+ import 'dart:io';
+ import 'package:my_cli/src/cli.dart';
- Use `test_process` and `test_descriptor` to write high-fidelity integration tests for your CLI.
+ Future<void> main(List<String> args) async {
+ exitCode = await runCli(args);
+ }
+ ```
- * 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()`.
+ ---
- ## Compilation & Distribution
+ ## 2. Output, Diagnostics & Formatting
- Select the appropriate compilation target based on your distribution requirements.
+ * **Data vs. Diagnostics**: Write intended program results and machine-readable data exclusively to `stdout`. Write warnings, error messages, and debug logs exclusively to `stderr`.
+ * **The Error Usage Rule**: When an argument parsing or mandatory option error
+ occurs (`FormatException`, `UsageException`, or `ArgumentError` thrown when
+ accessing a missing `mandatory: true` option via `results.option(...)`), **both
+ the error message and the usage text must write to `stderr`**, and exit code
+ `64` (`EX_USAGE` / `ExitCode.usage.code`) must be returned. `stdout` should
+ ONLY receive usage help when the user explicitly requests it via `--help` or
+ `-h`.
+ * **No `print()` in Error Handlers**: `print()` routes to `stdout`. Use `stderr.writeln()` for all failure notifications. For standard output, prefer `stdout.writeln()` over `print()` to comply with the [`avoid_print`](https://dart.dev/tools/linter-rules/avoid_print) lint rule (unless `analysis_options.yaml` explicitly configures `avoid_print: false`).
+ * **Terminal Capability Detection & `NO_COLOR`**: Verify `stdout.hasTerminal`, `stdout.supportsAnsiEscapes`, and `!Platform.environment.containsKey('NO_COLOR')` before emitting ANSI color or cursor escape codes:
+ ```dart
+ bool get useAnsi =>
+ stdout.hasTerminal &&
+ stdout.supportsAnsiEscapes &&
+ !Platform.environment.containsKey('NO_COLOR');
+ ```
+ * **Machine-Readable Modes**: When `--json` or `--machine` flags are passed, format data as JSON to `stdout` and route logs to `stderr`.
- * **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>
+ ## 3. Project Configuration & Packaging
- 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`.
+ ### Scaffolding & Pubspec Executable Mapping (`executables:`)
+ Scaffold new command-line projects using `dart create -t console <package_name>`, which initializes the standard `bin/` and `lib/` layout. Always declare executables in `pubspec.yaml` under `executables:` to map command names to scripts in `bin/`, enabling clean invocation via `dart run <command>` (without specifying `bin/...dart`) and configuring global binary symlinks for `dart install`:
- * `--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)
+ ```yaml
+ name: my_cli
+ description: High-performance CLI tool.
+ version: 1.0.0
- Example: `dart compile exe --target-os=linux --target-arch=arm64 bin/cli.dart`
- </details>
+ executables:
+ my_cli: # Maps to bin/my_cli.dart
+ secondary_cmd: helper # Maps to bin/helper.dart
+ ```
- ## Workflows
+ ### Single-Source Versioning (`package:build_version`)
+ Avoid hardcoding `--version` strings in `bin/*.dart` or manually synchronizing constant files. Use `package:build_version` to generate `lib/src/version.dart` containing `const packageVersion = 'x.y.z';` directly from `pubspec.yaml` during builds.
- ### Task Progress: Implement a New CLI Command
- - [ ] 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>`).
+ ### Caching Conventions
+ Store transient cache files in `.dart_tool/<package_name>/`. Never write persistent cache files directly to the project root.
- ### 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`
+ ---
- ## Examples
+ ## 4. Argument Parsing & Command Routing
- ### Example: CommandRunner Implementation
+ Import `package:args` to manage command-line arguments:
- ```dart
- import 'dart:io';
- import 'package:args/command_runner.dart';
- import 'package:stack_trace/stack_trace.dart';
+ * **Simple Scripts**: Use `ArgParser` directly with `addFlag()` and `addOption()`.
+ * **Multi-Command Tools**: Implement `CommandRunner<int>` and extend `Command<int>` for each subcommand, returning POSIX exit codes directly.
+ * **Type-Safe Accessors**: Use `results.flag('name')`, `results.option('name')`, and `results.multiOption('name')` (available in `package:args` 2.5+) instead of map indexing `operator []` to eliminate manual type casts (`as bool`, `as String?`).
+ * **Complex Options Models**: For applications with extensive flags, use `package:build_cli` to generate strongly-typed options classes. Leverage named default overrides (e.g. `{String? hostDefaultOverride}`) to cleanly merge configuration files with CLI flags.
- 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.');
- }
+ ## 5. Native Async & Modern Stack Traces
- @override
- Future<void> run() async {
- final commitAll = argResults?['all'] as bool? ?? false;
- print('Committing... (All: $commitAll)');
- }
- }
+ * **Avoid `Chain.capture()`**: The Dart VM natively preserves asynchronous stack frames across `await` suspension points. `Chain.capture` wraps the event loop in custom Zones, incurring substantial allocation overhead and trapping errors across Zone boundaries.
+ * **Sanitize with `Trace.from(st).terse`**: Use static utilities from `package:stack_trace` on uncaught errors without capturing zones:
- void main(List<String> args) {
- Chain.capture(() async {
- final runner = CommandRunner('dgit', 'Distributed version control.')
- ..addCommand(CommitCommand());
+ ```dart
+ import 'dart:io';
+ import 'package:io/io.dart' show ExitCode;
+ import 'package:stack_trace/stack_trace.dart';
- 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);
+ 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;
+ }
}
```
- ### Example: Integration Testing with Subprocesses
+ ---
+ ## 6. Subprocess Spawning & AOT Resilience
+
+ When spawning Dart SDK subprocesses or executing other Dart tools (e.g., `dart format`, `dart test`, `build_runner`):
+
+ * **Do not assume `Platform.resolvedExecutable` or `Platform.executable` points to the `dart` command-line executable**: In standalone AOT-compiled binaries (`dart install` / `dart compile exe`), `resolvedExecutable` points to the compiled application binary itself, causing recursive self-invocation loops or flag rejection crashes.
+ * **Use `package:cli_util`**: Resolve the Dart SDK executable using `cli_util.dartExecutable` or `cli_util.sdkPath` instead of writing custom PATH or directory scrapers.
+ * See version requirements and detailed technical guide in [references/aot_sdk_discovery.md](references/aot_sdk_discovery.md).
+
+ ---
+
+ ## 7. Signal Handling & Terminal Teardown
+
+ If your CLI alters terminal modes, displays spinners, or opens listening sockets:
+
+ * **Windows Signal Guard**: On Windows, `ProcessSignal.sigterm.watch()` throws `UnsupportedError`. Guard `sigterm` with `if (!Platform.isWindows)`.
+ * **Echo & Line Mode Teardown**: If setting `stdin.echoMode = false` or `stdin.lineMode = false`, check `if (!stdin.hasTerminal) return;` first, and install a `SIGINT` listener and `finally` block to restore them so user keystrokes remain visible after exit.
+ * **Cursor Visibility**: If emitting ANSI hide-cursor (`\x1B[?25l`), always restore cursor visibility (`\x1B[?25h`) on exit or cancellation.
+ * **Socket Cleanup**: Explicitly close listening `HttpServer` or `ServerSocket` instances (`server.close(force: true)`) on termination signals to immediately release OS ports.
+ * See detailed patterns in [references/signals_and_terminal.md](references/signals_and_terminal.md).
+
+ ---
+
+ ## 8. Testing CLI Applications
+
+ Structure testing across two distinct layers:
+
+ 1. **Unit Tests (In-Memory, `< 5ms`)**: Test command classes, option parsing, and business logic directly by importing `package:<pkg>/src/...` in `test/`.
+ 2. **Integration Tests (Subprocesses)**: Use `package:test_process` and `package:test_descriptor` to verify end-to-end binary execution, process I/O streaming, and OS exit codes:
+
```dart
import 'package:test/test.dart';
- import 'package:test_process/test_process.dart';
import 'package:test_descriptor/test_descriptor.dart' as d;
+ import 'package:test_process/test_process.dart';
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();
+ test('CLI processes input and exits cleanly', () async {
+ await d.file('input.txt', 'hello').create();
- // 2. Spawn the CLI process
- final process = await TestProcess.start(
- 'dart',
- ['run', 'bin/cli.dart', 'process', '--path', '${d.sandbox}/project']
- );
+ final process = await TestProcess.start('dart', [
+ 'run',
+ 'bin/my_cli.dart',
+ '--input',
+ d.path('input.txt'),
+ ]);
- // 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();
});
}
```
+
+ ---
+
+ ## 9. Modern Compilation & Distribution
+
+ Dart 3.12+ standardizes CLI distribution around `dart run` and `dart install` (moving away from `dart pub global activate`):
+
+ * **Ephemeral Execution (JIT)**: `dart run <package>@<version> [args]` downloads and runs the CLI on demand.
+ * **Global Installation (Native AOT)**: `dart install <package>` compiles the package entrypoint to a fast native standalone binary in `~/.dart/install/bin/`.
+ * **Local Development**: Use `dart run <command>` (resolves via `executables:` in `pubspec.yaml`) or `dart run bin/cli.dart`.
+ * **Bundling Dynamic Libraries & Code Assets**: Use `dart build cli`. Outputs bundle to `build/cli/_/bundle/`.
+ * **Standalone Executable Compilation**: Use `dart compile exe bin/cli.dart -o <output_path>`.
+
+ ---
+
+ ## 10. Workflows & Audit Checklist
+
+ ### Implementation Workflow
+ - [ ] Declare entry points in `pubspec.yaml` under `executables:`.
+ - [ ] Keep `bin/*.dart` as a thin entrypoint; place command logic in `lib/src/`.
+ - [ ] Return integer exit codes or set `exitCode = N`; avoid raw `exit(N)`.
+ - [ ] Direct errors, warnings, and usage on parse failure to `stderr`.
+ - [ ] Use `results.flag()`, `results.option()`, and `results.multiOption()` for type safety.
+ - [ ] Validate `useAnsi` (checking `stdout.hasTerminal`, `supportsAnsiEscapes`, and `NO_COLOR`) before emitting ANSI codes.
+ - [ ] Spawn child tools using `cli_util.dartExecutable`, never `Platform.resolvedExecutable`.
+ - [ ] Unit-test command runners in-memory; test end-to-end binary execution with `test_process`.
+
+ ---
+
+ ## References & Examples
+
+ * **Single-Command Tool Template**: [examples/single_command_tool.dart](examples/single_command_tool.dart)
+ * **Multi-Command Runner Template**: [examples/multi_command_runner.dart](examples/multi_command_runner.dart)
+ * **AOT SDK Discovery & Subprocess Spawning**: [references/aot_sdk_discovery.md](references/aot_sdk_discovery.md)
+ * **Signal Handling & Terminal Teardown**: [references/signals_and_terminal.md](references/signals_and_terminal.md)