从Node.js执行shell命令

3,914 阅读11分钟

在这篇博文中,我们将探讨如何通过模块'node:child_process' ,从Node.js执行shell命令。


目录。


本博文概述

模块'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.shelltrue ,参数里面的许多元字符都会被解释,诸如通配符和变量名等功能都会起作用。
    • 如果options.shellfalse ,字符串将被逐字使用,我们不必转义元字符。

这两种模式将在本篇文章的后面演示。

参数:options

下面的options 是最有趣的。

  • .shell: boolean|string (默认: )false
    是否应该使用shell来执行命令?

    • 在Windows上,这个选项几乎都应该是 。例如, 和 文件不能以其他方式执行。true .bat .cmd
    • 在Unix上,如果 是 ,只有核心shell功能(例如管道、I/O重定向、文件名通配符和变量)不能使用。.shell false
    • 如果 是 ,我们必须小心处理用户输入,对其进行消毒,因为这很容易执行任意代码。如果我们想把元字符作为非元字符使用,我们还必须转义元字符。.shell true
    • 我们也可以把 设置为一个shell可执行文件的路径。然后Node.js使用该可执行文件来执行命令。如果我们把 设为 ,Node.js就会使用:.shell .shell true
      • Unix。

      • '/bin/sh'

      • Windows。

      • process.env.ComSpec

  • .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
    如果子进程的时间超过.timeout milliseconds,它就会被杀死。

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
  • 'ignore':忽略子进程的流。

  • 'inherit':将子进程的流管到父进程的相应流中。

    • 例如,如果我们希望子进程的stderr被记录到控制台,我们可以在索引2处使用'inherit'
  • 本地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)。如果催生失败,.pidundefined 。这个值在调用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 是 。1
  • signalCode 是 。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上被杀死。那么exitCodenullsignalCode 是非空的。
      • 在Windows上杀死子进程会产生一个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.signalnull

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.statusnull

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 是1
  • result.signalnull
  • result.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() 支持 , 则不支持。args exec()

进一步阅读