Ejecución de comandos Shell con Node.js

Ejecución de comandos Shell con Node.js

Los administradores de sistemas y desarrolladores recurren con frecuencia a la automatización para reducir su carga de trabajo y mejorar sus procesos. Cuando se trabaja con servidores, las tareas automatizadas suelen programarse con shell scripts. Sin embargo, un desarrollador puede preferir utilizar un lenguaje de alto nivel más general para tareas complejas. Muchas aplicaciones también necesitan interactuar con el sistema de archivos y otros componentes a nivel de sistema operativo, lo que a menudo se hace más fácilmente con utilidades a nivel de línea de comandos.

Con Node.js, podemos ejecutar comandos shell y procesar sus entradas y salidas utilizando JavaScript. Por lo tanto, podemos escribir la mayoría de estas operaciones complejas en JavaScript en lugar del lenguaje de secuencias de comandos de shell, lo que potencialmente hace que el programa sea más fácil de mantener.

En este artículo, aprenderemos las distintas formas de ejecutar comandos shell en Node.js utilizando el módulo child_process.

El módulo child_proccess

Node.js ejecuta su bucle de eventos principal en un único hilo. Sin embargo, eso no significa que todo su procesamiento se realice en ese único hilo. Las tareas asíncronas en Node.js se ejecutan en otros hilos internos. Cuando se completan, el código de la llamada de retorno, o error, se devuelve al hilo principal.

Estos distintos hilos se ejecutan en el mismo proceso de Node.js. Sin embargo, a veces es deseable crear otro proceso para ejecutar código. Cuando se crea un nuevo proceso, el sistema operativo determina qué procesador utiliza y cómo programar sus tareas.

El módulo child_process crea nuevos procesos hijos de nuestro proceso principal Node.js. Podemos ejecutar comandos shell con estos procesos hijo.

El uso de procesos externos puede mejorar el rendimiento de tu aplicación si se utiliza correctamente. Por ejemplo, si una característica de una aplicación Node.js hace un uso intensivo de la CPU, como Node.js es monohilo bloquearía la ejecución de las demás tareas mientras se está ejecutando.

Sin embargo, podemos delegar ese código intensivo en recursos a un proceso hijo, digamos un programa C++ muy eficiente. Nuestro código Node.js ejecutará entonces ese programa C++ en un nuevo proceso, sin bloquear sus otras actividades, y cuando se complete procesará su salida.

Dos funciones que usaremos para ejecutar comandos shell son exec() y spawn().

La función exec()

La función exec() crea un nuevo shell y ejecuta un comando dado. La salida de la ejecución se almacena en búfer, lo que significa que se mantiene en memoria, y está disponible para su uso en una llamada de retorno.

Usemos la función exec() para listar todas las carpetas y archivos de nuestro directorio actual. En un nuevo archivo Node.js llamado lsExec.js, escribe el siguiente código:

const { exec } = require("child_process");

exec("ls -la", (error, stdout, stderr) => {
    if (error) {
        console.log(`error: ${error.message}`);
        return;
    }
    if (stderr) {
        console.log(`stderr: ${stderr}`);
        return;
    }
    console.log(`stdout: ${stdout}`);
});

En primer lugar, requerimos el módulo child_process en nuestro programa, concretamente utilizando la función exec() (mediante destructuring ES6). A continuación, llamamos a la función exec() con dos parámetros:

Una cadena con el comando shell que queremos que se ejecute.
Una función callback con tres parámetros: error, stdout, stderr.

El comando de shell que estamos ejecutando es ls -la, que debería listar todos los archivos y carpetas de nuestro directorio actual línea por línea, incluyendo los archivos/carpetas ocultos. La función callback registra si hemos obtenido un error al intentar ejecutar el comando o la salida en los flujos stdout o stderr del shell.

Nota: El objeto error es diferente de stderr. El objeto error no es nulo cuando el módulo child_process falla al ejecutar un comando. Esto podría ocurrir si se intenta ejecutar otro script Node.js en exec() pero no se encuentra el archivo, por ejemplo. Por otro lado, si el comando se ejecuta con éxito y escribe un mensaje en el flujo de error estándar, entonces el objeto stderr no sería nulo.

Si ejecutas ese archivo Node.js, deberías ver una salida similar a:

$ node lsExec.js
stdout: total 0
drwxr-xr-x@ 9 arpan arpan  0 Dec  7 00:14 .
drwxr-xr-x@ 4 arpan arpan  0 Dec  7 22:09 ..
-rw-r--r--@ 1 arpan arpan  0 Dec  7 15:10 lsExec.js

child process exited with code 0

Ahora que hemos entendido cómo ejecutar comandos con exec(), aprendamos otra forma de ejecutar comandos con spawn().

La función spawn()

La función spawn() ejecuta un comando en un nuevo proceso. Esta función utiliza un Stream API, por lo que su salida del comando está disponible a través de listeners.

Como antes, usaremos la función spawn() para listar todas las carpetas y archivos de nuestro directorio actual. Creemos un nuevo archivo Node.js, lsSpawn.js, e introduzcamos lo siguiente:

const { spawn } = require("child_process");

const ls = spawn("ls", ["-la"]);

ls.stdout.on("data", data => {
    console.log(`stdout: ${data}`);
});

ls.stderr.on("data", data => {
    console.log(`stderr: ${data}`);
});

ls.on('error', (error) => {
    console.log(`error: ${error.message}`);
});

ls.on("close", code => {
    console.log(`child process exited with code ${code}`);
});

Comenzamos requiriendo la función spawn() del módulo child_process. Luego, creamos un nuevo proceso que ejecuta el comando ls, pasando -la como argumento. Observa cómo los argumentos se mantienen en un array y no se incluyen en la cadena de comandos.

A continuación, configuramos nuestros escuchadores. El objeto stdout de ls, dispara un evento de datos cuando el comando escribe en ese flujo. Del mismo modo, el stderr también dispara un evento de datos cuando el comando escribe en ese flujo.

Los errores se capturan escuchándolos directamente en el objeto que almacena la referencia del comando. Sólo obtendrá un error si child_process falla al ejecutar el comando.

El evento close ocurre cuando el comando ha terminado.

Si ejecutamos este archivo Node.js, deberíamos obtener una salida como antes con exec():

$ node lsSpawn.js
stdout: total 0
drwxr-xr-x@ 9 arpan arpan  0 Dec  7 00:14 .
drwxr-xr-x@ 4 arpan arpan  0 Dec  7 22:09 ..
-rw-r--r--@ 1 arpan arpan  0 Dec  7 15:10 lsExec.js
-rw-r--r--@ 1 arpan arpan  0 Dec  7 15:40 lsSpawn.js

child process exited with code 0

¿Cuándo utilizar exec() y spawn()?

La diferencia clave entre exec() y spawn() es cómo devuelven los datos. Como exec() almacena toda la salida en un búfer, consume más memoria que spawn(), que transmite la salida a medida que se produce.

Generalmente, si no espera que se devuelvan grandes cantidades de datos, puede utilizar exec() por simplicidad. Buenos ejemplos de casos de uso son crear una carpeta u obtener el estado de un fichero. Sin embargo, si está esperando una gran cantidad de salida de su comando, entonces debería usar spawn(). Un buen ejemplo sería usar comandos para manipular datos binarios y luego cargarlos en tu programa Node.js.

Conclusión

Node.js puede ejecutar comandos shell utilizando el módulo estándar child_process. Si usamos la función exec(), nuestro comando se ejecutará y su salida estará disponible para nosotros en un callback. Si utilizamos el módulo spawn(), su salida estará disponible a través de escuchadores de eventos.

Si nuestra aplicación espera mucha salida de nuestros comandos, deberíamos preferir spawn() sobre exec(). Si no, podríamos optar por usar exec() por su simplicidad.

Ahora que puedes ejecutar tareas externas a Node.js, ¿qué aplicaciones construirías?

Documentación de Node.js

Comentarios