インタラクティブなJScriptコンソール2008年06月02日 02時33分25秒

久々にJSでも書いてみるか。

気がついたら1ヶ月近くもブログ放置状態だったので、ちょっと前になんとなく作ったスクリプトをば。

もうとっくに誰かがやってるであろう(つか、自分で昔作ってるし)、対話型JScriptシェル(っていうのか?)なんですが、ライブラリのロード機構とドラッグドロップでのバッチ処理をサポートしてるのでライブラリの整備次第ではちょっとしたデータ処理なんかに使えるのでは、と。

いきなりソース

以下、本体の全ソース。「jsi.js」とでもしてローカルに保存してください。例によってNYSLで。

var Global = {
  fso : new ActiveXObject("Scripting.FileSystemObject"),
  shell : new ActiveXObject("WScript.Shell"),
  arguments : (function() {
    var result = {
      raw : [],
      named : {},
      unnamed : [],
      toArray : function() {
        var result = [];
        for(var i = 0, l = this.raw.length; i < l; i++) {
          result.push( '"' + this.raw[i] + '"' );
        }
        return result;
      }
    };
    for(var list = new Enumerator(WSH.Arguments); ! list.atEnd(); list.moveNext()) {
      result.raw.push( String(list.item()) );
    }
    for(var list = new Enumerator(WSH.Arguments.Unnamed); ! list.atEnd(); list.moveNext()) {
      result.unnamed.push( String(list.item()) );
    }
    for(var list = new Enumerator(WSH.Arguments.Named); ! list.atEnd(); list.moveNext()) {
      result.named[ list.item() ] = String( WSH.Arguments.Named.item( list.item() ) );
    }
    return result;
  })(),
  fullName : WSH.ScriptFullName,
  startupPath : (function() {
    var fso = new ActiveXObject("Scripting.FileSystemObject");
    return fso.GetFile( WSH.ScriptFullName ).ParentFolder.Path;
  })(),
  buildPath : function(path1, path2, separator) {
    separator = separator || "\\";
    var reg = new RegExp( separator + "$" );
    return [ path1, path2 ].join( reg.test( path1 ) ? "" : separator );
  },
  getTextContents : function(path) {
    try {
      var stream = Global.fso.OpenTextFile( path, 1 );
      return stream.ReadAll();
    } finally {
      if( stream ) stream.Close();
    }
  },
  echo : function() {
    for(var i = 0, l = arguments.length; i < l; i++) {
      Global.print( arguments[i] + "\n" );
    }
  },
  print : function(msg) {
    WSH.StdOut.Write(msg);
  },
  input : function(msg) {
    if( msg ) Global.print( msg + " > " );
    return WSH.StdIn.ReadLine();
  },
  registCurrentScript : function(script) {
    return this.__currentScript = script;
  },
  unregistCurrentScript : function() {
    this.__currentScript = null;
  },
  getCurrentScript : function() {
    return this.__currentScript || this.fullName;
  }
}

if( /wscript\.exe$/i.test( WSH.FullName ) ) {
  Global.shell.run( [
    "cscript",
    "\"" + WSH.ScriptfullName + "\""
  ].concat( Global.arguments.toArray() ).join(" ") );
  WSH.Quit();
}

Global.shell.CurrentDirectory = Global.startupPath;

Error.prototype.toString = function() {
  return [ this.description || this.message || "", " [", this.number, "]" ].join("");
};

var Runtime = function() {
  this.initialize.apply( this, arguments );
};

Runtime.prototype = {
  initialize : function() {
  },
  echo : function() {
    Global.echo.apply( Global, arguments );
    return this;
  },
  print : function() {
    Global.print.apply( Global, arguments );
    return this;
  },
  input : function() {
    return Global.input.apply( Global, arguments );
  },
  run : function(libs, args) {
    var _isBreak = false;
    var _, __sources__ = libs || [], __buf__ = [];
    if( args instanceof Array ) __sources__ = __sources__.concat( args );
    while(! _isBreak) {
      var echo = function() { Global.echo.apply( Global, arguments ); };
      var print = function() { Global.print.apply( Global, arguments ); };
      var input = function() { return Global.input.apply( Global, arguments ); };
      var exit = function() { _isBreak = true; };
      
      var __cmd__ = __sources__.length ?
        " " : this.input( __buf__.length ? ( "   " + ( __buf__.length + 1 ) ).slice(-3) : "jsi" );
      if( __cmd__.length ) {
        try {
          if( /^\./.test( __cmd__ ) && ! __sources__.length ) {
            if( /^\.load\s/.test( __cmd__ ) ) {
              __sources__ = __cmd__.replace( /^\.load\s+/, "" ).split( " " );
            } else if( /^\.quit.*$/.test( __cmd__ ) ) {
              exit();
            } else {
              throw new Error( "コマンド '" + __cmd__ + "' は定義されていません。" );
            }
          } else if( __sources__.length ) {
            with( { ___s : null } ) {
              while( __sources__.length ) {
                ___s = Global.registCurrentScript( __sources__.shift() );
                this.print( "'" + ___s + "' loading..." );
                try {
                  eval( Global.getTextContents( ___s ) );
                  this.echo( "done." );
                } catch(err) {
                  this.echo( "ERROR !! : ", err );
                }
                Global.unregistCurrentScript();
              }
            }
            __sources__ = [];
          } else {
            __buf__.push( __cmd__.replace( / _$/, "" ) );
            if( /;$/.test( __cmd__ ) ) {
              this.print( "--> " );
              _ = eval( __buf__.join("\n").replace( /;$/, "") );
              this.echo(
                _ != null ? _ : ( _ === undefined ? "(undefined)" : "(null)" ),
                ""
              );
              __buf__ = [];
            }
          }
        } catch(err) {
          this.echo( "ERROR !! : ", err );
          __buf__ = [];
        }
      }
    }
  }
};
with( {
  confPath : Global.buildPath( Global.startupPath, "libsettings" ),
  libs : null
} ) {
  try {
    if( Global.fso.FileExists( confPath ) ) {
      libs = Global.getTextContents( confPath ).split( /((\r\n)|\r|\n)/g );
    }
    new Runtime().run( libs, Global.arguments.unnamed );
  } catch(err) {
    Global.echo( err );
    Global.input("");
  }
}

一式ダウンロード

上記ソースのみで使えますが、ライブラリロードのサンプルなども含めた一式を以下からダウンロードできます。

添付のライブラリは以下のとおりです。以前このブログに載せたやつの寄せ集めみたいな感じですが。

  • dummy.js - prototype.jsをWSH環境で使用するためのダミーオブジェクト定義
  • prototype.js - 懐かしの1.5.1です。別に1.6使う意味もないので。
  • json.js - 2006-10-29版と、かなり古めです。使った限りではもっともパフォーマンスがよかったもので、個人的にずっと愛用しているもので。
  • format.js - NumberやDateの書式指定をサポートしたフォーマットライブラリです。使い方はリンク先を参照してください。
  • EnumeratorEx.js - JScriptのEnumeratorオブジェクトをprototype.jsのEnumerableに対応させるライブラリです。たいしたことしてませんが。
  • json_formatter.js - 整形エンコード専用のJSONライブラリです。こっそりパフォーマンス改善しています。微量ですが。
prototype.js、json.js以外はすべてNYSLとします。

使い方

上のソースを保存するか、jsi_v001.zipを適当なディレクトリに解凍し、jsi.jsをダブルクリックで起動してください。次のようなコンソールが起動します。

Microsoft (R) Windows Scripting Host Version 5.6
Copyright (C) Microsoft Corporation 1996-2001. All rights reserved.

jsi >

で、適当にコードを書きます。末尾のセミコロンでステートメント区切りになります。

jsi > 1+1;
--> 2

jsi > "abc".replace(/[ab]/g, "_");
--> __c

jsi >

セミコロンが出現するまでは同一ステートメントとして認識されます。

jsi > (1 + 1)
  2 > * 3;
--> 6

jsi >

ブロックがネストしてたりで、終了前にセミコロンが登場する場合は行末に「 _」(半角スペースとアンダースコア)を入れると途中改行として扱われます。

jsi > $R(1, 10).each( function(i) {
  2 >   echo( i ); _
  3 > } );
--> 1
2
3
4
5
6
7
8
9
10
[object Object]

jsi >

最後に評価された結果は変数「_」(アンダースコア)に格納されます。

jsi > "ABC";
--> ABC

jsi > _;
--> ABC

jsi > _.length;
--> 3

jsi > _;
--> 3

jsi >

当然、変数も使えます。

jsi > var a = 1000;
--> (undefined)

jsi > var b = 200;
--> (undefined)

jsi > a * b;
--> 200000

jsi >

組み込み関数

コンソール入出力用に「echo」「print」「input」の3つの組み込み関数を提供しています。

echoは引数をコンソールへ出力します。末尾に必ず改行がつきます。(以下の例では評価結果「undefined」が改行後に出力されています)

jsi > echo( "abc" );
--> abc
(undefined)

jsi >

printも引数をコンソールへ出力しますが、自動改行はされません。

jsi > print( "abc" );
--> abc(undefined)

jsi >

inputはコンソールからの入力を受け取ります。

jsi > input();
--> aaa
aaa

jsi >

引数に指定した文字列をプロンプトとして出力することもできます。

jsi > input("入力してくれ");
--> 入力してくれ > 入力したった
入力したった

jsi >

特殊コマンド

通常のJSコードとは別にピリオドから始まる特殊コマンドを2つ定義しています。「.quit」と「.load」です。これらは末尾にセミコロンがなくても認識されます。

「.quit」はjsi環境を終了させます(以下の例はコマンドプロンプトから明示的にcscriptで起動した場合で、ダブルクリックでwscript起動した場合は即座にコンソールが終了します)。

jsi > .quit

C:\Documents and Settings\dara-j\jsi>

.loadは指定パスにあるファイルをJSコードとして評価します。ライブラリのロードやバッチ実行に利用できます。

例えば、

var array1 = [ "a", "b", "c" ];
var array2 = [ "D", "E", "F" ];
var array3 = array1.concat( array2 );
echo( array3.join("\n") );

なんてソースを「test.js」としてjsi.jsと同じディレクトリに設置して、これをバッチ起動する場合は以下のようにします。
jsi > .load test.js
a
b
c
D
E
F
jsi >

ロードするファイルパスは続けて指定でき、指定順に実行されます。

例えば「test2.js」を以下のように定義し、

echo( "test2.js" );
for(var i = 0; i < array3.length; i++) {
	array3[i] = array3[i].toUpperCase();
}
echo( array3.join("\n") );

test.js → test2.jsの順に実行するには以下のようにします。
jsi > .load test.js test2.js
a
b
c
D
E
F
test2.js
A
B
C
D
E
F
jsi >

ライブラリの自動ロード

jsi.jsと同じディレクトリに「libsettings」というファイルが設置されていると、その内容に応じて起動時に自動的にライブラリをロードします。

書式は非常に単純で、ライブラリのパスを行単位に記述するだけです。詳しくはjsi_v001.zipに添付の「libsettings」をテキストエディタで開いて参照してみてください。

バッチ処理

jsi.jsに.jsファイルをドラッグドロップすると、起動後にそのファイルを.load特殊コマンドで評価します。これによりバッチ処理が行えます。

先ほどのtest.jsなどをjsi.jsにドラッグドロップすると、コンソールから.loadしたのと同様に処理されます。複数のjsファイルをドラッグドロップすることも可能です。

以下、バッチ固有というわけでもありませんがちょっとしたTIPSを。

バッチスクリプト内で実行中スクリプト名を取得する

コードを見ればわかりますが、.loadでの評価はeval()なため、バッチスクリプト内で「WSH.ScriptFullName」とかしてもjsi.jsの情報しか取得できません。せめて自身のパスくらいは取得できるように、ちょっとした仕掛けを用意してあります。

jsi.jsでは「Global」というオブジェクトを定義しているのですが、.loadによるロード・評価時に、このオブジェクトに評価中スクリプトの情報を設定していおり、これをgetCurrentScript()メソッドで取得することができます。

print( "WSH.ScriptFullName = " );
echo( WSH.ScriptFullName );

print( "Global.getCurrentScript() = " );
echo( Global.getCurrentScript() );

をtest3.jsとすると、以下のようになります。
jsi > .load test3.js
WSH.ScriptFullName = C:\Documents and Settings\dara-j\jsi\jsi.js
Global.getCurrentScript() = test3.js
jsi >

上記は.loadでロードしたためtest3.jsのパスが相対パスになっていますが、ドラッグドロップ起動した場合はフルパスになります。

バッチスクリプトから終了

バッチスクリプトは実行が完了するとそのままコンソールの入力待ち状態になりますが、exit()を実行することで即座にjsi.jsそのものを終了させることができます。

確認をとってから自動終了させるには、input()で入力を受け付けた後にexit()すればOKです。

予定というか課題というか

現状ではライブラリのロードにfsoを使用しているため、ローカルファイルに限定されています。できればこれhttp対応したいなぁ。まぁライブラリでGlobal.getTextContents()をオーバーライドしちゃえば済むんですが。

また、「 _」で途中改行ってのはちとみっともないのでまともなステートメントブロックの検出ロジックを入れたいなぁ。んで、バッチスクリプトにコメントでライブラリロードさせると。

面倒くさいのでやらないかも。

今度はノートブックっすか!2008年06月02日 02時46分27秒

ちょっと前まではアサブロの対策が功を奏したのか、コメント・トラバともスパムが激減していたのだが、コメントのほうは最近また増えてきた。

以前はリンク先がGoogleグループのものばかりだったのだが、こんどはGoogleノートブックがブームらしい。

便利なWebアプリが簡単にアカウント取れるのは便利なんだけど、こういう使われ方に対してなんか制約取れないのかなー。

コントローラ関連のシーケンス図など2008年06月03日 02時56分26秒

久々のZFネタっす。といっても自分でなんか書いたりするわけじゃなく、よそ様で紹介されていた資料の紹介。すまぬ。

すばらしい図面が紹介されてた

PHPSPOTさんで紹介されてたZF-users.jpというZendFrameworkユーザ向けのハブサイトさんで、Zend_Controller/Zend_View関連の図面が紹介されてました。

このページで「シーケンス図 (http://www.kitpages.fr/zf_helper_plugin.php)」と「Zend_Controller と Zend_View 図でまとめ」という2つの図面が紹介されていて、どちらも非常に有用なのですが、特に「シーケンス図」がお勧め。個人的に。

ソース追っかけるときのお供に

この図、ブラウザからリクエストが発生して、Zend Frameworkのコントローラ群が処理を行ってブラウザにレスポンスを返すまでの流れをあらわしていて、これを片手にZend/Controller/Front.phpから流れを追っかけてくとコントローラ周辺の処理がよく理解できると思います。

ソース読めばまぁわかるんだけどもちと迷いそうなところだけ補足しておきます。

プラグインブローカー

まずカーキ色のボックス「Plugins」を呼び出しているところは、実際は Zend_Controller_Plugin_Broker (フロントコントローラ内の「_plugins」)に対する呼び出しです。ブローカーはこれらのメソッドを呼ばれると、自身に登録されている各プラグインクラスの同名メソッドを順次呼び出す仕組みになっています。この機構を利用すれば、例えばdispatchLoopStartupをフックして、特定条件下では必ずログインフォームへナビゲートさせるような認証プラグインなんかを実装することができます(フックメソッド中にリクエストオブジェクトのコントローラ名/アクション名を上書きしたりもできるので)。

ディスパッチループ

「12: preDispatch」~「28: postDispatch」まで(背景がカーキになっている部分)は「ディスパッチループ」で、特定の条件下、例えばアクションコントローラで _forward() した場合は再びpreDispatchへ処理が移るような流れになっています。

具体的な処理についてはソースを見てみてください。Zend_Controller_Action::_forward でリクエストオブジェクト(Zend_Controller_Request_Abstract)に対して setActrionNameメソッドで次に実行するアクションを登録し、 setDispatchedメソッドにfalseを渡しているのがわかると思います。この「setDispatched(false)」が、ディスパッチループを繰り返すという決定を下している部分になります。

その他の部分

その他の部分は特に解説はいらないと思います。「Helpers」(=アクションヘルパー)の部分は考え方としてはプラグインブローカー/プラグインと同様に捉えられると思いますので。

まとめ

なんて具合に人様のコンテンツの尻馬に乗ってお茶を濁しましたが、本当はいずれこういうシーケンス図を書くつもりだったんですよ、いや、まじで。(つか、リファレンスマニュアルにこういうの載せてくれればいいのに)

今回紹介されていた2つの図面を見るとコントローラ(とビュー)の基本クラスの関連はぐっと把握しやすくなり、ソースを追っかけた場合の混乱も軽減されると思うので、ぜひ印刷するなりで手元においておいて、さらにZFのソースを読んでみることをお勧めします。

ぴったりとまってるのはなぜ?2008年06月06日 00時54分03秒

この前のエントリで「Googleノートブックブームでコメントスパムが少し増えてきた」と書いたらその日を境にぱったりとコメント・トラバともにスパムが途絶えた。

どちらも丸3日間、1件もない。なんで?

やっぱり、アサブロのスパム対策はよくわからん。どんな処理をしているのか不思議だ。まぁ、スパムがないのはよいことなのだが。

wget.js2008年06月11日 03時54分48秒

wget.js

まったくもってくだらんと思うが、コマンドライン引数でURL渡すとローカルに保存するコンソールスクリプト。なんで作ろうとしたのか忘れたけど、なんか作ってみたので載せてく。

ソース

※:同じローカルファイル名で上書きできないバグを修正しました(08.06.13)

※:この記事のコメント欄でhATrayfloodさんからContent-Disposition対応ソースのURLを教えていただきました。ソースはこちら(JSファイル直リン)。(11.08.25)

以下のソースをコピーして「wget.js」とでもつけてローカルに保存するか、これを名前をつけて保存。

依存ライブラリは一切なし。ライセンスはNYSLね。

var fso = new ActiveXObject("Scripting.FileSystemObject");
var shell = new ActiveXObject("WScript.Shell");

// コマンドライン引数の取得
var args = new function() {
  var args = WSH.Arguments, result = [];
  for(var col = new Enumerator( args ); ! col.atEnd(); col.moveNext()) {
    result[ result.length ] = String( col.item() );
  }
  var named = {};
  for(var col = new Enumerator( args.Named ); ! col.atEnd(); col.moveNext()) {
    named[ String( col.item() ) ] = String( args.Named.Item( col.item() ) );
  }
  var unnamed = [];
  for(var col = new Enumerator( args.Unnamed ); ! col.atEnd(); col.moveNext()) {
    unnamed[ unnamed.length ] = String( col.item() );
  }
  result.named = named;
  result.unnamed = unnamed;
  
  result.toArray = function() {
    var result = [];
    for(var i = 0, l = this.length; i < l; i++) {
      result[ i ] = /((^".*"$)|(^'.*'$))/.test( this[i] ) ?
        this[i] : [ '"', this[i], '"' ].join("");
    }
    return result;
  };
  result.toString = function() {
    return this.toArray().join(" ");
  };
  return result;
}();

// cscriptで強制起動
if( /wscript\.exe$/i.test( WSH.FullName ) ) {
  shell.Run( [
    "cscript",
    /((^".*"$)|(^'.*'$))/.test( WSH.ScriptFullName ) ? WSH.ScriptFullName : [ '"', WSH.ScriptFullName, '"' ].join(""),
    args
  ].join(" ") );
  WSH.Quit();
}

// ユーティリティ関数定義
var echo = function(s) {
  print( [ s, "\n" ].join("") );
}
var print = function(s) {
  WSH.StdOut.Write( s || "" );
}
var input = function() {
  if( arguments[0] ) print( arguments[0] );
  print( ">" );
  return WSH.StdIn.ReadLine();
}
var nameFromUrl = function(url) {
  var parts = url.split( /[\\\/]/g );
  var f = parts[parts.length - 1];
  f = f.replace( /[\\\/:,;\*\?"<>\|]/g, "_" );
  parts[parts.length - 1] = f;
  return f;
}

Error.prototype.toString = function() {
  return this.description || this.message || this.number || this;
}

if( ! args.length || /^\/(\?|(help))/i.test( args[0] ) ) {
  // 引数がないか、ヘルプスイッチが指定された場合は使い方を表示して終了
  echo( "使い方 : [cscript | wscript] wget.js [ ]" );
  echo( "オプション : " );
  echo( "        アクセスするインターネットリソースのURL" );
  echo( "  保存先ファイルパス。省略時はURLのファイル名と" );
  echo( "             同じ名前でカレントディレクトリに保存" );
  echo();
  input( "Enter キーで終了します" );
} else {
  // メイン処理
  var url = args.unnamed[0], fileName = args.unnamed[1] || nameFromUrl(url);

  var xhr = new ActiveXObject("Microsoft.XMLHTTP");
  xhr.open( "get", url, true );
  var completed = false;
  xhr.onreadystatechange = function() {
    print( "." );
    if( xhr.readyState < 4 ) return;
    echo();
    try {
      var headers = (function(headers) {
        var result = {}, lines = headers.split( /((\r\n)|\r|\n)/g );
        for(var i = 0, l = lines.length; i < l; i++) {
          var matches = /^([^:]+): (.*)$/.exec( lines[i] );
          if( matches ) result[ matches[1].toLowerCase() ] = matches[2];
        }
        return result;
      })( xhr.getAllResponseHeaders() );
      echo( [ "ファイルサイズ : ", headers[ "content-length" ] || "不明" ].join("") );
      
      var stream = new ActiveXObject("ADODB.Stream");
      stream.Open();
      stream.Position = 0;
      stream.Type = 1;
      stream.Write( xhr.responseBody );
      stream.SaveToFile( fileName, 2 );  // adSaveCreateOverWrite
      echo( "完了" );
      
    } catch(e) {
      echo( e );
    } finally {
      if( stream ) {
        try { stream.Close(); } catch(ie) {}
      }
    }
    completed = true;
  }
  print( [ "url '", url, "' に接続中..." ].join("") );
  xhr.send();
  while( ! completed ) {
    WSH.Sleep( 50 );
  }
}

使い方

コマンドラインからcscript経由で使う。別にwscriptでもいいけど。

んで、カレントにwget.jsがあるとして、「http://server.domain/path/to/file.zip」をダウンロードするには

C:\wget_js>cscript wget.js http://server.domain/path/to/file.zip
Microsoft (R) Windows Script Host Version 5.6
Copyright (C) Microsoft Corporation 1996-2001. All rights reserved.

url 'http://server.domain/path/to/file.zip' に接続中...
ファイルサイズ:1024
完了

C:\wget_js>

なんて感じ。

保存時のファイル名を指定したい場合は第二引数を使う。

C:\wget_js>cscript wget.js http://server.domain/path/to/file.zip myfile.zip
Microsoft (R) Windows Script Host Version 5.6
Copyright (C) Microsoft Corporation 1996-2001. All rights reserved.

url 'http://server.domain/path/to/file.zip' に接続中...
ファイルサイズ:1024
完了

C:\wget_js>

て具合。保存先は当然パス指定もOK。

あとは

@echo off
cscript //nologo C:\wget_js\wget.js %*

みたいなバッチファイルを%SystemRoot%にでも入れとけばそこはかとなくwget風。

仕組みとか注意点とか

Microsoft.XMLHTTPでURLにアクセスして、受信したresponseBodyをそのままADODB.Streamに喰わせて保存してるだけ。

こんな乱暴なアプローチなので、いきおいすべてのデータをメモリ上で取り扱う構造なので、大きいファイルをこれで落とそうなんてしないほうがよいかと。あと、ftpでコケたときあったなー。XHRがエラー吐きやがるの。

あ、あとURLにfile://~とかやるとコピーコマンド代わりになるとか。まったく意味ないけど。

作ったはいいけど、自分でも使わなそうだなー。