Construcción de Aplicaciones CLI en Dart
Contenidos
- 1. Arquitectura Central y Ciclo de Vida del Proceso
- 2. Flujos, Diagnósticos y Formateo
- 3. Configuración del Proyecto y Empaquetado
- 4. Análisis de Argumentos y Enrutamiento de Comandos
- 5. Asincronía Nativa y Trazas de Pila Modernas
- 6. Lanzamiento de Subprocesos y Resiliencia AOT
- 7. Manejo de Señales y Desmontaje de Terminal
- 8. Pruebas de Aplicaciones CLI
- 9. Compilación y Distribución Modernas
- 10. Flujos de Trabajo y Lista de Verificación de Auditoría
- Referencias y Ejemplos
1. Arquitectura Central y Ciclo de Vida del Proceso
Evitar Salidas Destructivas (exit(N))
Llamar a exit(int code) de dart:io invoca Platform::Exit(code) en el tiempo de ejecución de C++. Termina inmediatamente el proceso del sistema operativo sin desenrollar la pila de Dart:
- Desconexión del Depurador: Cuando se inicia con
--pause-isolates-on-exit, el Servicio VM pausa los aislamientos antes del apagado para permitir la inspección del IDE.exit()termina el proceso del SO antes de que el Servicio VM pueda pausar o inspeccionar el estado. - Pérdida de Cobertura:
package:coverageconsulta las líneas de ejecución a través de RPC del Servicio VM durante el estado de pausa en la salida.exit()destruye el proceso antes de la extracción de RPC, dando como resultado una cobertura del 0%. - Truncamiento de Búfer:
stdoutystderrson flujosIOSinkasíncronos almacenados en búfer.exit()descarta los bytes no vaciados. - Fugas de Recursos: Los bloques
finally(cerrar bloqueos, eliminar directorios temporales) se omiten.
Regla: Evita llamar a exit(code) directamente durante la ejecución normal; establece exitCode = code o devuelve un código de salida entero desde CommandRunner<int> (de package:args) y permite que la función main() asíncrona retorne de forma natural. No llames a exit() en errores no manejados; lanza un Error o excepción no manejado para que el tiempo de ejecución se desenrolle limpiamente y salga con un estado distinto de cero.
Códigos de salida POSIX estándar (/usr/include/sysexits.h):
0: Éxito (EX_OK/ExitCode.success.code)64: Error de uso de línea de comandos (EX_USAGE/ExitCode.usage.code)65: Error de formato de datos (EX_DATAERR/ExitCode.data.code)70: Fallo interno de software (EX_SOFTWARE/ExitCode.software.code)78: Error de configuración (EX_CONFIG/ExitCode.config.code)
Nota: Prefiere importar package:io/io.dart y usar las constantes ExitCode (por ejemplo, ExitCode.usage.code, ExitCode.software.code) en lugar de literales enteros mágicos. Para scripts independientes mínimos sin dependencias de paquetes, se pueden usar literales enteros POSIX estándar (0, 64, 70).
import 'dart:io';
import 'package:args/command_runner.dart';
import 'package:io/io.dart' show ExitCode; // Proporciona constantes POSIX estándar de ExitCode
Future<void> main(List<String> args) async {
final runner = CommandRunner<int>('tool', 'Descripción de la herramienta 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;
}
}
El Patrón de Punto de Entrada Delgado (bin/ vs. lib/src/)
Mantén los archivos bin/*.dart estrictamente como trampolines de punto de entrada mínimos (instanciar el runner, pasar args, esperar el código de salida). Coloca todas las definiciones de comandos, analizadores de argumentos, formateadores y lógica de negocio dentro de lib/src/.
- Justificación: El código en
bin/no se puede importar limpiamente mediante URIspackage:. Mover la lógica alib/src/permite que todo el runner de comandos, la jerarquía de subcomandos y la lógica de negocio se prueben unitariamente en memoria en milisegundos (< 2ms) sin lanzar subprocesos del SO.
// bin/my_cli.dart — Trampolín de punto de entrada delgado
import 'dart:io';
import 'package:my_cli/src/cli.dart';
Future<void> main(List<String> args) async {
exitCode = await runCli(args);
}
2. Flujos, Diagnósticos y Formateo
- Datos vs. Diagnósticos: Escribe los resultados previstos del programa y los datos legibles por máquina exclusivamente en
stdout. Escribe advertencias, mensajes de error y registros de depuración exclusivamente enstderr. - La Regla de Error de Uso: Cuando ocurre un error de análisis de argumentos o de opción obligatoria (
FormatException,UsageExceptionoArgumentErrorlanzado al acceder a una opciónmandatory: truefaltante a través deresults.option(...)), tanto el mensaje de error como el texto de uso deben escribirse enstderr, y se debe devolver el código de salida64(EX_USAGE/ExitCode.usage.code).stdoutSOLO debe recibir ayuda de uso cuando el usuario la solicite explícitamente a través de--helpo-h. - Sin
print()en Manejadores de Error:print()dirige astdout. Usastderr.writeln()para todas las notificaciones de fallo. Para la salida estándar, prefierestdout.writeln()sobreprint()para cumplir con la regla lintavoid_print(a menos queanalysis_options.yamlconfigure explícitamenteavoid_print: false). - Detección de Capacidad del Terminal y
NO_COLOR: Verificastdout.hasTerminal,stdout.supportsAnsiEscapesy!Platform.environment.containsKey('NO_COLOR')antes de emitir códigos de escape ANSI de color o cursor:bool get useAnsi => stdout.hasTerminal && stdout.supportsAnsiEscapes && !Platform.environment.containsKey('NO_COLOR'); - Modos Legibles por Máquina: Cuando se pasan las banderas
--jsono--machine, formatea los datos como JSON astdouty dirige los registros astderr.
3. Configuración del Proyecto y Empaquetado
Andamiaje y Mapeo de Ejecutables de Pubspec (executables:)
Andamia nuevos proyectos de línea de comandos usando dart create -t console <nombre_paquete>, que inicializa el diseño estándar bin/ y lib/. Siempre declara los ejecutables en pubspec.yaml bajo executables: para mapear nombres de comandos a scripts en bin/, lo que permite una invocación limpia a través de dart run <comando> (sin especificar bin/archivo.dart) y configura enlaces simbólicos binarios globales para dart install:
name: my_cli
description: Herramienta CLI de alto rendimiento.
version: 1.0.0
executables:
my_cli: # Mapea a bin/my_cli.dart
secondary_cmd: helper # Mapea a bin/helper.dart
Versionado de Fuente Única (package:build_version)
Evita codificar cadenas --version en bin/*.dart o sincronizar manualmente archivos de constantes. Usa package:build_version para generar lib/src/version.dart que contenga const packageVersion = 'x.y.z'; directamente desde pubspec.yaml durante las compilaciones.
Convenciones de Caché
Almacena archivos de caché transitorios en .dart_tool/<nombre_paquete>/. Nunca escribas archivos de caché persistentes directamente en la raíz del proyecto.
4. Análisis de Argumentos y Enrutamiento de Comandos
Importa package:args para gestionar los argumentos de línea de comandos:
- Scripts Simples: Usa
ArgParserdirectamente conaddFlag()yaddOption(). - Herramientas de Múltiples Comandos: Implementa
CommandRunner<int>y extiendeCommand<int>para cada subcomando, devolviendo códigos de salida POSIX directamente. - Accesores Seguros en Tipo: Usa
results.flag('name'),results.option('name')yresults.multiOption('name')(disponibles enpackage:args2.5+) en lugar del indexado de mapasoperator []para eliminar conversiones de tipo manuales (as bool,as String?). - Modelos de Opciones Complejos: Para aplicaciones con banderas extensas, usa
package:build_clipara generar clases de opciones fuertemente tipadas. Aprovecha las anulaciones predeterminadas con nombre (por ejemplo,{String? hostDefaultOverride}) para fusionar limpiamente los archivos de configuración con las banderas CLI.
5. Asincronía Nativa y Trazas de Pila Modernas
- Evita
Chain.capture(): La VM de Dart preserva de forma nativa los marcos de pila asíncronos a través de los puntos de suspensiónawait.Chain.captureenvuelve el bucle de eventos en Zones personalizadas, incurriendo en una sobrecarga de asignación sustancial y atrapando errores a través de los límites de Zone. - Sanitiza con
Trace.from(st).terse: Usa utilidades estáticas depackage:stack_traceen errores no capturados sin capturar 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('Error fatal: $e');
if (args.contains('-v') || args.contains('--verbose')) {
stderr.writeln(Trace.from(st).terse);
}
exitCode = ExitCode.software.code;
}
}
6. Lanzamiento de Subprocesos y Resiliencia AOT
Al lanzar subprocesos del SDK de Dart o ejecutar otras herramientas de Dart (por ejemplo, dart format, dart test, build_runner):
- No asumas que
Platform.resolvedExecutableoPlatform.executableapunta al ejecutable de línea de comandosdart: En binarios compilados AOT independientes (dart install/dart compile exe),resolvedExecutableapunta al binario de la aplicación compilada en sí, causando bucles de auto-invocación recursiva o fallos de rechazo de banderas. - Usa
package:cli_util: Resuelve el ejecutable del SDK de Dart usandocli_util.dartExecutableocli_util.sdkPathen lugar de escribir scripts personalizados de búsqueda en PATH o directorios. - Consulta los requisitos de versión y la guía técnica detallada en references/aot_sdk_discovery.md.
7. Manejo de Señales y Desmontaje de Terminal
Si tu CLI altera los modos del terminal, muestra indicadores de progreso o abre sockets de escucha:
- Protección de Señal en Windows: En Windows,
ProcessSignal.sigterm.watch()lanzaUnsupportedError. Protegesigtermconif (!Platform.isWindows). - Desmontaje de Eco y Modo Línea: Si estableces
stdin.echoMode = falseostdin.lineMode = false, verifica primeroif (!stdin.hasTerminal) return;, e instala un listenerSIGINTy un bloquefinallypara restaurarlos, de modo que las pulsaciones de teclas del usuario permanezcan visibles después de la salida. - Visibilidad del Cursor: Si emites ocultación de cursor ANSI (
\x1B[?25l), siempre restaura la visibilidad del cursor (\x1B[?25h) al salir o cancelar. - Limpieza de Sockets: Cierra explícitamente las instancias de
HttpServeroServerSocketen escucha (server.close(force: true)) en las señales de terminación para liberar inmediatamente los puertos del SO. - Consulta los patrones detallados en references/signals_and_terminal.md.
8. Pruebas de Aplicaciones CLI
Estructura las pruebas en dos capas distintas:
- Pruebas Unitarias (En Memoria,
< 5ms): Prueba las clases de comando, el análisis de opciones y la lógica de negocio directamente importandopackage:<pkg>/src/...entest/. - Pruebas de Integración (Subprocesos): Usa
package:test_processypackage:test_descriptorpara verificar la ejecución binaria de extremo a extremo, la transmisión de E/S del proceso y los códigos de salida del SO:
import 'package:test/test.dart';
import 'package:test_descriptor/test_descriptor.dart' as d;
import 'package:test_process/test_process.dart';
void main() {
test('La CLI procesa la entrada y sale limpiamente', () async {
await d.file('input.txt', 'hola').create();
final process = await TestProcess.start('dart', [
'run',
'bin/my_cli.dart',
'--input',
d.path('input.txt'),
]);
await expectLater(process.stdout, emitsThrough('Procesamiento completo.'));
await process.shouldExit(0);
});
}
9. Compilación y Distribución Modernas
Dart 3.12+ estandariza la distribución CLI en torno a dart run y dart install (alejándose de dart pub global activate):
- Ejecución Efímera (JIT):
dart run <paquete>@<version> [args]descarga y ejecuta la CLI bajo demanda. - Instalación Global (AOT Nativa):
dart install <paquete>compila el punto de entrada del paquete en un binario independiente nativo rápido en~/.dart/install/bin/. - Desarrollo Local: Usa
dart run <comando>(se resuelve a través deexecutables:enpubspec.yaml) odart run bin/cli.dart. - Agrupación de Bibliotecas Dinámicas y Activos de Código: Usa
dart build cli. Envía el paquete abuild/cli/_/bundle/. - Compilación de Ejecutable Independiente: Usa
dart compile exe bin/cli.dart -o <ruta_salida>.
10. Flujos de Trabajo y Lista de Verificación de Auditoría
Flujo de Trabajo de Implementación
- Declarar puntos de entrada en
pubspec.yamlbajoexecutables:. - Mantener
bin/*.dartcomo un punto de entrada delgado; colocar la lógica de comandos enlib/src/. - Devolver códigos de salida enteros o establecer
exitCode = N; evitarexit(N)directo. - Dirigir errores, advertencias y el uso en caso de fallo de análisis a
stderr. - Usar
results.flag(),results.option()yresults.multiOption()para seguridad de tipos. - Validar
useAnsi(verificandostdout.hasTerminal,supportsAnsiEscapesyNO_COLOR) antes de emitir códigos ANSI. - Lanzar herramientas hijas usando
cli_util.dartExecutable, nuncaPlatform.resolvedExecutable. - Probar unitariamente los runners de comandos en memoria; probar la ejecución binaria de extremo a extremo con
test_process.
Referencias y Ejemplos
- Plantilla de Herramienta de Comando Único: examples/single_command_tool.dart
- Plantilla de Runner de Múltiples Comandos: examples/multi_command_runner.dart
- Descubrimiento de SDK AOT y Lanzamiento de Subprocesos: references/aot_sdk_discovery.md
- Manejo de Señales y Desmontaje de Terminal: references/signals_and_terminal.md