执行:显示标准输出“实时”

257
我有这个简单的脚本:
var exec = require('child_process').exec;

exec('coffee -cw my_file.coffee', function(error, stdout, stderr) {
    console.log(stdout);
});

我只需执行一个命令来编译一个coffee-script文件。但是stdout从未在控制台中显示,因为该命令永远不会结束(因为coffee的-w选项)。 如果我直接从控制台执行该命令,我会收到如下消息:

18:05:59 - compiled my_file.coffee

我的问题是:是否可以使用node.js exec显示这些消息?如果可以,怎么做?!

3
我来到这里是为了捕获Python可执行文件的标准输出。请注意下面的所有方法都可以使用,但您需要在运行python时加上"-u"选项,以使输出不经过缓冲并因此实时更新。 - Andy
10个回答

324
不要使用exec。使用spawn,它是一个EventEmmiter对象。然后您可以监听stdout/stderr事件(spawn.stdout.on('data',callback..)) 随时发生的

来自NodeJS文档:

var spawn = require('child_process').spawn,
    ls    = spawn('ls', ['-lh', '/usr']);

ls.stdout.on('data', function (data) {
  console.log('stdout: ' + data.toString());
});

ls.stderr.on('data', function (data) {
  console.log('stderr: ' + data.toString());
});

ls.on('exit', function (code) {
  console.log('child process exited with code ' + code.toString());
});

exec 缓冲输出并在命令执行完成后通常返回它。


27
很好。FYI:stdout/stderr事件的回调参数'data'是一个缓冲区,因此请使用.toString()进行调用。 - SergeL
5
如果你无法在Windows中使用"spawn",可以查看这个很好的答案 - tomekwi
31
在最新的版本中,exec也是一个EventEmitter。 - Nikolay Tsenkov
7
请注意,每当程序输出一个换行符时,回调函数将不会被调用。如果你想从子进程接收“事件”,此进程必须刷新缓冲区(在 C 中使用 flush(stdout); ),以便在 Node.js 中触发事件。 - Julian F. Weinert
9
+1 在执行时也是一个 EventEmitter... 我花了2个小时把我的字符串重构成参数数组(非常长且复杂的ffmpeg命令行)...只是发现我实际上并不需要这样做。 - deadconversations
显示剩余9条评论

271

exec会返回一个ChildProcess对象,它也是一个EventEmitter。

var exec = require('child_process').exec;
var coffeeProcess = exec('coffee -cw my_file.coffee');

coffeeProcess.stdout.on('data', function(data) {
    console.log(data); 
});

将子进程的标准输出通过管道 pipe 到主进程的标准输出。

coffeeProcess.stdout.pipe(process.stdout);

或使用 spawn 继承 stdio

spawn('coffee -cw my_file.coffee', { stdio: 'inherit' });

39
似乎可以通过使用“管道”(pipe)来简化此代码:coffeeProcess.stdout.pipe(process.stdout) - Eric Freese
4
@EricFreese的评论是我正在寻找的,因为我想利用标准输出的字符替换功能(在一个Node脚本中使用Protractor)。 - LoremIpsum
26
更简单的写法:spawn(cmd, argv, { stdio: 'inherit' })。详见 https://nodejs.org/api/child_process.html#child_process_options_stdio,了解其他示例。 - Morgan Touverey Quilling
3
对于@MorganTouvereyQuilling的建议,使用spawnstdio: 'inherit'可以得到更准确的输出结果。相比使用exec和管道传输stdout/stderr,例如当显示从git clone中获取的进度信息时,这种方式更为有效。 - Livven

103

已经有几个答案,但是它们都没有提到最好(也是最容易)的方法,使用 spawn{ stdio: 'inherit' } 选项。它似乎产生了最精确的输出,例如在显示从git clone复制的进度信息时。

只需要这样做:

var spawn = require('child_process').spawn;

spawn('coffee', ['-cw', 'my_file.coffee'], { stdio: 'inherit' });

感谢@MorganTouvereyQuilling在这个评论中指出这一点。


1
我发现当子进程使用格式化输出,例如彩色文本时,“stdio: inherit”会保留该格式,而“child.stdout.pipe(process.stdout)”则不会。 - Rikki Gibson
1
这可以完美地保留输出,即使在具有复杂输出的进程上,例如 npm 安装中的进度条。太棒了! - Dave Koo
5
为什么这不是被采纳的答案?它是唯一对我有用的,而且只有两行代码!!! - Lincoln
1
这应该是被接受的答案 - 唯一保留完美输出表示并且最简单的东西?是的,请。 - evnp
正是我正在寻找的,非常好的答案,必须被接受。 - A. Askarov
显示剩余2条评论

28
受Nathanael Smith的回答和Eric Freese的评论启发,这可能很简单,就像这样:
var exec = require('child_process').exec;
exec('coffee -cw my_file.coffee').stdout.pipe(process.stdout);

2
这对于像 ls 这样的简单命令似乎运行良好,但对于更复杂的命令如 npm install 则失败了。我甚至尝试将 stdout 和 stderr 都导向它们各自的进程对象。 - linuxdan
@linuxdan 可能是因为 npm 在 stderr 中写入(我看到有些人在那里写进度条)。你也可以将 stderr 管道化,或者扩展 Tongfa 的解决方案以侦听 stderr。 - Sergiu
@linuxdan 从我所看到的,最可靠的方法是使用spawn(command, args, { stdio: 'inherit' }),正如这里建议的那样:https://dev59.com/gGkv5IYBdhLWcg3w9lai#MrGiEYcBWogLw_1bDfXU - Livven
最佳答案,谢谢。非常有效。 - Abhishek Sharma

23

我想补充一下,使用console.log()输出由子进程生成的缓冲字符串的一个小问题是它会添加换行符,这会使生成的输出跨越多行。如果你使用process.stdout.write()输出stdoutstderr而不是console.log(),那么你将得到来自生成的进程的控制台输出。

我在这里看到了这个解决方案:Node.js:在没有尾随换行符的情况下打印到控制台?

希望这对使用上述解决方案的人有所帮助(即使它来自于文档,但对于实时输出来说确实是一个很好的解决方案)。


1
为了获得更准确的输出,请使用spawn(command, args, { stdio: 'inherit' }),正如@MorganTouvereyQuilling在这里建议的那样:https://dev59.com/gGkv5IYBdhLWcg3w9lai#MrGiEYcBWogLw_1bDfXU - Livven

21

我发现在我的工具中添加一个自定义的执行脚本非常有帮助,可以完成这个操作。

utilities.js

const { exec } = require('child_process')

module.exports.exec = (command) => {
  const process = exec(command)
  
  process.stdout.on('data', (data) => {
    console.log('stdout: ' + data.toString())
  })
  
  process.stderr.on('data', (data) => {
    console.log('stderr: ' + data.toString())
  })
  
  process.on('exit', (code) => {
    console.log('child process exited with code ' + code.toString())
  })
}

app.js

const { exec } = require('./utilities.js')
    
exec('coffee -cw my_file.coffee')

它能正常工作,但其格式与安装npm时的进度条和彩色文本不同。 - Akshay Kumar

5

在查看了所有其他答案后,我最终得出了这个:

function oldSchoolMakeBuild(cb) {
    var makeProcess = exec('make -C ./oldSchoolMakeBuild',
         function (error, stdout, stderr) {
             stderr && console.error(stderr);
             cb(error);
        });
    makeProcess.stdout.on('data', function(data) {
        process.stdout.write('oldSchoolMakeBuild: '+ data);
    });
}

有时候 data 会有多行,所以 oldSchoolMakeBuild 标题将一次性用于多个行。但这并不足以让我改变它。

3

child_process.spawn返回一个带有标准输出stdout和标准错误stderr流的对象。您可以在stdout流上读取子进程发送回Node的数据。由于stdout是一个流,所以具有" data"、" end"和其他流事件。当您想让子进程返回大量数据给Node时,spawn最好用于解决问题——例如图像处理、读取二进制数据等。

因此,您可以按以下方式使用child_process.spawn来解决您的问题。

var spawn = require('child_process').spawn,
ls = spawn('coffee -cw my_file.coffee');

ls.stdout.on('data', function (data) {
  console.log('stdout: ' + data.toString());
});

ls.stderr.on('data', function (data) {
  console.log('stderr: ' + data.toString());
});

ls.on('exit', function (code) {
  console.log('code ' + code.toString());
});

2

这是一个用Typescript编写的异步辅助函数,对我来说似乎很有效。我猜这不适用于长期运行的进程,但对某些人可能仍然有用?

import * as child_process from "child_process";

private async spawn(command: string, args: string[]): Promise<{code: number | null, result: string}> {
    return new Promise((resolve, reject) => {
        const spawn = child_process.spawn(command, args)
        let result: string
        spawn.stdout.on('data', (data: any) => {
            if (result) {
                reject(Error('Helper function does not work for long lived proccess'))
            }
            result = data.toString()
        })
        spawn.stderr.on('data', (error: any) => {
            reject(Error(error.toString()))
        })
        spawn.on('exit', code => {
            resolve({code, result})
        })
    })
}

0

我找到了另一种方法来做它:

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

export const execCommand = async (command) => {
  return new Promise((resolve, reject) => {
    const [cmd, ...args] = command.split(' ');
    const childProcess = spawn(cmd, args);
    childProcess.stdout.on('data', (data) => {
      process.stdout.write(data.toString());
    });
    childProcess.stderr.on('data', (data) => {
      process.stderr.write(data.toString());
    });
    childProcess.on('error', (error) => {
      reject(error);
    });
    childProcess.on('exit', (code) => {
      if (code === 0) {
        resolve();
      } else {
        reject(new Error(`Command exited with code ${code}.`));
      }
    });
  });
};

这段代码提供了实时获取执行命令的输出并将所有标准输出和标准错误重定向到父进程的能力。它还允许像在bash/sh中使用命令一样使用该命令(单个字符串输入)。在这里,我使用process.stdout.write来获得更精确的输出,而不是其他答案中使用的console.log

用法:

await execCommand('sudo apt-get update');
await execCommand('sudo apt-get install -y docker.io docker-compose');

注意:与exec相比,它不支持使用&&执行多个命令。因此,每个单独的命令都应该使用一个execCommand语句来执行。
这里有一个简化版本,支持实时流和shell执行:
const { spawn } = require('child_process');

export const execCommand = async (command) => {
  return new Promise((resolve, reject) => {
    const childProcess = spawn(command, { 
      stdio: 'inherit',
      shell: true
    });
    childProcess.on('error', (error) => {
      reject(error);
    });
    childProcess.on('exit', (code) => {
      if (code === 0) {
        resolve();
      } else {
        reject(new Error(`Command exited with code ${code}.`));
      }
    });
  });
};

使用方法:

await execCommand('sudo apt-get update && sudo apt-get install -y docker.io docker-compose');

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接