VBScriptラッパー2007年05月11日 21時27分31秒

やっぱメソッドを文字列で指定するのはねぇ。。。

以前のエントリで、J(ava)?ScriptからVBScriptのコードを実行する方法を紹介したが、やはり

vb.Run( "VbFunc", param1, param2 )
とかって、文字列ベースで呼び出すのはなんとなく落ち着かない。さらに、JavaScript使ってるのに引数の数が束縛されるのも個人的には落ち着かない。 なので、ラッパーを作ってみることにした。

prototype.jsと、String.format、さらにEnumeratorのEnumerable化が前提だが、こんな感じ。

// VBScript実行エンジンラッパークラス
var VBScript = Class.create();
VBScript.prototype = {
	initialize : function() {
		this._vbe = new ActiveXObject("ScriptControl");
		this._vbe.Language = "VBScript";
	},
	addFunction : function(code) {
		this._vbe.AddCode( code );
		this._copyMembers();
	},
	reset : function() {
		this._vbe.reset();
		[ "initialize", "addFunction", "reset", "_copyMembers" ].each( function(key) {
			delete( this[ key ] );
		} );
	},
	_copyMembers : function() {
		var self = this;
		var vbe = this._vbe;
		
		new Enumerator( vbe.Procedures ).each( function(proc) {
			self[ proc.Name ] = function() {
				var args = arguments;
				var p = $R( 0, proc.NumArgs, true ).map( function(i) {
					return args[i];
				} );
				return eval( "vbe.Run( \"{0}\", {1} )".format(
					proc.Name,
					p.map( function(obj, i) { return "p[{0}]".format( i ); } ).join(", ")
				) );
			}.bind( self );
		} );
	}
}
ごめんなさい。クラス名がベタなのは勘弁してください。

使い方

  • インスタンスの生成

    パラメータなしでnewするだけ。
    var vb = new VBScript;
    
  • 関数・プロシージャの追加

    addFunctionメソッドを使用する。パラメータにはVBSコードを直接渡す。
    vb.addFunction( "Function prompt(msg, title) : prompt = InputBox(msg, title) : End Function" );
    
    この例では、Functionプロシージャを1つ登録しているだけだが、1回のaddFunction()で一気に複数のプロシージャを食わせることができる。まだ試していないが、.vbsファイルを読み込んで直接流し込むことができると思う。
  • 関数・プロシージャの呼び出し

    以下のように、VBSコードで定義したプロシージャ名・関数名をそのままVBScriptインスタンスのメソッド名として呼び出せる。
    // VBS関数名で直接呼び出せる
    vb.prompt( "なんか入力すれ。", "VBS テスト" );
    
    VBSの定義より引数が多い場合は自動的に切り詰められ、少ない場合はundefined(多分VBSではNothing扱い)が渡されるので、引数の数によるエラーは発生しない。 ちなみに、VBS側がSubプロシージャで登録されていた場合、実行結果は当然undefinedになる。

注意点とか、制限とか

  • プロシージャ名にVBSの組み込み関数名を使用してはいけない。例えば「Function msgBox : msgBox = MsgBox~」のようなコードを与えると、無限再帰呼び出しになってコールスタックを食いつぶすので注意(VBSはcase-sensitivityではないのでこれまた注意!)
  • VBS側で参照可能な変数を与える方法がわからない。多分addFunction()に渡すコード中に含めればよいが、その変数をJS側で知覚する術がない。
  • VBScriptクラスのメンバ名と同じプロシージャ名を使用してはいけない。原理的にはVBS実行環境内のプロシージャオブジェクトを列挙して、その名前をVBScriptインスタンスのメソッド名に流用しているため、「addFunction」なんて名前のプロシージャを定義したコードを食わせるとそれ以降VBSコードの追加ができない。

最後に長いサンプルを

いろいろとライブラリ依存になっているため、VBScriptクラスのコードだけ載せても試せないから、過去のエントリからかき集めてマージしたサンプルコードを示す。あ、dummy.jsとprototype.jsは同じディレクトリに設置してください。

// CScriptで起動しなおし
if( /wscript\.exe/i.test( WSH.FullName ) ) {
	new ActiveXObject( "WScript.Shell" ).Run( "cmd /k cscript //nologo \"" + WSH.ScriptFullName + "\"" );
	WSH.Quit();
}

// ライブラリロード処理
with( {
	// ライブラリのパスリスト
	libs : [
		"dummy.js",
		"prototype.js"
	],
	// libs[]のインデックス
	index : 0,
	// ソース取得メソッド
	getSource : function(url) {
		var stream = new ActiveXObject("ADODB.Stream");
		var xhr = new ActiveXObject("Microsoft.XMLHTTP");
		try {
			xhr.open( "GET", url, false );
			xhr.send();
			
			stream.Open();
			stream.Type = 1; // バイナリモード
			stream.Write( xhr.responseBody ); 
			
			stream.Position = 0; // 読み取り位置を巻き戻す
			stream.Type = 2; // テキストモードに変更
			stream.Charset = "_autodetect";
			
			return stream.ReadText();
		} finally {
			stream.Close();
		}
	}
} ) {
	while( index < libs.length ) {
		try {
			eval( getSource( libs[ index++ ] ) );
		} catch(e) {
			WScript.Echo( e.description || e.message || "error" );
		}
	}
}

// String.format
String.format = function() {
	var args = [];
	for(var i = 0; i < arguments.length; i++) args[i] = arguments[i];
	var format = args.shift();
	
	var reg = /\{((\d)|([1-9]\d+))\}/g;
	return format.replace( reg, function() {
		var index = Number( arguments[1] );
		var result = args[ index ];
		if( typeof( result ) == "undefined" )
			throw new Error( "arguments[ " + index + " ] is undefined." );
		
		return result;
	} );
}
String.prototype.format = function() {
	var args = [];
	for(var i = 0; i < arguments.length; i++) args[i] = arguments[i];
	return String.format.apply( String, [ this ].concat( args ) );
}

// EnumeratorをEnumerableに拡張
Enumerator.prototype._each = function(iterator) {
	this.moveFirst();
	for(; ! this.atEnd(); this.moveNext()) {
		iterator( this.item() );
	}
}
Object.extend( Enumerator.prototype, Enumerable );

// VBScript実行エンジンラッパークラス
var VBScript = Class.create();
VBScript.prototype = {
	initialize : function() {
		this._vbe = new ActiveXObject("ScriptControl");
		this._vbe.Language = "VBScript";
	},
	addFunction : function(code) {
		this._vbe.AddCode( code );
		this._copyMembers();
	},
	reset : function() {
		this._vbe.reset();
		[ "initialize", "addFunction", "reset", "_copyMembers" ].each( function(key) {
			delete( this[ key ] );
		} );
	},
	_copyMembers : function() {
		var self = this;
		var vbe = this._vbe;
		
		new Enumerator( vbe.Procedures ).each( function(proc) {
			self[ proc.Name ] = function() {
				var args = arguments;
				var p = $R( 0, proc.NumArgs, true ).map( function(i) {
					return args[i];
				} );
				return eval( "vbe.Run( \"{0}\", {1} )".format(
					proc.Name,
					p.map( function(obj, i) { return "p[{0}]".format( i ); } ).join(", ")
				) );
			}.bind( self );
		} );
	}
}

with( {
	$_A : function( args ) {
		var a = [];
		for( var i = 0; i < args.length; i++ ) a.push( args[ i ] );
		return a;
	},
	_p : "js>",
	_print : function() {
		var a = arguments;
		for( var i = 0; i < a.length; i++ ) WSH.StdOut.Write( a[ i ] );
	},
	_in : "",
	print : function() {
		WSH.Echo( this.$_A( arguments ).join( "" ) );
	},
	read : function(m) {
		if( m ) this._print( m );
		return WSH.StdIn.ReadLine();
	},
	vb : new VBScript
} ) {
	vb.addFunction( [
		"Function getTypeName(obj)",
			"getTypeName = TypeName( obj )",
		"End Function",
		
		"Function prompt(msg, title, default)",
			"prompt = InputBox( msg, title, default )",
		"End Function",
		
		"Function yesNo(msg, title)",
			"yesNo = MsgBox( msg, vbYesNo, title )",
		"End Function"
	].join("\r\n") );
	while( ( _in = read( _p ) ) != "exit" ) {
		try {
			print( eval( _in ) );
		} catch( e ) {
			print( e.description );
		}
	}
}
たぶん、こんなんでよろこんでるの俺だけだろうなぁ。

NaNの検出2007年05月11日 22時19分45秒

知らんかったわぁ

たしかはてブ経由だと思ったが、集積蔵 - NaNを見つける役に立たないテクニックほかという記事を見つけた。いやぁ、「( NaN == NaN ) == false」とは知らんかった!!

2を使いたい誘惑に駆られるけど、さすがにアレなので
と書かれているが、dara-j的には2でしょう!これ、かっこいいよ。

他にも文字列足したり、Infinity足したり、いろいろ考えつくなぁ、と感心することしきり。

蛇足

しかし、id:trickstar_osさんは

といいたいところなんですが、このページ(はてな)でやるとprototype.jsのArray拡張がうざい。。
とおっしゃっておられますが、prototype.jsあるおかげで
[ 1, NaN, "1", "a1", null, undefined, -Infinity ].each( function(a) { console.log( [ a, a + "", a + Infinity, isNaN(a) && typeof a == "number", a != a, a + "" == "NaN" ] ); } )
みたいにワンライナー(ちょっと長いか)で書けるところがよいと思うんだけどなぁ。

引用符について2007年05月11日 22時31分57秒

このエントリは中身がほとんどありません。でもちょっと気になってる。

他の人が書いたコードとか見ていると、わりと

var s = 'string';
と文字列リテラルを単一引用符で囲っているのを見かける。

dara-jはよほどのことがない限り

var s = "string";
と2重引用符を使用している。これはなんとなく意地に近いものがあり、
var attr = "bgcolor='red'";
とすればいいものをわざわざ
var attr = "bgcolor=\"red\"";
とエスケープまでしてしまう。これはこれで可読性を落とすのでどうかと自分でも思うが。

どっちが王道なのかと思い検索はしてみるものの、「単一引用符または2重引用符で~」みたいな話しか見つからない。

どっちが王道なのかなぁ。だれか教えてください。またはよい検索キーワード教えてください。

まぁ、それがわかったところでどうだという話ではないんだけどね。