JavaScript REPL控制台的实现与调试

尽管不是一名Web应用开发者,但是JavaScript的狂热用户。在node.js流行之前,微软Windows用户就可以在命令行中使用脚本引擎的全部功能——cscript.exe。这是一个非常方便且强大的语言+宿主组合,用于开发简单的实用脚本。它比编写CLI(命令行界面)C++实用程序要快得多。cscript.exe在所有现代Windows版本中都有,因此它与等效的C++ EXE一样便携(Windows视角)。

这篇文章分为三个部分,讲述了是如何实现一个带有简单调试器REPL和断点的JavaScriptREPL控制台的。在第一部分,将解释是如何决定编写JSREPL的,以及这一切是如何开始的。JSREPL的全部源代码都可以在GPL 2许可证下在GitHub上免费获得。如果不想跟随旅程,可以直接跳转到那里。

动机

Visual Studio是使用cscript.exe进行JavaScript开发和调试的默认工具。虽然它是一个非常强大且功能齐全的调试器,但快速开发和调试受到两个事实的阻碍——首先,Visual Studio不支持cscript.exe JavaScript项目。其次,通过//X命令行选项进行的即时调试非常繁琐。每次启动新的调试会话时,它都会弹出一个弹出式对话框,要求选择要用于调试的Visual Studio版本和实例。而且,Visual Studio 2010的启动时间如何呢?

对来说,JavaScript的力量在于它的即时零编译时间特性。希望能够快速移动,在快速编辑-运行-修复的连续过程中。这种愿望让在网上寻找替代的JavaScript调试器。有一些关于旧的Microsoft Script Debugger的提及,它没有Visual Studio那么功能丰富,也不支持Windows的新版本,但没有什么符合对简单REPL(读取-评估-打印-循环)风格调试器的希望。

因此,决定自己写一个。

选择

几年前,写了一个微不足道的JavaScriptREPL脚本(见下文),想继续朝类似的方向发展——命令行界面,简单快速,尽可能多地重用JavaScript引擎的能力。

// JavaScript REPL示例 var WScript = WScript || require('wscript'); var cmdLine; while ((cmdLine = WScript.StdIn.ReadLine()).toLowerCase() != "quit") { try { WScript.Echo("\n", eval(cmdLine)); } catch (err) { WScript.Echo("\n", err.toString()); } WScript.StdOut.Write("# "); }

早期的REPL对来说很有用——从调试正则表达式到分析COM对象API行为。随着时间的推移,增加了更多的功能——多行模式(行继续字符)和一些全局实用函数(print, println, string原型等)。脚本变得更加有用,但它仍然严重缺乏调试功能——至少,一些能够中断程序执行并检查变量的能力。

所以不得不写调试器。查看MSDN上的Windows Script Runtime文档,一个选择变得明显——使用Script Runtime Hosting API用C++编写一个完整的调试器。尽管以写C++为生,COM流淌在血液中,但并不热衷于为脚本运行时编写一个完整的宿主,然后必须在目标机器上注册它等。决定寻找一个更简单的解决方案。

更简单的解决方案是发展早期REPL。

由于有一个相当有用的REPL,想扩展它而不是从头开始。新功能清单如下:

  • 将脚本文件加载到REPL中——一个加载命令
  • 列出函数的源代码——一个列表命令
  • 设置断点和调试REPL

加载命令似乎足够简单:

// 从文件加载脚本的简单函数 function load(filePath) { var fso = new ActiveXObject("Scripting.FileSystemObject"); if (fso.FileExists(filePath)) { return fso.OpenTextFile(filePath).ReadAll(); } return null; }

然后在主循环中eval返回的文本,以在全局作用域中创建所有对象:

// 通过eval加载脚本 eval(load("Test1.js"));

load函数可以通过搜索JSREPL文件夹和当前PATH来进一步增强加载文件。load函数的额外好处是,现在可以轻松地将REPL实现分解成更小的文件,而不必担心WSF格式。

列表命令似乎也不太难——只需获取函数对象的引用,然后对其执行toString()以获得字符串表示,分割换行符等。如果正在查看源代码,这个功能在dbg.js中——到这个时候,JSREPL项目有三个文件——js.cmd(稍后更多关于.cmd部分),dbg.js和util.js。

// 显示源代码的函数 listFunction: function(fn) { var fnText = fn.toString().split(/\r\n|\n/); for (var l in fnText) { println(l.rJustify(4), "\n", fnText[l]); } }

在上述代码中,有几个实用函数——rJustify和println。这两个都在util.js中。该函数是DBG类的方法,因此语法如此。

js.cmd是一个JavaScript文件,但它首先由cmd.exe执行,以便cscript.exe以特定的命令行选项被调用。这是通过一些巧妙的方法完成的:

// Cmd到cscript的引导代码 @set @a=0/* @set @a= @%js32path%cscript //E:JScript //I //Nologo %~dpnx0 % @goto :EOF */

这段代码需要一些解释。这不是发明——在2004年左右看到了它——它非常聪明。一些脚本解释器,如Python或Perl,有一个特殊的命令行选项来跳过源文件的一行或多行。这允许批处理文件(shell脚本)通过解释器调用自己。

cscript.exe没有这样的选项。所以代码的第一行的目的是愚弄cscript.exe进入JavaScript注释,同时让cmd.exe对它保持沉默。

Cmd.exe将第一行读取为“quietly assign 0 /* to variable @a”,而cscript.exe将该行读取为“条件编译变量@set,后面跟着赋值给条件编译变量@a,然后跟着一个JavaScript注释”。

沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485