dart-build-cli-app

dart-build-cli-app

熱門

進入點結構、結束碼、跨平台指令碼。適用於建置命令列工具、指令碼或應用程式。

2792星標
164分支
更新於 2026/8/5
SKILL.md
唯讀
名稱
dart-build-cli-app
描述

進入點結構、結束碼、跨平台指令碼。適用於建置命令列工具、指令碼或應用程式。

建置 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)。
  • 若有多個非同步接聽器(listener)需要循序存取標準輸入(stdin),請使用 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)驗證標準輸出(stdout)與錯誤串流(stderr)。
  • 使用 await process.shouldExit(0) 斷言最終的結束碼。
  • 使用 await d.Descriptor.validate() 驗證變更後的檔案系統狀態。

編譯與發布

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

  • 若在開發期間進行本機測試: 使用 dart run bin/cli.dart。這會使用 JIT 編譯器以支援快速迭代。
  • 若要打包程式碼資產與動態函式庫: 使用 dart build cli。這會執行建置掛鉤(build hooks)並輸出至 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 exedart 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>

工作流程

任務進度:實作新的 CLI 指令

  • [ ] 在 lib/src/commands/ 中建立一個繼承 Command 的新類別。
  • [ ] 定義 namedescription 屬性。
  • [ ] 在建構子中使用 argParser.addFlag()argParser.addOption() 註冊該指令專用的旗標。
  • [ ] 實作包含核心邏輯的 run() 方法。
  • [ ] 在 bin/cli.dartCommandRunner 實例中使用 addCommand() 註冊新指令。
  • [ ] 使用 test_process 或標準測試在 test/ 目錄中為新指令建立測試。
  • [ ] 執行驗證器 -> 執行 dart run bin/cli.dart help <command_name> 以驗證說明文字的生成。
  • [ ] 驗證最終 UX:使用 dart compile exe 編譯應用程式並執行產出的可執行檔,以驗證目標使用者體驗(例如 ./bin/cli <command>)。

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

  • [ ] 執行驗證器 -> 執行 dart format . --set-exit-if-changed 以確保程式碼格式正確。
  • [ ] 執行驗證器 -> 執行 dart analyze 以確保沒有靜態分析錯誤。
  • [ ] 執行驗證器 -> 執行 dart test 以通過所有整合測試。
  • [ ] 為目前主機 OS 編譯: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 = 'Record changes to the repository.';

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

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

範例:使用子行程進行整合測試

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. 驗證標準輸出串流
    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();
  });
}