Dot Net 脚本






4.74/5 (58投票s)
2004年2月17日
11分钟阅读

437750

6790
像脚本语言一样编写和执行 C# 或 VB.NET 代码(使用 CodeDom)
引言
你知道我怀念 .NET 之前的日子什么吗?脚本!我喜欢创建小脚本文件来处理一些小任务,或者测试一小段代码,而无需创建新项目或解决方案。我喜欢只有一个漂亮的小文件需要处理和清理,而不是一个解决方案文件夹、一个项目文件夹以及由此产生的 bin 和 obj 文件夹。我渴望那样的日子,所以才创建了 Dot Net Script。
Dot Net Script 是什么?基本上,它就是一个控制台应用程序,它从一个 .dnml 文件(Dot Net Markup Language。是的,我编的)读取 XML 文档。这个 XML 文档包含子元素,这些子元素保存有关程序集引用、代码语言以及将被编译和执行的实际代码的信息。我称之为脚本引擎的控制台应用程序读取 XML 文本并解析出所需数据。然后,它使用 CSharp、VisualBasic 和 CodeDom 命名空间中的类来编译代码,并将生成的程序集加载到内存中。脚本引擎然后使用反射来执行生成程序集中的入口函数。当用户关闭控制台窗口时,脚本引擎也会关闭,内存中的程序集将超出范围,并由 GC 进行清理。永远不会创建 DLL 或 EXE 文件。
.NET 标记语言
因此,让我们看看 Dot Net Markup Language 的样子。实际上它非常简单。下面是一个 Dot 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> 元素,这对应于您将在 Visual Studio 项目中添加的引用列表。对于代码执行所需的每个引用,都应该添加一个 <reference assembly="" /> 元素。
至于程序集探测,任何您引用的位于 GAC 中的程序集都将被 CLR 自动找到。但是,如果您有一个不在 GAC 中的程序集,情况就有些不同了。假设您有一个名为 Common.dll 的程序集,它没有被 GAC 化。为了使您的 Dot Net 脚本能够执行,Common.dll 需要放在两个地方。首先,它需要与您的 .dnml 文件位于同一文件夹中。其次,Common.dll 需要位于脚本引擎 EXE 所在的文件夹中。我正在努力简化这一点,但目前,非 GAC 化程序集需要存在于两个不同的文件夹中。
下一个元素称为 <language>,它有一个名为 'name' 的属性。一个 .dnml 文档只能有一个语言元素。'name' 属性的两个可能值是 'C#' 和 'VB',我相信它们不言自明。
最后一个元素称为 <scriptCode>,它包含一个 CDATA XML 元素。在此元素内部是执行 .dnml 文件时将执行的代码。不过,您需要遵循一些接口规则才能使用它。首先,因为这实际上是纯粹的 C# 或 VB.NET,所有方法和字段都必须包含在一个类中。其次,您可以定义任意多的类,但其中一个类必须有一个名为 'Main' 的 public 'static' / 'Shared' 方法,该方法不返回任何内容且不接受任何输入参数。这将是脚本引擎通过反射查找的入口方法,找到后将被调用。另外,将 '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 对象,该对象可用于根据传递给它的 string 源代码填充 CodeDom 对象图(目前在 1.1 中,这只返回 null)。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 additional references needed
foreach (string refAssembly in code.References)
   compilerParams.ReferencedAssemblies.Add(refAssembly);
接下来,我创建一个 CompilerParameters 对象。此类基本上封装了如果您手动使用 csc.exe(C# 编译器)或 vbc.exe(VB.NET 编译器)编译程序集时会使用的所有命令行参数。有一行特别重要,即 GenerateInMemory 属性,我将其设置为 true。这将确保在编译代码时,不会创建任何最终程序集文件。生成的程序集将仅存在于内存中。
此代码的最后一部分是将代码所需的所有引用添加到 CompilerParameters。默认情况下,我添加对 mscorlib.dll 和 system.dll 的引用。然后,我通过 <reference> 元素添加对 .dnml 文件中定义的每个程序集的引用。
//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 对象和一个保存要编译的实际源代码的 string 变量。返回的对象是 CompilerResults 类型。如果编译失败,此对象将包含一个 CompileError 对象集合,我使用它来向用户显示编译出了什么问题。
//get a hold of the actual assembly that was generated
Assembly generatedAssembly = results.CompiledAssembly; 
//return the assembly
return generatedAssembly;
}
如果代码成功编译,CompilerReslts 对象将包含对新编译和创建的 Assembly 对象的引用。我获取此对象并将其返回给调用方法。
一旦程序集成功创建并返回,脚本引擎将使用反射遍历所有创建的类型,并查找名为 'Main' 的 static 方法。如果找到,它将再次使用反射来执行它。如果引擎找不到 Main 函数,它将向用户返回一个错误,解释问题。
收尾工作
Dot Net Script Engine 还可以为 .dnml 文件创建和删除到脚本引擎的文件关联。这意味着一旦进行了此关联,您要执行 .dnml 文件所要做的就是双击它。执行此操作时,将执行脚本引擎,并将一个命令行参数传递给它,该参数包含 .dnml 文件的路径和名称。脚本引擎将读取文件并相应地处理 XML。
为了在 .dnml 文件和 Dot Net Script Engine 之间创建文件关联,只需双击 DotNetScriptEngine.exe。当它在没有命令行参数的情况下运行时,它将自动在您的服务器上创建文件关联。如果您通过命令提示符运行 DotNetScriptEngine.exe 并传递 'remove' 参数,引擎将自动从您的服务器中删除文件关联。
其他更新
更新 2004/3/18:版本 2.0.0.1
这个版本如此庞大并且变化如此之多,我认为它值得一次主版本号的增加。以下是修复和增强的列表。
添加了一个 app.config 文件,用于从 .dnml 文件中移除一些常用重复的可选 XML 元素:例如 waitForUserAction、方法入口点、脚本语言和常用引用的程序集。我还添加了向脚本引擎添加新语言的功能,这样您就可以使用任何定义了自己的 'CodeProvider' 类的语言来编写 Dot Net Script 文件(稍后会详细介绍)。app.config 文件中的值就像机器配置一样。也就是说,这些是基本值,但可以被 dnml 文件中设置的值覆盖。dnml 文件中的值具有最高优先级,并将被脚本引擎使用。但是,如果您的 .dnml 脚本文件没有定义任何设置,则将使用 app.config 文件中的值。这允许您仅在 .dnml 文件中定义实际脚本。
用户偏好配置节:在 app.config 文件中添加了一个用户偏好配置节。此节定义了三个设置。默认语言、脚本入口点以及等待用户操作标志。如果 dnml 文件中未定义语言元素,则默认语言可用于设置脚本将编译的语言。入口点是如果 dnml 文件未定义入口点,脚本引擎将调用的方法。waitForUserAction 标志是一个 bool,用于确定脚本运行完成后控制台窗口是否会保持打开状态并等待回车换行。如果 dnml 文件中未定义此项,脚本引擎将使用配置文件中的值。下面是此节的示例。
<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 属性是包含语言代码提供程序实现的程序集的完整路径和文件名。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>            
更新 2004/2/18:版本 1.0.2.0
我修复了 Charlie 指出的关于 .dnml 文件扩展名文件类型与 DotNetScriptEngine.exe 注册的问题。
此外,还在 dnml XML 格式的 language 元素中添加了一个可选的 entryPoint 属性。这将允许用户指定一个非“Main”方法的程序集入口点。如果 entryPoint 属性填入了方法名,那么该方法将成为入口点。如果 entryPoint 属性缺失或为空,则“Main”将成为程序集入口点。
语言元素可以按以下三种方式之一定义
<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> 元素。这是另一个新功能,它允许您在 .dnml 文件中定义脚本运行完成后是否要保持控制台窗口打开。waitForUserAction 元素是一个可选元素。如果它未包含在 .dnml 文件中,则窗口将保持打开状态。value 属性可以包含 'true' 或 'false' 的值。如果为 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
最后,我添加了从 dot net 脚本返回 int 到调用进程、cmd 或批处理文件的能力。现在,定义脚本入口方法的返回值有两种不同的方式。您可以将其定义为 void,也可以将其定义为 int。如果您使用 void,则脚本不会返回任何内容。如果您返回一个 int,那么脚本引擎将把 int 的值返回给调用进程。
两个不同 dnml 脚本入口方法的示例
//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
许可证
本文未附加明确的许可证,但可能在文章文本或下载文件本身中包含使用条款。如有疑问,请通过下面的讨论区联系作者。
作者可能使用的许可证列表可以在此处找到。
