C# 经典实例 第一章 类和泛型 #1.5 解析命令行参数

问题:

需要应用程序以标准格式(在1.5.3中介绍)接受一个或多个命令行参数。你需要访问和解析传递给应用程序的完整的命令行。

解决方案:

在例1-5中,结合使用以下类来帮助解析命令行参数:Argument\ArgumentDefinition和ArgumentSemanticAnalyzer。

例1-5:Argument类:

using System;
using System.Diagnostics;
using System.Linq;
using System.Collections.ObjectModel;
using System.Collections.Generic;
using System.Text;

public sealed class Argument
{
    public string Original { get; }
    public string Switch { get; private set; }
    public ReadOnlyCollection SubArguments { get; }
    private List subArguments;
    public Argument(string original)
    {
        Original = original; Switch = string.Empty;
        subArguments = new List();
        SubArguments = new ReadOnlyCollection(subArguments);
        Parse();
    }

    private void Parse()
    {
        if (string.IsNullOrEmpty(Original))
        {
            return;
        }
        char[] switchChars = { '/', '-' };

        if (!switchChars.Contains(Original[0]))

        {
            return;
        }

        string switchString = Original.Substring(1);
        string subArgsString = string.Empty;
        int colon = switchString.IndexOf(':');

        if (colon >= 0)
        {
            subArgsString = switchString.Substring(colon + 1);
            switchString = switchString.Substring(0, colon);
        }

        Switch = switchString;

        if (!string.IsNullOrEmpty(subArgsString))
            subArguments.AddRange(subArgsString.Split(';'));
    }

    // 一组谓词,提供关于参数的有用信息
    // 使用lambda表达式实现 
    public bool IsSimple => SubArguments.Count == 0;
    public bool IsSimpleSwitch =>
        !string.IsNullOrEmpty(Switch) && SubArguments.Count == 0;
    public bool IsCompoundSwitch => !string.IsNullOrEmpty(Switch) && SubArguments.Count == 1;
    public bool IsComplexSwitch => !string.IsNullOrEmpty(Switch) && SubArguments.Count > 0;
}

public sealed class ArgumentDefinition
{
    public string ArgumentSwitch { get; }
    public string Syntax { get; }
    public string Description { get; }
    public Func Verifier { get; }

    public ArgumentDefinition(string argumentSwitch, string syntax, string description, Func verifier)
    {
        ArgumentSwitch = argumentSwitch.ToUpper();
        Syntax = syntax;
        Description = description;
        Verifier = verifier;
    }

    public bool Verify(Argument arg) => Verifier(arg);
}

public sealed class ArgumentSemanticAnalyzer
{

    private List argumentDefinitions = new List();
    private Dictionary> argumentActions = new Dictionary>();

    public ReadOnlyCollection UnrecognizedArguments { get; private set; }
    public ReadOnlyCollection MalformedArguments { get; private set; }
    public ReadOnlyCollection RepeatedArguments { get; private set; }

    public ReadOnlyCollection ArgumentDefinitions =>
        new ReadOnlyCollection(argumentDefinitions);

    public IEnumerable DefinedSwitches => from argumentDefinition in argumentDefinitions select argumentDefinition.ArgumentSwitch;

    public void AddArgumentVerifier(ArgumentDefinition verifier) => argumentDefinitions.Add(verifier);

    public void RemoveArgumentVerifier(ArgumentDefinition verifier)
    {
        var verifiersToRemove = from v in argumentDefinitions
                                where v.ArgumentSwitch == verifier.ArgumentSwitch
                                select v;
        foreach (var v in verifiersToRemove)
            argumentDefinitions.Remove(v);
    }

    public void AddArgumentAction(string argumentSwitch, Action action) => argumentActions.Add(argumentSwitch, action);

    public void RemoveArgumentAction(string argumentSwitch)
    {
        if (argumentActions.Keys.Contains(argumentSwitch))
            argumentActions.Remove(argumentSwitch);
    }

    public bool VerifyArguments(IEnumerable arguments)
    {
        // 没有任何参数进行验证,失败     
        if (!argumentDefinitions.Any())
            return false;

        // 确认是否存在任一未定义的参数       
        this.UnrecognizedArguments = (from argumentItem in arguments
                                      where !DefinedSwitches.Contains(argumentItem.Switch.ToUpper())
                                      select argumentItem).ToList().AsReadOnly();

        if (this.UnrecognizedArguments.Any())
            return false;

        // 检查开关与某个已知开关匹配但是检查格式是否正确的谓词为false的所有参数
        this.MalformedArguments = (from argument in arguments
                                   join argumentDefinition in argumentDefinitions
                                   on argument.Switch.ToUpper() equals argumentDefinition.ArgumentSwitch
                                   where !argumentDefinition.Verify(argument)
                                   select argument).ToList().AsReadOnly();

        if (this.MalformedArguments.Any())
            return false;

        // 将所有参数按照开关进行分组,统计每个组的数量,
        // 并选出包含超过一个元素的所有组,
        // 然后获得一个包含这些数据项的只读列表
        this.RepeatedArguments = (
            from argumentGroup in from argument in arguments
                                  where !argument.IsSimple
                                  group argument by argument.Switch.ToUpper()
            where argumentGroup.Count() > 1
            select argumentGroup).SelectMany(ag => ag).ToList().AsReadOnly();

        if (this.RepeatedArguments.Any())
            return false;

        return true;
    }

    public void EvaluateArguments(IEnumerable arguments)
    {
        //此时只需要应用每个动作:
        foreach (Argument argument in arguments)
            argumentActions[argument.Switch.ToUpper()](argument);
    }

    public string InvalidArgumentsDisplay()
    {
        StringBuilder builder = new StringBuilder();
        builder.AppendFormat($"Invalid arguments: {Environment.NewLine}");         // 的 

        FormatInvalidArguments(builder, this.UnrecognizedArguments, "Unrecognized argument: {0}{1}");

        // 添加格式不正式的参数
        FormatInvalidArguments(builder, this.MalformedArguments, "Malformed argument: {0}{1}");

        // 对于重复的参数,我们想要将其分组以用于显示,
        // 因此通过开关分组并且将其添加到正在构建的字符串
        var argumentGroups = from argument in this.RepeatedArguments
                             group argument by argument.Switch.ToUpper() into ag
                             select new { Switch = ag.Key, Instances = ag };

        foreach (var argumentGroup in argumentGroups)
        {
            builder.AppendFormat($"Repeated argument:                                " +
                $"  {argumentGroup.Switch}{Environment.NewLine}"); 
            FormatInvalidArguments(builder, argumentGroup.Instances.ToList(), "\t{0}{1}");
        }
        return builder.ToString();
    }

    private void FormatInvalidArguments(StringBuilder builder, IEnumerable invalidArguments, string errorFormat)
    {
        if (invalidArguments != null)
        {
            foreach (Argument argument in invalidArguments)
            {
                builder.AppendFormat(errorFormat, argument.Original, Environment.NewLine);
            }
        }
    }

    // 如何使用这些类为应用程序处理命令行?方法如下所示

    public static void Main(string[] argumentStrings)
    {
        var arguments = (from argument in argumentStrings select new Argument(argument)).ToArray();

        Console.Write("Command line: "); foreach (Argument a in arguments) { Console.Write($"{a.Original} "); }
        Console.WriteLine("");

        ArgumentSemanticAnalyzer analyzer = new ArgumentSemanticAnalyzer();
        analyzer.AddArgumentVerifier(new ArgumentDefinition("output", "/output:[path to output]", "Specifies the location of the output file.", x => x.IsCompoundSwitch));
        analyzer.AddArgumentVerifier(new ArgumentDefinition("trialMode", "/trialmode", "If this is specified it places the product into trial mode", x => x.IsSimpleSwitch));
        analyzer.AddArgumentVerifier(new ArgumentDefinition("DEBUGOUTPUT", "/debugoutput:[value1];[value2];[value3]", "A listing of the files the debug output " + "information will be written to", x => x.IsComplexSwitch));
        analyzer.AddArgumentVerifier(new ArgumentDefinition("", "[literal value]", "A literal value", x => x.IsSimple));

        if (!analyzer.VerifyArguments(arguments))
        {
            string invalidArguments = analyzer.InvalidArgumentsDisplay(); 
            Console.WriteLine(invalidArguments); 
            ShowUsage(analyzer); 
            return;
        }

        // 设置命令行解析结果的容器      
        string output = string.Empty;
        bool trialmode = false;
        IEnumerable debugOutput = null;
        List literals = new List();

        //我们相对每一个解析出的参数应用一个动作,因此将他们添加到分析器
        analyzer.AddArgumentAction("OUTPUT", x => { output = x.SubArguments[0]; });
        analyzer.AddArgumentAction("TRIALMODE", x => { trialmode = true; });
        analyzer.AddArgumentAction("DEBUGOUTPUT", x => { debugOutput = x.SubArguments; });

        analyzer.AddArgumentAction("", x => { literals.Add(x.Original); });

        // 检查参数并运行动作
        analyzer.EvaluateArguments(arguments);

        // 显示结果
        Console.WriteLine("");
        Console.WriteLine($"OUTPUT: {output}");
        Console.WriteLine($"TRIALMODE: {trialmode}");
        if (debugOutput != null)
        {
            foreach (string item in debugOutput)
            {
                Console.WriteLine($"DEBUGOUTPUT: {item}");
            }
        }

        foreach (string literal in literals)
        {
            Console.WriteLine($"LITERAL: {literal}");
        }
    }

    public static void ShowUsage(ArgumentSemanticAnalyzer analyzer)
    {
        Console.WriteLine("Program.exe allows the following arguments:");
        foreach (ArgumentDefinition definition in analyzer.ArgumentDefinitions)
        {
            Console.WriteLine($"\t{definition.ArgumentSwitch}:                    " +
                $"            ({definition.Description}){Environment.NewLine}              " +
                $"             \tSyntax: {definition.Syntax}");
        }
    }

}

讨论:

在解析命令行参数之前,必须明确选用一种通用格式。本范例中使用的格式遵循用于Visual C#.NET语言编译器的命令行格式。使用的格式定义如下所示。

  • 通过一个或多个空白字符分隔命令行参数。
  • 每个参数可以以一个-或/字符开头,但不能同时以这两个字符开头。如果不以其中一个字符开头,就把参数视为一个字面量,比如文件名。
  • 以-或/字符开头的参数可被划分为:以一个选项开关开头,后接一个冒号,再接一个或多个用;字符分隔的参数。命令行参数-sw:arg1;arg2;arg3可被划分为一个选项开关(sw)和三个参数(arg1,arg2和arg3)。注意,在完整的参数中不应该由任何空格,否则运行时命令行解析器将把参数分拆为两个或更多的参数。
  • 用双引号包裹住的字符串(如“C:\test\file.log")会去除双引号。这是操作系统解释传入应用程序中的参数时的一项功能。
  • 不会去除单引号。
  • 要保留双引号,可在双引号字符前放置\转义序列字符。
  • 仅当\字符后接着双引号时,才将\字符作为转义序列字符处理。在这种情况下,只会显示双引号。
  • ^字符被运行时解析器作为特殊字符处理。

幸运的是,在应用程序接受各个解析出的参数之前,运行时命令行解析器可以处理其中大部分任务。

运行时命令行解析器把一个包含每个解析过的参数的string[]传递给应用程序的入口点。

入口点可以采用以下形式之一:

 public static void Main()    

public static int Main()    

public static void Main(string[] args)    

public static int Main(string[] args)

前两种形式不接受参数,但是后两种形式接受解析过的命令行参数的数组。注意,静态属性Environment.CommandLine将返回一个字符串,其中包含完整的命令行;静态方法Environment.GetCommandLineArgs将返回一个字符串数组,其中包含解析过的命令行参数。

1.5.2节介绍的三个类涉及命令行参数的各个阶段:

 • Argument 封装一个命令行参数并负责解析该参数
• ArgumentDefinition 定义一个对当行命令行有效的参数
• ArgumentSemanticAnalyzer 基于设置的 ArgumentDefinition 进行参数的验证和获取。

把以下命令行参数传入这个应用程序中:

 MyApp c:\input\infile.txt -output:d:\outfile.txt -trialmode
将得到以下解析过的选项开关和参数:

           Command line: c:\input\infile.txt - output:d:\outfile.txt - trialmode     
            OUTPUT: d:\outfile.txt 
            TRIALMODE: True 
            LITERAL: c:\input\infile.txt

如果没有正确的输入命令行参数,比如忘记了向-output选项开关添加参数,得到的输出将如下所示:


        Command line: c:\input\infile.txt - output: -trialmode     
            Invalid arguments:     
            Malformed argument: -output


    Program.exe allows the following arguments: 
            OUTPUT: (Specifies the location of the output file.)            
            Syntax: / output:[path to output]            
            TRIALMODE: (If this is specified, it places the product into trial mode)            
            Syntax: / trialmode            
            DEBUGOUTPUT: (A listing of the files the debug output information will be written to)            
            Syntax: / debugoutput:[value1];[value2];[value3]            : (A literal value)            
            Syntax:[literal value]

每个Argument实例都需要能确定它自身的某些事项。相应的,作为Argument的属性暴露了一组谓词,告诉我们这个Argument的一些有用的信息。ArgumentSemanticAnalyzer将使用这些属性来确定参数的特征。

 public bool IsSimple => SubArguments.Count == 0;
    public bool IsSimpleSwitch => !string.IsNullOrEmpty(Switch) && SubArguments.Count == 0;
    public bool IsCompoundSwitch => !string.IsNullOrEmpty(Switch) && SubArguments.Count == 1;
    public bool IsComplexSwitch => !string.IsNullOrEmpty(Switch) && SubArguments.Count > 0;
 

这段代码由多出在LINQ查询的结果上调用了ToArray或ToList方法。这是由于查询结果是延迟执行的。这不仅意味着将以迟缓方式来计算结果,而且意味着每次访问结果是都需要从新计算他们。使用ToArray或ToList方法会强制积极计算结果,生成一份不需要在每次使用时都重新计算的副本。查询逻辑并不知道正在操作的集合是否发生了变化,因此每次都必须重新计算结果,除非使用这些方法创建出一份"即时“副本。

为了验证这些参数是否正确,必须创建ArgumentDefinition,并将每个可接受的参数类型与ArgumentSemanticAnalyzer相关联,代码如下所示:

   ArgumentSemanticAnalyzer analyzer = new ArgumentSemanticAnalyzer();
        analyzer.AddArgumentVerifier(new ArgumentDefinition(
            "output", "/output:[path to output]",
            "Specifies the location of the output file.",
            x => x.IsCompoundSwitch));

        analyzer.AddArgumentVerifier(new ArgumentDefinition(
            "trialMode", "/trialmode", "If this is specified it places the product into trial mode", 
            x => x.IsSimpleSwitch)); 
        
        analyzer.AddArgumentVerifier(new ArgumentDefinition(
            "DEBUGOUTPUT", "/debugoutput:[value1];[value2];[value3]",
            "A listing of the files the debug output " + "information will be written to", 
            x => x.IsComplexSwitch)); 
        
        analyzer.AddArgumentVerifier(new ArgumentDefinition("", "[literal value]", "A literal value", x => x.IsSimple));
每个ArgumentDefinition都包含4个部分:参数选项开关,显示参数语法的字符串、参数说明以及用于验证参数的验证谓词。这些信息可以用于验证参数,如下所示。

//检查开关与某个已知开关匹配但是检查格式是否正确的谓词为false的所有参数
        this.MalformedArguments = ( from argument in arguments    
                                    join argumentDefinition in argumentDefinitions       
                                    on argument.Switch.ToUpper() equals            
                                    argumentDefinition.ArgumentSwitch                
                                    where !argumentDefinition.Verify(argument)       
                                    select argument).ToList().AsReadOnly();

ArgumentFinition还允许为程序编写一个使用说明方法。


        public static void ShowUsage(ArgumentSemanticAnalyzer analyzer)
        {
            Console.WriteLine("Program.exe allows the following arguments:");
            foreach (ArgumentDefinition definition in analyzer.ArgumentDefinitions)
            {
                Console.WriteLine("\t{0}: ({1}){2}\tSyntax: {3}", definition.ArgumentSwitch,
                    definition.Description, Environment.NewLine, definition.Syntax);
            }
        }

 

为了获取参数的值以便使用他们,需要从解析过的参数中提取信息。对于解决方案示例,问哦们需要以下信息。

   // 设置命令行解析结果的容器
        string output = string.Empty;   
        bool trialmode = false;    
        IEnumerable debugOutput = null;  
        List literals = new List();
如何填充这些值?对于每个参数,都需要一个与之关联的动作,以确定如何从Argument实例获得值。每个动作就是一个谓词,这使得这种方式非常强大,因为你在这里可以使用任何谓词。下面的代码说明如何定义这些Argument动作并将其与ArgumentSemanticAnalyzer相关联。

//对于每一个解析出的参数,我们想要对其应用一个动作,因此将他们添加到分析器
        analyzer.AddArgumentAction("OUTPUT", x => { output = x.SubArguments[0]; });
        analyzer.AddArgumentAction("TRIALMODE", x => { trialmode = true; });
        analyzer.AddArgumentAction("DEBUGOUTPUT", x => { debugOutput = x.SubArguments; }); 
        analyzer.AddArgumentAction("", x => { literals.Add(x.Original); });

现在已经建立了所有的动作,就可以对ArgumentSemanticAnalyzer应用EvaluateArguments方法来获取值,代码如下所示。

// 检查参数并运行动作

 analyzer.EvaluateArguments(arguments);

如果在验证参数时使用LINQ来查询未识别的、格式错误的或者重复的实参(argument),其中任何一项都会导致形参(parameter)无效。


    public bool VerifyArguments(IEnumerable arguments)
    {
        // 没有任何参数进行验证,失败     
        if (!argumentDefinitions.Any())
            return false;

        // 确认是否存在任一未定义的参数       
        this.UnrecognizedArguments = (from argumentItem in arguments
                                      where !DefinedSwitches.Contains(argumentItem.Switch.ToUpper())
                                      select argumentItem).ToList().AsReadOnly();

        if (this.UnrecognizedArguments.Any())
            return false;

        // 检查开关与某个已知开关匹配但是检查格式是否正确的谓词为false的所有参数
        this.MalformedArguments = (from argument in arguments
                                   join argumentDefinition in argumentDefinitions
                                   on argument.Switch.ToUpper() equals argumentDefinition.ArgumentSwitch
                                   where !argumentDefinition.Verify(argument)
                                   select argument).ToList().AsReadOnly();

        if (this.MalformedArguments.Any())
            return false;

        // 将所有参数按照开关进行分组,统计每个组的数量,
        // 并选出包含超过一个元素的所有组,
        // 然后获得一个包含这些数据项的只读列表
        this.RepeatedArguments = (
            from argumentGroup in from argument in arguments
                                  where !argument.IsSimple
                                  group argument by argument.Switch.ToUpper()
            where argumentGroup.Count() > 1
            select argumentGroup).SelectMany(ag => ag).ToList().AsReadOnly();

        if (this.RepeatedArguments.Any())
            return false;

        return true;
    }
 

与LINQ出现之前通过多重嵌套循环、switch语句、IndexOf方法及其他机制实现同样功能的代码相比,上述使用LINQ的代码更加易于理解每一个验证阶段。每个查询都用问题领域的语言简介的指出了他在尝试执行什么任务。

参考:

MSDN文档中的”Main“和”命令行参数“主题。

你可能感兴趣的:(C#经典实例,类和泛型,c#)