exécution de la commande shell node.js

113

J'essaie toujours de saisir les subtilités de la façon dont je peux exécuter une commande Linux ou Windows Shell et capturer la sortie dans node.js; finalement, je veux faire quelque chose comme ça ...

//pseudocode
output = run_command(cmd, args)

L'élément important est qu'il outputdoit être disponible pour une variable (ou un objet) à portée globale. J'ai essayé la fonction suivante, mais pour une raison quelconque, je suis undefinedimprimé sur la 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" <------

J'ai du mal à comprendre où se situe le code ci-dessus ... un prototype très simple de ce modèle fonctionne ...

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

Quelqu'un peut-il m'aider à comprendre pourquoi try_this()fonctionne et run_cmd()ne fonctionne pas? FWIW, je dois utiliser child_process.spawn, car child_process.execa une limite de tampon de 200 Ko.

Résolution finale

J'accepte la réponse de James White, mais c'est le code exact qui a fonctionné pour moi ...

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
la source
2
Dans la résolution finale , vous devez définir me.stdout = "";dans cmd_exec()pour éviter concaténer undefinedau début du résultat.
aorcsik le
Hé, ce code de résolution finale est complètement horrible, et si cela prend plus de 0,25 seconde pour exécuter votre netstat?
Steven Lu
Ummmm ... peut-être utiliser l'une des réponses auxquelles j'ai attribué un bonus ?????
Mike Pennington

Réponses:

89

Il y a trois problèmes ici qui doivent être résolus:

Tout d'abord , vous vous attendez à un comportement synchrone lors de l'utilisation asynchrone de stdout. Tous les appels de votre run_cmdfonction sont asynchrones, donc il engendrera le processus enfant et retournera immédiatement indépendamment du fait que certaines, toutes ou aucune des données aient été lues sur stdout. En tant que tel, lorsque vous courez

console.log(foo.stdout);

vous obtenez tout ce qui se trouve être stocké dans foo.stdout pour le moment, et il n'y a aucune garantie de ce que ce sera parce que votre processus enfant est peut-être toujours en cours d'exécution.

Deuxièmement , stdout est un flux lisible , donc 1) l'événement de données peut être appelé plusieurs fois, et 2) le rappel reçoit un tampon, pas une chaîne. Facile à remédier; juste changer

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

dans

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

afin que nous convertissions notre tampon en une chaîne et ajoutions cette chaîne à notre variable stdout.

Troisièmement , vous ne pouvez savoir que vous avez reçu toute la sortie lorsque vous obtenez l'événement 'end', ce qui signifie que nous avons besoin d'un autre auditeur et d'un autre rappel:

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

Donc, votre résultat final est le suivant:

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
la source
"il n'y a aucune garantie de ce que cela sera parce que votre processus enfant est peut-être toujours en cours d'exécution" close ... mais il y a une garantie qu'il ne sera pas défini à ce moment-là, et ne le sera que lorsque le rappel sera finalement appelé, comme vous l'avez indiqué ailleurs
4
C'est une excellente réponse avec de grandes explications sur certains concepts JS très importants. Agréable!
L0j1k
1
Vous voudrez faire this.stdout = "";à l'intérieur de la run()fonction, sinon votre console.log(foo.sdtout);sera préfixé avec undefined.
f1lt3r
78

Une version simplifiée de la réponse acceptée (troisième point), a juste fonctionné pour moi.

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) });
} // ()

Usage:

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

run_cmd( "hostname", [], function(text) { console.log (text) });
cibercitoyen1
la source
1
Qu'en est-il de la valeur de retour, lorsque le processus renvoie une valeur non nulle, comment puis-je l'obtenir?
Vitim.us
2
child.stdout.on('close', (errCode) => { console.log(errCode) } )
Tushar Gautam
52

J'ai utilisé ceci de manière plus concise:

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

cela fonctionne parfaitement. :)

Mimouni
la source
1
Cela fonctionne bien et ne nécessite aucun module de nœud supplémentaire. Je l'aime!
bearvarine
9
sys.puts()était obsolète en 2011 (avec Node.js v0.2.3). Vous devriez utiliser à la console.log()place.
tfmontague
1
cela fonctionne réellement ... alors je me demande, pourquoi n'est-ce pas la réponse? it's so simple
ekkis
Sensationnel!! cette réponse est parfaite et élégante. Merci.
Em Ji Madhu
43

Le moyen le plus simple est d'utiliser simplement la lib ShellJS ...

$ npm install [-g] shelljs

Exemple 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 prend en charge de nombreuses commandes shell courantes mappées en tant que fonctions NodeJS, notamment:

  • chat
  • CD
  • chmod
  • cp
  • dirs
  • écho
  • exec
  • sortie
  • trouver
  • grep
  • ln
  • ls
  • mkdir
  • mv
  • popd
  • pushd
  • pwd
  • rm
  • sed
  • tester
  • lequel
Tony O'Hagan
la source
comment ajouter un paramètre au script shell appelé par shell.exec ("foo.sh")?
pseudozach
1
Vous pouvez simplement ajouter vos arguments haut de la chaîne: shell.exec("foo.sh arg1 arg2 ... "). Votre foo.shscript peut les référencer en utilisant $1, $2... etc.
Tony O'Hagan
Si la commande que vous essayez d'exécuter nécessite une entrée de l'utilisateur, n'utilisez PAS ShellJS exec (). Cette fonction n'est pas de nature interactive, car elle prendra juste la commande et la sortie d'impression, ne peut pas accepter les entrées entre les deux. Utilisez plutôt le child_process intégré. Par exemple: https://stackoverflow.com/a/31104898/9749509
MPatel1
4

J'ai eu un problème similaire et j'ai fini par écrire une extension de nœud pour cela. Vous pouvez extraire le référentiel git. C'est open source et gratuit et toutes ces bonnes choses!

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

ExecXI est une extension de nœud écrite en C ++ pour exécuter les commandes shell une par une, en envoyant la sortie de la commande à la console en temps réel. Des moyens optionnels chaînés et non chaînés sont présents; ce qui signifie que vous pouvez choisir d'arrêter le script après l'échec d'une commande (chaînée), ou vous pouvez continuer comme si de rien n'était!

Les instructions d'utilisation se trouvent dans le fichier ReadMe . N'hésitez pas à faire des pull requests ou à soumettre des problèmes!

J'ai pensé qu'il valait la peine de le mentionner.

Logan
la source
4

@ TonyO'Hagan est une shelljsréponse complète , mais je voudrais souligner la version synchrone de sa réponse:

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

Monotouche synchrone:

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

Victorio Berra
la source
0

Il y a un conflit de variable dans votre run_cmdfonction:

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

Changez-le simplement en ceci:

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

Afin de voir les erreurs, ajoutez toujours ceci:

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

EDIT Votre code échoue parce que vous essayez d'exécuter dirce qui n'est pas fourni en tant que programme autonome séparé. C'est une commande en cmdcours. Si vous voulez jouer avec le système de fichiers, utilisez natifrequire( 'fs' ) .

Alternativement (ce que je ne recommande pas) vous pouvez créer un fichier batch que vous pouvez ensuite exécuter. Notez que le système d'exploitation déclenche par défaut les fichiers de commandes via cmd.

bizarre
la source
merci pour votre aide ... cependant, même lorsque je cours C:\Windows\System32\netstat.exe, cela ne donne toujours pas de résultats ... Ma syntaxe exacte était foo = new run_cmd('netstat.exe', ['-an'], function (me, data){me.stdout=data;});... J'ai également essayé le chemin complet sans succès jusqu'à présent
Mike Pennington
0

Vous ne retournez rien de votre fonction 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...'

Fonctionne très bien. :)

Chris Eineke
la source
Je ne pense pas que ce returnsoit nécessaire tant que vous définissez correctement les attributs de l'objet
Mike Pennington
0

Une version promis de la réponse la plus récompensée:

  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) })
    })
  }

Utiliser:

 runCmd('ls').then(ret => console.log(ret))
Franc
la source