【问题标题】:How to parse and execute a command-line style string?如何解析和执行命令行样式的字符串?
【发布时间】:2011-09-08 06:51:07
【问题描述】:

最后我有一个具体问题,但我想提供大量背景和上下文,以便读者了解我的目标。

背景

我正在使用 ASP.NET MVC 3 构建一个控制台样式的应用程序。这个概念本身很简单:从客户端接收命令字符串,检查提供的命令是否存在,以及命令提供的参数是否有效,执行命令,返回一组结果。

内部工作

通过这个应用程序,我决定有点创意。对终端式应用程序最明显的解决方案是构建世界上最大的 IF 语句。通过 IF 语句运行每个命令并从内部调用适当的函数。我不喜欢这个主意。在旧版本的应用程序中,这就是它的运行方式,而且是一团糟。向应用程序添加功能非常困难。

经过深思熟虑,我决定构建一个称为命令模块的自定义对象。这个想法是为每个请求构建这个命令模块。模块对象将包含所有可用命令作为方法,然后站点将使用反射来检查用户提供的命令是否与方法名称匹配。命令模块对象位于名为ICommandModule 的接口后面,如下所示。

namespace U413.Business.Interfaces
{
    /// <summary>
    /// All command modules must ultimately inherit from ICommandModule.
    /// </summary>
    public interface ICommandModule
    {
        /// <summary>
        /// The method that will locate and execute a given command and pass in all relevant arguments.
        /// </summary>
        /// <param name="command">The command to locate and execute.</param>
        /// <param name="args">A list of relevant arguments.</param>
        /// <param name="commandContext">The current command context.</param>
        /// <param name="controller">The current controller.</param>
        /// <returns>A result object to be passed back tot he client.</returns>
        object InvokeCommand(string command, List<string> args, CommandContext commandContext, Controller controller);
    }
}

InvokeCommand() 方法是我的 MVC 控制器立即知道的命令模块上的唯一方法。然后这个方法负责使用反射并查看自身的实例并定位所有可用的命令方法。

我使用 Ninject 进行依赖注入。我的 MVC 控制器对 ICommandModule 有一个构造函数依赖。我构建了一个自定义的 Ninject 提供程序,它在解析 ICommandModule 依赖项时构建了这个命令模块。 Ninject 可以构建 4 种类型的命令模块:

  1. VisitorCommandModule
  2. UserCommandModule
  3. ModeratorCommandModule
  4. AdministratorCommandModule

还有一个类BaseCommandModule,所有其他模块类都继承自该类。很快,这里是继承关系:

  • BaseCommandModule : ICommandModule
  • VisitorCommandModule : BaseCommandModule
  • UserCommandModule : BaseCommandModule
  • ModeratorCommandModule : UserCommandModule
  • AdministratorCommandModule : ModeratorCommandModule

希望您现在可以看到它是如何构建的。根据用户的会员状态(未登录、普通用户、版主等),Ninject 将提供适当的命令模块,仅包含用户应该有权访问的命令方法。

所有这些都很好用。当我解析命令字符串并弄清楚如何在命令模块对象上构造命令方法时,我的困境就出现了。

问题

命令字符串应该如何解析和执行?

当前解决方案

目前我在 MVC 控制器中分解命令字符串(用户传入的包含命令和所有参数的字符串)。然后我在注入的ICommandModule 上调用InvokeCommand() 方法,并传入string 命令和List&lt;string&gt; 参数。

假设我有以下命令:

TOPIC <id> [page #] [reply “reply”]

这一行定义了 TOPIC 命令,它接受一个必需的 ID 号、一个可选的页码和一个带有回复值的可选回复命令。

我目前是这样实现命令方法的(方法上面的属性是帮助菜单信息。HELP命令使用反射来读取所有这些并显示一个有组织的帮助菜单):

    /// <summary>
    /// Shows a topic and all replies to that topic.
    /// </summary>
    /// <param name="args">A string list of user-supplied arguments.</param>
    [CommandInfo("Displays a topic and its replies.")]
    [CommandArgInfo(Name="ID", Description="Specify topic ID to display the topic and all associated replies.", RequiredArgument=true)]
    [CommandArgInfo(Name="REPLY \"reply\"", Description="Subcommands can be used to navigate pages, reply to the topic, edit topic or a reply, or delete topic or a reply.", RequiredArgument=false)]
    public void TOPIC(List<string> args)
    {
        if ((args.Count == 1) && (args[0].IsInt64()))
            TOPIC_Execute(args); // View the topic.
        else if ((args.Count == 2) && (args[0].IsInt64()))
            if (args[1].ToLower() == "reply")
                TOPIC_ReplyPrompt(args); // Prompt user to input reply content.
            else
                _result.DisplayArray.Add("Subcommand Not Found");
        else if ((args.Count >= 3) && (args[0].IsInt64()))
            if (args[1].ToLower() == "reply")
                TOPIC_ReplyExecute(args); // Post user's reply to the topic.
            else
                _result.DisplayArray.Add("Subcommand Not Found");
        else
            _result.DisplayArray.Add("Subcommand Not Found");
    }

我目前的实现是一团糟。我想避免使用巨大的 IF 语句,但我所做的只是为所有命令交换一个巨大的 IF 语句,为每个命令及其参数换成大量稍微不那么巨大的 IF 语句。这甚至不是它的一半;我为这个问题简化了这个命令。在实际实现中,这个命令可以提供更多的参数,而 IF 语句是我见过的最丑陋的东西。这是非常多余的,而且根本不是 DRY(不要重复自己),因为我必须在三个不同的地方显示“未找到子命令”。

我只想说,我需要一个比这更好的解决方案。

理想的实现

理想情况下,我希望我的命令方法结构类似于他的:

public void TOPIC(int Id, int? page)
{
    // Display topic to user, at specific page number if supplied.
}

public void TOPIC(int Id, string reply)
{
    if (reply == null)
    {
        // prompt user for reply text.
    }
    else
    {
        // Add reply to topic.
    }
}

那么我很乐意这样做:

  1. 从客户端接收命令字符串。
  2. 将命令字符串直接传递到InvokeCommand() on ICommandModule
  3. InvokeCommand() 执行一些神奇的解析和反射,以选择具有正确参数的正确命令方法并调用该方法,仅传入必要的参数。

理想实现的困境

我不确定如何构建这个逻辑。这几天我一直在挠头。我希望我有第二双眼睛来帮助我解决这个问题(因此最终求助于一本关于 SO 问题的小说)。事情应该按什么顺序发生?

我是否应该拉出命令,找到具有该命令名称的所有方法,然后遍历所有可能的参数,然后遍历我的命令字符串的参数?我如何确定成对出现的内容和参数。例如,如果我遍历我的命令字符串并找到Reply "reply",我如何将回复内容与回复变量配对,同时遇到&lt;ID&gt; 数字并将其提供给Id 参数?

我确定我现在把你搞糊涂了。让我用一些用户可能传入的命令字符串示例来说明:

TOPIC 36 reply // Should prompt the user to enter reply text.
TOPIC 36 reply "Hey what's up?" // Should post a reply to the topic.
TOPIC 36 // Should display page 1 of the topic.
TOPIC 36 page 4 // Should display page 4 of the topic.

我如何知道将 36 发送到 Id 参数?我怎么知道将回复与“嘿,怎么了?”配对?并通过“嘿,怎么了?”作为方法上的回复参数的值?

为了知道调用哪个方法重载,我需要知道提供了多少参数,以便我可以将该数字与采用相同数量参数的命令方法的重载相匹配。问题是,`TOPIC 36 回复“嘿,怎么了?”实际上是两个论点,而不是三个作为回复和“嘿...”一起作为一个论点。

我不介意使InvokeCommand() 方法膨胀一点(或很多),只要这意味着所有复杂的解析和反射废话都在那里处理并且我的命令方法可以保持良好、干净且易于编写.

我想我真的只是在这里寻找一些见解。有没有人有任何创造性的想法来解决这个问题?这确实是一个大问题,因为参数 IF 语句当前使得为应用程序编写新命令变得非常复杂。这些命令是我希望超级简单的应用程序的一部分,以便可以轻松扩展和更新它们。以下是我的应用中实际的 TOPIC 命令方法:

    /// <summary>
    /// Shows a topic and all replies to that topic.
    /// </summary>
    /// <param name="args">A string list of user-supplied arguments.</param>
    [CommandInfo("Displays a topic and its replies.")]
    [CommandArgInfo("ID", "Specify topic ID to display the topic and all associated replies.", true, 0)]
    [CommandArgInfo("Page#/REPLY/EDIT/DELETE [Reply ID]", "Subcommands can be used to navigate pages, reply to the topic, edit topic or a reply, or delete topic or a reply.", false, 1)]
    public void TOPIC(List<string> args)
    {
        if ((args.Count == 1) && (args[0].IsLong()))
            TOPIC_Execute(args);
        else if ((args.Count == 2) && (args[0].IsLong()))
            if (args[1].ToLower() == "reply" || args[1].ToLower() == "modreply")
                TOPIC_ReplyPrompt(args);
            else if (args[1].ToLower() == "edit")
                TOPIC_EditPrompt(args);
            else if (args[1].ToLower() == "delete")
                TOPIC_DeletePrompt(args);
            else
                TOPIC_Execute(args);
        else if ((args.Count == 3) && (args[0].IsLong()))
            if ((args[1].ToLower() == "edit") && (args[2].IsLong()))
                TOPIC_EditReplyPrompt(args);
            else if ((args[1].ToLower() == "delete") && (args[2].IsLong()))
                TOPIC_DeleteReply(args);
            else if (args[1].ToLower() == "edit")
                TOPIC_EditExecute(args);
            else if (args[1].ToLower() == "reply" || args[1].ToLower() == "modreply")
                TOPIC_ReplyExecute(args);
            else if (args[1].ToLower() == "delete")
                TOPIC_DeleteExecute(args);
            else
                _result.DisplayArray.Add(DisplayObject.InvalidArguments);
        else if ((args.Count >= 3) && (args[0].IsLong()))
            if (args[1].ToLower() == "reply" || args[1].ToLower() == "modreply")
                TOPIC_ReplyExecute(args);
            else if ((args[1].ToLower() == "edit") && (args[2].IsLong()))
                TOPIC_EditReplyExecute(args);
            else if (args[1].ToLower() == "edit")
                TOPIC_EditExecute(args);
            else
                _result.DisplayArray.Add(DisplayObject.InvalidArguments);
        else
            _result.DisplayArray.Add(DisplayObject.InvalidArguments);
    }

这不是很可笑吗?每个命令都有这样的怪物,这是不可接受的。我只是在脑海中回顾一下场景以及代码如何处理它。我为我的命令模块设置感到非常自豪,现在如果我可以为命令方法的实现感到自豪。

虽然我不打算将我的整个模型(命令模块)用于该应用程序,但我绝对愿意接受建议。我最感兴趣的是与解析命令行字符串并将其参数映射到正确的方法重载相关的建议。我确信我采用的任何解决方案都需要进行大量的重新设计,所以不要害怕提出任何你认为有价值的建议;即使我不一定会使用您的建议,它也可能使我走上正轨。

进一步说明

我只是想快速澄清一下,命令到命令方法的映射并不是我真正担心的事情。我最关心的是如何解析和组织命令行字符串。目前InvokeCommand() 方法使用一些非常简单的 C# 反射来找到合适的方法:

    /// <summary>
    /// Invokes the specified command method and passes it a list of user-supplied arguments.
    /// </summary>
    /// <param name="command">The name of the command to be executed.</param>
    /// <param name="args">A string list of user-supplied arguments.</param>
    /// <param name="commandContext">The current command context.</param>
    /// <param name="controller">The current controller.</param>
    /// <returns>The modified result object to be sent to the client.</returns>
    public object InvokeCommand(string command, List<string> args, CommandContext commandContext, Controller controller)
    {
        _result.CurrentContext = commandContext;
        _controller = controller;

        MethodInfo commandModuleMethods = this.GetType().GetMethod(command.ToUpper());
        if (commandModuleMethods != null)
        {
            commandModuleMethods.Invoke(this, new object[] { args });
            return _result;
        }
        else
            return null;
    }

如您所见,我并不担心如何找到命令方法,因为它已经在工作了。我只是在思考一种解析命令字符串、组织参数、然后使用该信息通过反射选择正确的命令方法/重载的好方法。

最终设计目标

我正在寻找一种非常好的方法来解析我传入的命令字符串。我希望解析器识别几件事:

  • 选项。识别命令字符串中的选项。
  • 名称/值对。识别名称/值对(例如 [page #]
  • 仅值。仅识别价值。

我希望通过第一个命令方法重载的元数据来识别这些。这是我要编写的示例方法列表,其中装饰有解析器在进行反射时要使用的一些元数据。我会给你这些方法示例和一些应该映射到该方法的示例命令字符串。这些信息应该有助于我制定一个好的解析器解决方案。

// Metadata to be used by the HELP command when displaying HELP menu, and by the
// command string parser when deciding what types of arguments to look for in the
// string. I want to place these above the first overload of a command method.
// I don't want to do an attribute on each argument as some arguments get passed
// into multiple overloads, so instead the attribute just has a name property
// that is set to the name of the argument. Same name the user should type as well
// when supplying a name/value pair argument (e.g. Page 3).

[CommandInfo("Test command tests things.")]
[ArgInfo(
    Name="ID",
    Description="The ID of the topic.",
    ArgType=ArgType.ValueOnly,
    Optional=false
    )]
[ArgInfo(
    Name="PAGE",
    Description="The page number of the topic.",
    ArgType=ArgType.NameValuePair,
    Optional=true
    )]
[ArgInfo(
    Name="REPLY",
    Description="Context shortcut to execute a reply.",
    ArgType=ArgType.NameValuePair,
    Optional=true
    )]
[ArgInfo(
    Name="OPTIONS",
    Description="One or more options.",
    ArgType=ArgType.MultiOption,
    Optional=true
    PossibleValues=
    {
        { "-S", "Sort by page" },
        { "-R", "Refresh page" },
        { "-F", "Follow topic." }
    }
    )]
[ArgInfo(
    Name="SUBCOMMAND",
    Description="One of several possible subcommands.",
    ArgType=ArgType.SingleOption,
    Optional=true
    PossibleValues=
    {
        { "NEXT", "Advance current page by one." },
        { "PREV", "Go back a page." },
        { "FIRST", "Go to first page." },
            { "LAST", "Go to last page." }
    }
    )]
public void TOPIC(int id)
{
    // Example Command String: "TOPIC 13"
}

public void TOPIC(int id, int page)
{
    // Example Command String: "TOPIC 13 page 2"
}

public void TOPIC(int id, string reply)
{
    // Example Command String: TOPIC 13 reply "reply"

    // Just a shortcut argument to another command.
    // Executes actual reply command.
    REPLY(id, reply, { "-T" });
}

public void TOPIC(int id, List<string> options)
{
    // options collection should contain a list of supplied options

    Example Command String: "TOPIC 13 -S",
                            "TOPIC 13 -S -R",
                            "TOPIC 13 -R -S -F",
                            etc...
}

解析器必须接受一个命令字符串,使用反射来查找所有可能的命令方法重载,使用反射来读取参数属性以帮助确定如何将字符串划分为适当的参数列表,然后调用适当的命令方法重载,传入正确的参数。

【问题讨论】:

  • 幸运的是没有“Long Winded”关闭选项。 ;)
  • @Will 是的,幸运的是。我在这个问题上花了很多时间。如果它像那样关闭,那就糟透了。
  • @Chevex - 哈哈。这个问题可能是主观的和有争议的——让我想想……不,我不能那样做! :)
  • 哈哈,希望不会!我试图尽可能将其缩小到一个特定的问题。这很难,因为我认为我会盲目地看着我的代码并认为自己要死 LOL:P
  • 您是否考虑过使用 Apache CLI 解析器? commons.apache.org/clisourceforge.net/projects/dotnetcli有一个c#端口。

标签: c# .net asp.net-mvc object methods


【解决方案1】:

看看 Mono.Options。它目前是 Mono 框架的一部分,但可以作为单个库下载和使用。

您可以obtain it here,也可以grab the current version used in Mono as a single file

string data = null;
bool help   = false;
int verbose = 0;
var p = new OptionSet () {
    { "file=",      v => data = v },
    { "v|verbose",  v => { ++verbose } },
    { "h|?|help",   v => help = v != null },
};
List<string> extra = p.Parse (args);

【讨论】:

  • 经过大量学习后,我意识到这就是我一直以来的要求。谢谢你。 Mono.Options (Ndesk.Options) 非常完美,帮助我解决了我的问题。我希望我可以切换并将赏金奖励给你。
  • 很高兴听到这个消息。有时轮子值得重新发明,但您似乎并非如此。
【解决方案2】:

请注意,您可以将属性放在可能使内容更具可读性的参数上,例如

public void TOPIC (
    [ArgInfo("Specify topic ID...")] int Id, 
    [ArgInfo("Specify topic page...")] int? page) 
{
    ...
}

【讨论】:

  • 你给了我一个想法。谢谢你。能够提供带有参数的元数据将使解析器在弄清楚要做什么时占得先机。
【解决方案3】:

我可以在这里看到两个不同的问题:

将方法名称(如string)解析为命令模块

您可以使用Dictionary 将字符串映射到方法,就像在比利的回答中一样。如果您只喜欢方法而不是命令对象,您可以直接在 C# 中将字符串映射到方法。

    static Dictionary<string, Action<List<string>>> commandMapper;

    static void Main(string[] args)
    {
        InitMapper();

        Invoke("TOPIC", new string[]{"1","2","3"}.ToList());
        Invoke("Topic", new string[] { "1", "2", "3" }.ToList());
        Invoke("Browse", new string[] { "1", "2", "3" }.ToList());
        Invoke("BadCommand", new string[] { "1", "2", "3" }.ToList());
    }

    private static void Invoke(string command, List<string> args)
    {
        command = command.ToLower();
        if (commandMapper.ContainsKey(command))
        {
            // Execute the method
            commandMapper[command](args);
        }
        else
        {
            // Command not found
            Console.WriteLine("{0} : Command not found!", command);
        }
    }

    private static void InitMapper()
    {
        // Add more command to the mapper here as you have more
        commandMapper = new Dictionary<string, Action<List<string>>>();
        commandMapper.Add("topic", Topic);
        commandMapper.Add("browse", Browse);
    }

    static void Topic(List<string> args)
    {
        // ..
        Console.WriteLine("Executing Topic");
    }

    static void Browse(List<string> args)
    {
        // ..
        Console.WriteLine("Executing Browse");
    }

命令行参数解析

早期人们一直在摸索解决这个问题..

但是现在我们有专门处理这个问题的库。请参阅http://tirania.org/blog/archive/2008/Oct-14.htmlNDesk.Options。这应该比推出新的更容易,并且可以处理一些陷阱案例。

【讨论】:

  • 我比较关心命令行参数的解析。实际上,我已经使用 C# 反射很好地解决了方法名称。将方法和方法参数解析为字符串并不是我担心的事情,我可以做到这一点。我只是在摸索如何解析命令行字符串并将其分开。我一定会检查你的链接。非常感谢!
【解决方案4】:

我通常使用的解决方案看起来像这样。请忽略我的语法错误......自从我使用 C# 以来已经有几个月了。基本上,将 if/else/switch 替换为 System.Collections.Generic.Dictionary&lt;string, /* Blah Blah */&gt; 查找和虚函数调用。

interface ICommand
{
    string Name { get; }
    void Invoke();
}

//Example commands
class Edit : ICommand
{
    string Name { get { return "edit"; } }
    void Invoke()
    {
        //Do whatever you need to do for the edit command
    }
}

class Delete : ICommand
{
    string Name { get { return "delete"; } }
    void Invoke()
    {
        //Do whatever you need to do for the delete command
    }
}

class CommandParser
{
    private Dictionary<string, ICommand> commands = new ...;

    public void AddCommand(ICommand cmd)
    {
        commands.Insert(cmd.Name, cmd);
    }

    public void Parse(string commandLine)
    {
        string[] args = SplitIntoArguments(commandLine); //Write that method yourself :)
        foreach(string arg in args)
        {
            ICommand cmd = commands.Find(arg);
            if (!cmd)
            {
                throw new SyntaxError(String.Format("{0} is not a valid command.", arg));
            }
            cmd.Invoke();
        }
    }
}

class CommandParserXyz : CommandParser
{
    CommandParserXyz()
    {
        AddCommand(new Edit);
        AddCommand(new Delete);
    }
}

【讨论】:

  • 非常有趣。这个模型与我的命令模块模型完全不同。你的命令是对象,而我的只是方法。这样做的想法是,为了向我的应用程序添加一个新命令,只需编写一个与所需命令同名的方法,仅此而已。但是,您的模型确实给了我一些思考。感谢您的建议!
  • @Chevex:这里的基本思想是尽可能遵循 SRP。将这些方法拆分为单独的类意味着这些类中的每一个都做得更少(这很好)。真的,我不能把这归功于这一点——我只是做了一个很大的 if/else/switch 并在上面使用了Replace Conditional with Polymorphism
  • 我喜欢。不确定我是否准备好与我的模型一起跳船,但这绝对是值得考虑的事情。再次感谢:)
  • 只是想知道如何将相同的键添加到字典中;)。鉴于命令 Delete 接受名称字符串或 id int。
猜你喜欢
  • 1970-01-01
  • 2016-03-30
  • 2019-09-04
  • 2016-07-30
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-05-02
相关资源
最近更新 更多