How to parse and execute a command-line style string?
I have a specific question at the end but I want to provide plenty of background and context so that readers can understand my objective.
Background
I am building a console-style application with ASP.NET MVC 3. The concept itself is simple: receive command strings from the client, check if the command supplied exists and if the arguments provided with the command are valid, execute the command, return a set of results.
Inner-workings
With this application I decided to get a little creative. The most obvious solution to a terminal-style application is to build the world's largest IF statement. Run every command through the IF statement and call the appropriate functions from within. I did not like this idea. In an older version of the application this was how it operated and it was a huge mess. Adding functionality to the application was ridiculously difficult.
After much thought I decided to build a custom object called a command module. The idea is to build this command module with each request. The module object would contain all available commands as methods and the site would then use reflection to check if a command supplied by the user matches a method name. The command module object sits behind an interface called ICommandModule
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 on the command module that my MVC controller is immediately aware of. It is then this method's responsibility to use reflection and look at the instance of itself and locate all available command methods.
I use Ninject for dependency injection. My MVC controller has a constructor dependency on ICommandModule
. I built a custom Ninject provder that builds this command module 开发者_JAVA技巧when resolving the ICommandModule
dependency. There are 4 types of command modules Ninject can build:
VisitorCommandModule
UserCommandModule
ModeratorCommandModule
AdministratorCommandModule
There is one more class BaseCommandModule
which all other module classes inherit from. Real quickly, here are the inheritance relationships:
BaseCommandModule : ICommandModule
VisitorCommandModule : BaseCommandModule
UserCommandModule : BaseCommandModule
ModeratorCommandModule : UserCommandModule
AdministratorCommandModule : ModeratorCommandModule
Hopefully you can see how this is constructed by now. Based on the user's membership status (not logged in, regular user, moderator, etc) Ninject will provide the proper command module with only the command methods the user should have access to.
All of this works great. My dilemma comes in when I am parsing the command string and figuring out how to structure the command methods on the command module object.
The Question
How should the command string be parsed and executed?
Current Solution
Currently I break up the command string (the string passed in by the user containing the command and all arguments) in the MVC controller. I then call the InvokeCommand()
method on my injected ICommandModule
and I pass in a string
command and a List<string>
args.
Let's say I have the following command:
TOPIC <id> [page #] [reply “reply”]
This line defines the TOPIC command accepting a required ID number, an optional page number, and an optional reply command with a reply value.
I currently implement the command method like this (The attributes above the method are for help menu information. The HELP command uses reflection to read all these 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's 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 the commands, for a ton of slightly less giant IF statements for every command and its arguments. This isn't even the half of it; I simplified this command for this question. In actual implementation there are quite a few more arguments that can be provided with this command and that IF statement is the ugliest thing I have ever seen. It's very redundant and not at all DRY (don't repeat yourself) as I have to display "Subcommand Not Found" in three different places.
Suffice it to say, I need a better solution than this.
The Ideal Implementation
Ideally I would love to structure my command methods something like 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'd love to do this:
- Receive command string from client.
- Pass command string directly into
InvokeCommand()
onICommandModule
. InvokeCommand()
performs some magic parsing and reflection to choose the right command method with the right arguments and invokes that method, passing in only the necessary arguments.
The Dilemma with the Ideal Implementation
I'm not sure how to structure this logic. I've been scratching my head for days. I wish I had a second pair of eyes to help me out on this (hence finally resorting to a novel of an SO question). In what order should things happen?
Should I pull out the command, find all methods with that command name, then loop through all the possible arguments, then loop through my command string's arguments? How do I determine what goes where and what arguments go in pairs. For instance, if I loop through my command string and find Reply "reply"
how do I pair the reply content with the reply variable, while encountering <ID>
number and supplying it for the Id
argument?
I'm sure I'm confusing the hell out of you now. Let me illustrate with some examples of command strings the user might pass in:
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.
How do I know to send 36 to the Id
parameter? How do I know to pair reply with "Hey what's up?" and pass "Hey what's up?" as the value for the reply argument on the method?
In order to know which method overload to call I need to know how many arguments where supplied so that I can match that number to the overload of the command method that takes that same number of arguments. The problem is, `TOPIC 36 reply "Hey what's up?" is actually two arguments, not three as reply and "Hey..." go together as one argument.
I don't mind bloating the InvokeCommand()
method a little (or a lot) as long as it means that all the complex parsing and reflection nonsense is handled there and my command methods can remain nice and clean and easy to write.
I guess I'm really just looking for some insight here. Does anyone have any creative ideas to solve this problem? It really is a big issue because the argument IF statements are currently making it very complicated to write new commands for the application. The commands are the one part of the application that I want to be super simple so that they can be easily extended and updated. Here is what the actual TOPIC command method looks like in my app:
/// <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 ridiculous? Every command has a monster like this and it's unacceptable. I am just going over scenarios in my head and how code might handle it. I was pretty proud of my command module setup, now if I could just be proud of the command method implementation.
While I'm not looking to jump ship with my entire model (command modules) for the application, I am definitely open to suggestions. I'm mostly interested in suggestions related to parsing the command line string and mapping its arguments to the right method overloads. I'm sure whatever solution I go with will require a fair amount of redesign so don't be afraid to suggest anything you think is valuable; even if I don't necessarily use your suggestion, it may put me on the right track.
Further Clarifications
I just wanted to clarify real quick that the mapping of commands to command methods is not really something I'm worried about. I'm mostly concerned about how to parse and organize the command line string. Currently the InvokeCommand()
method employs some 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 the command methods as that is already working. I'm just pondering a good way to parse the command string, organize arguments, and then using that information to pick the right command method/overload using reflection.
Final Design Goal
I am looking for a really good way to parse the command string I'm passing in. I want the parser to identify several things:
- Options. Identify options in the command string.
- Name/Value Pairs. Identify name/value pairs (e.g. [page #] <- includes keyword "page" and value "#")
- Value Only. Identify value only.
I want these to be identified via metadata on the first command method overload. Here is a list of sample methods I want to write, decorated with some metadata to be used by the parser when it is doing reflection. I will give you these method samples and some sample command strings that should map to that method. That information should aid me in formulating 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 (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...
}
The parser must take in a command string, use reflection to find all possible command method overloads, use reflection to read the argument attributes to help determine how to divide up the string into a proper list of arguments, then invoke the proper command method overload, passing in the proper arguments.
Take a look at Mono.Options. It's currently part of Mono framework but can be downloaded and used as a single library.
You can obtain it here, or you can 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);
The solution I generally use looks something like this. Please ignore my syntax errors... been a few months since I've used C#. Basically, replace the if/else/switch with a System.Collections.Generic.Dictionary<string, /* Blah Blah */>
lookup 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);
}
}
Be aware that you can put attributes on parameters which might make things more readable, e.g.
public void TOPIC (
[ArgInfo("Specify topic ID...")] int Id,
[ArgInfo("Specify topic page...")] int? page)
{
...
}
I can see two different problems here:
Resolving method name (as string
) to command module
You could use Dictionary
to map string to method just like in Billy's answer. If you prefer only method over command object, you can map string to 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");
}
Command-line arguments parsing
People have been scratching their heads solving this problem in early days ..
But now we has library that specifically handle this problem. See http://tirania.org/blog/archive/2008/Oct-14.html or NDesk.Options. This should be easier and could handle some pitfall cases than rolling out new one.
精彩评论