mcpField
D
dart-build-cli-app

dart-build-cli-app

github.com/dhruvanbhalara開発3059 回インストール9 週間前に更新
リポジトリ

dart-build-cli-app について

Dartでコマンドラインアプリを開発するための実践ガイドです。プロジェクト構成、引数処理、テスト、コンパイルと配布の手順を扱います。

主な機能

  1. 01Dart CLIのプロジェクト構成
  2. 02引数とサブコマンドのルーティング
  3. 03エラー処理と終了コード
  4. 04サブプロセスの統合テスト
  5. 05コンパイルと配布の手順

活用シーン

  1. 01新しいDart CLIプロジェクトの作成
  2. 02引数付きCLIサブコマンドの追加
  3. 03コマンド出力とファイル変更のテスト
  4. 04単体のネイティブ実行ファイルのコンパイル

インストール

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

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

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

詳細

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

目次

プロジェクトのセットアップとアーキテクチャ

標準的なディレクトリ構造を確保するため、公式の Dart テンプレートを使用して新しい CLI プロジェクトを初期化します。

  • dart create -t cli <project_name> を実行して、基本的な引数解析を備えたコンソールアプリケーションを生成します。
  • 実行可能なエントリポイント(main() を含むファイル)は、必ず bin/ ディレクトリに配置します。
  • 内部の実装ロジックは lib/src/ に配置し、パブリック API は lib/<project_name>.dart を介して公開します。
  • CI 環境では dart format . --set-exit-if-changed を実行してフォーマットを強制します。フォーマット違反が存在する場合、これは終了コード 1 を返します。

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

コマンドライン引数、フラグ、サブコマンドを管理するために args パッケージを実装します。

  • シンプルなスクリプトを構築する場合: ArgParser を直接使用して、フラグ(addFlag)とオプション(addOption)を定義します。
  • 複雑なマルチコマンド CLI(git など)を構築する場合: CommandRunner を実装し、各サブコマンドごとに Command を拡張します。
  • グローバル引数は CommandRunner.argParser に、コマンド固有の引数は個々の Command.argParser に定義します。
  • UsageException をキャッチして、無効な引数を適切に処理し、自動生成されたヘルプテキストを表示します。

実行とエラーハンドリング

io および stack_trace パッケージを使用して、信頼性の高い本番環境対応の CLI ツールを構築します。

  • 標準的な POSIX 終了コード(例: ExitCode.success.code、ExitCode.usage.code)を返すには、io パッケージの ExitCode 列挙型を使用します。
  • 複数の非同期リスナーが標準入力に順次アクセスする必要がある場合は、io パッケージの sharedStdIn を使用します。
  • 非同期スタックチェーンを追跡するには、アプリケーションの実行を stack_trace パッケージの Chain.capture() でラップします。
  • 出力するスタックトレースは Trace.terse または Chain.terse を使用してフォーマットし、ノイズの多いコアライブラリのフレームを除去して、ユーザーに読みやすいエラーを提示します。

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

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() を使用して、結果として生じたファイルシステムの変更を検証します。

コンパイルと配布

配布要件に応じて、適切なコンパイルターゲットを選択してください。

  • 開発中にローカルでテストする場合: 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 を使用します。生成された .aot ファイルは dartaotruntime を使用して実行します。
クロスコンパイルターゲット(Linux のみ)

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

ワークフロー

タスク進捗: 新しい CLI コマンドの実装
  • lib/src/commands/ に Command を拡張する新しいクラスを作成します。
  • name および description プロパティを定義します。
  • コンストラクタで argParser.addFlag() または argParser.addOption() を使用して、コマンド固有のフラグを登録します。
  • コアロジックを含む run() メソッドを実装します。
  • bin/cli.dart の CommandRunner インスタンスに addCommand() を使用して新しいコマンドを登録します。
  • バリデーターを実行 -> dart run bin/cli.dart help <command_name> を実行して、ヘルプテキストの生成を確認します。
タスク進捗: ネイティブ実行ファイルのコンパイルとリリース
  • バリデーターを実行 -> 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. 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();
  });
}

コメント