简介
你知道我对于前.NET时代有什么留恋吗?脚本!我喜欢创建一个小巧的脚本文件为我完成一些小任务,或者为了测试一小段代码而无需创建一项工程或是解决方案。我喜欢处理和清除的仅仅是一个小巧的文件而不是一个解决方案文件夹,工程文件夹和附带的bin 和obj 文件夹。我怀念那些时光,这正是我创建 .NET 脚本的原因。
什么是.NET 脚本呢?基本上,它就是一个简单的控制台应用程序,从 .dnml 文件( Dot Net Markup Language, .NET 标记语言, 这是我定义的,哈哈)中读取 XML 文档。这个XML文档包含如下子元素,存储程序集引用,编写的代码所属的语言以及实际的要编译和执行的代码。那个控制台应用程序,我称之为脚本引擎,读取XML 文本并分析出需要的数据。然后它利用CSharp, VisualBasic, 和 CodeDom 命名空间中的类编译代码并将作为结果的程序集装载到内存中。教本引擎利用反射机制执行生成的程序集中的入口函数。当用户关闭控制台窗口时,脚本引擎被关闭,在内存中的程序集将不复存在,它将被垃圾回收器清理掉。没有任何的库或可执行程序生成。
Dot Net 标记语言
让我们来看看.NET 标记语言是什么模样的。它其实非常简单。下面就是一个它的例子。我会一一说明XML 文档中的每个元素。
<dnml><reference assembly="System.Windows.Forms.dll" />
<language name="C#" />
<scriptCode><![CDATA[using System.Windows.Forms;
public class Test
{public static void Main()
{Console.WriteLine("This is a test");
MessageBox.Show("This is another test");
Test2 two = new Test2();
two.Stuff(); }}public class Test2
{public void Stuff()
{Console.WriteLine("Instance call");
}}]]></scriptCode></dnml>
该文档XML元素称为 <dnml> (你能猜出它代表什么吗?)。在这个元素内部有三个不同的子元素,你能用它们来定义脚本如何编译。
首先是 <reference> 元素,它只有一个属性,叫“Assembly”。“Assembly”属性包含你要引用的程序集名称(包含文件扩展名)。一个.dnml 文档可以包含许多<reference> 元素,它对应于你在VS.NET 中向工程中添加的引用列表。 对于每一个你的代码执行所需要的引用,都必须添加一个<reference assembly="" /> 元素。
基于程序集探测的考虑,任何你所引用的GAC 程序集都会被CLR自动找到。但是如果你引用了一个不是GAC中的程序集,情况就不同了。假设你引用了一个称为Common.dll 的非GAC 程序集。为了让您的.NET 脚本正确执行,Common.dll 必须放在两个地方。首先它必须放在你的.dnml 文件所属的文件夹中。其次,它必须放在脚本引擎所在的文件夹中。我正在试图解决这个问题,但是就目前来说非GAC 程序集必须存放在两个不同的文件夹中。
下一个元素是<language> ,它有一个属性,称为 'name'。一个.dnml 文件只能有一个语言元素。对于’name’ 属性两个可能的值是 'C#' 和 'VB', 我希望他们是自描述的。
最后一个元素是<scriptCode> , 它含有一个CDATA XML 元素。这个元素里包含了当你执行该.dnml 文件时将要执行的代码。但是为了使用它你必须遵循一些接口规则。首先,它实际上只是普通的内嵌C# 和 VB.NET, 所有的方法和字段都必须放在类里面。其次,你可以定义任意多个类,但是必须有一个类拥有一个公有静态函数,称为 “Main”, 没有任何输入参数,也不返回结果。只是脚本引擎通过反射搜索到的入口方法;如果找到,将会调用它。当然,“Main”方法放在哪个类中是无关紧要的,因为脚本引擎将会遍历定义的每个类型,直到它找到Main 方法为止。
脚本引擎是怎样工作的?
脚本引擎的大多数代码都是很直观的,因此我会一一描述它的每个方面。当中有一个非常有趣的部分就是一个名为AssemblyGenerator 的类,它只有一个方法,称为CreateAssembly()。这个方法将完成所有的工作,编译并生成一个新的程序集,正如下面所看到的.
//Create an instance of the C# compiler
CodeDomProvider codeProvider = null;
if (code.IsCSharp)
codeProvider = new CSharpCodeProvider();
else
codeProvider = new VBCodeProvider();
ICodeCompiler compiler = codeProvider.CreateCompiler();
首先我需要声明一个类CodeDomProvider 的实例。它是类 CSharpCodeProvider 和类VBCodeProvider 的基类。你可以使用这些语言的特定 XxxProvider 对象来创建一个 CodeGenerator 对象,它将被用来根据它包含的你创建的CodeDom 对象图生成代码。你可以创建一个CodeParser 对象,它将根据你传入的源代码字符串生成一个CodeDom 对象图(在当前1.1 .NET Framework 版本中, 它将返回空值) 。XxxProvider 对象也可以用来创建一个CodeCompiler , 这正是我这里所使用的。 CodeCompiler 类就是我用来编译.dnml 文件中的代码,并生成新的程序集的类。
所以,基于.dnml 文件中所定义的语言类型,我创建一个合适的XxxCodeProvider 对象。从这个对象,我请求一个CodeCompiler 实例,它将是基于语言不同而不同的。
//add compiler parameters
CompilerParameters compilerParams = new CompilerParameters();
compilerParams.CompilerOptions = "/target:library /optimize";
compilerParams.GenerateExecutable = false;
compilerParams.GenerateInMemory = true;
compilerParams.IncludeDebugInformation = false;
compilerParams.ReferencedAssemblies.Add("mscorlib.dll");
compilerParams.ReferencedAssemblies.Add("System.dll");
//add any aditional references needed
foreach (string refAssembly in code.References)
compilerParams.ReferencedAssemblies.Add(refAssembly);
下一步,我创建一个CompilerParameters 对象。这个类基本上包装了当你手工通过csc.exe (C# 编译器) 和 vbc.exe (VB.NET 编译器) 编译一个程序集时所用的所有命令行参数。一个特别重要的参数就是属性GenerateInMemory, 这里我用到了它。它能确保当代码编译时,生成的程序集只会驻存在内存中,而不会作为结果创建任何文件。
该代码的最后一部分将脚本代码所需要的所有引用添加到CompilerParameters中。 默认情况下,我添加了对于mscorlib.dll 和 system.dll 的引用。然后我添加了对于.dnml 文件中每一个<reference> 元素所标明的程序集的引用。
//actually compile the code
CompilerResults results = compiler.CompileAssemblyFromSource( compilerParams, code.SourceCode)); //Do we have any compiler errors
if (results.Errors.Count > 0)
{foreach (CompilerError error in results.Errors)
DotNetScriptEngine.LogAllErrMsgs("Compine Error:"+error.ErrorText);
return null;
}
然后,我调用了CodeCompiler.CompileAssemblyFromSource, 它传入CompilerParameters 对象和包含所要编译的实际代码的字符串变量。返回的对象属于类CompilerResults. 当编译出现错误时,这个对象包含一个CompileError 对象的集合,我将用它显示给用户当编译时那些地方出错了。
//get a hold of the actual assembly that was generated
Assembly generatedAssembly = results.CompiledAssembly; //return the assembly
return generatedAssembly;
}
如果脚本代码编译成功,CompilerReslts 对象将包含一个对于新编译和创建的程序集的引用。我保留该对象并返回给调用方法。
一旦程序集成功创建并返回,脚本引擎将利用反射遍历每一个生成的类型,寻找一个叫'Main' 的静态方法。如果找到了,就再次利用反射执行它。如果没有找到,它将返回给用户一个错误,用来解释发生的问题。
最后一步
.NET 脚本引擎还能够添加和删除.dnml 文件的关联。这意味着一旦文件关联好,你为了执行.dnml 文件只需双击它就可以了。 当你做个时,脚本引擎将会执行,一个相关路径的命令行参数和该 .dnml 文件名将传递给它。接着教本引擎将读取该文件并处理相应的XML。
为了创建.dnml 文件和.NET 脚本引擎之间的关联,你只需双击DotNetScriptEngine.exe 就可以了。当它不带任何命令行参数执行时,它将在您的服务器上创建文件关联。如果你在控制台运行DotNetScriptEngine.exe, 并传入'remove' 参数,该引擎将会在您的服务器上删除文件关联。
这个版本太大了,以至于我想把它提到一个主版本号的升级。下面是修复和提高的列表。
一个app.config 文件被添加进来, 用来替代在.dnml 文件中重复的选项XML 元素,如: waitForUserAction, 入口方法,脚本语言和常见的引用程序集。我还为脚本引擎添加了添加新语言的功能,因此你可以用任何语言编写.NET 脚本文件,只要定义了该语言的'CodeProvider' 类(在后面会更详细地提到)。在app.config 文件中定义的值就如机器配置一样。也就是是或,这些基本值可以被dnml 文件中的值所覆盖。Dnml 文件中的值具有最高优先级,它将被脚本引擎使用。但是如果你的.dnml 脚本文件没有定义任何配置,app.config 文件中的值将被使用。这样可以让你在.dnml 文件中只定义实际的代码。
用户偏好配置段: 一个用户偏好段被添加到app.config 文件中。这个段定义了三个配置。默认语言,脚本入口和等待用户动作标志。默认语言用来确定当dnml 文件中没有定义语言元素时脚本语言的语言。入口是指当dnml 文件没有定义入口时脚本引擎将会调用的方法。waitForUserAction 标签是一个布尔值,用来决定当脚本执行完毕后控制台窗口是否保留,并等待一个crlf 按键。如果这没有在dnml 文件中定义,config 文件中的值将被脚本引擎调用。下面是这个段的一个例子。
<userPreferences defaultLanguage="C#" entryPoint="Main"
waitForUserAction="true" />
程序集引用配置段: 这个段被用来定义脚本执行需要的程序集。任何在该段定义的程序集都会在每个脚本运行时编译进来。只需要程序集名称,而不是完全路径名。下面是这个段的一个例子。
<referencedAssemblies>
<assembly path="System.dll" />
<assembly path="System.Messaging.dll" />
<assembly path="System.Messaging.dll" />
<assembly path="System.Security.dll" />
</referencedAssemblies>
语言支持配置段: 这个段让你动态为脚本引擎添加新的支持语言,而不需重新便宜引擎代码。属性name 就是在dnml 文件中定义的名称,或是在用户偏好段中的defaultLanguage 属性。属性 assembly 就是包含codeprovder 该语言实现的程序集完整路径名和文件名。属性codeProviderName 就是该语言的code provider 类,包含所在名字空间。查看一下类AssemblyGenerator 的LateBindCodeProvider() 方法可以了解一下我是怎样为脚本引擎添加该项功能的。
<supportedLanguages>
<language name="JScript"
assembly="C:\WINNT\Microsoft.NET\Framework\v1.1.4322\Microsoft.JScript.dll"
codeProviderName="Microsoft.JScript.JScriptCodeProvider" />
<language name="J#"
assembly="c:\winnt\microsoft.net\framework\v1.1.4322\vjsharpcodeprovider.dll"
codeProviderName="Microsoft.VJSharp.VJSharpCodeProvider" />
</supportedLanguages>
更新 2/18/04: 版本 1.0.2.0
我修补了Charlie pointed out 提到的用DotNetScriptEngine.exe 注册.dnml 文件扩展名关联的一个错误。
还为dnml 文件格式添加了一个可选的entryPoint 属性。这样可以让用户指定一个程序集入口而不仅仅是"Main" 方法。如果属性entryPoint 被一个方法名称填充,则该方法将成为入口点。否则,"Main" 将成为默认的程序集入口点。
元素language 可以用以下三种方式定义。
<language name="C#" /> Main() will be the
entry method to the assembly
<language name="C#" entryPoint"" /> Main() will
be the entry method to the assembly
<language name="C#" entryPoint"Stuff" /> Stuff()
will be the entry method to the assembly
我还为.dnml XML 格式添加了一个可选的<waitForUserAction> 元素。这是一个新假如的特征,它可以让你当脚本执行完毕后仍保留控制台窗口。元素 waitForUserAction 是可选的。如果它没有包含在.dnml 文件中,那么窗口将会保留(打开)。该属性值可以是'true' 或 'false'。如果为真,窗口会保留。如果为假,当脚本执行完毕后控制台窗口会马上关闭。 这样可以让你把若干个脚本文件链接到一个批处理文件中来。
使用该元素的可能途径。
--nothing-- Console window will remain open after script has run
<waitForUserAction value="true"/> Console window will
remain open after script has run
<waitForUserAction value="True"/> Console window will
remain open after script has run
<waitForUserAction value="TRUE"/> Console window will
remain open after script has run
<waitForUserAction value="false"/> Console window will
close after script has run
<waitForUserAction value="False"/> Console window will
close after script has run
<waitForUserAction value="FALSE"/> Console window will
close after script has run
最后,我添加了.NET 脚本返回给调用进程, cmd或批处理文件,一个值的功能,该值可以是空的,也可以是一个整数。现在有两种定义脚本入口方法返回值的方式。你可以定义它为空,也可以定义它为一个整型值。如果你使用空值,脚本将不会返回任何东西。如果你使用整型值,脚本引擎将会返回给调用它的进程一个整型值。
两个不同的脚本入口方法的例子:
//The script engine will return nothing when this script is called.
public static void Main()
{
//...do stuff
return;
}
//The script engine will return a 5 when this script is called.
public static int Main()
{
//...do stuff
return 5;
}
//The script engine will return nothing when this script is called.
Public Shared Sub Main()
'...do stuff
return
End Sub
//The script engine will return a 5 when this script is called.
Public Shared Function Main() as Integer
'...do stuff
return 5
End Function
这个版本太大了,以至于我想把它提到一个主版本号的升级。下面是修复和提高的列表。
一个app.config 文件被添加进来, 用来替代在.dnml 文件中重复的选项XML 元素,如: waitForUserAction, 入口方法,脚本语言和常见的引用程序集。我还为脚本引擎添加了添加新语言的功能,因此你可以用任何语言编写.NET 脚本文件,只要定义了该语言的'CodeProvider' 类(在后面会更详细地提到)。在app.config 文件中定义的值就如机器配置一样。也就是是或,这些基本值可以被dnml 文件中的值所覆盖。Dnml 文件中的值具有最高优先级,它将被脚本引擎使用。但是如果你的.dnml 脚本文件没有定义任何配置,app.config 文件中的值将被使用。这样可以让你在.dnml 文件中只定义实际的代码。
用户偏好配置段: 一个用户偏好段被添加到app.config 文件中。这个段定义了三个配置。默认语言,脚本入口和等待用户动作标志。默认语言用来确定当dnml 文件中没有定义语言元素时脚本语言的语言。入口是指当dnml 文件没有定义入口时脚本引擎将会调用的方法。waitForUserAction 标签是一个布尔值,用来决定当脚本执行完毕后控制台窗口是否保留,并等待一个crlf 按键。如果这没有在dnml 文件中定义,config 文件中的值将被脚本引擎调用。下面是这个段的一个例子。
<userPreferences defaultLanguage="C#" entryPoint="Main"
waitForUserAction="true" />
程序集引用配置段: 这个段被用来定义脚本执行需要的程序集。任何在该段定义的程序集都会在每个脚本运行时编译进来。只需要程序集名称,而不是完全路径名。下面是这个段的一个例子。
<referencedAssemblies>
<assembly path="System.dll" />
<assembly path="System.Messaging.dll" />
<assembly path="System.Messaging.dll" />
<assembly path="System.Security.dll" />
</referencedAssemblies>
语言支持配置段: 这个段让你动态为脚本引擎添加新的支持语言,而不需重新便宜引擎代码。属性name 就是在dnml 文件中定义的名称,或是在用户偏好段中的defaultLanguage 属性。属性 assembly 就是包含codeprovder 该语言实现的程序集完整路径名和文件名。属性codeProviderName 就是该语言的code provider 类,包含所在名字空间。查看一下类AssemblyGenerator 的LateBindCodeProvider() 方法可以了解一下我是怎样为脚本引擎添加该项功能的。
<supportedLanguages>
<language name="JScript"
assembly="C:\WINNT\Microsoft.NET\Framework\v1.1.4322\Microsoft.JScript.dll"
codeProviderName="Microsoft.JScript.JScriptCodeProvider" />
<language name="J#"
assembly="c:\winnt\microsoft.net\framework\v1.1.4322\vjsharpcodeprovider.dll"
codeProviderName="Microsoft.VJSharp.VJSharpCodeProvider" />
</supportedLanguages>
更新 2/18/04: 版本 1.0.2.0
我修补了Charlie pointed out 提到的用DotNetScriptEngine.exe 注册.dnml 文件扩展名关联的一个错误。
还为dnml 文件格式添加了一个可选的entryPoint 属性。这样可以让用户指定一个程序集入口而不仅仅是"Main" 方法。如果属性entryPoint 被一个方法名称填充,则该方法将成为入口点。否则,"Main" 将成为默认的程序集入口点。
元素language 可以用以下三种方式定义。
<language name="C#" /> Main() will be the
entry method to the assembly
<language name="C#" entryPoint"" /> Main() will
be the entry method to the assembly
<language name="C#" entryPoint"Stuff" /> Stuff()
will be the entry method to the assembly
我还为.dnml XML 格式添加了一个可选的<waitForUserAction> 元素。这是一个新假如的特征,它可以让你当脚本执行完毕后仍保留控制台窗口。元素 waitForUserAction 是可选的。如果它没有包含在.dnml 文件中,那么窗口将会保留(打开)。该属性值可以是'true' 或 'false'。如果为真,窗口会保留。如果为假,当脚本执行完毕后控制台窗口会马上关闭。 这样可以让你把若干个脚本文件链接到一个批处理文件中来。
使用该元素的可能途径。
--nothing-- Console window will remain open after script has run
<waitForUserAction value="true"/> Console window will
remain open after script has run
<waitForUserAction value="True"/> Console window will
remain open after script has run
<waitForUserAction value="TRUE"/> Console window will
remain open after script has run
<waitForUserAction value="false"/> Console window will
close after script has run
<waitForUserAction value="False"/> Console window will
close after script has run
<waitForUserAction value="FALSE"/> Console window will
close after script has run
最后,我添加了.NET 脚本返回给调用进程, cmd或批处理文件,一个值的功能,该值可以是空的,也可以是一个整数。现在有两种定义脚本入口方法返回值的方式。你可以定义它为空,也可以定义它为一个整型值。如果你使用空值,脚本将不会返回任何东西。如果你使用整型值,脚本引擎将会返回给调用它的进程一个整型值。
两个不同的脚本入口方法的例子:
//The script engine will return nothing when this script is called.
public static void Main()
{
//...do stuff
return;
}
//The script engine will return a 5 when this script is called.
public static int Main()
{
//...do stuff
return 5;
}
//The script engine will return nothing when this script is called.
Public Shared Sub Main()
'...do stuff
return
End Sub
//The script engine will return a 5 when this script is called.
Public Shared Function Main() as Integer
'...do stuff
return 5
End Function
浙公网安备 33010602011771号