65.9K
CodeProject 正在变化。 阅读更多。
Home

在控制台应用程序中实现命令执行。

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.83/5 (6投票s)

2012年10月18日

CPOL

5分钟阅读

viewsIcon

30080

downloadIcon

370

描述了一种使交互式控制台应用程序的简单而优雅的方法。

引言

有时,我们需要实现一个作为控制台应用程序的程序。通常,我们需要从程序中获得一些反馈,看看发生了什么,甚至更多 - 执行一个改变域状态的命令。

本文描述了简单的实现方法以及可能出现问题的解决方案。

请注意,这里提供的方法适用于“中等规模”的程序,大约有 20 到 50 个可能的命令,因此不需要复杂的解析。如果您正在编写一个包含 100 多个命令、具有自动完成功能的命令行界面,那么这里展示的方法可能只是一个起点。

背景

当一个 .NET 程序集打算在 Linux 上运行,并且可能存在 UI 或不存在 UI(例如,执行了 init(3),或者根本没有安装 UI),最好的解决方案是创建一个控制台应用程序并使用 Mono 启动它。您可能有其他原因将程序实现为控制台应用程序。

如果程序足够复杂,您会想实时查看发生了什么。例如:“显示这个队列,它是否太满了?”、“显示通道的状态,连接是否断开了?”等等。时间过去了 - 您需要执行一些在域中进行搜索,甚至更改域的操作。这样您就可以获得交互式控制台应用程序。

简单的实现方法

这是实现一个假设的交互式控制台应用程序的代码。示例将包含 3 到 5 个命令 - 只是为了保持简单 - 但请记住,我们正在考虑命令数量超过 20 的情况。

class Program
{
    static void printCounters1()
    {
        Console.WriteLine("Printing first counters");
    }

    static void printCounters2()
    {
        Console.WriteLine("Printing second counters");
    }

    static void printHelp()
    {
        Console.WriteLine("Supported: stop, print first counters (pfc), 
                           print second counters (psc)");
    }

    static void Main(string[] args)
    {
        Dictionary<string, string> domain = new Dictionary<string, string>
                            {{"first", "value 1"}, {"second", "value 2"}};
        bool shouldfinish = false;
        while (shouldfinish == false)
        {
            string command = Console.ReadLine();
            switch(command)
            {
                case "stop":
                    {
                        Console.WriteLine
                        ("Do you really want to stop? (type 'yes' to stop)");
                        string acceptString = Console.ReadLine();
                        if (acceptString == "yes")
                        {
                            shouldfinish = true;
                            continue;
                    }
                    printHelp();
                    break;
                    }
                case "print first counters":
                case "pfc":
                    {
                        printCounters1();
                        break;
                    }
                case "print second counters":
                case "psc":
                    {
                        printCounters2();
                        break;
                    }
                    
                default:
                    {
                        if (command.StartsWith("add"))
                        {
                            Console.WriteLine("Parsing plus executing the command 
                                               \" "+ command + "\"");
                            break;
                        }
                        printHelp();
                        break;
                    }
            }
        }
        Console.WriteLine("Disposing domain and stopping");
        Console.ReadKey();
    }
}

没什么难的。如果用户输入“pfc”,他将获得 printCounters1() 函数的执行,并且屏幕上会显示一些有用的信息。

如果命令未输入或输入错误,将显示帮助字符串。考虑 default 子句:我们在这里执行了一个命令,它应该尝试将一个对象添加到“域” - 字典中。具体的解析和字典交互并未实现,以免使代码变得更难。

我们遇到的问题

  • 添加新命令也需要将其描述添加到帮助字符串中
  • 混乱的 switch 语句
  • 一些不容易注意到的问题

简单来说,这种方法不够灵活。

建议的解决方案

让我们先考虑解决方案图

如果您想做一些简单的事情,例如显示系统的某一部分,并且命令不需要任何解析 - 您可以始终使用 SimpleCommand 类。但是要实现复杂的事情,您应该为每个问题实现一个类。例如示例中的 AddCommand 类。抽象函数 getSyntaxDescriptionpossibleStartsprocessCommand 迫使您不要忘记所有必需的属性,以实现良好的功能。

所有命令都注册在 ConsoleCommandHolder 实例中。现在看一下 Main 代码

class Program
{
    static void printCounters1()
    {
        Console.WriteLine("Printing first counters");
    }

    static void printCounters2()
    {
        Console.WriteLine("Printing second counters");
    }
 
    static void printHelp()
    {
        Console.WriteLine("Supported: stop, print first counters (pfc), 
                           print second counters (psc)");
    }

    static void Main(string[] args)
    {
        Dictionary<string, string> domain = new Dictionary<string, 
          string> { { "first", "value 1" }, { "second", "value 2" } };
        bool shouldfinish = false;
        ConsoleCommandHolder holder = new ConsoleCommandHolder();
        holder.AddRange( new ConsoleCommand[]{ new SimpleCommand
                       ("Stop application", new string[]{"stop"}, 
            (x)=>
                {
                    Console.WriteLine("Do you really want to stop? (type 'yes' to stop)");
                    string acceptString = Console.ReadLine();
                    if (acceptString == "yes")
                    {
                        shouldfinish = true;
                    }

                }), 
                new SimpleCommand("Print first command", 
                new string[]{"print first command", "pfc"},(x)=>printCounters1() ),
                new SimpleCommand("Print second command", 
                new string[]{"print second command", "psc"},(x)=>printCounters2() ), 
                new SimpleCommand("Print dictionary", new string[]{"print dictionary", "pd"},
                    (x)=>
                        {
                            foreach (var next in domain)
                                Console.WriteLine("{0} -> {1}", next.Key, next.Value);
                        }), 
                new AddCommand(domain), 
        });
        while (shouldfinish == false)
        {
            string command = Console.ReadLine();
            ConsoleCommand toExecute = holder.Find(command);
            if(toExecute != null)
                toExecute.Do(command);
            else Console.WriteLine(holder);
        }
        Console.WriteLine("Disposing domain and stopping");
        Console.ReadKey();
    }
}

每个命令都会“记住”它的前缀,所以 ConsoleCommandHolder 的任务是找到匹配的命令,该命令的前缀与输入的字符串相同。在主循环中,我们调用 holder.Find,如下所示

public ConsoleCommand Find(string command)
{
     return unique.FirstOrDefault(x => x.Prefixes.Any(command.StartsWith));
}

其中 unique 是命令的集合。

下面是程序的输出(输入:“pd”、“add 2 3”、“l”、“psc”、“stop”、“yes”)

pd
first -> value 1
second -> value 2
add 2 3
OK
pd
first -> value 1
second -> value 2
2 -> 3
l
 
Stop application        Syntax: <stop>
Print first command     Syntax: <print first command><pfc>
Print second command    Syntax: <print second command><psc>
Print dictionary        Syntax: <print dictionary><pd>
Add string  to dictionary       Syntax: <add 'v1' 'v2'>
psc
Printing second counters
stop
Do you really want to stop? (type 'yes' to stop)
yes
Disposing domain and stopping

有人可能会说,程序仍然有点混乱。对一些人来说,读一个普通的 switch 语句比读一堆构造函数调用和 lambda 表达式的代码更容易。但这只是第一印象。当命令数量增加时,这种印象就会消失。我们获得的收益

  • 正如我刚才提到的,代码变得不那么混乱了。更容易阅读 ConsoleCommand 类内部的细节,而不是在 Main 方法中。
  • 帮助更容易维护,添加新命令时您不会忘记提供帮助。
  • 我们现在可以以有趣的方式开发程序
    • IsForbidden 字段添加到 ConsoleCommand 类中。通过(再次通过一个单独的命令)提供登录过程,我们可以允许/禁止特定类型的命令。可以很容易地使被禁止的命令不显示在帮助信息中。
    • 在每次命令执行时添加带有时间的日志条目(您只需要更改基类,而不是所有子类)。
    • 开发可能执行相反操作的命令类型。例如,“add...”向域添加数据,而“no add...”则删除该对象。
    • 使用 N 个 ConsoleCommandHolder 对象 - 这些对象填充了不同的 ConsoleCommand 对象集。根据模式,您向用户提供不同的命令集。

依此类推。

请注意,所有这些更改都会以一种不那么混乱的方式影响 ConsoleCommand 类。只需想象一下,当您已经有一个包含 20 个“show”命令和 20 个需要解析的命令的 switch 语句时,如何实现这些逻辑。

关注点

  • 描述这种方法让我想到了 Command 模式。实现方式与经典的实现方式不同:在经典的实现方式中,每个命令都是一个单独的对象。在我们的示例中,只有每个命令类型是一个单独的对象。
  • 如果您有 2-5 个命令,那么简单的实现方法可能更适合您。
  • 我想知道是否有标准的自动完成功能开发方法。例如,如果我按下 '?' - 我将获得所有可能的输入词,或者如果我按下 Tab - 我正在输入的词会自动完成。

历史

  • 2012.10.16 - 在编辑 switch 语句时获得灵感并开始撰写文章
  • 2012.10.18 - 首次发布
© . All rights reserved.