Node.jsによるシェルスクリプティング
この本のオフライン版(HTML、PDF、EPUB、MOBI)を購入して、無料のオンライン版をサポートすることができます。
(広告です。ブロックしないでください。)

12 子プロセスでシェルコマンドを実行する



この章では、モジュール'node:child_process'を使用して、Node.jsからシェルコマンドを実行する方法について説明します。

12.1 この章の概要

モジュール'node:child_process'には、シェルコマンドを(*生成された*子プロセスで)実行するための関数が2つのバージョンで用意されています。

最初にspawn()、次にspawnSync()について説明します。最後に、これらに基づいており、比較的類似している以下の関数について説明します。

12.1.1 WindowsとUnixの比較

この章で示されているコードはUnix上で動作しますが、Windows上でもテスト済みです。Windowsでは、ほとんどのコードがわずかな変更(行末を'\n'ではなく'\r\n'にするなど)で動作します。

12.1.2 例でよく使用する機能

以下の機能は例でよく出てきます。そのため、ここで一度説明します。

12.2 非同期でプロセスを生成する: spawn()

12.2.1 spawn()の仕組み

spawn(
  command: string,
  args?: Array<string>,
  options?: Object
): ChildProcess

spawn()は、新しいプロセスでコマンドを非同期に実行します。プロセスはNodeのメインJavaScriptプロセスと並行して実行され、さまざまな方法(多くの場合、ストリームを介して)で通信できます。

次に、spawn()のパラメータと結果のドキュメントがあります。例から学びたい場合は、このコンテンツをスキップして、次のサブセクションに進んでください。

12.2.1.1 パラメータ: command

commandはシェルコマンドを含む文字列です。このパラメータを使用するには、2つのモードがあります。

どちらのモードもこの章の後半で説明します。

12.2.1.2 パラメータ: options

以下のoptionsが最も重要です。

12.2.1.3 options.stdio

子プロセスの標準I/Oストリームにはそれぞれ、数値ID、いわゆる*ファイル記述子*があります。

ファイル記述子は他にもありますが、まれです。

options.stdioは、子プロセスのストリームが親プロセスのストリームにパイプされるかどうか、およびどのようにパイプされるかを設定します。これは配列にすることができ、各要素はインデックスと同じファイル記述子を設定します。配列要素として以下の値を使用できます。

配列を介してoptions.stdioを指定する代わりに、省略することもできます。

12.2.1.4 結果: ChildProcessのインスタンス

spawn()ChildProcessのインスタンスを返します。

興味深いデータプロパティ

興味深いメソッド

興味深いイベント

後でイベントを待機可能なPromiseに変換する方法について説明します。

12.2.2 シェルコマンドはいつ実行されるか?

非同期spawn()を使用する場合、コマンドの子プロセスは非同期に開始されます。次のコードはそれを示しています。

import {spawn} from 'node:child_process';

spawn(
  'echo', ['Command starts'],
  {
    stdio: 'inherit',
    shell: true,
  }
);
console.log('After spawn()');

これが出力です。

After spawn()
Command starts

12.2.3 コマンドのみモードと引数モード

このセクションでは、同じコマンド呼び出しを2つの方法で指定します。

12.2.3.1 コマンドのみモード
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'

引数を持つ各コマンドのみのスポーンでは、たとえこれが単純なものであっても、.shelltrueにする必要があります(行A)。

行Bでは、標準I/Oの処理方法をspawn()に指示します。

この場合、子プロセスの出力のみに関心があります。そのため、出力を処理したら完了です。他の場合は、子が終了するまで待機する必要がある場合があります。その方法は後で説明します。

コマンドのみモードでは、シェルのより特異な点がわかります。たとえば、Windowsコマンドシェルの出力には二重引用符が含まれています(最後の行)。

12.2.3.2 引数モード
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'
12.2.3.3 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'
);

アスタリスク(*)などの他のメタ文字でも同様の効果が発生します。

これらは、Unixシェルのメタ文字の2つの例でした。Windowsシェルには独自のメタ文字と独自の If we don't use a shell, meta-characters such as the dollar sign ($) have no effect (line A).エスケープ方法があります。

12.2.3.4 より複雑なシェルコマンド

より多くのシェル機能を使用してみましょう(コマンドのみモードが必要です)。

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'
);

12.2.4 子プロセスの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'
);

シェルコマンドsort(行A)を使用して、テキスト行をソートします。

行Bでは、Writable.toWeb()を使用して、ネイティブNode.jsストリームをWebストリームに変換します(詳細については、§10「Node.jsでのWebストリームの使用」を参照してください)。

ライターを介してWritableStreamに書き込む方法(行C)についても、Webストリームに関する章で説明しています。

12.2.5 手動でのパイプ

以前は、シェルに次のコマンドを実行させていました。

(echo cherry && echo apple && echo banana) | sort

次の例では、エコー(行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'
);

echoOutなどのReadableStreamsは、非同期で反復可能です。そのため、for-await-ofループを使用して、それらの_チャンク_(ストリーミングデータのフラグメント)を読み取ることができます。詳細については、§10「Node.jsでのWebストリームの使用」を参照してください。

12.2.6 失敗した終了の処理(エラーを含む)

失敗した終了には、主に3種類あります。

12.2.6.1 子プロセスをスポーンできない

次のコードは、子プロセスをスポーンできない場合にどうなるかを示しています。この場合、原因はシェルのパスが実行可能ファイルを指していないことです(行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'イベントのイベントリスナーを登録します。子プロセスは、現在のコードフラグメントが終了した後に開始されます。これは、競合状態を防ぐのに役立ちます。リスニングを開始すると、イベントがまだ発生していないことを確認できます。

12.2.6.2 シェルでエラーが発生する

シェルコードにエラーが含まれている場合、'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!');
});
12.2.6.3 プロセスが強制終了される

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で子プロセスを強制終了するとどうなりますか?

12.2.7 子プロセスの終了を待つ

コマンドが終了するまで待機したい場合があります。これは、イベントとPromiseを介して実現できます。

12.2.7.1 イベントを介して待機する
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)。

12.2.7.2 Promiseを介して待機する
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()は、'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()は2種類の障害を処理します。

12.2.8 子プロセスの終了

12.2.8.1 AbortControllerを介して子プロセスを終了する

この例では、AbortControllerを使用してシェルコマンドを終了します。

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

子プロセスは非同期に開始されます(現在のコードフラグメントが実行された後)。そのため、プロセスが開始される前に中止することができ、この場合、出力は表示されません。

12.2.8.2 .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'

ここでも、子プロセスが開始される前に(非同期に!)強制終了するため、出力はありません。

12.3 プロセスを同期的にスポーンする:spawnSync()

spawnSync(
  command: string,
  args?: Array<string>,
  options?: Object
): Object

spawnSync()spawn()の同期バージョンです。子プロセスが終了するまで待機し、同期的に(!)オブジェクトを返します。

パラメータは、ほとんどspawn()のパラメータと同じです。optionsには、いくつかの追加プロパティがあります。たとえば、

関数はオブジェクトを返します。その最も興味深いプロパティは次のとおりです。

非同期spawn()では、子プロセスは同時に実行され、ストリームを介して標準I/Oを読み取ることができました。対照的に、同期spawnSync()はストリームの内容を収集し、同期的に返します(次のサブセクションを参照)。

12.3.1 シェルコマンドはいつ実行されるか?

同期spawnSync()を使用する場合、コマンドの子プロセスは同期的に開始されます。次のコードはそれを示しています。

import {spawnSync} from 'node:child_process';

spawnSync(
  'echo', ['Command starts'],
  {
    stdio: 'inherit',
    shell: true,
  }
);
console.log('After spawnSync()');

これが出力です。

Command starts
After spawnSync()

12.3.2 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)を介して使用するエンコーディングを指示します。

12.3.3 子プロセスの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'
);

12.3.4 失敗した終了の処理(エラーを含む)

失敗した終了には、主に3種類あります(終了コードがゼロでない場合)。

12.3.4.1 子プロセスをスポーンできない

スポーンに失敗した場合、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'
);
12.3.4.2 シェルでエラーが発生した場合

シェルでエラーが発生した場合、終了コード 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'
);
12.3.4.3 プロセスが強制終了された場合

子プロセスが 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 で子プロセスを強制終了した場合

12.4 spawn() に基づく非同期ヘルパー関数

このセクションでは、node:child_process モジュールの spawn() に基づく 2 つの非同期関数について説明します。

この章では、fork() については無視します。Node.js のドキュメントを引用すると、

fork() は新しい Node.js プロセスをスポーンし、指定されたモジュールを呼び出します。この際、親プロセスと子プロセスの間でメッセージを送信できる IPC 通信チャネルが確立されます。

12.4.1 exec()

exec(
  command: string,
  options?: Object,
  callback?: (error, stdout, stderr) => void
): ChildProcess

exec() は、新しくスポーンされたシェルでコマンドを実行します。spawn() との主な違いは次のとおりです。

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() は、util.promisify() を介して Promise ベースの関数に変換できます。

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

12.4.2 execFile()

execFile(file, args?, options?, callback?): ChildProcess

exec() と同様に機能しますが、次の点が異なります。

exec() と同様に、execFile()util.promisify() を介して Promise ベースの関数に変換できます。

12.5 spawnAsync() に基づく同期ヘルパー関数

12.5.1 execSync()

execSync(
  command: string,
  options?: Object
): Buffer | string

execSync() は、新しい子プロセスでコマンドを実行し、そのプロセスが終了するまで同期的に待機します。spawnSync() との主な違いは次のとおりです。

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

12.5.2 execFileSync()

execFileSync(file, args?, options?): Buffer | string

execSync() と同様に機能しますが、次の点が異なります。

下記の通りです。

12.6 便利なライブラリ

12.6.1 tinysh:シェルコマンドのスポーンを支援するヘルパー

import sh from 'tinysh';

console.log(sh.ls('-l'));
console.log(sh.cat('README.md'));

Anton Medvedev によるtinysh は、シェルコマンドのスポーンを支援する小さなライブラリです。例:

sh.tee.call({input: 'Hello, world!'}, 'file.txt');

.call() を使用してオブジェクトを this として渡すことで、デフォルトのオプションをオーバーライドできます。

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

任意のプロパティ名を使用でき、tinysh はその名前でシェルコマンドを実行します。Proxy を介してこの機能を実現しています。これは、実際のライブラリを少し変更したバージョンです。

行 A では、sh から bin という名前のプロパティを取得すると、execFileSync() を呼び出し、bin を最初の引数として使用する関数が返されることがわかります。

行 B で this をスプレッドすることで、.call() を介してオプションを指定できます。デフォルトが最初に来るため、this を介してオーバーライドできます。

12.6.2 node-powershell:Node.js を介して Windows PowerShell コマンドを実行する

import { PowerShell } from 'node-powershell';
PowerShell.$`echo "hello from PowerShell"`;

Windows でnode-powershell ライブラリを使用すると、次のようになります。

12.7 'node:child_process' モジュールの関数の選択

同期オプション:spawnSync()execSync()execFileSync()

これらの利点が重要でない場合は、spawn() を選択できます。(オプションの)コールバックがないため、シグネチャがよりシンプルです。

execSync()execFileSync() が戻り値と例外によって提供するよりも多くの情報が必要な場合は、spawnSync() を選択してください。