JavaScript Shell






4.25/5 (4投票s)
创建您自己的应用程序 JavaScript Shell 的基础。
引言
您在应用程序中需要使用 shell 的次数有多少?其中有多少次您自己编写了解析器?对我来说,两者都各有几次。
几周前,我正在使用MongoDB,查看其出色的 JavaScript shell 时,我突然想到:我也想要一个!
因为我需要它用于 .NET,所以我没有使用 Mongo 的代码(它是用 C++ 编写的,而且我不了解它可能使用的第三方依赖项)。我也不想第 n 次花很多时间构建解析器;所以我使用了现有的Microsoft.JScript 和Microsoft.Vsa,这两个都相当模糊,文档匮乏且不可扩展。但是,它们出色地完成了任务。
在本文件中,我不会过多解释(如果有的话)CodeDom 或 VSA,请参考我的参考文献或有用链接部分。
Using the Code
首先,让我们看看使用 shell 会有多容易。以下示例是一个测试程序:LocalShell
。
using (var shell = ShellFactory<SampleShell>.Default.CreateShell())
{
shell.OpenShell();
shell.AddGlobalObject("tokens", new List<string>());
shell.AddGlobalObject("globals", new Dictionary<string, object>());
do
{
Console.Write(" > ");
try
{
shell.Eval(Console.ReadLine());
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
} while (shell.Running);
SampleShell
类型定义了从提示符可用的全局函数。将有两个全局vars
可用:tokens
(一个列表)和globals
(一个字典)。shell 实例本身会在退出时通知我们。
查看 shell 控制台
> for(var i = 0; i < 5; i++) tokens.Add(i.ToString("D8"))
> Print(tokens[0])
00000000
> Print(tokens[1])
00000001
> for(var t in tokens) Print(t)
00000000
00000001
00000002
00000003
00000004
> Exit()
在前面的示例中,我们使用了全局变量 tokens 和全局函数Print
。shell 类型中的方法将作为全局函数工作。
创建 Shell
客户端将使用Eval 和CreateMethodHandler
与 shell 交互,这两个名称都很清楚,稍后会解释。要创建 shell,您只需定义从Scaredfinger.JSShell.Shell
扩展的 shell 类,然后使用Scaredfinger.JSShell.ShellFactory<TShell>
创建 shell 实例。幕后会执行一些操作,这些操作相当隐秘,这就是 shell 实例使用Factory
创建的原因。
namespace SampleShell
{
public class SampleShell : Shell
{
public TextWriter Output { get; set; }
[ShellOperation]
public void Help() { … }
[ShellOperation]
public void Print(string text) { … }
[ShellOperation]
public void Exit() { … }
}
}
SampleShell
的完整实现可以在同名示例项目中找到。
CreateMethodHandler 方法
我能想到的一个 shell 用途是,在运行时为我的应用程序触发的事件编程事件侦听器。
EventHandler<EventArgType> handler = shell.CreateEventHandler<EventArgs>(jsBody);
...
application.SomeEvent += handler ;
是不是很简单?现在让我们看看jsBody
的代码,其中有一些技巧。
string jsBody = @"
// Any javascript statement, as many as you’d need
// this, sender and e are available. No globals, not vars nor functions
// Print(globals) ;
// Error, actually a compile time one, but in our case, there is no difference
this.Print(this.globals);
// Now, we’re talking
";
没有函数声明,没有大括号。shell 会添加它们。作用域也存在问题。在函数内部,shell 的作用域不起作用(不幸的是,VSA 太过模糊,无法以简单优雅的方式修复此问题)。但是,我可以指定任何对象作为this
,因此this
将是 shell 作用域的引用,如果您在其中更改某些内容,则在返回外部时将发生更改。是的,我知道:打印字典没有意义。我从来没有擅长举例子。
Eval 方法
至少对我来说是最常用的方法。执行代码块。您可以包含任意数量的语句,并像往常一样用分号分隔。此方法还会返回一个结果对象,该对象将是具有值的最后一个表达式,如果没有任何表达式返回值,则返回null
。
还有一个非常特殊的返回值……
result = shell.Eval("Print(1)") ;
// result is null
result = shell.Eval("var x = 1; Print(x);") ;
// result is 1
result = shell.Eval("var x = 1; Print(x); var y = 2");
// result is 2
// Now
result = shell.Eval("var Foo = function() { ... }") ;
// result is a function, kind of a delegate.
还记得我提到的那个非常特殊的返回值吗?它将是最后一个。它返回一个ScriptFunction,这是一个可立即使用的对象。
这是我用来创建事件处理程序的机制,您也可以用它来创建任何类型的委托。我没有进行通用的委托创建,因为这需要为每个结果生成委托类型,而且我确实不想让事情变得复杂。
创建特定委托
var function = shell.Eval("var __F = function() {...}") as ScriptFunction;
var @this = null ; // Or any value you like to make as the this inside the function body
if (function != null)
{
var dlg = (DelegateType)delegate(T1 p1, T2 p2, T3 p3... )
{
function.Invoke(@this, new {p1, p2, p3, ...});
} ;
}
示例项目
- 创建一个简单的 shell,包含 3 个操作:
Help
、Print
和Exit
。允许自定义输出,默认为Console.Out
。 LocalTest
:从本地命令提示符使用之前的 shell。定义了一些导入和全局变量。ShellServer
:使用SampleShell
而不是bash
的 telnet 服务器。
安全问题
使用如此灵活的 shell 涉及许多危险。因此,请自行承担风险使用它。
例如:如果您允许远程访问本地运行的 shell,即使您只引用了*System.dll*,也始终可以使用反射来加载更多有害的包。理论上,如果您在 shell 定义中没有使用“import System.Reflection
”,则您应该无法使用反射,但是,黑客通常会找到难以想象的变通方法。