キーワードが悪かったのか、なかなか見つけられなかったので。

以下の流れです。

  1. タスクを作る
  2. シェルへコマンドを投げる
  3. 非同期処理が終わるまで待つ
    • Node.js
    • GruntJS
  4. エラーとか拾う

とりあえず、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を使うと良さそうです。

それもまたいずれ。

参考