mcpField
D
dart-build-cli-app

dart-build-cli-app

github.com/flutter開発3.1k14.5k 回インストール2 日前に更新
リポジトリ

dart-build-cli-app について

Dart のコマンドラインアプリを作る開発者向けのガイドです。設計、引数処理、テスト、配布に加え、非同期終了、端末の復元、AOT ビルド時のサブプロセス処理を扱います。

主な機能

  1. 01CLI のエントリポイントと構成設計
  2. 02引数解析とサブコマンドの振り分け
  3. 03標準出力とエラー出力の分離
  4. 04シグナル処理と端末状態の復元
  5. 05コマンド処理とプロセスのテスト
  6. 06CLI プログラムのコンパイルと配布

活用シーン

  1. 01新しい Dart CLI プロジェクトの設計
  2. 02既存 CLI の終了処理の見直し
  3. 03コマンドラインツールのテスト
  4. 04AOT コンパイルと配布の準備

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/flutter/agent-plugins --skill dart-build-cli-app

インストール後、Claude Code で/dart-build-cli-appを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

examples/
multi_command_runner.dart
single_command_tool.dart
references/
aot_sdk_discovery.md
signals_and_terminal.md

詳細

Dart CLI アプリケーションの構築

目次


1. コアアーキテクチャとプロセスのライフサイクル

破壊的な終了(exit(N))を避ける

dart:io の exit(int code) を呼び出すと、C++ ランタイムで Platform::Exit(code) が実行されます。Dart のスタックを巻き戻さずに、OS プロセスを直ちに終了します。

  • デバッガーの切断:--pause-isolates-on-exit を指定して起動すると、VM Service は IDE で調査できるよう、シャットダウン前にアイソレートを一時停止します。exit() は VM Service が一時停止や状態の調査を行う前に、OS プロセスを終了させます。
  • カバレッジの損失:package:coverage は、一時停止状態での VM Service RPC を通じて実行行を取得します。exit() は RPC による取得前にプロセスを破棄するため、カバレッジは 0% になります。
  • バッファーの切り捨て:stdout と stderr は、非同期でバッファリングされる IOSink ストリームです。exit() は未フラッシュのバイトを破棄します。
  • リソースリーク:finally ブロック(ロックの解放、一時ディレクトリの削除など)が実行されません。

ルール:通常の実行中に exit(code) を直接呼び出すのは避けてください。exitCode = code を設定するか、CommandRunner<int>(package:args から)から終了コードを整数で返し、非同期の main() 関数が自然に戻るようにします。未処理のエラーに対して exit() を呼び出してはいけません。ランタイムが適切にスタックを巻き戻してゼロ以外のステータスで終了できるよう、未処理の Error または例外をスローしてください。

標準 POSIX 終了コード(/usr/include/sysexits.h):

  • 0:成功(EX_OK / ExitCode.success.code)
  • 64:コマンドラインの使用方法エラー(EX_USAGE / ExitCode.usage.code)
  • 65:データ形式エラー(EX_DATAERR / ExitCode.data.code)
  • 70:ソフトウェア内部のクラッシュ(EX_SOFTWARE / ExitCode.software.code)
  • 78:構成エラー(EX_CONFIG / ExitCode.config.code)

注:マジックナンバーの整数リテラルではなく、package:io/io.dart をインポートし、ExitCode 定数(例:ExitCode.usage.code、ExitCode.software.code)を使用してください。パッケージへの依存関係がない最小限の単独スクリプトでは、標準 POSIX の整数リテラル(0、64、70)を使用してもかまいません。

import 'dart:io';
import 'package:args/command_runner.dart';
import 'package:io/io.dart' show ExitCode; // Provides standard POSIX ExitCode constants

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;
  }
}
薄いエントリーポイントのパターン(bin/ と lib/src/)

bin/*.dart ファイルは、ランナーをインスタンス化し、args を渡して終了コードを待つだけの、最小限のエントリーポイントの中継役として維持してください。すべてのコマンド定義、引数パーサー、フォーマッター、ビジネスロジックは lib/src/ に配置します。

  • 理由:bin/ のコードは package: URI 経由で簡単にインポートできません。ロジックを lib/src/ に移すことで、OS のサブプロセスを起動せずに、コマンドランナー全体、サブコマンド階層、ビジネスロジックをインメモリで数ミリ秒(< 2ms)以内にユニットテストできます。
// bin/my_cli.dart — Thin entrypoint trampoline
import 'dart:io';
import 'package:my_cli/src/cli.dart';

Future<void> main(List<String> args) async {
  exitCode = await runCli(args);
}

2. 出力、診断、フォーマット

  • データと診断情報:プログラムの意図した結果と機械可読データは、stdout のみに出力してください。警告、エラーメッセージ、デバッグログは、stderr のみに出力してください。
  • エラー時の使用法ルール:引数の解析エラーまたは必須オプションのエラー(FormatException、UsageException、または mandatory: true のオプションへのアクセス時に、オプションが指定されていないため results.option(...) がスローする ArgumentError)が発生した場合、エラーメッセージと使用方法のテキストは、どちらも stderr に出力する必要があります。終了コードには 64(EX_USAGE / ExitCode.usage.code)を返してください。ユーザーが --help または -h を明示的に指定した場合に限り、使用方法のヘルプを stdout に出力してください。
  • エラーハンドラーで print() を使わない:print() は stdout に出力します。すべての失敗通知には stderr.writeln() を使用してください。標準出力には、avoid_print リントルールに準拠するため、print() の代わりに stdout.writeln() を使用してください(ただし、analysis_options.yaml で avoid_print: false が明示的に設定されている場合を除きます)。
  • ターミナル機能の検出と NO_COLOR:ANSI カラーやカーソルのエスケープコードを出力する前に、stdout.hasTerminal、stdout.supportsAnsiEscapes、および !Platform.environment.containsKey('NO_COLOR') を確認してください。
    bool get useAnsi =>
        stdout.hasTerminal &&
        stdout.supportsAnsiEscapes &&
        !Platform.environment.containsKey('NO_COLOR');
    
  • 機械可読モード:--json または --machine フラグが渡された場合は、データを JSON 形式で stdout に出力し、ログは stderr に出力してください。

3. プロジェクトの構成とパッケージ化

スキャフォールディングと Pubspec の実行可能ファイルのマッピング(executables:)

dart create -t console <package_name> を使って新しいコマンドラインプロジェクトをスキャフォールドすると、標準の bin/ および lib/ レイアウトが初期化されます。pubspec.yaml の executables: の下に必ず実行可能ファイルを宣言し、コマンド名を bin/ 内のスクリプトにマッピングしてください。これにより、dart run <command>(bin/...dart を指定せずに実行できます)で簡単に呼び出せるようになり、dart install 用のグローバルなバイナリシンボリックリンクも構成されます。

name: my_cli
description: High-performance CLI tool.
version: 1.0.0

executables:
  my_cli: # Maps to bin/my_cli.dart
  secondary_cmd: helper # Maps to bin/helper.dart
単一ソースでのバージョン管理(package:build_version)

bin/*.dart に --version の文字列をハードコードしたり、定数ファイルを手動で同期したりすることは避けてください。package:build_version を使用すると、ビルド時に pubspec.yaml から const packageVersion = 'x.y.z'; を含む lib/src/version.dart が生成されます。

キャッシュの規約

一時的なキャッシュファイルは .dart_tool/<package_name>/ に保存してください。プロジェクトのルートに永続的なキャッシュファイルを直接書き込まないでください。


4. 引数の解析とコマンドのルーティング

コマンドライン引数を管理するには、package:args をインポートしてください。

  • 単純なスクリプト:ArgParser を addFlag() および addOption() と直接組み合わせて使用してください。
  • 複数コマンドのツール:CommandRunner<int> を実装し、各サブコマンドには Command<int> を拡張して、POSIX の終了コードを直接返してください。
  • 型安全なアクセサー:手動での型キャスト(as bool、as String?)をなくすため、マップのインデックス参照 operator [] の代わりに、results.flag('name')、results.option('name')、results.multiOption('name')(package:args 2.5 以降で利用可能)を使用してください。
  • 複雑なオプションモデル:フラグが多いアプリケーションでは、package:build_cli を使用して、型安全性の高いオプションクラスを生成してください。名前付きのデフォルト値オーバーライド(例:{String? hostDefaultOverride})を活用すると、構成ファイルと CLI フラグを簡潔に統合できます。

5. ネイティブな非同期処理と最新のスタックトレース

  • Chain.capture() を避ける:Dart VM は、await の中断ポイントをまたいで非同期スタックフレームをネイティブに保持します。Chain.capture はカスタム Zone でイベントループをラップするため、割り当てのオーバーヘッドが大幅に増加し、Zone の境界を越えてエラーを捕捉します。
  • Trace.from(st).terse で整形:Zone をキャプチャせず、未捕捉のエラーに対して package:stack_trace の静的ユーティリティを使用してください。
import 'dart:io';
import 'package:io/io.dart' show ExitCode;
import 'package:stack_trace/stack_trace.dart';

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

6. サブプロセスの起動と AOT の耐障害性

Dart SDK のサブプロセスを起動したり、ほかの Dart ツール(例:dart format、dart test、build_runner)を実行したりする場合:

  • Platform.resolvedExecutable や Platform.executable が dart コマンドライン実行ファイルを指すとは限りません:スタンドアロンの AOT コンパイル済みバイナリ(dart install/dart compile exe)では、resolvedExecutable はコンパイル済みアプリケーションのバイナリを指すため、再帰的な自己起動ループや、フラグの拒否によるクラッシュが発生します。
  • package:cli_util を使用してください:独自に PATH やディレクトリを走査して処理を実装するのではなく、cli_util.dartExecutable または cli_util.sdkPath を使用して Dart SDK の実行ファイルを解決してください。
  • バージョン要件と詳しい技術ガイドについては、references/aot_sdk_discovery.md を参照してください。

7. シグナル処理とターミナルの終了処理

CLI がターミナルモードを変更する、スピナーを表示する、またはリッスンソケットを開く場合:

  • Windows のシグナルガード:Windows では、ProcessSignal.sigterm.watch() は UnsupportedError をスローします。if (!Platform.isWindows) で sigterm をガードしてください。
  • エコーと行モードの終了処理:stdin.echoMode = false または stdin.lineMode = false を設定する場合は、最初に if (!stdin.hasTerminal) return; を確認し、終了後もユーザーのキー入力が表示されるよう、SIGINT リスナーと finally ブロックを設置して状態を復元してください。
  • カーソルの表示:ANSI のカーソル非表示コード(\x1B[?25l)を出力する場合は、終了時またはキャンセル時に必ずカーソルを再表示(\x1B[?25h)してください。
  • ソケットのクリーンアップ:終了シグナルを受け取ったら、リッスン中の HttpServer または ServerSocket インスタンスを明示的に閉じ(server.close(force: true))、OS のポートを直ちに解放してください。
  • 詳しい実装パターンについては、references/signals_and_terminal.md を参照してください。

8. CLI アプリケーションのテスト

テストは、次の 2 つの異なる層に分けて構成してください。

  1. ユニットテスト(インメモリ、< 5ms):test/ 内で package:<pkg>/src/... をインポートし、コマンドクラス、オプションの解析、ビジネスロジックを直接テストします。
  2. 統合テスト(サブプロセス):package:test_process と package:test_descriptor を使用して、バイナリのエンドツーエンド実行、プロセスの I/O ストリーミング、OS の終了コードを検証します。
import 'package:test/test.dart';
import 'package:test_descriptor/test_descriptor.dart' as d;
import 'package:test_process/test_process.dart';

void main() {
  test('CLI processes input and exits cleanly', () async {
    await d.file('input.txt', 'hello').create();

    final process = await TestProcess.start('dart', [
      'run',
      'bin/my_cli.dart',
      '--input',
      d.path('input.txt'),
    ]);

    await expectLater(process.stdout, emitsThrough('Processing complete.'));
    await process.shouldExit(0);
  });
}

9. 最新のコンパイルと配布

Dart 3.12+ では、CLI の配布方式が dart run と dart install を中心に標準化されています(dart pub global activate から移行)。

  • 一時的な実行(JIT):dart run <package>@<version> [args] は、CLI をオンデマンドでダウンロードして実行します。
  • グローバルインストール(ネイティブ AOT):dart install <package> は、パッケージのエントリーポイントを ~/.dart/install/bin/ に高速なネイティブのスタンドアロンバイナリとしてコンパイルします。
  • ローカル開発:dart run <command>(pubspec.yaml の executables: を通じて解決)または dart run bin/cli.dart を使用してください。
  • 動的ライブラリとコードアセットのバンドル:dart build cli を使用してください。出力バンドルは build/cli/_/bundle/ に作成されます。
  • スタンドアロン実行ファイルのコンパイル:dart compile exe bin/cli.dart -o <output_path> を使用してください。

10. ワークフローと監査チェックリスト

実装ワークフロー
  • pubspec.yaml の executables: の下にエントリーポイントを宣言します。
  • bin/*.dart は薄いエントリーポイントに留め、コマンドのロジックは lib/src/ に配置します。
  • 整数の終了コードを返すか、exitCode = N を設定します。exit(N) を直接呼び出すのは避けてください。
  • 解析に失敗した場合、エラー、警告、使用方法を stderr に出力します。
  • 型安全性を確保するため、results.flag()、results.option()、results.multiOption() を使用します。
  • ANSI コードを出力する前に、stdout.hasTerminal、supportsAnsiEscapes、NO_COLOR を確認して useAnsi を検証します。
  • 子ツールの起動には cli_util.dartExecutable を使用し、Platform.resolvedExecutable は決して使用しません。
  • コマンドランナーはインメモリでユニットテストし、バイナリのエンドツーエンド実行は test_process でテストします。

References & Examples

コメント