grunt.util.spawn()のリプレース

Gruntのタスクで実行ファイルなど別プロセスを立ちあげたい場合、通常はうまくラップされたgrunt.util.spawn()を使う。今のところ非推奨というわけではないが、最近の感じだと別パッケージを利用すれば簡単にリプレースできるものはなるべくリプレースした方が良い。例えばgrunt.file.write()fs-extraパッケージが良い。grunt.util.spawn()はというとio.jsかNode.jsのUnstableが入っている環境ならばspawnSync()which.sync()を組み合わせてリプレースすると良さそうだ。

別プロセスを立ち上げる辺りはだいたい以下のような形になっていることと思う。

var done = this.async();

grunt.util.spawn({
  cmd: 'foo',
  args: ['--bar'],
  opts: {
    stdio: 'inherit'
  }
}, function (error, result, code) {
  if (error) {
    return done(error);
  }

  done();
});

ストレートに書き換えると、以下のようになるだろう。

var spawn = require('child_process').spawnSync;
var which = require('which').sync;
var child = spawn(which('foo'), ['--bar'], {
  stdio: 'inherit'
});

if (child.error) {
  grunt.fail.warn(child.error);
}

まず必要なパッケージを読み込む。コールバックがなくなるのでthis.async()を呼ぶ必要はない。child_process.spawnSync()はエラーが起きたら返り値のオブジェクトのerrorプロパティーにErrorオブジェクトを格納するので、それをチェックしてエラーを返してやるのみ。

コマンドをwhich.sync()でラップしてやっているのは、child_process.spawnSync() (child_process.spawn()も)がWindowsでPATHEXT環境変数を考慮してくれないことへ対策するためだ。ラップしてやらないと.batや.cmdで定義されているコマンドの場合、見つけることが出来ずENOENTで落ちてしまう。

別プロセスに何かをしてもらうだけのタスクの場合はこれで良いが、標準出力を受け取って……という場合はstdoutプロパティーを拾ってやれば良い。ただしデフォルトではBufferオブジェクトなので、child_process.spawnSync()の第三引数であるオプションでencodingを指定してやりStringオブジェクトになるようにして、後に受け取るのが簡単だろう。

var child = spawn(which('foo'), ['--bar'], {
  encoding: 'utf8'
});

エラーチェックはもうちょっと厳密な必要がありそうだ。そうしないとゾンビプロセスが残ったり、タスクがうまく終了しなくなる可能性がある。でもまだchild_process.spawnSync()の知見がなくてよくわからない。


なぜリプレースするかというと、後にNode.js製の別の何か向けに書きなおす時にまずは書きなおしやすくするためだ。最終的にはフルスクラッチで書きなおした方が良いことが多いだろうが、まず簡単に書きなおせるようにしておくと移行コストが大きく下がる。依存を大きくする(Gruntべったり)か依存を増やす(Node.jsパッケージいっぱい)かという選択で、後者を取るというわけだ。

もちろん今はまだ公開するGruntタスクは書きかえるべきではない。io.jsのインストールを要求するのは馬鹿げている。そのうちNode.js v0.12系などが出て安心版でサポートされるようになったら書きかえてやっても良いかもしれない。まずは手元のオレオレタスクを書きかえてみるくらいが良い。