キーワードが悪かったのか、なかなか見つけられなかったので。
以下の流れです。
- タスクを作る
- シェルへコマンドを投げる
- 非同期処理が終わるまで待つ
- Node.js
- GruntJS
 
- エラーとか拾う
とりあえず、lsしてファイル一覧を得る処理を作ってみる事にします。
環境
$ grunt --version grunt v0.4.0rc2
タスクを作る
- Gruntfile.js
module.exports = function(grunt) {
  // …
  grunt.registerTask('ls', function() {
    console.log('ls');
  });
};
これでgrunt lsが使えるようになりました。何の意味もないタスクですが。
シェルへコマンドを投げる
  grunt.registerTask('ls', function() {
    var exec = require('child_process').exec;
    var command = 'ls > list.txt';
    exec(command);
  });
grunt lsとするとコマンドls > list.txtが実行され、list.txtが作られます。
カレントディレクトリ
gruntのコマンドはGruntfile.jsが置いてあるディレクトリ以下のどこでも実行できます。その際のカレントディレクトリは常にGruntfile.jsが置いてあるディレクトリになります。
なので、先の例ではGruntfile.jsと同じ位置にlist.txtが出力されます。下位ディレクトリで実行しても、です。
画面に出力してくれない
ファイルに出力するのは良いんだけど、画面に出力しようとするとうまく行かない。
リダイレクト>list.txtを削って、画面に結果を表示するようにしてみました。
  grunt.registerTask('ls', function() {
    var exec = require('child_process').exec;
    var command = 'ls';
    exec(command);
  });
これ駄目。
何故かというと、処理が非同期に実行されるためです。exec()の処理にどれくらい時間がかかるかわからないので、本来の処理をブロックしないようになっているようです。
Node.jsの面目躍如ですな。
非同期処理終了時のコールバックを設定する
こんな感じ。(まだこれだけじゃ駄目です。)
  // not works yet...
  grunt.registerTask('ls', function() {
    var exec = require('child_process').exec;
    var command = 'ls';
    var options = { timeout:1000 };
    var callback = function(error, stdout, stderr) {
      console.log(stdout);
    };
    exec(command, options, callback);
  });
options.timeoutで最長待ち時間を指定できます。単位はミリ病。なおoptionsは省略可能です。
非同期処理が終了するとcallbackが実行されます。
コールバック地獄へようこそ。
callbackの引数
error, stdout, stderrの三つです。
成功時errorはnullになります。失敗時はErrorオブジェクトで、error.codeで終了コードを、error.signalでシグナルを得る事が出来ます。
stdout, stderrはそれぞれ標準出力、標準エラーへの出力文字列が格納されてるみたいです。
コールバック実行まで待つ
Node.js的にはこれでよし。
でもこれだとまだ問題があって、コールバックは登録されるだけで実行されないです。先にGruntJSのタスク実行自体が終了してしまいます。
というわけで、待ちます。async()という仕組みを使います。
  // works well!
  grunt.registerTask('ls', function() {
    var exec = require('child_process').exec;
    var done = this.async();
    var command = 'ls';
    var options = { timeout:1000 };
    var callback = function(error, stdout, stderr) {
      console.log(stdout);
      done();
    };
    exec(command, options, callback);
  });
this.async()で終了通知用のコールバック関数を得て、それが呼ばれるまでGruntJSが終了しないようにしました。
処理を終えたら終了通知用のコールバック関数done()を実行しましょう。
エラーとか拾う
これで完璧、ですか? (まだよくわかってない……。)
  grunt.registerTask('ls', function() {
    var exec = require('child_process').exec;
    var done = this.async();
    var command = 'ls';
    var options = { timeout:1000 };
    var callback = function(error, stdout, stderr) {
      if (error) {
        console.log('ERR', error, stderr);
        done(false);
      }
      else {
        console.log(stdout);
        done();
      }
    };
    exec(command, options, callback);
  });
exec()がタイムアウトした場合は終了シグナルSIGTERMが飛んできます。(シグナルはUNIX系の仕組みです。)
あとdone()にfalseを与えると、GruntJSの実行の表示が通常の”Done, without errors.”(緑色)ではなく”Aborted due to warnings.”(赤色)になります。
まとめ
以下の手順で実現できます。
- require('child_process').exec()でシェルへコマンドを投げる
- exec(command, callback)でコールバックを与える
- done =this.async()を得て、コールバックで- done()する
処理が長いときは
find /的な、処理時間が長くてタイムアウトしちゃうようなものもあります。そういう場合はexecではなくてspawnを使うと良さそうです。
それもまたいずれ。
参考
- Child Process Node.js v0.8.15 Manual & Documentation
- child_process.exec(command, [options], callback) … Node.jsのドキュメント邦訳
 
- grunt-contrib-watch/test/fixtures/tasks/echo.js at master · gruntjs/grunt-contrib-watch · GitHub … GruntJSにおける非同期処理の例
- Frequently Asked Questions · gruntjs/grunt Wiki … GruntJSのFAQ
- Why doesn’t my asynchronous task complete? … 非同期処理のやり方
 
 
          