c# – How do I parse command line arguments?

Question:

I am writing a console application that must accept more than 5 parameters. In this case, there are required parameters, there are optional ones, some parameters should work only with others, etc. I am already mired in various if..else if..else. How to make your life easier? Are there guidelines and standard practices for working with command line arguments?

Answer:

If there are many parameters, it is best to move their parsing into a separate class or set of classes. When if / else form a complex structure, you can use C # "increased declarativeness" and describe the structure of commands, for example, like this:

var commandLinePattern = Command.WithName('add')
                                .HasOption('-fileName')

                       | Command.WithName('help')
                                .HasParameter("CommandName");

Of course, this will require a certain amount of ingenuity in design, so I'll explain in more detail below.

In the next step, we will describe our commands:

public interface ICommand
{
    void Run();
}

public HelpCommand : ICommand
{
    public string CommandName { get; set; }

    public bool Verbose { get; set; }

    public int PageSize { get; set; }

    public void Run()
    {
        if (CommandName == "add")
        {
            Console.WriteLine("Help on the ADD command");
        }
        . . .
    }
}

The command line pattern (the commandLinePattern variable in the first example) can be applied to a specific parameter string to instantiate a specific command class and populate its properties using reflection:

var command = commandLinePattern.Parse(args);
command.Run();

UPDATE

So, we get something like this:

static class Program
{
    private static readonly СommandLinePattern commandLinePattern = Command.WithName('add')
                                                        .HasOption('-fileName')
                                                        .HasOption('-move')

                                               | Command.WithName('help')
                                                        .Parameter("CommandName");

    static void Main(string[] args)
    {
        var command = commandLinePattern.Parse(args);
        command.Run();
    }
}

UPDATE "for a beginner"

A little about how to realize all this beauty. Our miracle in appearance and internal structure is very similar to regular expressions: first we describe a certain pattern of command line arguments, and then we apply it to real arguments. The result of such an application will be an object that can be executed . Let's call it a team .

The name team arose for a reason – it is one of the design patterns described in the classic work of the gang of four.

I don't know what command lines you have to parse, so I'll make a few assumptions:

  1. A program can execute one and only one command at a time. The command comes first on the command line. The command does not require prefixes such as hyphen or forward slash.
  2. The command can be followed by one or more parameters. Parameters can be named or unnamed. Named parameters are prefixed with a hyphen. Named parameters can have a value, in which case it is separated from the name by a colon.

Approximately such rules are used in a large number of utilities, for example, in the archivers pkzip , arj and rar ; in the version control utilities git and hg – that is, these two rules are really enough for a wide range of tasks.

arj add archive *.c -m0

arj is the name of the program, add is the command, archive ( archive name) and *.c (what to archive) are unnamed parameters; -m – named parameter (option), 0 – parameter value.

In arj, the parameter value does not need to be separated by: or =. We will go the other way, solely for the fact that the example is a demo.

Such a command line parameter pattern can be described with a simple class:

public class CommandLinePattern
{
    public string Name { get; set; }

    public List<string> Parameters { get; set; }

    public List<string> Options { get; set; }
}

For example, the help command might have a pattern like this

program Help CommandName [-Verbose] [-PageSize:num]

Which corresponds ekzmeplyar class CommandLinePattern :

var pattern = new CommandLinePattern
{
    Name = "Help",
    Parameters = new List<string> { "CommandName" },
    Options = new List<string> { "Verbose", "PageSize" },
};

We need to solve two problems: build such a template, and apply it. In the code above, we used the static Command class to start constructing the template:

public static class Command
{
    public static CommandLinePattern WithName(string name)
    {
        return new CommandLinePattern
        {
            Name = name,
            Parameters = new List<string>(),
            Options = new List<string>(),
        };
    }
}

The rest of the construction methods will be placed directly in the CommandLinePattern class:

public CommandLinePattern HasOption(string name)
{
    Options.Add(name);

    return this;
}

public CommandLinePattern HasParameter(string name)
{
    Parameters.Add(name);

    return this;
}

Thanks to the return this; we can construct an object sequentially by chaining methods:

var pattern = Command.WithName("Help")
                     .HasParameter("CommandName")
                     .HasOption("Verbose")
                     .HasOption("PageSize");

This is how we describe the command templates. Now let's solve the second task – parsing the command line and creating a command object.

In the CommandLinePattern class, CommandLinePattern implement the TryParse method, which will try to parse the command line and, if successful, create an object that implements the following ICommand interface:

public virtual bool TryParse(string[] args, out ICommand result)
{
    result = null;
    // Конечно, наш шаблон не может соответствовать пустой командной строке.
    if (args.Length == 0)
        return false;

    // И он не может соответствовать какой-то другой команде.
    if (args[0] != Name)
        return false;

    var properties = new Dictionary<string, string>();
    var nextParameterIndex = 0;

    for (int i = 1; i < args.Length; i++)
    {
        if (args[i].StartsWith("-"))
        {
            var parameterWithoutHyphen = args[i].Substring(1);
            var nameValue = parameterWithoutHyphen.Split(':');
            if (nameValue.Lenth == 1)
                properties.Add(nameValue[0], null);
            else
                properties.Add(nameValue[0], nameValue[1]);
        }
        else
        {
            var name = Parameters[nextParameterIndex++];

            var value = args[i];
            properties.Add(name, value);
        }
    }

    // Для команды с имененем Help мы найдём класс HelpCommand:
    var className = Name + "Command";
    var type = Type.GetType(className);
    // И создадим его экземпляр:
    result = (ICommand)Activator.CreateInstance(type);

    // Теперь значения всех параметров запишем в свойства
    // только что созданного экземляра:
    foreach (var property in properties)
    {
        var name = property.Key;
        var value = property.Value;

        type.GetProperty(name)
            .SetValue(result, value);
    }

    return true;
}

public virtual ICommand Parse(string[] args)
{
    ICommand result;
    if (TryParse(args, out result))
        return result;

    throw new FormatException();
}

Keep in mind that this writing of values ​​will only work with string properties – in a real program, you will need to convert string values ​​to property types.

Now we have everything ready. We can write our analysis of the parameters, but "for beauty" we will do one more final touch.

Let's create a class that will allow us to combine two templates and check first the left and then the right:

public class OrCommandLinePattern : CommandLinePattern
{
    private readonly CommandLinePattern left;
    private readonly CommandLinePattern right;

    public OrCommandLinePattern(CommandLinePattern left, CommandLinePattern right)
    {
        this.left = left;
        this.right = right;
    }

    public override bool TryParse(string[] args, out ICommand result)
    {
        if (left.TryParse(args, out result))
            return true;

        return right.TryParse(args, out result);
    }
}

Add the implementation of the OR operator to the base class:

public static CommandLinePattern operator |(CommandLinePattern left, CommandLinePattern right)
{
    return new OrCommandLinePattern(left, right);
}

We can now combine multiple patterns with a vertical bar:

var commandLinePattern = Command.WithName('add')
                                .HasOption('-fileName')

                       | Command.WithName('help')
                                .HasParameter("CommandName");

And at the end, we start parsing by calling one single method:

var command = commandLinePattern.Parse(args);

The result of the parser's work will be an instance of a class that implements the ICommand interface with filled properties. We just have to run it:

command.Run();

UPDATE – finished implementation (Dec 2019)

In the end, I wrote the finished code and posted it as a NuGet project.

https://www.nuget.org/packages/Binateq.CommandLine/ – package

https://github.com/binateq/command-line-parser – source code on GitHub

Scroll to Top