dart-build-cli-app

dart-build-cli-app

熱門

進入點結構、退出碼、跨平台腳本。用於建構命令列工具、腳本或應用程式。

392星標
25分支
更新於 2026/7/10
SKILL.md
readonlyread-only
name
dart-build-cli-app
description

進入點結構、退出碼、跨平台腳本。用於建構命令列工具、腳本或應用程式。

建構 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 套件來管理命令列引數、旗標和子命令。

  • 如果建構簡單腳本:直接使用 ArgParser 定義旗標(addFlag)和選項(addOption)。
  • 如果建構複雜的多命令 CLI(如 git):實作 CommandRunner 並為每個子命令擴充 Command
  • CommandRunner.argParser 上定義全域引數,在個別 Command.argParser 上定義命令專屬引數。
  • 捕捉 UsageException 以優雅地處理無效引數並顯示自動產生的說明文字。
  • 驗證說明文字的正確性:確保說明文字提供執行工具所需的所有資訊。如果說明文字參考了已編譯的可執行檔名稱,且使用者需要將其加入 PATH 才能以該方式執行,請在說明文字或描述中提供明確的指示。

執行與錯誤處理

利用 iostack_trace 套件來建構穩健、可上線的 CLI 工具。

  • 使用 io 套件的 ExitCode 列舉來回傳標準 POSIX 退出碼(例如 ExitCode.success.codeExitCode.usage.code)。
  • 如果多個非同步監聽器需要依序存取標準輸入,請使用 io 套件的 sharedStdIn
  • 使用 stack_trace 套件的 Chain.capture() 包裝應用程式執行,以追蹤非同步堆疊鏈。
  • 使用 Trace.terseChain.terse 格式化輸出堆疊追蹤,以去除雜亂的核心函式庫框架,並向使用者呈現可讀的錯誤。
  • 除非可以復原,否則不要在底層邏輯或儲存類別中吞掉例外。讓它們向上傳遞或重新拋出,以便上層命令知道操作失敗。
  • 快速失敗並使用非零退出碼:確保操作失敗會向 stderr 輸出描述性錯誤訊息,並使用適當的非零退出碼(例如,使用 exit(1) 或在捕捉到 UsageException 後觸發退出碼 64)。

測試 CLI 應用程式

[!IMPORTANT]
所有新命令和重要功能都必須有自動化測試覆蓋。 手動驗證不足以測試邏輯。不過,仍然需要手動驗證說明文字和使用者體驗(UX),以確保介面直觀且正確。

使用 test_processtest_descriptor 為 CLI 編寫高保真整合測試。

  • 使用 test_descriptord.dird.file)定義預期的檔案系統狀態。
  • 在執行前使用 await d.Descriptor.create() 建立模擬檔案系統。
  • 使用 TestProcess.start('dart', ['run', 'bin/cli.dart', ...args]) 啟動 CLI 程序。
  • 使用 StreamQueue 匹配器(例如 emitsThroughemits)驗證標準輸出和錯誤串流。
  • 使用 await process.shouldExit(0) 斷言最終退出碼。
  • 使用 await d.Descriptor.validate() 驗證結果的檔案系統變更。

編譯與發布

根據發布需求選擇適當的編譯目標。

  • 如果在開發期間進行本地測試: 使用 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。
使用 --target-os--target-arch 旗標搭配 dart compile exedart compile aot-snapshot

  • --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> 以驗證說明文字產生。
  • [ ] 驗證最終使用者體驗:使用 dart compile exe 編譯應用程式,並執行產生的可執行檔以驗證目標使用者體驗(例如 ./bin/cli <command>)。

任務進度:編譯並發布原生可執行檔

  • [ ] 執行驗證器 -> 執行 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

範例

範例: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 = '記錄對儲存庫的變更。';

  CommitCommand() {
    argParser.addFlag('all', abbr: 'a', help: '提交所有已變更的檔案。');
  }

  @override
  Future<void> run() async {
    final commitAll = argResults?['all'] as bool? ?? false;
    print('正在提交...(全部:$commitAll)');
  }
}

void main(List<String> args) {
  Chain.capture(() async {
    final runner = CommandRunner('dgit', '分散式版本控制。')
      ..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('致命錯誤:$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 正確格式化輸出並修改檔案系統', () 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('處理完成。'));

    // 4. 驗證退出碼
    await process.shouldExit(0);

    // 5. 驗證檔案系統變更
    await d.dir('project', [
      d.file('config.json', '{"key": "value"}'),
      d.file('output.log', '成功')
    ]).validate();
  });
}