Erstellen von Dart-CLI-Anwendungen
Inhaltsverzeichnis
- 1. Kernarchitektur & Prozesslebenszyklus
- 2. Streams, Diagnose & Formatierung
- 3. Projektkonfiguration & Paketierung
- 4. Argumentanalyse & Befehlsrouting
- 5. Natives Async & Moderne Stacktraces
- 6. Subprozess-Erzeugung & AOT-Resilienz
- 7. Signalbehandlung & Terminalbereinigung
- 8. Testen von CLI-Anwendungen
- 9. Moderne Kompilierung & Distribution
- 10. Workflows & Prüfliste
- Referenzen & Beispiele
1. Kernarchitektur & Prozesslebenszyklus
Vermeiden Sie destruktive Beendigungen (exit(N))
Der Aufruf von dart:ios exit(int code) ruft Platform::Exit(code) in der C++-Laufzeit auf. Es beendet den Betriebssystemprozess sofort, ohne den Dart-Stack zu entrollen:
- Debugger-Trennung: Wenn mit
--pause-isolates-on-exitgestartet, pausiert der VM-Service die Isolate vor dem Herunterfahren, um eine IDE-Inspektion zu ermöglichen.exit()beendet den Betriebssystemprozess, bevor der VM-Service den Zustand pausieren oder inspizieren kann. - Codeabdeckungsverlust:
package:coveragefragt Ausführungszeilen über VM-Service-RPCs während des pausierten Zustands ab.exit()zerstört den Prozess vor der RPC-Extraktion, was zu 0% Codeabdeckung führt. - Pufferabschneidung:
stdoutundstderrsind gepufferte asynchroneIOSink-Streams.exit()verwirft ungeschriebene Bytes. - Ressourcenlecks:
finally-Blöcke (Schließen von Locks, Löschen temporärer Verzeichnisse) werden umgangen.
Regel: Vermeiden Sie den direkten Aufruf von exit(code) während der normalen Ausführung; setzen Sie exitCode = code oder geben Sie einen ganzzahligen Exit-Code von CommandRunner<int> (aus package:args) zurück und lassen Sie die asynchrone main()-Funktion natürlich zurückkehren. Rufen Sie exit() nicht bei unbehandelten Fehlern auf; werfen Sie eine unbehandelte Error- oder Exception, damit die Laufzeit sauber entrollt und mit einem Status ungleich Null beendet wird.
Standard-POSIX-Exit-Codes (/usr/include/sysexits.h):
0: Erfolg (EX_OK/ExitCode.success.code)64: Befehlszeilen-Nutzungsfehler (EX_USAGE/ExitCode.usage.code)65: Datenformatfehler (EX_DATAERR/ExitCode.data.code)70: Interner Softwareabsturz (EX_SOFTWARE/ExitCode.software.code)78: Konfigurationsfehler (EX_CONFIG/ExitCode.config.code)
Hinweis: Bevorzugen Sie den Import von package:io/io.dart und die Verwendung von ExitCode-Konstanten (z. B. ExitCode.usage.code, ExitCode.software.code) anstelle von magischen Integer-Literalen. Für minimale eigenständige Skripte ohne Paketabhängigkeiten können standardmäßige POSIX-Integer-Literale (0, 64, 70) verwendet werden.
import 'dart:io';
import 'package:args/command_runner.dart';
import 'package:io/io.dart' show ExitCode; // Stellt standardmäßige POSIX ExitCode-Konstanten bereit
Future<void> main(List<String> args) async {
final runner = CommandRunner<int>('tool', 'CLI-Tool-Beschreibung.');
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;
}
}
Das schlanke Einstiegspunktmuster (bin/ vs. lib/src/)
Halten Sie bin/*.dart-Dateien strikt als minimale Einstiegspunkt-Trampoline (Instanziieren des Runners, Übergeben von args, Abwarten des Exit-Codes). Platzieren Sie alle Befehlsdefinitionen, Argumentparser, Formatierer und Geschäftslogik innerhalb von lib/src/.
- Begründung: Code in
bin/kann nicht sauber überpackage:-URIs importiert werden. Das Verschieben der Logik inlib/src/ermöglicht es, den gesamten Befehlsrunner, die Unterbefehlshierarchie und die Geschäftslogik im Speicher in Millisekunden (< 2ms) zu testen, ohne Betriebssystem-Subprozesse zu erzeugen.
// bin/my_cli.dart — Schlankes Einstiegspunkt-Trampolin
import 'dart:io';
import 'package:my_cli/src/cli.dart';
Future<void> main(List<String> args) async {
exitCode = await runCli(args);
}
2. Ausgabe, Diagnose & Formatierung
- Daten vs. Diagnose: Schreiben Sie beabsichtigte Programmergebnisse und maschinenlesbare Daten ausschließlich auf
stdout. Schreiben Sie Warnungen, Fehlermeldungen und Debug-Logs ausschließlich aufstderr. - Die Fehler-Nutzungsregel: Wenn ein Argument-Parsing- oder Pflichtoptionenfehler auftritt (
FormatException,UsageExceptionoderArgumentError, das beim Zugriff auf eine fehlendemandatory: true-Option überresults.option(...)ausgelöst wird), müssen sowohl die Fehlermeldung als auch der Nutzungstext aufstderrgeschrieben werden, und der Exit-Code64(EX_USAGE/ExitCode.usage.code) muss zurückgegeben werden.stdoutsollte NUR dann die Nutzungshilfe erhalten, wenn der Benutzer sie explizit über--helpoder-hanfordert. - Kein
print()in Fehlerbehandlern:print()leitet aufstdoutum. Verwenden Siestderr.writeln()für alle Fehlermeldungen. Für die Standardausgabe bevorzugen Siestdout.writeln()gegenüberprint(), um deravoid_print-Lint-Regel zu entsprechen (es sei denn,analysis_options.yamlkonfiguriert explizitavoid_print: false). - Terminalerkennung &
NO_COLOR: Überprüfen Siestdout.hasTerminal,stdout.supportsAnsiEscapesund!Platform.environment.containsKey('NO_COLOR'), bevor Sie ANSI-Farb- oder Cursor-Steuercodes ausgeben:bool get useAnsi => stdout.hasTerminal && stdout.supportsAnsiEscapes && !Platform.environment.containsKey('NO_COLOR'); - Maschinenlesbare Modi: Wenn
--json- oder--machine-Flags übergeben werden, formatieren Sie Daten als JSON aufstdoutund leiten Sie Logs aufstderrum.
3. Projektkonfiguration & Paketierung
Gerüstbau & Pubspec-Ausführbare-Zuordnung (executables:)
Erstellen Sie neue Befehlszeilenprojekte mit dart create -t console <paketname>, das das standardmäßige bin/- und lib/-Layout initialisiert. Deklarieren Sie ausführbare Dateien immer in pubspec.yaml unter executables:, um Befehlsnamen Skripten in bin/ zuzuordnen, was einen sauberen Aufruf über dart run <befehl> (ohne Angabe von bin/...dart) ermöglicht und globale binäre Symlinks für dart install konfiguriert:
name: my_cli
description: Hochleistungs-CLI-Tool.
version: 1.0.0
executables:
my_cli: # Kartiert auf bin/my_cli.dart
secondary_cmd: helper # Kartiert auf bin/helper.dart
Single-Source-Versionierung (package:build_version)
Vermeiden Sie das Hartcodieren von --version-Strings in bin/*.dart oder das manuelle Synchronisieren von Konstantendateien. Verwenden Sie package:build_version, um lib/src/version.dart zu generieren, das const packageVersion = 'x.y.z'; direkt aus pubspec.yaml während der Builds enthält.
Caching-Konventionen
Speichern Sie temporäre Cache-Dateien in .dart_tool/<paketname>/. Schreiben Sie niemals persistente Cache-Dateien direkt in das Projektwurzelverzeichnis.
4. Argumentanalyse & Befehlsrouting
Importieren Sie package:args, um Befehlszeilenargumente zu verwalten:
- Einfache Skripte: Verwenden Sie
ArgParserdirekt mitaddFlag()undaddOption(). - Multi-Befehls-Tools: Implementieren Sie
CommandRunner<int>und erweitern SieCommand<int>für jeden Unterbefehl, wobei Sie direkt POSIX-Exit-Codes zurückgeben. - Typsichere Zugriffsmethoden: Verwenden Sie
results.flag('name'),results.option('name')undresults.multiOption('name')(verfügbar inpackage:args2.5+) anstelle der Map-Indizierungoperator [], um manuelle Typumwandlungen (as bool,as String?) zu eliminieren. - Komplexe Optionsmodelle: Für Anwendungen mit umfangreichen Flags verwenden Sie
package:build_cli, um stark typisierte Optionsklassen zu generieren. Nutzen Sie benannte Standard-Überschreibungen (z. B.{String? hostDefaultOverride}), um Konfigurationsdateien sauber mit CLI-Flags zusammenzuführen.
5. Natives Async & Moderne Stacktraces
- Vermeiden Sie
Chain.capture(): Die Dart-VM bewahrt nativ asynchrone Stackframes überawait-Unterbrechungspunkte hinweg auf.Chain.captureumschließt die Ereignisschleife in benutzerdefinierten Zonen, was zu erheblichem Allokationsaufwand führt und Fehler über Zonengrenzen hinweg einfängt. - Bereinigen Sie mit
Trace.from(st).terse: Verwenden Sie statische Hilfsprogramme auspackage:stack_tracebei unbehandelten Fehlern, ohne Zonen zu erfassen:
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('Schwerwiegender Fehler: $e');
if (args.contains('-v') || args.contains('--verbose')) {
stderr.writeln(Trace.from(st).terse);
}
exitCode = ExitCode.software.code;
}
}
6. Subprozess-Erzeugung & AOT-Resilienz
Beim Erzeugen von Dart-SDK-Subprozessen oder Ausführen anderer Dart-Tools (z. B. dart format, dart test, build_runner):
- Gehen Sie nicht davon aus, dass
Platform.resolvedExecutableoderPlatform.executableauf das ausführbaredart-Befehlszeilenprogramm zeigt: In eigenständigen AOT-kompilierten Binärdateien (dart install/dart compile exe) zeigtresolvedExecutableauf die kompilierte Anwendungsbinärdatei selbst, was zu rekursiven Selbstaufrufschleifen oder Abstürzen aufgrund von Flag-Ablehnungen führt. - Verwenden Sie
package:cli_util: Lösen Sie das ausführbare Dart-SDK-Programm mitcli_util.dartExecutableodercli_util.sdkPathauf, anstatt benutzerdefinierte PATH- oder Verzeichnisscanner zu schreiben. - Siehe Versionsanforderungen und detaillierte technische Anleitung in references/aot_sdk_discovery.md.
7. Signalbehandlung & Terminalbereinigung
Wenn Ihr CLI Terminalmodi ändert, Spinner anzeigt oder Listening-Sockets öffnet:
- Windows-Signalwächter: Unter Windows löst
ProcessSignal.sigterm.watch()einenUnsupportedErroraus. Schützen Siesigtermmitif (!Platform.isWindows). - Echo- & Zeilenmodus-Bereinigung: Wenn Sie
stdin.echoMode = falseoderstdin.lineMode = falsesetzen, überprüfen Sie zuerstif (!stdin.hasTerminal) return;und installieren Sie einenSIGINT-Listener und einenfinally-Block, um sie wiederherzustellen, sodass Benutzertastatureingaben nach dem Beenden sichtbar bleiben. - Cursorsichtbarkeit: Wenn Sie ANSI-Cursor-verstecken (
\x1B[?25l) ausgeben, stellen Sie die Cursorsichtbarkeit immer bei Beendigung oder Abbruch wieder her (\x1B[?25h). - Socket-Bereinigung: Schließen Sie explizit lauschende
HttpServer- oderServerSocket-Instanzen (server.close(force: true)) bei Beendigungssignalen, um sofort Betriebssystem-Ports freizugeben. - Siehe detaillierte Muster in references/signals_and_terminal.md.
8. Testen von CLI-Anwendungen
Strukturieren Sie Tests über zwei verschiedene Ebenen:
- Unit-Tests (In-Memory,
< 5ms): Testen Sie Befehlsklassen, Optionsparsing und Geschäftslogik direkt, indem Siepackage:<pkg>/src/...intest/importieren. - Integrationstests (Subprozesse): Verwenden Sie
package:test_processundpackage:test_descriptor, um die End-to-End-Binärausführung, Prozess-I/O-Streaming und Betriebssystem-Exit-Codes zu überprüfen:
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 verarbeitet Eingabe und beendet sich sauber', () async {
await d.file('input.txt', 'hallo').create();
final process = await TestProcess.start('dart', [
'run',
'bin/my_cli.dart',
'--input',
d.path('input.txt'),
]);
await expectLater(process.stdout, emitsThrough('Verarbeitung abgeschlossen.'));
await process.shouldExit(0);
});
}
9. Moderne Kompilierung & Distribution
Dart 3.12+ standardisiert die CLI-Distribution um dart run und dart install (weg von dart pub global activate):
- Flüchtige Ausführung (JIT):
dart run <paket>@<version> [args]lädt das CLI herunter und führt es bei Bedarf aus. - Globale Installation (Native AOT):
dart install <paket>kompiliert den Paketeinstiegspunkt zu einer schnellen nativen eigenständigen Binärdatei in~/.dart/install/bin/. - Lokale Entwicklung: Verwenden Sie
dart run <befehl>(wird überexecutables:inpubspec.yamlaufgelöst) oderdart run bin/cli.dart. - Bündeln dynamischer Bibliotheken & Code-Assets: Verwenden Sie
dart build cli. Gibt das Bundle nachbuild/cli/_/bundle/aus. - Kompilierung eigenständiger ausführbarer Dateien: Verwenden Sie
dart compile exe bin/cli.dart -o <ausgabepfad>.
10. Workflows & Prüfliste
Implementierungs-Workflow
- Einstiegspunkte in
pubspec.yamlunterexecutables:deklarieren. -
bin/*.dartals schlanken Einstiegspunkt halten; Befehlslogik inlib/src/platzieren. - Ganzzahlige Exit-Codes zurückgeben oder
exitCode = Nsetzen; rohesexit(N)vermeiden. - Fehler, Warnungen und Nutzungshilfe bei Parsing-Fehlern auf
stderrleiten. -
results.flag(),results.option()undresults.multiOption()für Typsicherheit verwenden. -
useAnsivalidieren (Überprüfung vonstdout.hasTerminal,supportsAnsiEscapesundNO_COLOR), bevor ANSI-Codes ausgegeben werden. - Kind-Tools mit
cli_util.dartExecutableerzeugen, niemals mitPlatform.resolvedExecutable. - Befehlsrunner im Speicher per Unit-Test testen; End-to-End-Binärausführung mit
test_processtesten.
Referenzen & Beispiele
- Single-Command-Tool-Vorlage: examples/single_command_tool.dart
- Multi-Command-Runner-Vorlage: examples/multi_command_runner.dart
- AOT-SDK-Erkennung & Subprozess-Erzeugung: references/aot_sdk_discovery.md
- Signalbehandlung & Terminalbereinigung: references/signals_and_terminal.md