Ejecutar comandos de Shell con Node.js

Con Node, podemos ejecutar comandos de shell y procesar su E/S usando JavaScript, en lugar del lenguaje de scripting de shell. Esto hace que la aplicación sea más fácil de mantener y desarrollar mientras permanecemos en el mismo entorno.

Introducción

Los administradores y desarrolladores de sistemas 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 se programan con frecuencia con scripts de shell. Sin embargo, un desarrollador puede preferir usar 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 del nivel del sistema operativo, lo que a menudo se hace más fácilmente con las utilidades del nivel de la línea de comandos.

Con Node.js, podemos ejecutar comandos de shell y procesar sus entradas y salidas usando 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 podría hacer que el programa sea más fácil de mantener.

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

El módulo child_proccess

Node.js ejecuta su ciclo de eventos principal en un solo hilo. Sin embargo, eso no significa que todo su procesamiento se realice en ese hilo. Las tareas asincrónicas en Node.js se ejecutan en otros subprocesos internos. Cuando están completos, el código en la devolución de llamada, o error, se devuelve al subproceso único principal.

Estos diversos subprocesos 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 secundarios de nuestro proceso principal de Node.js. Podemos ejecutar comandos de shell con estos procesos secundarios.

El uso de procesos externos puede mejorar el rendimiento de su aplicación si se usa correctamente. Por ejemplo, si una función de una aplicación de Node.js hace un uso intensivo de la CPU, dado que Node.js tiene un único subproceso, bloquearía la ejecución de otras tareas mientras se ejecuta.

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 luego ejecutará ese programa C ++ en un nuevo proceso, sin bloquear sus otras actividades, y cuando complete el proceso, su salida.

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

La función ejecutiva

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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}`);
});

Primero, requerimos el módulo child_process en nuestro programa, específicamente usando la función exec() (a través de la desestructuración de ES6). A continuación, llamamos a la función exec() con dos parámetros:

  • Una cadena con el comando de shell que queremos ejecutar.
  • Una función de devolución de llamada con tres parámetros: error, stdout, stderr.

El comando de shell que estamos ejecutando es ls -la, que debe enumerar todos los archivos y carpetas en nuestro directorio actual línea por línea, incluidos los archivos/carpetas ocultos. La función de devolución de llamada registra si recibimos 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 suceder si intenta ejecutar otro script de Node.js en exec() pero no se pudo encontrar 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 ejecuta ese archivo Node.js, debería ver un resultado similar a:

1
2
3
4
5
6
7
$ node lsExec.js
stdout: total 0
[correo electrónico protegido] 9 arpan arpan  0 Dec  7 00:14 .
[correo electrónico protegido] 4 arpan arpan  0 Dec  7 22:09 ..
[correo electrónico protegido] 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 de generación

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

Similar a antes, usaremos la función spawn() para listar todas las carpetas y archivos en nuestro directorio actual. Vamos a crear un nuevo archivo Node.js, lsSpawn.js, e ingrese lo siguiente:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
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 solicitando la función spawn() del módulo child_process. Luego, creamos un nuevo proceso que ejecuta el comando ls, pasando -la como argumento. Observe cómo los argumentos se mantienen en una matriz y no se incluyen en la cadena de comandos.

Luego configuramos a nuestros oyentes. El objeto stdout de ls dispara un evento data cuando el comando escribe en ese flujo. De manera similar, stderr también activa un evento data cuando el comando escribe en ese flujo.

Los errores se detectan escuchándolos directamente en el objeto que almacena la referencia para el comando. Solo obtendrá un error si child_process no puede ejecutar el comando.

El evento close ocurre cuando el comando ha terminado.

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

1
2
3
4
5
6
7
8
$ node lsSpawn.js
stdout: total 0
[correo electrónico protegido] 9 arpan arpan  0 Dec  7 00:14 .
[correo electrónico protegido] 4 arpan arpan  0 Dec  7 22:09 ..
[correo electrónico protegido] 1 arpan arpan  0 Dec  7 15:10 lsExec.js
[correo electrónico protegido] 1 arpan arpan  0 Dec  7 15:40 lsSpawn.js

child process exited with code 0

¿Cuándo usar 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 tal como llega.

En general, si no espera que se devuelvan grandes cantidades de datos, puede usar exec() para simplificar. Buenos ejemplos de casos de uso son crear una carpeta u obtener el estado de un archivo. Sin embargo, si espera una gran cantidad de resultados de su comando, entonces debe usar spawn(). Un buen ejemplo sería usar el comando para manipular datos binarios y luego cargarlos en su programa Node.js.

Conclusión

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

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

Ahora que puede ejecutar tareas externas a Node.js, ¿qué aplicaciones crearía?