在控制台应用程序中实现命令执行。
描述了一种使交互式控制台应用程序的简单而优雅的方法。
引言
有时,我们需要实现一个作为控制台应用程序的程序。通常,我们需要从程序中获得一些反馈,看看发生了什么,甚至更多 - 执行一个改变域状态的命令。
本文描述了简单的实现方法以及可能出现问题的解决方案。
请注意,这里提供的方法适用于“中等规模”的程序,大约有 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
类。抽象函数 getSyntaxDescription
、possibleStarts
、processCommand
迫使您不要忘记所有必需的属性,以实现良好的功能。
所有命令都注册在 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 - 首次发布