ejecución del comando de shell node.js

113

Todavía estoy tratando de comprender los puntos más finos de cómo puedo ejecutar un comando de shell de Linux o Windows y capturar la salida dentro de node.js; en última instancia, quiero hacer algo como esto ...

//pseudocode
output = run_command(cmd, args)

La pieza importante es que outputdebe estar disponible para una variable (u objeto) de ámbito global. Probé la siguiente función, pero por alguna razón, me undefinedimprimen en la consola ...

function run_cmd(cmd, args, cb) {
  var spawn = require('child_process').spawn
  var child = spawn(cmd, args);
  var me = this;
  child.stdout.on('data', function(me, data) {
    cb(me, data);
  });
}
foo = new run_cmd('dir', ['/B'], function (me, data){me.stdout=data;});
console.log(foo.stdout);  // yields "undefined" <------

Tengo problemas para entender dónde se rompe el código anterior ... un prototipo muy simple de ese modelo funciona ...

function try_this(cmd, cb) {
  var me = this;
  cb(me, cmd)
}
bar = new try_this('guacamole', function (me, cmd){me.output=cmd;})
console.log(bar.output); // yields "guacamole" <----

¿Puede alguien ayudarme a entender por qué try_this()funciona y por qué run_cmd()no? FWIW, necesito usar child_process.spawn, porque child_process.exectiene un límite de búfer de 200 KB .

Resolución final

Acepto la respuesta de James White, pero este es el código exacto que funcionó para mí ...

function cmd_exec(cmd, args, cb_stdout, cb_end) {
  var spawn = require('child_process').spawn,
    child = spawn(cmd, args),
    me = this;
  me.exit = 0;  // Send a cb to set 1 when cmd exits
  me.stdout = "";
  child.stdout.on('data', function (data) { cb_stdout(me, data) });
  child.stdout.on('end', function () { cb_end(me) });
}
foo = new cmd_exec('netstat', ['-rn'], 
  function (me, data) {me.stdout += data.toString();},
  function (me) {me.exit = 1;}
);
function log_console() {
  console.log(foo.stdout);
}
setTimeout(
  // wait 0.25 seconds and print the output
  log_console,
250);
Mike Pennington
fuente
2
En la resolución final debe establecer me.stdout = "";en cmd_exec()para evitar la concatenación undefinedal principio del resultado.
aorcsik
Oye, ese código de resolución final es completamente horrible, ¿qué pasa si se tarda más de 0,25 segundos en ejecutar tu netstat?
Steven Lu
Ummmm ... tal vez use una de las respuestas a las que le otorgué un bono ?????
Mike Pennington

Respuestas:

89

Aquí hay tres problemas que deben solucionarse:

Primero es que espera un comportamiento sincrónico mientras usa stdout de forma asincrónica. Todas las llamadas en su run_cmdfunción son asincrónicas, por lo que generará el proceso hijo y regresará inmediatamente, independientemente de si algunos, todos o ninguno de los datos se han leído en la salida estándar. Como tal, cuando corres

console.log(foo.stdout);

obtiene lo que sea que esté almacenado en foo.stdout en este momento, y no hay garantía de cuál será porque su proceso hijo aún podría estar ejecutándose.

El segundo es que stdout es un flujo legible , por lo que 1) el evento de datos se puede llamar varias veces y 2) la devolución de llamada recibe un búfer, no una cadena. Fácil de remediar; solo cambia

foo = new run_cmd(
    'netstat.exe', ['-an'], function (me, data){me.stdout=data;}
);

dentro

foo = new run_cmd(
    'netstat.exe', ['-an'], function (me, buffer){me.stdout+=buffer.toString();}
);

de modo que convertimos nuestro búfer en una cadena y agregamos esa cadena a nuestra variable stdout.

En tercer lugar , solo puede saber que ha recibido toda la salida cuando obtiene el evento 'final', lo que significa que necesitamos otro oyente y devolución de llamada:

function run_cmd(cmd, args, cb, end) {
    // ...
    child.stdout.on('end', end);
}

Entonces, su resultado final es este:

function run_cmd(cmd, args, cb, end) {
    var spawn = require('child_process').spawn,
        child = spawn(cmd, args),
        me = this;
    child.stdout.on('data', function (buffer) { cb(me, buffer) });
    child.stdout.on('end', end);
}

// Run C:\Windows\System32\netstat.exe -an
var foo = new run_cmd(
    'netstat.exe', ['-an'],
    function (me, buffer) { me.stdout += buffer.toString() },
    function () { console.log(foo.stdout) }
);
James blanco
fuente
"No hay garantía de cuál será porque su proceso hijo aún podría estar ejecutándose" cerrar ... pero hay una garantía de que no se configurará en ese momento y solo se configurará cuando finalmente se llame a la devolución de llamada, como indicaste en otro lugar
4
Esta es una excelente respuesta con grandes explicaciones de algunos conceptos JS muy importantes. ¡Agradable!
L0j1k
1
Querrá hacerlo this.stdout = "";dentro de la run()función, de lo contrario, console.log(foo.sdtout);tendrá el prefijo undefined.
f1lt3r
78

Una versión simplificada de la respuesta aceptada (tercer punto), simplemente funcionó para mí.

function run_cmd(cmd, args, callBack ) {
    var spawn = require('child_process').spawn;
    var child = spawn(cmd, args);
    var resp = "";

    child.stdout.on('data', function (buffer) { resp += buffer.toString() });
    child.stdout.on('end', function() { callBack (resp) });
} // ()

Uso:

run_cmd( "ls", ["-l"], function(text) { console.log (text) });

run_cmd( "hostname", [], function(text) { console.log (text) });
cibercitizen1
fuente
1
¿Qué pasa con el valor de retorno, cuando el proceso devuelve un valor distinto de cero, cómo puedo obtenerlo?
Vitim.us
2
child.stdout.on('close', (errCode) => { console.log(errCode) } )
Tushar Gautam
52

Usé esto de manera más concisa:

var sys = require('sys')
var exec = require('child_process').exec;
function puts(error, stdout, stderr) { sys.puts(stdout) }
exec("ls -la", puts);

funciona perfectamente. :)

Mimouni
fuente
1
Esto funciona bien y no requiere ningún módulo de nodo adicional. ¡Me gusta!
Bearvarine
9
sys.puts()quedó obsoleto en 2011 (con Node.js v0.2.3). Deberías usar console.log()en su lugar.
tfmontague
1
eso realmente funciona ... así que me pregunto, ¿por qué no es esta la respuesta? es tan simple
ekkis
¡¡Guau!! esta respuesta es perfecta y elegante. gracias.
Em Ji Madhu
43

La forma más sencilla es usar la biblioteca ShellJS ...

$ npm install [-g] shelljs

Ejemplo de EXEC:

require('shelljs/global');

// Sync call to exec()
var version = exec('node --version', {silent:true}).output;

// Async call to exec()
exec('netstat.exe -an', function(status, output) {
  console.log('Exit status:', status);
  console.log('Program output:', output);
});

ShellJs.org admite muchos comandos de shell comunes asignados como funciones de NodeJS, que incluyen:

  • gato
  • discos compactos
  • chmod
  • cp
  • dirs
  • eco
  • ejecutivo
  • salida
  • encontrar
  • grep
  • en
  • ls
  • mkdir
  • mv
  • popd
  • empujado
  • pwd
  • rm
  • sed
  • prueba
  • cual
Tony O'Hagan
fuente
¿Cómo agregar un parámetro al script de shell llamado por shell.exec ("foo.sh")?
pseudozach
1
Usted puede simplemente añadir sus argumentos encabezan la cadena: shell.exec("foo.sh arg1 arg2 ... "). Su foo.shguión puede hacer referencia a estos usando $1, $2... etc.
Tony O'Hagan
Si el comando que está intentando ejecutar va a necesitar información del usuario, NO use ShellJS exec (). Esta función no es de naturaleza interactiva, ya que solo tomará el comando y la salida de impresión. No se pueden aceptar entradas intermedias. Utilice child_process integrado en su lugar. Por ejemplo, https://stackoverflow.com/a/31104898/9749509
MPatel1
4

Tuve un problema similar y terminé escribiendo una extensión de nodo para esto. Puedes consultar el repositorio de git. ¡Es de código abierto y gratuito y todas esas cosas buenas!

https://github.com/aponxi/npm-execxi

ExecXI es una extensión de nodo escrita en C ++ para ejecutar comandos de shell uno por uno, enviando la salida del comando a la consola en tiempo real. Hay formas opcionales encadenadas y desencadenadas; lo que significa que puede optar por detener el script después de que un comando falla (encadenado), o puede continuar como si nada hubiera pasado.

Las instrucciones de uso están en el archivo Léame . ¡No dude en hacer solicitudes de extracción o enviar problemas!

Pensé que valía la pena mencionarlo.

Logan
fuente
4

@ TonyO'Hagan es una shelljsrespuesta completa, pero me gustaría resaltar la versión sincrónica de su respuesta:

var shell = require('shelljs');
var output = shell.exec('netstat -rn', {silent:true}).output;
console.log(output);
Stephen Quan
fuente
1

Una línea sincrónica:

require('child_process').execSync("echo 'hi'", function puts(error, stdout, stderr) { console.log(stdout) });

Victorio Berra
fuente
0

Hay un conflicto variable en su run_cmdfunción:

  var me = this;
  child.stdout.on('data', function(me, data) {
    // me is overriden by function argument
    cb(me, data);
  });

Simplemente cámbielo a esto:

  var me = this;
  child.stdout.on('data', function(data) {
    // One argument only!
    cb(me, data);
  });

Para ver los errores, agregue siempre esto:

  child.stderr.on('data', function(data) {
      console.log( data );
  });

EDITAR Su código falla porque está intentando ejecutar lo dirque no se proporciona como un programa independiente separado. Es un comando en cmdproceso. Si quieres jugar con el sistema de archivos, usa native require( 'fs' ).

Alternativamente (que no recomiendo) puede crear un archivo por lotes que luego puede ejecutar. Tenga en cuenta que el sistema operativo por defecto dispara archivos por lotes a través de cmd.

monstruoso
fuente
gracias por su ayuda ... sin embargo, incluso cuando corro C:\Windows\System32\netstat.exe, esto todavía no produce resultados ... Mi sintaxis exacta fue foo = new run_cmd('netstat.exe', ['-an'], function (me, data){me.stdout=data;});... También probé la ruta completa sin éxito hasta ahora
Mike Pennington
0

En realidad, no devuelve nada de su función run_cmd.

function run_cmd(cmd, args, done) {
    var spawn = require("child_process").spawn;
    var child = spawn(cmd, args);
    var result = { stdout: "" };
    child.stdout.on("data", function (data) {
            result.stdout += data;
    });
    child.stdout.on("end", function () {
            done();
    });
    return result;
}

> foo = run_cmd("ls", ["-al"], function () { console.log("done!"); });
{ stdout: '' }
done!
> foo.stdout
'total 28520...'

Funciona muy bien. :)

Chris Eineke
fuente
No creo que returnsea ​​necesario siempre que establezca correctamente los atributos del objeto
Mike Pennington
0

Una versión prometida de la respuesta más premiada:

  runCmd: (cmd, args) => {
    return new Promise((resolve, reject) => {
      var spawn = require('child_process').spawn
      var child = spawn(cmd, args)
      var resp = ''
      child.stdout.on('data', function (buffer) { resp += buffer.toString() })
      child.stdout.on('end', function () { resolve(resp) })
    })
  }

Usar:

 runCmd('ls').then(ret => console.log(ret))
Franco
fuente