インタラクティブな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()をオーバーライドしちゃえば済むんですが。

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

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

コメント

コメントをどうぞ

※メールアドレスとURLの入力は必須ではありません。 入力されたメールアドレスは記事に反映されず、ブログの管理者のみが参照できます。

※なお、送られたコメントはブログの管理者が確認するまで公開されません。

名前:
メールアドレス:
URL:
コメント:

トラックバック

このエントリのトラックバックURL: http://dara-j.asablo.jp/blog/2008/06/02/3556447/tb

※なお、送られたトラックバックはブログの管理者が確認するまで公開されません。