Construction d'applications CLI en Dart
Table des matières
- 1. Architecture cœur et cycle de vie des processus
- 2. Flux, diagnostics et formatage
- 3. Configuration du projet et empaquetage
- 4. Analyse des arguments et routage des commandes
- 5. Async natif et traces de pile modernes
- 6. Lancement de sous-processus et résilience AOT
- 7. Gestion des signaux et nettoyage du terminal
- 8. Test des applications CLI
- 9. Compilation et distribution modernes
- 10. Flux de travail et liste de contrôle d'audit
- Références et exemples
1. Architecture cœur et cycle de vie des processus
Éviter les sorties destructrices (exit(N))
Appeler exit(int code) de dart:io invoque Platform::Exit(code) dans le runtime C++. Il termine immédiatement le processus OS sans dérouler la pile Dart :
- Déconnexion du débogueur : Lorsqu'il est lancé avec
--pause-isolates-on-exit, le service VM met en pause les isolates avant l'arrêt pour permettre l'inspection par l'IDE.exit()termine le processus OS avant que le service VM ne puisse mettre en pause ou inspecter l'état. - Perte de couverture :
package:coverageinterroge les lignes d'exécution via les RPC du service VM pendant l'état de pause.exit()détruit le processus avant l'extraction RPC, donnant une couverture de 0%. - Troncature des tampons :
stdoutetstderrsont des fluxIOSinkasynchrones tamponnés.exit()perd les octets non vidés. - Fuite de ressources : Les blocs
finally(fermeture de verrous, suppression de répertoires temporaires) sont contournés.
Règle : Évitez d'appeler exit(code) directement pendant l'exécution normale ; définissez exitCode = code ou retournez un code de sortie entier depuis CommandRunner<int> (de package:args) et laissez la fonction main() asynchrone se terminer naturellement. N'appelez pas exit() sur des erreurs non gérées ; levez une Error ou une exception non gérée pour que le runtime se déroule proprement et se termine avec un statut non nul.
Codes de sortie POSIX standard (/usr/include/sysexits.h) :
0: Succès (EX_OK/ExitCode.success.code)64: Erreur d'utilisation de la ligne de commande (EX_USAGE/ExitCode.usage.code)65: Erreur de format de données (EX_DATAERR/ExitCode.data.code)70: Crash logiciel interne (EX_SOFTWARE/ExitCode.software.code)78: Erreur de configuration (EX_CONFIG/ExitCode.config.code)
Remarque : Préférez importer package:io/io.dart et utiliser les constantes ExitCode
(par exemple, ExitCode.usage.code, ExitCode.software.code) plutôt que des
littéraux entiers magiques. Pour les scripts autonomes minimaux sans dépendances de package,
les littéraux entiers POSIX standard (0, 64, 70) peuvent être utilisés.
import 'dart:io';
import 'package:args/command_runner.dart';
import 'package:io/io.dart' show ExitCode; // Fournit les constantes ExitCode POSIX standard
Future<void> main(List<String> args) async {
final runner = CommandRunner<int>('tool', 'Description de l\'outil CLI.');
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;
}
}
Le motif du point d'entrée mince (bin/ vs. lib/src/)
Gardez les fichiers bin/*.dart strictement comme des trampolines de point d'entrée minimaux (instancier le runner, passer args, attendre le code de sortie). Placez toutes les définitions de commandes, analyseurs d'arguments, formateurs et logique métier dans lib/src/.
- Raison : Le code dans
bin/ne peut pas être importé proprement via les URIpackage:. Déplacer la logique danslib/src/permet de tester unitairement l'ensemble du runner de commandes, la hiérarchie des sous-commandes et la logique métier en mémoire en quelques millisecondes (< 2ms) sans lancer de sous-processus OS.
// bin/my_cli.dart — Trampoline de point d'entrée mince
import 'dart:io';
import 'package:my_cli/src/cli.dart';
Future<void> main(List<String> args) async {
exitCode = await runCli(args);
}
2. Sortie, diagnostics et formatage
- Données vs. Diagnostics : Écrivez les résultats du programme prévus et les données lisibles par machine exclusivement sur
stdout. Écrivez les avertissements, messages d'erreur et journaux de débogage exclusivement surstderr. - La règle d'utilisation des erreurs : Lorsqu'une erreur d'analyse d'argument ou d'option obligatoire se produit (
FormatException,UsageException, ouArgumentErrorlevée lors de l'accès à une optionmandatory: trueviaresults.option(...)), le message d'erreur et le texte d'utilisation doivent être écrits surstderr, et le code de sortie64(EX_USAGE/ExitCode.usage.code) doit être retourné.stdoutne doit recevoir l'aide d'utilisation que lorsque l'utilisateur la demande explicitement via--helpou-h. - Pas de
print()dans les gestionnaires d'erreurs :print()route versstdout. Utilisezstderr.writeln()pour toutes les notifications d'échec. Pour la sortie standard, préférezstdout.writeln()àprint()pour se conformer à la règle de lintavoid_print(sauf sianalysis_options.yamlconfigure explicitementavoid_print: false). - Détection des capacités du terminal et
NO_COLOR: Vérifiezstdout.hasTerminal,stdout.supportsAnsiEscapeset!Platform.environment.containsKey('NO_COLOR')avant d'émettre des codes d'échappement ANSI de couleur ou de curseur :bool get useAnsi => stdout.hasTerminal && stdout.supportsAnsiEscapes && !Platform.environment.containsKey('NO_COLOR'); - Modes lisibles par machine : Lorsque les drapeaux
--jsonou--machinesont passés, formatez les données en JSON surstdoutet acheminez les journaux versstderr.
3. Configuration du projet et empaquetage
Échafaudage et mappage des exécutables (executables:)
Échafaudez de nouveaux projets en ligne de commande en utilisant dart create -t console <package_name>, qui initialise la disposition standard bin/ et lib/. Déclarez toujours les exécutables dans pubspec.yaml sous executables: pour mapper les noms de commandes aux scripts dans bin/, permettant une invocation propre via dart run <command> (sans spécifier bin/...dart) et configurant les liens symboliques binaires globaux pour dart install :
name: my_cli
description: Outil CLI haute performance.
version: 1.0.0
executables:
my_cli: # Mappe vers bin/my_cli.dart
secondary_cmd: helper # Mappe vers bin/helper.dart
Versionnage à source unique (package:build_version)
Évitez de coder en dur les chaînes --version dans bin/*.dart ou de synchroniser manuellement les fichiers de constantes. Utilisez package:build_version pour générer lib/src/version.dart contenant const packageVersion = 'x.y.z'; directement à partir de pubspec.yaml pendant les builds.
Conventions de mise en cache
Stockez les fichiers de cache transitoires dans .dart_tool/<package_name>/. N'écrivez jamais de fichiers de cache persistants directement à la racine du projet.
4. Analyse des arguments et routage des commandes
Importez package:args pour gérer les arguments de ligne de commande :
- Scripts simples : Utilisez
ArgParserdirectement avecaddFlag()etaddOption(). - Outils multi-commandes : Implémentez
CommandRunner<int>et étendezCommand<int>pour chaque sous-commande, en retournant directement les codes de sortie POSIX. - Accesseurs typés : Utilisez
results.flag('name'),results.option('name')etresults.multiOption('name')(disponibles danspackage:args2.5+) au lieu de l'indexation de carteoperator []pour éliminer les casts de type manuels (as bool,as String?). - Modèles d'options complexes : Pour les applications avec de nombreux drapeaux, utilisez
package:build_clipour générer des classes d'options fortement typées. Tirez parti des remplacements par défaut nommés (par exemple{String? hostDefaultOverride}) pour fusionner proprement les fichiers de configuration avec les drapeaux CLI.
5. Async natif et traces de pile modernes
- Éviter
Chain.capture(): La VM Dart préserve nativement les trames de pile asynchrones à travers les points de suspensionawait.Chain.captureenveloppe la boucle d'événements dans des Zones personnalisées, entraînant une surcharge d'allocation substantielle et piégeant les erreurs à travers les limites de Zone. - Assainir avec
Trace.from(st).terse: Utilisez les utilitaires statiques depackage:stack_tracesur les erreurs non capturées sans capturer de zones :
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('Erreur fatale : $e');
if (args.contains('-v') || args.contains('--verbose')) {
stderr.writeln(Trace.from(st).terse);
}
exitCode = ExitCode.software.code;
}
}
6. Lancement de sous-processus et résilience AOT
Lors du lancement de sous-processus du SDK Dart ou de l'exécution d'autres outils Dart (par exemple, dart format, dart test, build_runner) :
- Ne présumez pas que
Platform.resolvedExecutableouPlatform.executablepointe vers l'exécutable en ligne de commandedart: Dans les binaires compilés AOT autonomes (dart install/dart compile exe),resolvedExecutablepointe vers le binaire de l'application compilée elle-même, provoquant des boucles d'auto-invocation récursives ou des crashes de rejet de drapeau. - Utilisez
package:cli_util: Résolvez l'exécutable du SDK Dart en utilisantcli_util.dartExecutableoucli_util.sdkPathau lieu d'écrire des scrapers de PATH ou de répertoire personnalisés. - Voir les exigences de version et le guide technique détaillé dans references/aot_sdk_discovery.md.
7. Gestion des signaux et nettoyage du terminal
Si votre CLI modifie les modes du terminal, affiche des spinners ou ouvre des sockets d'écoute :
- Garde des signaux Windows : Sur Windows,
ProcessSignal.sigterm.watch()lèveUnsupportedError. Protégezsigtermavecif (!Platform.isWindows). - Nettoyage de l'écho et du mode ligne : Si vous définissez
stdin.echoMode = falseoustdin.lineMode = false, vérifiez d'abordif (!stdin.hasTerminal) return;, et installez un écouteurSIGINTet un blocfinallypour les restaurer afin que les frappes utilisateur restent visibles après la sortie. - Visibilité du curseur : Si vous émettez un curseur masqué ANSI (
\x1B[?25l), restaurez toujours la visibilité du curseur (\x1B[?25h) à la sortie ou à l'annulation. - Nettoyage des sockets : Fermez explicitement les instances
HttpServerouServerSocketen écoute (server.close(force: true)) sur les signaux de terminaison pour libérer immédiatement les ports OS. - Voir les motifs détaillés dans references/signals_and_terminal.md.
8. Test des applications CLI
Structurez les tests à travers deux couches distinctes :
- Tests unitaires (en mémoire,
< 5ms) : Testez les classes de commandes, l'analyse des options et la logique métier directement en importantpackage:<pkg>/src/...danstest/. - Tests d'intégration (sous-processus) : Utilisez
package:test_processetpackage:test_descriptorpour vérifier l'exécution de bout en bout du binaire, le flux I/O du processus et les codes de sortie 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 traite l\'entrée et se termine proprement', () 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('Traitement terminé.'));
await process.shouldExit(0);
});
}
9. Compilation et distribution modernes
Dart 3.12+ standardise la distribution CLI autour de dart run et dart install (en s'éloignant de dart pub global activate) :
- Exécution éphémère (JIT) :
dart run <package>@<version> [args]télécharge et exécute la CLI à la demande. - Installation globale (AOT natif) :
dart install <package>compile le point d'entrée du package en un binaire natif autonome rapide dans~/.dart/install/bin/. - Développement local : Utilisez
dart run <command>(résout viaexecutables:danspubspec.yaml) oudart run bin/cli.dart. - Regroupement de bibliothèques dynamiques et d'actifs de code : Utilisez
dart build cli. La sortie du bundle est dansbuild/cli/_/bundle/. - Compilation d'exécutable autonome : Utilisez
dart compile exe bin/cli.dart -o <output_path>.
10. Flux de travail et liste de contrôle d'audit
Flux de travail d'implémentation
- Déclarez les points d'entrée dans
pubspec.yamlsousexecutables:. - Gardez
bin/*.dartcomme un point d'entrée mince ; placez la logique de commande danslib/src/. - Retournez des codes de sortie entiers ou définissez
exitCode = N; évitezexit(N)brut. - Dirigez les erreurs, avertissements et l'utilisation en cas d'échec d'analyse vers
stderr. - Utilisez
results.flag(),results.option()etresults.multiOption()pour la sécurité de type. - Validez
useAnsi(en vérifiantstdout.hasTerminal,supportsAnsiEscapesetNO_COLOR) avant d'émettre des codes ANSI. - Lancez les outils enfants en utilisant
cli_util.dartExecutable, jamaisPlatform.resolvedExecutable. - Testez unitairement les runners de commandes en mémoire ; testez l'exécution de bout en bout du binaire avec
test_process.
Références et exemples
- Modèle d'outil à commande unique : examples/single_command_tool.dart
- Modèle de runner multi-commandes : examples/multi_command_runner.dart
- Découverte SDK AOT et lancement de sous-processus : references/aot_sdk_discovery.md
- Gestion des signaux et nettoyage du terminal : references/signals_and_terminal.md