Cuando necesitas ejecutar comandos del sistema operativo (como git, docker o scripts de shell) directamente desde Dart, utilizas la clase Process de dart:io.
Si necesitas un comando rápido que termine pronto y solo te interesa su resultado final, utiliza Process.run [disponible desde Dart 1.0]. Este método devuelve un Future<ProcessResult>, que contiene el exitCode (un int indicando si el proceso tuvo éxito o error) y stdout y stderr ya convertidos a String. Es la opción ideal para tareas atómicas.
Si el proceso debe permanecer abierto, como un servidor o una herramienta que requiere interacción constante, debes usar Process.start [disponible desde Dart 1.0]. Este método devuelve un Future<Process>, proporcionándote acceso a stdin, stdout y stderr como Streams de bytes. Esto te permite enviar datos al proceso a través de su entrada estándar o leer su salida conforme se genera, sin esperar a que el proceso finalice.
Para interactuar con la entrada y salida del propio proceso de Dart que estás ejecutando, utilizas las constantes globales stdin, stdout y stderr. En scripts sencillos de terminal, stdin.readLineSync() es una forma rápida de pausar la ejecución y esperar una línea de texto del usuario.
¿Cuándo usar esto? Cuando estás construyendo herramientas de CLI que actúan como envoltorios (wrappers) de otras herramientas de sistema. ¿Qué pasa si lo haces mal? Si intentas leer grandes volúmenes de datos de un proceso hijo sin consumir sus streams de forma asíncrona, o si utilizas Process.runSync en un entorno con un event loop activo (como un servidor), bloquearás el hilo de ejecución y tu aplicación dejará de responder a cualquier otra tarea.
import 'dart:io';
import 'dart:convert';
void main() async {
// 1. Uso de Process.run para comandos rápidos (One-shot)
print('--- [1] Ejecución rápida con Process.run ---');
final cmd = Platform.isWindows ? 'cmd' : 'sh';
final args = Platform.isWindows
? ['/c', 'echo Hola desde el sistema operativo']
: ['-c', 'echo Hola desde el sistema operativo'];
final result = await Process.run(cmd, args);
if (result.exitCode == 0) {
// result.stdout es un String, ya procesado
print('Salida: ${result.stdout.trim()}');
} else {
print('Error detectado: ${result.stderr}');
}
// 2. Uso de Process.start para procesos interactivos (Streaming)
print('\n--- [2] Interacción con Process.start (cat) ---');
// Usamos 'cat' (Unix) o 'type' (Windows) para leer de stdin y escupir a stdout
final command = Platform.isWindows ? 'cmd' : 'cat';
final commandArgs = Platform.isWindows ? ['/k', 'echo Escribe algo...'] : [];
final process = await Process.start(command, commandArgs);
// Escuchamos el stream de salida del proceso hijo de forma asíncrona
final stdoutSubscription = process.stdout
.transform(utf8.decoder)
.listen((data) => stdout.write('[Hijo]: $data'));
// Escribimos datos al stdin del proceso hijo
process.stdin.write('Datos enviados al proceso hijo\n');
await process.stdin.flush();
// Cerramos el stdin para indicarle al proceso que no habrá más entrada
await process.stdin.close();
// Esperamos a que el proceso hijo termine
final exitCode = await process.exitCode;
print('\n[Main] El proceso hijo terminó con código $exitCode');
// Aseguramos que todos los datos del stream se procesen antes de salir
await stdoutSubscription.drain();
}
Desglose del código
En la primera parte, utilizamos Process.run. Fíjate en cómo result.stdout se trata directamente como una cadena de texto. Esto es porque run encapsula la lógica de esperar a que el proceso termine y acumular todos los bytes en un buffer de memoria para entregártelos de una vez. Es cómodo, pero peligroso si el comando genera gigabytes de salida, ya que podrías agotar la memoria RAM.
En el segundo bloque, la lógica cambia radicalmente con Process.start. Aquí no obtenemos un resultado final inmediato, sino un objeto Process que mantiene abiertos los canales de comunicación.
process.stdoutes unStream<List<Uint8List>>. Para poder leerlo como texto de forma eficiente, usamos.transform(utf8.decoder), convirtiendo el flujo de bytes en un flujo de strings.process.stdin.writenos permite inyectar datos al proceso hijo mientras este aún está corriendo. Es vital llamar aprocess.stdin.close(); de lo contrario, el proceso hijo (comocaten el ejemplo) se quedará esperando infinitamente a que le envíes más datos, y nunca llegará a suexitCode.stdoutSubscription.drain()es una buena práctica de ingeniería: asegura que elStreamhaya terminado de procesar todos los paquetes de datos que quedaron en el buffer antes de que nuestro programa principal finalice.
El error frecuente
Un error clásico al trabajar con procesos es el Deadlock por saturación de buffer.
Si intentas hacer esto:
final process = await Process.start('comando_que_genera_muchos_datos', []);
// ERROR: Estamos esperando el cierre del proceso antes de leer la salida
final exitCode = await process.exitCode;
print(await process.stdout.first);
Si comando_que_genera_muchos_datos produce más datos de los que el buffer del sistema operativo permite (usualmente unos pocos KB), el proceso hijo se detendrá y se quedará bloqueado esperando a que alguien lea su stdout. Sin embargo, tu programa de Dart está bloqueado en la línea await process.exitCode, esperando a que el proceso termine para continuar. El proceso no terminará porque el buffer está lleno, y tu programa no leerá el buffer porque el proceso no termina. El sistema se queda congelado indefinidamente. La solución es siempre consumir los streams de stdout y stderr de forma asíncrona (con .listen) desde el momento en que el proceso arranca.
N° 79