在这篇博文中,我们将探讨如何通过模块'node:child_process' ,从Node.js执行shell命令。
目录。
- 本博文的概述
- 异步产生进程: spawn()
- 同步地产生进程: spawnSync()
- 基于 spawn() 的异步辅助函数
- 基于 spawnAsync() 的同步辅助函数
- 有用的库
- 如何在'node:child_process'模块的功能中选择?
- 进一步阅读
本博文概述
模块'node:child_process' 有一个执行shell命令的功能(在生成的子进程中),它有两个版本。
- 一个异步版本
spawn()。 - 一个同步版本
spawnSync()。
我们将首先探讨spawn() ,然后是spawnSync() 。最后,我们将看看以下基于它们的、相对类似的功能。
- 基于
spawn()。exec()execFile()
- 基于
spawnSync()。execSync()execFileSync()
Windows与Unix的对比
这篇博文中显示的代码在Unix上运行,但我也在Windows上进行了测试--在Windows上,大部分代码只要稍作改动就可以使用(比如用'\r\n' 而不是'\n' 来结束行)。
我们在例子中经常使用的功能
以下功能经常出现在例子中。这就是为什么我们在这里解释一次。
-
断言:
assert.equal()用于原始值,assert.deepEqual()用于对象。必要的导入在例子中从未显示。import * as assert from 'node:assert/strict'; -
函数
Readable.toWeb()将Node的本地stream.Readable转换为网络流(ReadableStream的一个实例)。它在关于网络流的博文中解释了更多信息。Readable在例子中总是被导入。 -
异步函数
readableStreamToString()消耗一个可读的网络流并返回一个字符串(用Promise包装)。它在关于网络流的博文中有解释](2ality.com/2022/06/web…
异步生成进程:spawn()
spawn() 如何工作
spawn(
command: string,
args?: Array<string>,
options?: Object
): ChildProcess
spawn()异步地在一个新的进程中执行一个命令。该进程与Node的主JavaScript进程同时运行,我们可以通过各种方式(通常是通过流)与它通信。
接下来,有关于spawn() 的参数和结果的文档。如果你喜欢通过例子来学习,你可以跳过这些内容,继续下面的小节。
参数:command
command 是一个带有shell命令的字符串。有两种使用这个参数的模式。
- 纯命令模式:
args被省略,command包含整个 shell 命令。我们甚至可以使用shell的功能,如在多个可执行文件之间进行管道连接,将I/O重定向到文件、变量和通配符。options.shell必须是 ,因为我们需要一个shell来处理shell的功能。true
- Args模式:
command只包含命令的名称,args包含其参数。- 如果
options.shell是true,参数里面的许多元字符都会被解释,诸如通配符和变量名等功能都会起作用。 - 如果
options.shell是false,字符串将被逐字使用,我们不必转义元字符。
- 如果
这两种模式将在本篇文章的后面演示。
参数:options
下面的options 是最有趣的。
-
.shell: boolean|string(默认: )false
是否应该使用shell来执行命令?- 在Windows上,这个选项几乎都应该是 。例如, 和 文件不能以其他方式执行。
true.bat.cmd - 在Unix上,如果 是 ,只有核心shell功能(例如管道、I/O重定向、文件名通配符和变量)不能使用。
.shellfalse - 如果 是 ,我们必须小心处理用户输入,对其进行消毒,因为这很容易执行任意代码。如果我们想把元字符作为非元字符使用,我们还必须转义元字符。
.shelltrue - 我们也可以把 设置为一个shell可执行文件的路径。然后Node.js使用该可执行文件来执行命令。如果我们把 设为 ,Node.js就会使用:
.shell.shelltrue-
Unix。
-
-
-
'/bin/sh' -
Windows。
-
-
-
process.env.ComSpec
-
- 在Windows上,这个选项几乎都应该是 。例如, 和 文件不能以其他方式执行。
-
.cwd: string | URL
指定执行命令时要使用的当前工作目录(CWD)。 -
.stdio: Array<string|Stream>|string
配置标准I/O的设置方式。这在下面有解释。 -
.env: Object(默认: )process.env
让我们为子进程指定shell变量。提示:-
查看 (例如在Node.js REPL中),看看有哪些变量存在。
process.env -
我们可以使用spreading来非破坏性地覆盖一个现有的变量--或者在它还不存在的时候创建它:
{env: {...process.env, MY_VAR: 'Hi!'}}
-
-
.signal: AbortSignal
如果我们创建一个AbortControllerac,我们可以将ac.signal传递给spawn(),并通过ac.abort()终止子进程。这将在本篇文章的后面进行演示。 -
.timeout: number
如果子进程的时间超过.timeoutmilliseconds,它就会被杀死。
options.stdio
子进程的每个标准I/O流都有一个数字ID,即所谓的文件描述符。
- 标准输入(stdin)的文件描述符为0。
- 标准输出(stdout)有一个文件描述符1。
- 标准错误(stderr)的文件描述符为2。
可以有更多的文件描述符,但这很罕见。
options.stdio 配置子进程的流是否以及如何被输送到父进程的流中。它可以是一个数组,每个元素配置与其索引相等的文件描述符。下面的值可以作为数组元素使用。
-
'pipe':- 索引0:将
childProcess.stdin到子进程的stdin。注意,尽管它的名字,前者是一个属于父进程的流。 - 索引1:将子进程的stdout管道到
childProcess.stdout。 - 索引2:将子进程的stder发送到
childProcess.stderr。
- 索引0:将
-
'ignore':忽略子进程的流。 -
'inherit':将子进程的流管到父进程的相应流中。- 例如,如果我们希望子进程的stderr被记录到控制台,我们可以在索引2处使用
'inherit'。
- 例如,如果我们希望子进程的stderr被记录到控制台,我们可以在索引2处使用
-
本地Node.js流。管到或来自该流。
-
也支持其他值,但这超出了本帖的范围。
我们也可以不通过数组来指定options.stdio ,而是缩写。
'pipe'相当于 (默认的是 )。['pipe', 'pipe', 'pipe']options.stdio'ignore'相当于 。['ignore', 'ignore', 'ignore']'inherit'相当于 。['inherit', 'inherit', 'inherit']
结果:ChildProcess 的实例
spawn() 返回的实例 ChildProcess.
有趣的数据属性。
.exitCode: number | null
包含子进程退出时的代码。- 0(零)表示正常退出。
- 大于0的数字意味着发生了一个错误。
null表示该进程还没有退出。
.signalCode: string | null
子进程被杀死的POSIX信号,如果没有被杀死,则null。更多信息见下面对方法.kill()的描述。- 流。根据标准I/O的配置方式(见前一小节),以下流是可用的。
.stdin.stdout.stderr
.pid: number | undefined
子进程的进程标识符(PID)。如果催生失败,.pid是undefined。这个值在调用spawn()后立即可用。
有趣的方法。
-
.kill(signalCode?: number | string = 'SIGTERM'): boolean
向子进程发送一个POSIX信号(通常会导致进程的终止)。signal的man page包含了一个值的列表。- Windows不支持信号,但Node.js模拟了其中的一些信号--例如。
SIGINT,SIGTERM, 和SIGKILL。欲了解更多信息,请参见Node.js文档。
这个方法将在本篇文章的后面演示。
有趣的事件。
.on('exit', (exitCode: number|null, signalCode: string|null) => {})
这个事件在子进程结束后发出。- 回调参数为我们提供了退出代码或信号代码。其中一个将永远是非空的。
- 它的一些标准I/O流可能仍然是开放的,因为多个进程可能共享相同的流。当一个子进程退出后所有的stdio流被关闭时,事件
'close'通知我们。
.on('error', (err: Error) => {})
如果一个进程不能被生成(见后面的例子)或者子进程不能被杀死,这个事件最常被发出来。在这个事件之后,可能会也可能不会发出一个'exit'事件。
我们将在后面看到事件如何被转化为可以被等待的承诺。
什么时候执行shell命令?
当使用异步spawn() ,命令的子进程是异步启动的。下面的代码演示了这一点。
import {spawn} from 'node:child_process';
spawn(
'echo', ['Command starts'],
{
stdio: 'inherit',
shell: true,
}
);
console.log('After spawn()');
这就是输出结果。
After spawn()
Command starts
纯命令模式与args模式
在本节中,我们以两种方式指定同一命令的调用。
- 只用命令的模式。我们通过第一个参数
command,提供整个调用的内容。 - Args模式。我们通过第一个参数
command提供命令,通过第二个参数args提供其参数。
仅限命令模式
import {Readable} from 'node:stream';
import {spawn} from 'node:child_process';
const childProcess = spawn(
'echo "Hello, how are you?"',
{
shell: true, // (A)
stdio: ['ignore', 'pipe', 'inherit'], // (B)
}
);
const stdout = Readable.toWeb(
childProcess.stdout.setEncoding('utf-8'));
// Result on Unix
assert.equal(
await readableStreamToString(stdout),
'Hello, how are you?\n' // (C)
);
// Result on Windows: '"Hello, how are you?"\r\n'
true 每一个带参数的纯命令式催生都需要.shell (A行)--即使像这个一样简单。
在B行,我们告诉spawn() 如何处理标准I/O。
- 忽略标准输入。
- 将子进程的stdout管到
childProcess.stdout(一个属于父进程的流)。 - 把子进程的stderr转到父进程的stderr。
在这种情况下,我们只对子进程的输出感兴趣。因此,一旦我们处理完输出,我们就完成了。在其他情况下,我们可能需要等待,直到子进程退出。如何做到这一点,将在后面演示。
在纯命令模式下,我们可以看到更多的shell的奇特之处--例如,Windows命令shell的输出包括双引号(最后一行)。
Args模式
import {Readable} from 'node:stream';
import {spawn} from 'node:child_process';
const childProcess = spawn(
'echo', ['Hello, how are you?'],
{
shell: true,
stdio: ['ignore', 'pipe', 'inherit'],
}
);
const stdout = Readable.toWeb(
childProcess.stdout.setEncoding('utf-8'));
// Result on Unix
assert.equal(
await readableStreamToString(stdout),
'Hello, how are you?\n'
);
// Result on Windows: 'Hello, how are you?\r\n'
args 中的元字符
让我们来探讨一下如果args 中有元字符会发生什么。
import {Readable} from 'node:stream';
import {spawn} from 'node:child_process';
async function echoUser({shell, args}) {
const childProcess = spawn(
`echo`, args,
{
stdio: ['ignore', 'pipe', 'inherit'],
shell,
}
);
const stdout = Readable.toWeb(
childProcess.stdout.setEncoding('utf-8'));
return readableStreamToString(stdout);
}
// Results on Unix
assert.equal(
await echoUser({shell: false, args: ['$USER']}), // (A)
'$USER\n'
);
assert.equal(
await echoUser({shell: true, args: ['$USER']}), // (B)
'rauschma\n'
);
assert.equal(
await echoUser({shell: true, args: [String.raw`\$USER`]}), // (C)
'$USER\n'
);
- 如果我们不使用shell,元字符如美元符号(
$)就没有影响(A行)。 - 如果使用shell,
$USER会被解释为一个变量(B行)。 - 如果我们不希望这样,我们必须通过反斜杠转义美元符号(C行)。
类似的效果也发生在其他元字符上,如星号(*)。
这些是Unix shell元字符的两个例子。Windows shell有自己的元字符和自己的转义方式。
一个更复杂的shell命令
让我们使用更多的shell功能(这需要只用命令模式)。
import {Readable} from 'node:stream';
import {spawn} from 'node:child_process';
import {EOL} from 'node:os';
const childProcess = spawn(
`(echo cherry && echo apple && echo banana) | sort`,
{
stdio: ['ignore', 'pipe', 'inherit'],
shell: true,
}
);
const stdout = Readable.toWeb(
childProcess.stdout.setEncoding('utf-8'));
assert.equal(
await readableStreamToString(stdout),
'apple\nbanana\ncherry\n'
);
向子进程的stdin发送数据
到目前为止,我们只读取了子进程的标准输出。但是我们也可以向标准输入发送数据。
import {Readable, Writable} from 'node:stream';
import {spawn} from 'node:child_process';
const childProcess = spawn(
`sort`, // (A)
{
stdio: ['pipe', 'pipe', 'inherit'],
}
);
const stdin = Writable.toWeb(childProcess.stdin); // (B)
const writer = stdin.getWriter(); // (C)
try {
await writer.write('Cherry\n');
await writer.write('Apple\n');
await writer.write('Banana\n');
} finally {
writer.close();
}
const stdout = Readable.toWeb(
childProcess.stdout.setEncoding('utf-8'));
assert.equal(
await readableStreamToString(stdout),
'Apple\nBanana\nCherry\n'
);
我们使用shell命令sort (A行)来为我们对文本行进行排序。
在B行,我们使用Writable.toWeb() ,将一个原生的Node.js流转换为一个网络流(更多信息请参见关于网络流的博文)。
如何通过写入器向WritableStream写入(C行),在关于网络流的博文中也有解释。
手动管道
我们之前让一个shell执行以下命令。
(echo cherry && echo apple && echo banana) | sort
在下面的例子中,我们手动做管道,从echoes(A行)到排序(B行)。
import {Readable, Writable} from 'node:stream';
import {spawn} from 'node:child_process';
const echo = spawn( // (A)
`echo cherry && echo apple && echo banana`,
{
stdio: ['ignore', 'pipe', 'inherit'],
shell: true,
}
);
const sort = spawn( // (B)
`sort`,
{
stdio: ['pipe', 'pipe', 'inherit'],
shell: true,
}
);
//==== Transferring chunks from echo.stdout to sort.stdin ====
const echoOut = Readable.toWeb(
echo.stdout.setEncoding('utf-8'));
const sortIn = Writable.toWeb(sort.stdin);
const sortInWriter = sortIn.getWriter();
try {
for await (const chunk of echoOut) { // (C)
await sortInWriter.write(chunk);
}
} finally {
sortInWriter.close();
}
//==== Reading sort.stdout ====
const sortOut = Readable.toWeb(
sort.stdout.setEncoding('utf-8'));
assert.equal(
await readableStreamToString(sortOut),
'apple\nbanana\ncherry\n'
);
可读流(ReadableStreams),如echoOut ,是可以异步迭代的。这就是为什么我们可以使用for-await-of 循环来读取它们的块(流数据的片段)。欲了解更多信息,请参见关于网络流的博文。
处理不成功的退出(包括错误)
主要有三种不成功的退出。
- 子进程不能被催生。
- shell中发生错误。
- 一个进程被杀死。
子进程不能被催生
下面的代码演示了如果一个子进程不能被催生会发生什么。在这种情况下,原因是shell的路径没有指向一个可执行文件(A行)。
import {spawn} from 'node:child_process';
const childProcess = spawn(
'echo hello',
{
stdio: ['inherit', 'inherit', 'pipe'],
shell: '/bin/does-not-exist', // (A)
}
);
childProcess.on('error', (err) => { // (B)
assert.equal(
err.toString(),
'Error: spawn /bin/does-not-exist ENOENT'
);
});
这是我们第一次使用事件来处理子进程。在B行,我们为'error' 事件注册了一个事件监听器。子进程在当前代码片段完成后启动。这有助于防止竞赛条件。当我们开始监听时,我们可以确定该事件还没有被发射出来。
在shell中发生了一个错误
如果shell代码包含一个错误,我们不会得到一个'error' 事件(B行),我们会得到一个带有非零退出代码的'exit' 事件(A行)。
import {Readable} from 'node:stream';
import {spawn} from 'node:child_process';
const childProcess = spawn(
'does-not-exist',
{
stdio: ['inherit', 'inherit', 'pipe'],
shell: true,
}
);
childProcess.on('exit',
async (exitCode, signalCode) => { // (A)
assert.equal(exitCode, 127);
assert.equal(signalCode, null);
const stderr = Readable.toWeb(
childProcess.stderr.setEncoding('utf-8'));
assert.equal(
await readableStreamToString(stderr),
'/bin/sh: does-not-exist: command not found\n'
);
}
);
childProcess.on('error', (err) => { // (B)
console.error('We never get here!');
});
一个进程被杀死
如果一个进程在Unix上被杀死,退出代码是null (C行),信号代码是一个字符串(D行)。
import {Readable} from 'node:stream';
import {spawn} from 'node:child_process';
const childProcess = spawn(
'kill $$', // (A)
{
stdio: ['inherit', 'inherit', 'pipe'],
shell: true,
}
);
console.log(childProcess.pid); // (B)
childProcess.on('exit', async (exitCode, signalCode) => {
assert.equal(exitCode, null); // (C)
assert.equal(signalCode, 'SIGTERM'); // (D)
const stderr = Readable.toWeb(
childProcess.stderr.setEncoding('utf-8'));
assert.equal(
await readableStreamToString(stderr),
'' // (E)
);
});
请注意,没有错误输出(E行)。
与其让子进程自己杀死自己(A行),我们还可以让它暂停更长的时间,通过B行记录的进程ID手动杀死它。
如果我们在Windows上杀死一个子进程会发生什么?
exitCode是 。1signalCode是 。null
等待一个子进程的退出
有时我们只想等待一个命令的完成。这可以通过事件和承诺来实现。
通过事件等待
import * as fs from 'node:fs';
import {spawn} from 'node:child_process';
const childProcess = spawn(
`(echo first && echo second) > tmp-file.txt`,
{
shell: true,
stdio: 'inherit',
}
);
childProcess.on('exit', (exitCode, signalCode) => { // (A)
assert.equal(exitCode, 0);
assert.equal(signalCode, null);
assert.equal(
fs.readFileSync('tmp-file.txt', {encoding: 'utf-8'}),
'first\nsecond\n'
);
});
我们使用标准的Node.js事件模式,为'exit' 事件注册一个监听器(A行)。
通过承诺等待
import * as fs from 'node:fs';
import {spawn} from 'node:child_process';
const childProcess = spawn(
`(echo first && echo second) > tmp-file.txt`,
{
shell: true,
stdio: 'inherit',
}
);
const {exitCode, signalCode} = await onExit(childProcess); // (A)
assert.equal(exitCode, 0);
assert.equal(signalCode, null);
assert.equal(
fs.readFileSync('tmp-file.txt', {encoding: 'utf-8'}),
'first\nsecond\n'
);
我们在A行使用的辅助函数onExit() ,返回一个Promise,如果'exit' 事件被发出,这个Promise就会被履行。
export function onExit(eventEmitter) {
return new Promise((resolve, reject) => {
eventEmitter.once('exit', (exitCode, signalCode) => {
if (exitCode === 0) { // (B)
resolve({exitCode, signalCode});
} else {
reject(new Error(
`Non-zero exit: code ${exitCode}, signal ${signalCode}`));
}
});
eventEmitter.once('error', (err) => { // (C)
reject(err);
});
});
}
如果eventEmitter 失败,返回的Promise被拒绝,await 在A行抛出一个异常。onExit() 处理两种失败。
-
exitCode不为零(B行)。这就发生了。- 如果有一个shell错误。那么
exitCode是大于零的。 - 如果子进程在Unix上被杀死。那么
exitCode是null,signalCode是非空的。- 在Windows上杀死子进程会产生一个shell错误。
- 如果有一个shell错误。那么
-
一个
'error'事件被发射出来(C行)。这发生在子进程不能被生成的情况下。
终止子进程
通过AbortController终止一个子进程
在这个例子中,我们使用一个AbortController来终止一个shell命令。
import {spawn} from 'node:child_process';
const abortController = new AbortController(); // (A)
const childProcess = spawn(
`echo Hello`,
{
stdio: 'inherit',
shell: true,
signal: abortController.signal, // (B)
}
);
childProcess.on('error', (err) => {
assert.equal(
err.toString(),
'AbortError: The operation was aborted'
);
});
abortController.abort(); // (C)
我们创建一个AbortController(A行),将其信号传递给spawn() (B行),并通过AbortController(C行)终止shell命令。
子进程是异步启动的(在当前代码片段执行完毕后)。这就是为什么我们可以在进程开始之前就终止,以及为什么我们在这种情况下看不到任何输出。
通过.kill() 终止一个子进程
在下一个例子中,我们通过方法.kill() (最后一行)终止一个子进程。
import {spawn} from 'node:child_process';
const childProcess = spawn(
`echo Hello`,
{
stdio: 'inherit',
shell: true,
}
);
childProcess.on('exit', (exitCode, signalCode) => {
assert.equal(exitCode, null);
assert.equal(signalCode, 'SIGTERM');
});
childProcess.kill(); // default argument value: 'SIGTERM'
再一次,我们在子进程启动之前就杀死了它(异步的!),而且没有输出。
同步地产生进程:spawnSync()
spawnSync(
command: string,
args?: Array<string>,
options?: Object
): Object
spawnSync()是spawn() 的同步版本--它等到子进程退出后才同步(!)返回一个对象。
参数大多与 spawn() 相同。options 有一些额外的属性--例如。
.input: string | TypedArray | DataView
如果这个属性存在,它的值会被发送到子进程的标准输入。.encoding: string(默认: )'buffer'
指定所有标准I/O流使用的编码。
该函数返回一个对象。它最有趣的属性是。
.stdout: Buffer | string
包含写到子进程的标准输出流的内容。.stderr: Buffer | string
包含写到子进程的标准错误流中的内容。.status: number | null
包含子进程的退出代码或null。退出代码或信号代码都是非空的。.signal: string | null
包含子进程的信号代码或null。退出代码或信号代码都不是空的。.error?: Error
这个属性只有在产卵没有成功时才会被创建,然后包含一个错误对象。
使用异步spawn() ,子进程并发运行,我们可以通过流读取标准I/O。相比之下,同步的spawnSync() ,收集流的内容并同步返回给我们(见下一小节)。
shell命令何时执行?
当使用同步spawnSync() ,命令的子进程是同步启动的。下面的代码演示了这一点。
import {spawnSync} from 'node:child_process';
spawnSync(
'echo', ['Command starts'],
{
stdio: 'inherit',
shell: true,
}
);
console.log('After spawnSync()');
这就是输出。
Command starts
After spawnSync()
从stdout读取
下面的代码演示了如何读取标准输出。
import {spawnSync} from 'node:child_process';
const result = spawnSync(
`echo rock && echo paper && echo scissors`,
{
stdio: ['ignore', 'pipe', 'inherit'], // (A)
encoding: 'utf-8', // (B)
shell: true,
}
);
console.log(result);
assert.equal(
result.stdout, // (C)
'rock\npaper\nscissors\n'
);
assert.equal(result.stderr, null); // (D)
在A行,我们用options.stdio ,告诉spawnSync() ,我们只对标准输出感兴趣。我们忽略了标准输入,并将标准错误输送到父进程。
因此,我们只得到一个标准输出的结果属性(C行),标准错误的属性是null (D行)。
由于我们不能访问spawnSync() 内部用来处理子进程的标准I/O的流,我们通过options.encoding (B行)告诉它要使用哪种编码。
向子进程的stdin发送数据
我们可以通过选项属性.input (A行)向子进程的标准输入流发送数据。
import {spawnSync} from 'node:child_process';
const result = spawnSync(
`sort`,
{
stdio: ['pipe', 'pipe', 'inherit'],
encoding: 'utf-8',
input: 'Cherry\nApple\nBanana\n', // (A)
}
);
assert.equal(
result.stdout,
'Apple\nBanana\nCherry\n'
);
处理不成功的退出(包括错误)
主要有三种不成功的退出(当退出代码不为零时)。
- 子进程不能被生成。
- shell中发生了一个错误。
- 一个进程被杀死。
子进程不能被催生
如果产卵失败,spawn() 会发出一个'error' 事件。相反,spawnSync() 将result.error 设置为一个错误对象。
import {spawnSync} from 'node:child_process';
const result = spawnSync(
'echo hello',
{
stdio: ['ignore', 'inherit', 'pipe'],
encoding: 'utf-8',
shell: '/bin/does-not-exist',
}
);
assert.equal(
result.error.toString(),
'Error: spawnSync /bin/does-not-exist ENOENT'
);
在shell中发生了一个错误
如果在shell中发生错误,退出代码result.status 大于0,result.signal 为null 。
import {spawnSync} from 'node:child_process';
const result = spawnSync(
'does-not-exist',
{
stdio: ['ignore', 'inherit', 'pipe'],
encoding: 'utf-8',
shell: true,
}
);
assert.equal(result.status, 127);
assert.equal(result.signal, null);
assert.equal(
result.stderr, '/bin/sh: does-not-exist: command not found\n'
);
一个进程被杀死
如果子进程在Unix上被杀死,result.signal 包含信号的名称,result.status 是null 。
import {spawnSync} from 'node:child_process';
const result = spawnSync(
'kill $$',
{
stdio: ['ignore', 'inherit', 'pipe'],
encoding: 'utf-8',
shell: true,
}
);
assert.equal(result.status, null);
assert.equal(result.signal, 'SIGTERM');
assert.equal(result.stderr, ''); // (A)
注意,没有输出被发送到标准错误流(A行)。
如果我们在Windows上杀死一个子进程
result.status是1result.signal是nullresult.stderr是''
基于spawn() 的异步辅助函数
在本节中,我们看一下模块node:child_process 中的两个异步函数,它们是基于spawn() 。
exec()execFile()
我们在这篇博文中忽略了 fork()在这篇博文中。引用Node.js的文档。
fork()生成一个新的Node.js进程,并调用一个指定的模块,建立一个IPC通信通道,允许在父子之间发送消息。
exec()
exec(
command: string,
options?: Object,
callback?: (error, stdout, stderr) => void
): ChildProcess
exec()在一个新产生的shell中运行一个命令。与spawn() 的主要区别在于。
- 除了返回一个ChildProcess外,
exec()还通过回调提供一个结果。要么是一个错误对象,要么是stdout和stderr的内容。 - 错误的原因:子进程不能被生成,shell错误,子进程被杀死。
- 相比之下,
spawn()只在子进程不能被生成的情况下发出'error'事件。其他两种故障是通过退出代码和(在Unix中)信号代码来处理。
- 相比之下,
- 没有参数
args。 options.shell的默认值是true。
import {exec} from 'node:child_process';
const childProcess = exec(
'echo Hello',
(error, stdout, stderr) => {
if (error) {
console.error('error: ' + error.toString());
return;
}
console.log('stdout: ' + stdout); // 'stdout: Hello\n'
console.error('stderr: ' + stderr); // 'stderr: '
}
);
exec() 可以通过以下方式转换为基于Promise的函数 util.promisify():
- ChildProcess成为返回的Promise的一个属性。
- Promise的解决方式如下。
- 实现值:
{stdout, stderr} - 拒绝值:与回调的参数
error相同的值,但有两个额外的属性:.stdout和.stderr。
- 实现值:
import * as util from 'node:util';
import * as child_process from 'node:child_process';
const execAsync = util.promisify(child_process.exec);
try {
const resultPromise = execAsync('echo Hello');
const {childProcess} = resultPromise;
const obj = await resultPromise;
console.log(obj); // { stdout: 'Hello\n', stderr: '' }
} catch (err) {
console.error(err);
}
execFile()
execFile(file, args?, options?, callback?): ChildProcess
工作原理类似于exec() ,有以下区别。
- 支持参数
args。 options.shell的默认值是false。
像exec() 一样,execFile() 可以通过util.promisify() 转换为基于承诺的函数。
基于spawnAsync() 的同步辅助函数
execSync()
execSync(
command: string,
options?: Object
): Buffer | string
execSync()在一个新的子进程中运行一个命令并同步等待,直到该进程退出。与spawnSync() 的主要区别是。
- 只返回stdout的内容。
- 通过异常报告三种故障:子进程不能被生成,shell错误,子进程被杀死。
- 相比之下,
spawnSync()的结果只有在子进程不能被催生的情况下才有.error的属性。其他两种故障是通过退出代码和(在Unix上)信号代码处理的。
- 相比之下,
- 没有参数
args。 options.shell的默认值是true。
import {execSync} from 'node:child_process';
try {
const stdout = execSync('echo Hello');
console.log('stdout: ' + stdout); // 'stdout: Hello\n'
} catch (err) {
console.error('Error: ' + err.toString());
}
execFileSync()
execFileSync(file, args?, options?): Buffer | string
工作原理与execSync() 类似,但有以下区别。
- 支持参数
args。 options.shell的默认值是false。
有用的库
tinysh:生成shell命令的辅助工具
tinysh由Anton Medvedev编写,是一个帮助生成shell命令的小库--例如。
import sh from 'tinysh';
console.log(sh.ls('-l'));
console.log(sh.cat('README.md'));
我们可以通过使用.call() ,将一个对象作为this ,来覆盖默认的选项。
sh.tee.call({input: 'Hello, world!'}, 'file.txt');
我们可以使用任何属性名称,tinysh就会以该名称执行shell命令。它通过一个代理(Proxy)来实现这一壮举。这是实际库的一个稍加修改的版本。
import {execFileSync} from 'node:child_process';
const sh = new Proxy({}, {
get: (_, bin) => function (...args) { // (A)
return execFileSync(bin, args,
{
encoding: 'utf-8',
shell: true,
...this // (B)
}
);
},
});
在A行,我们可以看到,如果我们从sh ,得到一个名称为bin 的属性,就会返回一个函数,调用execFileSync() ,并使用bin 作为第一个参数。
在B行中展开this ,使我们能够通过.call() ,指定选项。默认值排在前面,这样就可以通过this 来覆盖它们。
node-powershell:通过Node.js执行Windows PowerShell命令
在Windows上使用node-powershell库,看起来如下。
import { PowerShell } from 'node-powershell';
PowerShell.$`echo "hello from PowerShell"`;
如何选择模块的功能'node:child_process'
一般约束。
- 在执行命令的同时,其他异步任务是否应该运行?
- 使用任何异步函数。
- 一次只执行一个命令吗(后台没有异步任务)?
- 使用任何同步函数。
- 你想通过一个流访问子进程的stdin或stdout吗?
- 只有异步函数可以让你访问流:
spawn()在这种情况下更简单,因为它没有一个传递错误和标准I/O内容的回调。
- 只有异步函数可以让你访问流:
- 你想在一个字符串中捕获stdout或stderr吗?
- 异步选项:
exec()和execFile() - 同步选项。
spawnSync(),execSync()。execFileSync()
- 异步选项:
异步函数--在spawn() 和exec() 或execFile() 之间选择。
exec()和 ,有两个好处。execFile()- 失败更容易处理,因为它们都以同样的方式报告--通过第一个回调参数。
- 获取stdout和stderr作为字符串更容易--由于回调的关系。
- 如果这些好处对你来说并不重要,你可以选择
spawn()。它的签名更简单,没有(可选的)回调。
同步函数--在spawnSync() 和execSync() 或execFileSync() 中选择。
execSync()和 有两个特长。execFileSync()- 它们返回一个带有stdout内容的字符串。
- 失败更容易处理,因为它们都是以同样的方式报告的--通过异常。
- 如果你需要比
execSync()和execFileSync()通过其返回值和异常提供更多的信息,请选择spawnSync()。
在exec() 和execFile() 之间进行选择(同样的参数适用于在execSync() 和execFileSync() 之间进行选择)。
options.shell的默认值是exec()中的true,但execFile()中的false。execFile()支持 , 则不支持。argsexec()