C# – Parsing commands and arguments in a console app

In a console app there are two ways to get commands:

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)

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: initCode 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 CommandLineParserCode 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.

2 thoughts on “C# – Parsing commands and arguments in a console app”

  1. It seems the approach with base ICommand for all command doesn’t work any longer with CommandLine NuGet – the code can’t be compiled because of the error like ‘error CS1929: ‘ParserResult’ does not contain a definition for ‘WithParsed’ and the best extension method overload ‘ParserResultExtensions.WithParsed(ParserResult, Action)’ requires a receiver of type ‘ParserResult”

    Reply
    • Thanks for pointing out this error.

      This error happens when you only have a single command type and try to use the interface with WithParse, like this:
      Parser.Default.ParseArguments<PushCommand>(args).WithParsed<ICommand>(t => t.Execute());

      To get rid of the error when you only have a single command type, use the command type in WithParsed (instead of the interface), like this:
      Parser.Default.ParseArguments<PushCommand>(args).WithParsed<PushCommand>(t => t.Execute());

      If you have multiple commands, you can use the interface with WithParsed without a problem (as shown in the article):
      Parser.Default.ParseArguments<PushCommand,CommitCommand>(args).WithParsed<ICommand>(t => t.Execute());

      Reply

Leave a Comment