用 C# 编写的命令行






4.67/5 (9投票s)
2003年7月19日
4分钟阅读

121532

2
这篇文章是关于如何使用 C# 编写的命令行。
引言
实际上,我主要是一名 C++ 开发人员,但我发现使用 .NET 框架的 C# 是一个完美的工具,可以用于我经常执行的辅助工作,例如日志文件和数据转储文件的分析以及其他类似任务。因此,我用 C# 编写了许多控制台实用程序。这些实用程序的每个 Main()
函数都进行命令行解析、参数验证和转换为适当的类型,决定要调用哪个方法,然后使用准备好的参数值调用该方法,这很正常。
但我厌倦了编写每个命令行规则,也厌倦了编写遵循这些规则的无聊代码。我厌倦了每次添加或更改实用程序功能后,都要更新这些规则和相应的代码。我找到了一个解决方案。从现在开始,我更喜欢直接在命令行中调用带有必要参数值的方法。例如:test.exe "Job1(1, Math.Sqrt(3.14), new MyClass())" or "Job2(@\"C:\Temp\", true)"
,其中 Job1()
和 Job2()
是现有的公共方法。我不再在我的实用程序中编写任何解析命令行的代码了。感谢 C# 和 .NET!
您可以使用 Accessor 类做什么?
- 您可以在运行时评估 C# 字符串表达式或执行方法文本;请注意,这些表达式和方法可以自由访问主程序集中的所有现有公共成员(或任何其他附加成员,如果指定);
- 我的建议是将此功能应用于在
Main()
中评估或执行命令行!由于您可以访问主程序集的公共成员,因此您可以调用为此设计的任何方法。命令行成为 C# 程序的一个真实部分,并且它也是用 C# 编写的。 - 尽管我以关于控制台应用程序的词语开头这篇文章,但您也可以将此技术应用于 Win Forms。
如果 Main() 评估命令行,您的好处
- 如果更改或扩展程序功能,则不再更改
Main()
。您可以自由地从命令行调用新的或更改的方法。 - 您可以有效地使用它进行调试。您是否刚刚创建了一个尚未调试的方法?您想立即调用它吗?只需在命令行中编写对该方法的调用,并使用一些参数值,然后运行调试。
- 您可以有效地使用它进行测试。您可以从命令行调用用于测试的程序的所有公共方法。最简单的方法是使用多个程序运行的批处理文件,这些程序使用不同的命令行,即:对不同方法的不同调用。
关于安全性的说明
这种方法不是给最终用户的玩具!不建议让客户能够从命令行调用程序中的任何公共方法。这种方法主要适用于软件开发人员本身,只是为了消除大量简单但非常无聊和不具创造性的工作。
然而,有不同的非常安全的方法可以在已发布的应用程序中保留和使用此功能。您可以设计您的应用程序,以便您的主程序集仅具有旨在从命令行调用的方法。这些方法本身可以访问其他应用程序集,但命令行中编写的源代码必须仅引用主程序集,才能访问为此设计的方法。在尝试访问不允许的其他内容时,命令行源代码编译将失败。当然,如何保护其应用程序免受笨蛋或黑客的侵害取决于开发人员。
代码
下面的代码只需添加到您的代码中即可使其成为可能。它定义了 Accessor
类,并且还包含其工作方式的示例(命名空间 Test
,请参阅 Test.Example.Main()
中的 3 个示例)。您必须定义 EXAMPLE
常量,或者您可以将其替换为 true
以启用示例代码。我似乎在这里无话可说了,代码和注释说明了其余部分。请注意,Accessor
也可以用作表达式求值器和方法执行器。
using Microsoft.CSharp;
using System.CodeDom.Compiler;
using System.Diagnostics;
using System.Reflection;
using System.Text;
using System;
// The example illustrates main purposes of Accessor:
// 1) Evaluation of a string expression
// 2) Execution of a method's code
// 3) Execution of a command line written in C#
#if EXAMPLE
namespace Test
{
// This class is used to show that:
// - we can freely create an instance of MyClass in our string source,
// it means that we can create any other public class of this
// assembly and access its public members;
// - we can access even the protected MyMethod() in our string source,
// it means that we can also access any protected members of
// a public not sealed class of this assembly.
public class MyClass
{
public MyClass()
{
Console.WriteLine("Hello from Test.MyClass()");
}
protected int MyMethod()
{
Console.WriteLine("Hello from Test.MyClass.MyMethod()");
return 12345;
}
}
// Main class
public class Example
{
// Suppose Job1() and Job2() implement this program functionality.
// Then we can just write command lines like:
// test.exe "Job1(0, 0.0, null)"
// test.exe "Job1(1, Math.Sqrt(3.14), new MyClass())"
// test.exe "Job2(@\"C:\Temp\", true)"
// test.exe "Job2(@\"C:\Documents and Settings\" , false)"
// Note: just try these command lines
public int Job1(int arg1, double arg2, MyClass arg3)
{
Console.WriteLine
("Hello from Job1() with {0}, {1}, {2}", arg1, arg2, arg3);
return 0;
}
public int Job2(string arg1, bool arg2)
{
Console.WriteLine
("Hello from Job2() with {0}, {1}", arg1, arg2);
return 0;
}
// Here there are 3 examples of 3 main purposes of Accessor
static int Main(string[] args)
{
// Example 1. Evaluation of a string expression.
string myExpression = "M.Sin(0.5) + M.Sqrt(2.0)";
Console.WriteLine("\nExample 1. Evaluating:\n{0}...",
myExpression);
double result1 = (double)Absolute.Accessor.Evaluate(
myExpression,
false, // we don't need main assembly reference
// just for fun: M instead of Math
"using M = System.Math;",
null); // we don't need any class base list
Console.WriteLine("Evaluation result: {0}", result1);
// Example 2. Execution of a method text.
string myMethod = @"
int Executable()
{
Console.WriteLine(""Hello from Executable()"");
return MyMethod();
}";
Console.WriteLine("\nExample 2. Executing method:{0}...",
myMethod);
int result2 = (int)Absolute.Accessor.Execute(
myMethod,
true, // add main assembly reference to access MyClass
// we can use MyClass instead of Test.MyClass
"using Test;",
// MyClass's child can access protected MyMethod()
"MyClass");
Console.WriteLine("Execution result: {0}", result2);
// Example 3.
// THE AIM IS TO USE OUR PROGRAM AS A COMMAND LINE DRIVEN TOOL
// WITHOUT WRITING A LINE OF CODE THAT PARSES ITS COMMAND LINE
// Let the very first argument be a piece of code invoking
// a program job and let Accessor make all parsing for us!
// Our benefits for this example:
// - we don't validate arguments and convert
// them into required types
// - we don't write any code to create required
// MyClass instance
// - we don't even call Job1() or Job2() methods;
// - if (oh God!) we change Job's parametes count or types
// then this Main() function always remains the same
// (as usually we have to use updated command lines)
int jobResult;
try
{// we have to catch possible compilation and execution errors
// args0 is args[0] if any else some default job
string args0 =
args.Length > 0 ?
args[0] : "Job1(1, Math.Sqrt(3.14), new MyClass())";
Console.WriteLine
("\nExample 3. Running command line:\n\"{0}\"", args0);
jobResult = (int)Absolute.Accessor.Evaluate(
args0,
true, // access to Example and MyClass
"using Test;", // MyClass istead of Test.MyClass
"Example"); // access to Job()
Console.WriteLine("Command line result: {0}", jobResult);
}
catch(Exception e)
{// syntax errors are reported here as well as any others
Console.WriteLine(e.Message);
jobResult = -1;
}
// result
return jobResult;
}
}
}
#endif
namespace Absolute
{
// summary: C# expessions evaluator and methods executor
// remarks: Author: Roman Kuzmin
public sealed class Accessor
{
// summary: Evaluates C# expression
// expression: C# expression
// mainAssembly: Add a reference to the main assembly
// headCode: null or a code to be added at a generated source head
// classBase: null or a generated class base list
// assemblies: [params] null or additional assemblies
// returns: Evaluated value as object
public static object Evaluate(
string expression,
bool mainAssembly,
string headCode,
string classBase,
params string[] assemblies)
{
string methodCode = String.Format(
"object Expression()\n{{return {0};}}", expression);
return Execute(
methodCode, mainAssembly, headCode, classBase, assemblies);
}
// summary: Executes C# method's code
// methodCode: Complete C# method's code
// mainAssembly: Add a reference to the main assembly
// headCode: null or a code to be added at a generated source head
// classBase: null or a generated class base list
// assemblies: [params] null or additional assemblies
// returns: null or the executed method's return value
public static object Execute(
string methodCode,
bool mainAssembly,
string headCode,
string classBase,
params string[] assemblies)
{
// extract method name
int i1, i2 = methodCode.IndexOf('(');
if(i2 < 1)
throw new Exception("Accessor: syntax error: ( expected");
for(--i2; i2 >= 0 && methodCode[i2] <= ' '; --i2);
for(i1 = i2; i1 >= 0 && methodCode[i1] > ' '; --i1);
string methodName = methodCode.Substring(i1 + 1, i2 - i1);
// code builder
StringBuilder code = new StringBuilder();
// << default using directives
code.Append("using System;\n");
// << head code
if(headCode != null)
code.AppendFormat("{0}\n", headCode);
// << class name
code.Append("public class AbsoluteClass");
// << clase base list
if(classBase != null)
code.AppendFormat(" : {0}", classBase);
// << class body with method
code.AppendFormat("\n{{\npublic {0}\n}}\n", methodCode);
// compiler parameters
CompilerParameters parameters = new CompilerParameters();
parameters.GenerateExecutable = false;
parameters.GenerateInMemory = true;
// referenced asseblies
parameters.ReferencedAssemblies.Add("system.dll");
if(mainAssembly)
parameters.ReferencedAssemblies.Add(
Process.GetCurrentProcess().MainModule.FileName);
if(assemblies != null)
parameters.ReferencedAssemblies.AddRange(assemblies);
// compiler and compilation
ICodeCompiler compiler =
new CSharpCodeProvider().CreateCompiler();
CompilerResults results =
compiler.CompileAssemblyFromSource(
parameters, code.ToString());
// errors?
if(results.Errors.HasErrors)
{
StringBuilder error = new StringBuilder();
error.Append("Accessor: compilation errors:\n");
foreach(CompilerError err in results.Errors)
error.AppendFormat(
"#{0} {1}. Line {2}.\n",
err.ErrorNumber, err.ErrorText, err.Line);
error.AppendFormat("\n{0}", code);
throw new Exception(error.ToString());
}
// class instance
object classInstance =
results.CompiledAssembly.CreateInstance("AbsoluteClass");
// execute method
MethodInfo info = classInstance.GetType().GetMethod(methodName);
return info.Invoke(classInstance, null);
}
// summary: Private constructor makes the class 'pure static'
private Accessor()
{
}
}
}
历史
- 2003 年 7 月 20 日 - 原始文章。
- 2003 年 7 月 21 日 - 更详细的解释和代码注释。