execução do comando shell node.js

113

Ainda estou tentando entender os pontos mais delicados de como posso executar um comando linux ou windows shell e capturar a saída em node.js; no final das contas, eu quero fazer algo assim ...

//pseudocode
output = run_command(cmd, args)

A parte importante é que outputdeve estar disponível para uma variável com escopo global (ou objeto). Tentei a seguinte função, mas por algum motivo, fui undefinedimpresso no console ...

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" <------

Estou tendo problemas para entender onde o código quebra acima ... um protótipo muito simples desse 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" <----

Alguém pode me ajudar a entender por que try_this()funciona e run_cmd()não funciona? FWIW, preciso usar child_process.spawn, porque child_process.exectem um limite de buffer de 200 KB .

Resolução Final

Estou aceitando a resposta de Tiago White, mas este é o código exato que funcionou para mim ...

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
fonte
2
Na resolução final você deve definir me.stdout = "";em cmd_exec()para evitar concatenação undefinedpara o início do resultado.
aorcsik
Ei, esse código de resolução final é completamente horrível, e se demorar mais de 0,25 segundo para executar seu netstat?
Steven Lu
Ummmm ... talvez use uma das respostas para as quais eu atribuí um bônus ?????
Mike Pennington
Possível duplicata de Execute e obtenha a saída de um comando shell em node.js
Damjan Pavlica

Respostas:

89

Existem três problemas aqui que precisam ser corrigidos:

A primeira é que você está esperando um comportamento síncrono ao usar o stdout de forma assíncrona. Todas as chamadas em sua run_cmdfunção são assíncronas, portanto, ele gerará o processo filho e retornará imediatamente, independentemente de alguns, todos ou nenhum dos dados terem sido lidos no stdout. Como tal, quando você executa

console.log(foo.stdout);

você obtém o que quer que esteja armazenado em foo.stdout no momento, e não há garantia do que será, porque seu processo filho ainda pode estar em execução.

O segundo é que stdout é um fluxo legível , portanto 1) o evento de dados pode ser chamado várias vezes e 2) o retorno de chamada recebe um buffer, não uma string. Fácil de remediar; Apenas mude

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

para dentro

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

para que possamos converter nosso buffer em uma string e anexar essa string à nossa variável stdout.

Em terceiro lugar, você só pode saber que recebeu todas as saídas ao obter o evento 'final', o que significa que precisamos de outro ouvinte e retorno de chamada:

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

Então, seu resultado final é 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 White
fonte
"não há garantia do que será porque seu processo filho ainda pode estar em execução" close ... mas há uma garantia de que não será definido naquele momento e só será definido quando o retorno de chamada for finalmente chamado, como você indicou em outro lugar
4
Esta é uma excelente resposta com ótimas explicações sobre alguns conceitos muito importantes de JS. Agradável!
L0j1k
1
Você vai querer fazer this.stdout = "";dentro da run()função, caso contrário, seu console.log(foo.sdtout);será prefixado com undefined.
f1lt3r
78

Uma versão simplificada da resposta aceita (terceiro ponto) funcionou para mim.

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
fonte
1
E quanto ao valor de retorno, quando o processo retorna um valor diferente de zero, como posso obtê-lo?
Vitim.us
2
child.stdout.on('close', (errCode) => { console.log(errCode) } )
Tushar Gautam
52

Usei isso de forma mais concisa:

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

funciona perfeitamente. :)

Mimouni
fonte
1
Isso funciona bem e não requer nenhum módulo de nó adicional. Eu gosto disso!
bearvarine,
9
sys.puts()foi descontinuado em 2011 (com Node.js v0.2.3). Você deve usar em seu console.log()lugar.
tfmontague 05 de
1
isso realmente funciona ... então eu me pergunto, por que não é esta a resposta? é tão simples
ekkis
Uau!! esta resposta é perfeita e elegante. obrigado.
Em Ji Madhu
43

A maneira mais simples é apenas usar a biblioteca ShellJS ...

$ npm install [-g] shelljs

Exemplo 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 suporta muitos comandos shell comuns mapeados como funções NodeJS, incluindo:

  • gato
  • CD
  • chmod
  • cp
  • dirs
  • eco
  • exec
  • Saída
  • encontrar
  • grep
  • em
  • ls
  • mkdir
  • mv
  • popd
  • empurrar
  • pwd
  • rm
  • sed
  • teste
  • qual
Tony O'Hagan
fonte
como adicionar um parâmetro ao script de shell chamado por shell.exec ("foo.sh")?
pseudozach
1
Você pode simplesmente acrescentar os seus argumentos no topo da cadeia: shell.exec("foo.sh arg1 arg2 ... "). Seu foo.shscript pode fazer referência a eles usando $1, $2... etc.
Tony O'Hagan
Se o comando que você está tentando executar precisar da entrada do usuário, NÃO use ShellJS exec (). Esta função não é interativa por natureza, uma vez que apenas aceita o comando e imprime a saída. Não é possível aceitar entradas no meio. Em vez disso, use child_process integrado. Por exemplo, https://stackoverflow.com/a/31104898/9749509
MPatel1
4

Eu tive um problema semelhante e acabei escrevendo uma extensão de nó para isso. Você pode verificar o repositório git. É open source e gratuito e todas essas coisas boas!

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

ExecXI é uma extensão de nó escrita em C ++ para executar comandos shell um por um, enviando a saída do comando para o console em tempo real. Formas opcionais encadeadas e desencadeadas estão presentes; o que significa que você pode optar por parar o script após a falha de um comando (encadeado) ou pode continuar como se nada tivesse acontecido!

As instruções de uso estão no arquivo LeiaMe . Sinta-se à vontade para fazer solicitações de pull ou enviar problemas!

Achei que valia a pena mencionar isso.

Logan
fonte
4

@ TonyO'Hagan é uma shelljsresposta abrangente , mas, gostaria de destacar a versão síncrona de sua resposta:

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

One-liner síncrono:

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

Victorio Berra
fonte
0

Há um conflito de variável em sua run_cmdfunção:

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

Simplesmente mude para este:

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

Para ver os erros, sempre adicione:

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

EDITAR Seu código falha porque você está tentando executar o dirque não é fornecido como um programa autônomo separado. É um comando em cmdandamento. Se você quiser brincar com o sistema de arquivos, use o nativo require( 'fs' ).

Como alternativa (o que eu não recomendo), você pode criar um arquivo em lote que pode ser executado. Observe que o sistema operacional, por padrão, dispara arquivos em lote via cmd.

esquisito
fonte
obrigado por sua ajuda ... entretanto, mesmo quando eu corro C:\Windows\System32\netstat.exe, isso ainda não produz resultados ... Minha sintaxe exata era foo = new run_cmd('netstat.exe', ['-an'], function (me, data){me.stdout=data;});... Eu também tentei o caminho completo sem sucesso até agora
Mike Pennington
0

Na verdade, você não está retornando nada da função 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 muito bem. :)

Chris Eineke
fonte
Não acho que returnseja necessário, desde que você defina corretamente os atributos do objeto
Mike Pennington
0

Uma versão prometida da resposta mais 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))
Frank
fonte