How to parse and execute a command line style string?

Well, this question will be a little long, but I hope it will be interesting for you. I have a specific question at the end, but I want to provide a lot of background and context so that you are on the same page as much as possible and can understand my purpose. If long questions are not your style, which is completely beautiful. However, if you want to contribute, I ask you to try to understand the vision as much as possible and be as possible on one page as possible.

Bounty Notice

If you just want to go to the bonus details section, scroll down and find the “Bounty Update” heading.

Background

I am creating a console-style application with ASP.NET MVC 3. It is located at http://www.u413.com if you need to get there and get an idea of ​​how it works. The concept itself is simple: get the command line from the client, check if the provided command exists, and if the arguments provided with the command are valid, execute the command, return the result set.

Internal workings

With this application, I decided to work a little. The most obvious solution for a terminal-style application is to create the world's largest IF statement. Run each command through an IF statement and call the corresponding functions from within. I did not like this idea. In the old version of the application, this was the way it acted, and it was a huge mess. Adding functionality to the application was ridiculously difficult.

After much deliberation, I decided to create a custom object called a command module. The idea is to create this command module with each request. The module object will contain all available commands as methods, and then the site will use reflection to check whether the command provided by the user matches the method name. The command module object is located behind the ICommandModule interface, shown below.

 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); } } 

The InvokeCommand() method is the only method in the command module that my MVC immediately learns about. It is this method that is responsible for the use of reflection and considers an instance of itself and finds all available methods of the command.

I use Ninject to inject dependencies. My MVC controller has a constructor dependency on ICommandModule . I built a custom Ninject index that builds this command module while resolving the ICommandModule dependency. There are 4 types of command modules that Ninject can create:

  • VisitorCommandModule
  • UserCommandModule
  • ModeratorCommandModule
  • AdministratorCommandModule

There is another BaseCommandModule class from which all other module classes are inherited. Real fast, here are the inheritance relationships:

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

I hope you see how this is built by now. Based on the membership status of the user (not logged in, regular user, moderator, etc.) Ninject will provide the correct command module only with those commands to which the user should have access.

This all works great. My dilemma comes when I parse the command line and figure out how to structure command methods in the command module object.

Question

How should the command line be processed and executed?

Current solution

I am currently breaking the command line (the line passed by the user containing the command and all arguments) in the MVC controller. Then I call the InvokeCommand() method for my nested ICommandModule and pass the string and List<string> args command.

Let's say I have the following command:

 TOPIC <id> [page #] [reply "reply"] 

This line defines the TOPIC command, which accepts the required identification number, optional page number, and optional response response command.

I am currently implementing such a command method (the attributes of the method above are for help menu information. The HELP command uses reflection to read all this and display an organized help menu):

  /// <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 reply to the topic. else _result.DisplayArray.Add("Subcommand Not Found"); else _result.DisplayArray.Add("Subcommand Not Found"); } 

My current implementation is a huge mess. I wanted to avoid giant IF statements, but all I did was trade one giant IF statement for all teams, for a ton of slightly smaller giant IF statements for each team and its arguments. This is not even half of it; I have simplified this command for this question. In a real implementation, there are a few more arguments that can be provided with this command, and that the IF statement is the ugliest thing I've ever seen. It is very redundant and not DRY at all (do not repeat yourself), since I have to display “Subcommand not found” in three different places.

Suffice it to say, I need a better solution than this.

Ideal implementation

Ideally, I would like to structure my command methods, for example, his:

 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. } } 

Then I would like to do this:

  • Get the command line from the client.
  • Enter the command line command directly in InvokeCommand() on the ICommandModule .
  • InvokeCommand() does a little parsing and reflection to select the correct command method with the correct arguments and calls this method, passing only the necessary arguments.

Perfect implementation dilemma

I am not sure how to structure this logic. I scratch my head all day. I am sorry that I did not have a second pair of eyes to help me with this (therefore, finally, resorting to the novel of the SO question). In what order should something happen?

Should I pull out the command, find all the methods with this command name, then skip all possible arguments and then skip the command line arguments? How to determine what is happening and what arguments fall into pairs. For example, if I go through my command line and find Reply "reply" , how do I match the response content with the response variable, meeting the number <ID> and passing it for the Id argument?

I am sure that now I am confused in you. I'm confusing me. Let me illustrate some examples of command lines that the user can pass:

 TOPIC 36 reply // Should prompt the user to enter reply text. TOPIC 36 reply "Hey guys what 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. 

How can I send 36 to Id parameter? How do I know to answer a guy: "Hi guys, what?" and convey "hey guys what?" How is the value for the method response argument?

To find out which method is overloaded for the call, I need to know how many arguments where they are supplied, so that I can match this number with an overload of the command method, which takes the same number of arguments. The problem is that "TOPIC 36 answers" Hey guys what? actually two arguments, not three as an answer, and "Hey guys ..." go together as one argument.

I am not opposed to inflating the InvokeCommand() method a little (or a lot), if that means that all complex legibility and reflection is handled there, and my command methods can remain beautiful, clean and easy to write.

I guess I'm really looking for something here. Anyone have creative ideas to solve this problem? This is really a big problem because IF IF arguments currently make it difficult to work with new commands for the application. Commands are one part of the application that I want to be very simple so that they can be easily expanded and updated. Here's what my TOPIC method looks like in my application:

  /// <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); } 

Isn't that funny? Each team has such a monster, and this is unacceptable, but I am at the end of my rope. It hurts me when my brain sits here, clinging for hours to looking at scripts in my head and how the code can handle it. I was very proud of setting up my command module, now if I could just be proud of implementing the command method ...

If I don’t get any really brilliant offers right after the battle, I plan to leave this question unanswered for some time so that I can get a lot of ideas.

While I'm not going to jump with my model (command modules) for the application, I am definitely open to suggestions. I'm mainly interested in suggestions related to parsing the command line and matching its arguments with the correct method overloads. I’m sure that any solution I go with will require a significant amount of redesign, so don’t be afraid to offer everything that you consider valuable, even if I don’t necessarily use your offer, it can put me on the right track.

Edit: long question longer

I just wanted to quickly make it clear that matching commands to method commands is not really something I'm worried about. I am mostly concerned about how to parse and arrange the command line. Currently, the InvokeCommand() method uses a very simple C # reflection to find the appropriate methods:

  /// <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; } 

So, as you can see, I'm not worried about how to find command methods, as this already works. I just figure out a good way to parse the command line, organize the arguments, and then use this information to select the correct method / overload the command using reflection.

BOUNTY UPDATE

I started a bounty on this. I am looking for a really good way to parse the command line in which I pass. I want the parser to identify several things:

  • Options. Define the options on the command line.
  • Name / value pairs. Define name / value pairs (for example, [page #] <- include the keyword "page" and the value "#")
  • Only value. Define only the value.

I want them to be identified using metadata when the first method of the command is overloaded. Here is a list of methods that I want to write, decorated with some metadata that the parser will use when reflecting it. I will give you these sample methods and some sample command lines that should map to this method. Then I will leave it to you a wonderful SO folk to come up with a good parser solution.

 // 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 (eg 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... } 

The parser should take the command line, use reflection to find all possible overloads of commands, use reflection to read the attributes of the argument, to help determine how to split the line into the correct list of arguments, and then call the correct command overloading the method, passing the correct arguments.

Let me know if you need clarification.

Any help is appreciated. Thanks everyone!

PS - If you are interested in looking at the source code of the application, it is open source and is located at http://u413.googlecode.com . Currently, I am the only developer, but if you are interested in the project, then leave me a line in accordance with the instructions on the project page.

+8
object methods c # asp.net-mvc
source share
4 answers

Take a look at Mono . Options . It is currently part of the Mono framework, but can be downloaded and used as a separate library.

For some reason, the site on which it was hosted does not work, however you can capture 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); 
+5
source share

The solution that I usually use looks something like this. Please ignore my syntax errors ... it has been several months since I used C #. Basically replace the if / else / key with a search for System.Collections.Generic.Dictionary<string, /* Blah Blah */> and a virtual function call.

 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); } } 
+4
source share

Remember that you can put attributes in parameters that can make reading more understandable, for example.

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

Here I see two different problems:

Defining a method name (like string ) in a command module

You can use Dictionary to match a string to a method, as in Billy's answer. If you prefer only the method over the command object, you can map the string to the method directly in 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"); } 

Parsing command line arguments

People began to scratch their heads, solving this problem in the early years.

But now we have a library that specifically deals with this problem. See http://tirania.org/blog/archive/2008/Oct-14.html or NDesk.Options . It should be simpler and can handle some cases of errors than with a new one.

+2
source share

All Articles