dart-build-cli-app について
Dartでコマンドラインアプリを開発するための実践ガイドです。プロジェクト構成、引数処理、テスト、コンパイルと配布の手順を扱います。
主な機能
- 01Dart CLIのプロジェクト構成
- 02引数とサブコマンドのルーティング
- 03エラー処理と終了コード
- 04サブプロセスの統合テスト
- 05コンパイルと配布の手順
活用シーン
- 01新しいDart CLIプロジェクトの作成
- 02引数付きCLIサブコマンドの追加
- 03コマンド出力とファイル変更のテスト
- 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();
});
}
コメント
Markdown 対応