In a console app there are two ways to get commands:
- Command line arguments passed into your program via Main(string[] args).
- User input from Console.ReadLine() (which you then split into a string[]).
After getting a command, you have to parse it to figure out what code to execute.
Typically commands have the following format: commandName -argumentName argumentValue. For example, take a look at this familiar git command:
git commit -m "init"
Code language: plaintext (plaintext)
This is passing the command line arguments into the git executable. In Main(string[] args), the arguments would look like this:
["commit", "-m", "init" ]
Code language: plaintext (plaintext)
The git executable has to parse this string to know that it has to execute the commit command.
In this article, I’ll show how to parse commands. First I’ll use a manual approach. Then I’ll show how to use the CommandLineParser library to do the parsing. The example code will support two commands: push and commit, like the following git commands:
git push
git commit -m "added commit example"
Code language: plaintext (plaintext)
Table of Contents
Manually parsing commands and arguments
You may want to start out by manually parsing your commands. As you add more commands and arguments, and the parsing becomes more and more tedious, you may decide to switch to using a parsing library instead. It pays to keep it simple.
Command line arguments are passed into a program into the Main method as a string array, like this:
["commit", "-m", "init" ]
Code language: plaintext (plaintext)
Using a switch statement
The following code shows how to parse the command line arguments by using a switch statement.
static void Main(string[] args)
{
if (args.Length == 0)
{
Console.WriteLine("Invalid args");
return;
}
var command = args[0];
switch (command)
{
case "push":
Push();
break;
case "commit" when args.Length == 3 && args[1] == "-m":
Commit(args[2]);
break;
default:
Console.WriteLine("Invalid command");
break;
}
}
static void Push()
{
Console.WriteLine("Executing Push");
}
static void Commit(string message)
{
Console.WriteLine($"Executing Commit with message: {message}");
}
Code language: C# (cs)
When I run this program with the following arguments:
commit -m "init"
Code language: plaintext (plaintext)
It outputs the following:
Executing Commit with message: init
Code language: plaintext (plaintext)
Using a dictionary
Instead of a switch statement, you can also use a dictionary. The key is the command name. The value is an Action<string[]> (a method delegate / function pointer accepting a string array).
In this example, I’m using a case insensitive dictionary so the command matching isn’t so strict (for example: the user can type any variation of “push” and it’ll match).
static void Main(string[] args)
{
if (args.Length == 0)
{
Console.WriteLine("Invalid args");
return;
}
var command = args[0];
if (!commandMap.ContainsKey(command))
{
Console.WriteLine("Invalid command");
}
commandMap[command](args.Skip(1).ToArray());
}
private static readonly Dictionary<string, Action<string[]>> commandMap = new Dictionary<string, Action<string[]>>(StringComparer.InvariantCultureIgnoreCase)
{
[nameof(Push)] = Push,
[nameof(Commit)] = Commit
};
static void Push(string[] args)
{
Console.WriteLine("Executing Push");
}
static void Commit(string[] args)
{
if (args.Length == 2 && args[0] == "-m")
{
Console.WriteLine($"Executing Commit with message: {args[1]}");
}
else
{
Console.WriteLine("Invalid args. Expected format: commit -m <some message>");
}
}
Code language: C# (cs)
When I run this program with the following arguments:
PUSH
It outputs:
Executing Push
Using CommandLineParser to parse commands and arguments
Instead of manually parsing commands and arguments, you can use a library like CommandLineParser to do the work for you. Then you can focus on what really matters: implementing the command logic.
I’ll show how to implement code that allows you use CommandLineParser to parse arguments and execute your commands like this:
static void Main(string[] args)
{
Parser.Default.ParseArguments<PushCommand, CommitCommand>(args)
.WithParsed<ICommand>(t => t.Execute());
}
Code language: C# (cs)
Step 1 – Add the CommandLineParser nuget package
Execute the following command to install the nuget package:
Install-Package CommandLineParser
Code language: PowerShell (powershell)
Note: This is using Package Manager Console (View > Other windows > Package Manager Console).
Step 2 – Optional – Add ICommand interface
If you have multiple command types, you have two options:
- Option 1 – Add WithParsed() for each command type (or use MapResult()).
- Option 2 – Add an interface for the command types, such as ICommand. With this, you only need to have a single .WithParsed<ICommand>().
So with option 2, you’d add an interface like this:
public interface ICommand
{
void Execute();
}
Code language: C# (cs)
Step 3 – Add commands
First add the PushCommand and put the Verb attribute. This attribute tells CommandLineParser that when it sees “push”, it needs to create a PushCommand object.
[Verb("push", HelpText = "Save all your commits to the cloud")]
public class PushCommand : ICommand
{
public void Execute()
{
Console.WriteLine("Executing Push");
}
}
Code language: C# (cs)
Now add the CommitCommand. The commit command has an argument called “message” (-m for short). So add a property called Message and add the Option attribute. This attribute tells CommandLineParser how to map arguments to properties.
[Verb("commit", HelpText = "Save a code change")]
public class CommitCommand : ICommand
{
[Option('m', "message", Required = true, HelpText = "Explain what code change you did")]
public string Message { get; set; }
public void Execute()
{
Console.WriteLine($"Executing Commit with message: {Message}");
}
}
Code language: C# (cs)
Step 4 – Execute commands
Now parse the arguments by passing them to CommandLineParser as type arguments. Add all possible command types you want to support. In this example, these two command types are supported: PushCommand and CommitCommand.
static void Main(string[] args)
{
Parser.Default.ParseArguments<PushCommand, CommitCommand>(args)
.WithParsed<ICommand>(t => t.Execute());
}
Code language: C# (cs)
When I run the program with the following arguments:
commit -m "changed code to use CommandLineParser"
Code language: plaintext (plaintext)
It outputs the following:
Executing Commit with message: changed code to use CommandLineParser
Code language: plaintext (plaintext)
Use WithParsed correctly to avoid compiler error CS1929
When you use WithParsed() incorrectly, you get compiler error CS1929 ParseResult does not contain a definition for WithParsed.
- If you have a single command type, use WithParsed() with that specific type. Don’t use WithParsed<ICommand> with a single command type.
- If you have multiple command types, you can either 1) Use WithParsed with each type or 2) Use WithParsed<ICommand>.
The following code shows examples of the right and wrong ways to use WithParsed:
//Wrong (CS1929 error). Single command type, can't use WithParsed<ICommand>
Parser.Default.ParseArguments<PushCommand>(args).WithParsed<ICommand>(t => t.Execute());
//Right. Single command type, use WithParsed with the specific type.
Parser.Default.ParseArguments<PushCommand>(args).WithParsed<PushCommand>(t => t.Execute());
//Right. Multiple command types, you can use WithParsed<ICommand>
Parser.Default.ParseArguments<PushCommand, CommitCommand>(args).WithParsed<ICommand>(t => t.Execute());
//Right - Multiple command types, you can use WithParsed with each command type
Parser.Default.ParseArguments<PushCommand, CommitCommand>(args)
.WithParsed<PushCommand>(t => t.Execute())
.WithParsed<CommitCommand>(t => t.Execute());
Code language: C# (cs)
Error handling
CommandLineParser handles invalid commands/arguments. For example, when I pass in “commita”, it shows the following error:
ERROR(S):
Verb 'commita' is not recognized.
--help Display this help screen.
--version Display version information.
Code language: plaintext (plaintext)
When I pass in “commit -a “hello”, it shows me the following error:
ERROR(S):
Option 'a' is unknown.
Required option 'm, message' is missing.
-m, --message Required. Explain what code change you did
--help Display this help screen.
--version Display version information.
Code language: plaintext (plaintext)
Notice that it’s displaying the HelpText I added to the Message property’s Option attribute.
The –help command
When you pass in –help, CommandLineParser will automatically show the list of commands available:
scc 1.0.0
Copyright (C) 2021 scc
push Save all your commits to the cloud
commit Save a code change
help Display more information on a specific command.
version Display version information.
Code language: plaintext (plaintext)
Notice that it’s showing the HelpText I specified on the Verb attribute for each command class.
Comments are closed.