キーワードが悪かったのか、なかなか見つけられなかったので。
以下の流れです。
- タスクを作る
- シェルへコマンドを投げる
- 非同期処理が終わるまで待つ
- 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? … 非同期処理のやり方