Plexdata Argument Parser

Overview

The Plexdata Argument Parser is a library that allows users to easily parse the command line arguments given to a program. The main feature of this library is that users only need to define their own class representing all possible command line arguments. Thereafter, each of the properties is tagged by an attribute which describes the type of the expected command line argument.

At runtime an instance of this pre‑defined class is consigned to the Plexdata Argument Parser together with the actual command line arguments. After the parsing procedure the class contains the values that have been assigned by command line.

Another feature of the Plexdata Argument Parser is the possibility of an automated generation of a program’s help text. For this purpose the pre‑defined class is tagged by a suitable set of attributes that include all information needed to generate the help text.

Licensing

The software has been published under the terms of

MIT License

Copyright © 2022 plexdata.de

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Installation

The binary files of the Plexdata Argument Parser are provided as NuGet package and can be obtained from https://www.nuget.org/packages/Plexdata.ArgumentParser.NET. How to install this NuGet package manually is explained there.

Using the Plexdata Argument Parser together with Visual Studio.

Additionally, all releases can be downloaded from GitHub. Please visit page Plexdata Argument Parser to find all available versions.

Declaration

A class that shall serve as representation of supported command line arguments should consist of a set of properties. Each of these properties must have a public setter as well as a public getter. All other properties, fields and/or methods are ignored.

Furthermore, all in all three different types of command line arguments are supported. These three different types are called Switch, Option and Verbal arguments.

Switch

Switches are command line arguments that either can be “on” or “off”. In other words, Switches represent some kind of Boolean type. One typical representative of this type of command line argument is for example the argument ‑‑verbose. A command line argument class can have multiple Switch types.

Option

Options are command line arguments that include additional information. Such kind of command line argument is for example the argument ‑‑username, because of this argument type requires that a user name has to be specified. Otherwise a login can’t be performed for instance. A command line argument class can have multiple Option types.

Verbal

Verbal arguments are types that don’t have a name. Furthermore, such kind of command line argument may consist of multiple values. A typical representative of this type is for example a list of files to be processed. A command line argument class can have only one Verbal type.

Usage

Task of this section is to show how the Plexdata Argument Parser can be used to configure a class that shall serve as command line argument representation.

Switch

As mentioned above, a program that wants to use on/off arguments should be configured as demonstrated below.

[ParametersGroup]
class CmdLineArgs
{
    [SwitchParameter(SolidLabel = "verbose", BriefLabel = "v")]
    public Boolean IsVerbose { get; set; }

    [SwitchParameter(SolidLabel = "debug")]
    public Boolean IsDebug { get; set; }
}

As shown above, each property is of type Boolean and provides a public setter as well as a public getter for those properties. Further, each of these properties is tagged by an attribute named SwitchParameter. The SolidLabel of each switch parameter defines the long name of the command line argument. Long name command line arguments are typically prefixed by a double‑dash.

Additionally, property IsVerbose uses a BriefLabel as well. A brief label typically represents an abbreviation of the long argument and is usually prefixed by a single‑dash at command line.

In contrast to that, property IsDebug only uses the SolidLabel which in turn means that only the long name is available as command line argument.

With this class in background, users can for instance call the program as shown as follows.

$> program.exe --verbose --debug
$> program.exe -v --debug
$> program.exe --verbose
$> program.exe -v
$> program.exe --debug

Multiple Brief Labels

Since version 1.0.7 it became possible to use multiple brief labels for one and the same switch parameter. This is pretty useful, especially when one and the same program parameter can be applied by different switches. The program help is a good example for such a switch, as code snippet below demonstrates.

[ParametersGroup]
class CmdLineArgs
{
    [SwitchParameter(SolidLabel = "help", BriefLabel = "h,?")]
    public Boolean IsHelp { get; set; }
}

As shown above, property BriefLabel takes a list of comma separated brief labels and the program can now be called with all of these parameters. See here for some calling examples.

$> program.exe --help
$> program.exe -h
$> program.exe -?

In each of these cases, property IsHelp of class CmdLineArgs is set to true.

For sure, it doesn’t really make sense to use multiple solid labels. Therefore, such a feature is not supported.

Option

As mentioned above, a program that wants to use arguments with additional data should be configured as demonstrated below.

[ParametersGroup]
class CmdLineArgs
{
    [OptionParameter(SolidLabel = "password", DependencyList = "Username")]
    public String Password { get; set; }

    [OptionParameter(SolidLabel = "username", DependencyList = "Password")]
    public String Username { get; set; }
}

As shown above, each of the Option attributes include a DependencyList tag. Task of this tag is to ensure that each of the command line arguments tagged this way are provided when a program is called.

This in detail means that if one of the arguments is provided without the other one than the parser throws an exception to inform that a rule check has failed.

With this class in background, users can for instance call the program as shown as follows.

$> program.exe --username <username> --password <password>
$> program.exe --password <password> --username <username>

Additionally, it is possible to use default values for optional parameters. How to use them is shown in the next example.

[ParametersGroup]
class CmdLineArgs
{
    [OptionParameter(SolidLabel = "host", DefaultValue = "http://example.org")]
    public String Host { get; set; }

    [OptionParameter(SolidLabel = "port", DefaultValue = 12345)]
    public UInt16 Port { get; set; }
}

Here are some rules of how to use this feature. Each default value can be applied as type–save constant value and as string literal as well for all of the supported types. An exception of type DefaultValueException is thrown if applying a default value fails for any reason. Default values are replaced by the argument parser as soon as they are provided as command line argument.

Multiple Brief Labels

Since version 1.0.7 it became possible to use multiple brief labels for one and the same option parameter. This is pretty useful, especially when one and the same program parameter can be applied by different option values.

[ParametersGroup]
class CmdLineArgs
{
    [OptionParameter(SolidLabel = "method", BriefLabel = "m,M")]
    public String Method { get; set; }
}

As shown above, property BriefLabel takes a list of comma separated brief labels and the program can now be called with all of these parameters. See here for some calling examples.

$> program.exe --method SHA
$> program.exe -m SHA
$> program.exe -M SHA

In each of these cases, property Method of class CmdLineArgs is set to SHA.

For sure, it doesn’t really make sense to use multiple solid labels. Therefore, such a feature is not supported.

Verbal

As mentioned above, a program that wants to use for example a list of files as arguments, then it would be configured as demonstrated below.

[ParametersGroup]
class CmdLineArgs
{
    [VerbalParameter]
    public String[] Files { get; set; }
}

As shown above, the Verbal attribute is used for such a purpose. In contrast to the usage of Switches and/or Options, the usage of Verbal attributes is limited to one per class. Otherwise a rule check exception is thrown by the parser.

With this class in background, users can for instance call the program as shown as follows.

$> program.exe file1.txt
$> program.exe file1.txt file2.txt
$> program.exe file1.txt file2.txt file3.txt

Parsing

Once a proper class has been implemented, the program’s main method can parse a given list of command line arguments as shown below.

static void Main(string[] args)
{
    try
    {
        if (args.Length > 0)
        {
            CmdLineArgs cmdLineArgs = new CmdLineArgs();
            cmdLineArgs.Process(args);
        }
    }
    catch (Exception exception)
    {
        Console.WriteLine(exception.Message);
    }
}

As shown above, it is strictly recommended to surround any call to the command line parser by a try…catch block.

Custom Types

It is also possible to convert a special combination of command line arguments into custom types. For this purpose the library supports the generic interface ICustomConverter. This interface allows to implement own argument parsing algorithms, one for each custom type. Below please find a fully qualified example of how to use this interface in combination with the argument parser.

using Plexdata.ArgumentParser.Attributes;
using Plexdata.ArgumentParser.Constants;
using Plexdata.ArgumentParser.Extensions;
using Plexdata.ArgumentParser.Interfaces;
using System;
using System.Collections.Generic;

namespace CustomTypeExample
{
    class Program
    {
        public class CustomType
        {
            public Int32 Top { get; set; }
            public Int32 Left { get; set; }
            public Int32 Height { get; set; }
            public Int32 Width { get; set; }
        }

        public class CustomConverter : ICustomConverter<CustomType>
        {
            public CustomType Convert(String parameter, String argument, String delimiter)
            {
                if (argument is null) { return null; }

                delimiter = delimiter ?? ArgumentDelimiters.DefaultDelimiter;

                String[] splitted = argument.Split(delimiter.ToCharArray());

                return new CustomType
                {
                    Top = Int32.Parse(splitted[0]),
                    Left = Int32.Parse(splitted[1]),
                    Height = Int32.Parse(splitted[2]),
                    Width = Int32.Parse(splitted[3])
                };
            }
        }

        [ParametersGroup]
        public class CmdLineArgs
        {
            [OptionParameter(SolidLabel = "region", BriefLabel = "r")]
            [CustomConverter(typeof(CustomConverter))]
            public CustomType Region { get; set; }
        }

        static void Main(String[] args)
        {
            try
            {
                if (args.Length > 0)
                {
                    CmdLineArgs cmdLineArgs = new CmdLineArgs();
                    cmdLineArgs.Process(args);
                    cmdLineArgs.Validate();
                }
            }
            catch (Exception exception)
            {
                Console.WriteLine(exception);
            }
        }
    }
}

The very first step in using custom type conversion is an implementation of a class that represents the custom type. This is done by class CustomType in the example above.

Thereafter, it would be useful to implement a class derived from interface ICustomConverter that is able to convert the wanted custom type. In the above example, class CustomConverter represents this implementation.

As next, a class representing all command line arguments becomes necessary. In the above example, class CmdLineArgs represents this implementation. Further, this class uses the custom type as optional parameter. This is important because of other parameter classifications, such as switch or verbal, are not supported for custom type conversion.

Now it’s time to register the user‑defined converter. This is easily done by using attribute CustomConverter which just takes the converter’s type as parameter. The related line of code is highlighted in the example above.

Finally, the program’s Main method just parses the actual command line arguments and calling the program as shown below will than trigger the custom type parsing.

$> program.exe --region 10:20:30:40

One detail to mention is still left. The attribute OptionParameter supports a new property named Delimiter which allows users to customize the delimiter. This delimiter is passed as parameter of the interface method Convert.

Problem

There is a known issue during parsing a program’s arguments and it comes to light when using the args array that is provided as parameter of the Main() method.

The meant issue gets visible as soon as using arguments that are surrounded by double‑quotes and a backslash is included right before the closing double‑quote. See below for such an example.

$> program.exe -o -p "*.xyz" -s "c:\folder1\" -t "c:\folder2\" -v

Is this particular case the provided arguments are split into an array that looks like the one shown as next.

[0]: "-o"
[1]: "-p"
[2]: "*.xyz"
[3]: "-s"
[4]: "c:\\folder1\" -t c:\\folder2\""
[5]: "-v"

The mistake is in line [4] of the above example. The reason behind, the framework’s parser did interpret each combination of backslash and double‑quote as an “escaped double‑quote character”, which indeed was not meant in this context. But the Plexdata Argument Parser provides a workaround allowing to prevent such an unwanted behavior. See next code snippet how to use this workaround.

static void Main(String[] args)
{
    try
    {
        // Use the string extension method Extract() of package 
        // Plexdata Argument Parser as alternative command line 
        // argument parser.
        String[] options = Environment.CommandLine.Extract(true);
        if (options != null && options.Length > 0)
        {
            CmdLineArgs cmdLineArgs = new CmdLineArgs();
            cmdLineArgs.Process(options);
            cmdLineArgs.Validate();
        }
    }
    catch (Exception exception)
    {
        Console.WriteLine(exception.Message);
    }
}

Dependencies

This feature allows making an argument depends on another argument or a list of arguments. At the moment there exist three types of dependencies; explicit, implicit and mutual dependencies. This section wants to clarify how to use the dependency feature of the Plexdata Argument Parser.

At this point it is important to know, the parameter DependencyList of any of the attributes always takes a list property names! The parameter DependencyList does neither accept the SolidLabel nor the BriefLabel as value.

Explicit Dependency

The term of an Explicit Dependency means that one argument depends on another argument or a list of arguments. A good example of this kind of dependency is the argument combination of ‑‑file‑name, ‑‑operation and ‑‑execution. In other words, the argument ‑‑file‑name requires the type of operation as well as the type of execution, but not the other way round.

Strong Explicit Dependency

The Strong Explicit Dependency means in detail that all referenced arguments are required to satisfy the needs of the source argument. The other way round, the Strong Explicit Dependency combines all referenced arguments with an AND operation. How to configure such kind of dependency is shown in code snippet below.

[ParametersGroup]
class CmdLineArgs
{
    [OptionParameter(SolidLabel = "file-name", DependencyList = "Operation, Execution", DependencyType = DependencyType.Required)]
    public String Filename { get; set; } // 'Filename' depends on 'Operation' and 'Execution'.

    [OptionParameter(SolidLabel = "operation", DependencyList = "Filename", DependencyType = DependencyType.Optional)]
    public String Operation { get; set; } // 'Operation' cannot be used without 'Filename'.

    [OptionParameter(SolidLabel = "execution", DependencyList = "Filename", DependencyType = DependencyType.Optional)]
    public String Execution { get; set; } // 'Execution' cannot be used without 'Filename'.
}

With the above class in mind, a program execution will only be possible like shown as follows.

$> program.exe --file-name file-name.ext --execution execution-value --operation operation-value

Weak Explicit Dependency

The Weak Explicit Dependency combines all referenced arguments with an AND operation as well. But in contrast to the Strong Explicit Dependency, the Weak Explicit Dependency allows a usage of the referenced arguments independently of the source argument. How to configure this kind of dependency is shown in code snippet below.

[ParametersGroup]
class CmdLineArgs
{
    [OptionParameter(SolidLabel = "file-name", DependencyList = "Operation, Execution", DependencyType = DependencyType.Required)]
    public String Filename { get; set; } // 'Filename' depends on 'Operation' and 'Execution'.

    [OptionParameter(SolidLabel = "operation")]
    public String Operation { get; set; } // 'Operation' can be used without 'Filename'.

    [OptionParameter(SolidLabel = "execution")]
    public String Execution { get; set; } // 'Execution' can be used without 'Filename'.
}

With the above class in mind, a program execution will be possible like shown below.

$> program.exe --operation operation-value
$> program.exe --execution execution-value
$> program.exe --operation operation-value --execution execution-value
$> program.exe --file-name file-name.ext --execution execution-value --operation operation-value

Implicit Dependency

The term of an Implicit Dependency also means that one argument depends on another argument or a list of arguments. But in contrast to an Explicit Dependency relation, an Implicit Dependency turns around the referencing direction. This in fact makes it possible to combine the source argument with its dependent arguments by an OR operation. The same example as above can be used right here as well to show how this kind of dependency works.

Strong Implicit Dependency

The Strong Implicit Dependency means in detail that only a subset of all referenced arguments is required to satisfy the needs of the source argument. As mentioned above, the Strong Implicit Dependency combines all referenced arguments with an OR operation. How to configure such kind of dependency is shown in code snippet below.

[ParametersGroup]
class CmdLineArgs
{
    [OptionParameter(SolidLabel = "file-name", DependencyList = "Operation, Execution", DependencyType = DependencyType.Optional)]
    public String Filename { get; set; } // 'Filename' depends on 'Operation' or 'Execution'.

    [OptionParameter(SolidLabel = "operation", DependencyList = "Filename", DependencyType = DependencyType.Required)]
    public String Operation { get; set; } // 'Operation' cannot be used without 'Filename'.

    [OptionParameter(SolidLabel = "execution", DependencyList = "Filename", DependencyType = DependencyType.Required)]
    public String Execution { get; set; } // 'Execution' cannot be used without 'Filename'.
}

With the above class in mind, a program execution will be possible as shown here.

$> program.exe --file-namefile-name.ext --operation operation-value
$> program.exe --file-name file-name.ext --execution execution-value
$> program.exe --file-name file-name.ext --execution execution-value --operation operation-value

Weak Implicit Dependency

Some readers may already guess, the difference between a Strong Implicit Dependency and the Weak Implicit Dependency is that an independent usage of the referenced arguments becomes possible. No, unfortunately not!

But it becomes possible to use the source argument (‑‑file‑name in the example above) without of any of the referenced arguments. See following code snippet for an example of how to configure this kind of dependency.

[ParametersGroup]
class CmdLineArgs
{
    [OptionParameter(SolidLabel = "file-name")]
    public String Filename { get; set; } // 'Filename' depends on 'Operation' or 'Execution'.

    [OptionParameter(SolidLabel = "operation", DependencyList = "Filename", DependencyType = DependencyType.Required)]
    public String Operation { get; set; } // 'Operation' cannot be used without 'Filename'.

    [OptionParameter(SolidLabel = "execution", DependencyList = "Filename", DependencyType = DependencyType.Required)]
    public String Execution { get; set; } // 'Execution' cannot be used without 'Filename'.
}

With the above class in mind, a program execution will be possible as shown here.

$> program.exe --file-name file-name.ext 
$> program.exe --file-name file-name.ext --operation operation-value 
$> program.exe --file-name file-name.ext --execution execution-value 
$> program.exe --file-name file-name.ext --execution execution-value --operation operation-value

Mutual Dependency

The term of a Mutual Dependency does actually mean that one argument depends on another argument and vice versa. A good example of this kind of dependency is the argument combination of ‑‑username and ‑‑password, because a user name does not make any sense without having a password.

How to configure such kind of dependency is shown in the example in section Option. Therefore, there is no need to explain it again right here. The only thing to mention in this section is that a cross reference is already applied by using the reference name of one property within the list of dependencies of the other property, and vice versa.

Help

The Plexdata Argument Parser is also able to generate a command line help text. For this purpose a class that describes all possible command line arguments should be configured with additional attributes. Here an example.

[HelpLicense]
[HelpUtilize]
[HelpPreface("This program does something useful.")]
[ParametersGroup]
class CmdLineArgs
{
    [HelpSummary("More output during runtime.")]
    [SwitchParameter(SolidLabel = "verbose", BriefLabel = "v")]
    public Boolean IsVerbose { get; set; }

    [HelpSummary("Run program in Debug mode.")]
    [SwitchParameter(SolidLabel = "debug")]
    public Boolean IsDebug { get; set; }
}

Parsing the help text and printing it out could be done as shown below.

static void Main(string[] args)
{
    try
    {
        CmdLineArgs cmdLineArgs = new CmdLineArgs();
        Console.WriteLine(cmdLineArgs.Generate());
        Console.ReadKey();
    }
    catch (Exception exception)
    {
        Console.WriteLine(exception.Message);
    }
}

For the above example, the help screen shown to the user would look like shown below.

Copyright © <company>

This program does something useful.

Usage:

  <program> [options]

Options:

  --verbose [-v]  More output during runtime.

  --debug         Run program in Debug mode.

For sure, this is a pretty simple example. In other words, the help generator is able to do more. For example it is possible to take some of the “properties” from the executing assembly and put them into the help text. The help usage part as well as the help option section can be grouped and so on.

It is also possible to enrich the help summary of a command line argument by for instance additional information. For such a purpose the attribute provides the property Options. How to use this property is demonstrated in the following example.

[HelpLicense]
[HelpUtilize]
[HelpPreface("This program does something useful.")]
[ParametersGroup]
class CmdLineArgs
{
    [HelpSummary(Options = "<method>", Content = "Apply the name of the method to be used.")]
    [OptionParameter(SolidLabel = "method", BriefLabel = "m")]
    public String File { get; set; }

    [HelpSummary(Options = "<files>", Content = "Provide a list of fully qualified file names. Wild cards such as '*' and '?' can be used.")]
    [VerbalParameter]
    public String[] Files { get; set; }
}

When the help has been generated, as shown in method Main() from above, the output would look like as shown below.

Copyright © <company>

This program does something useful.

Usage:

  <program> [options]

Options:

  --method [-m] <method>  Apply the name of the method to be used.

  <files>                 Provide a list of fully qualified file names. Wild
                          cards such as '*' and '?' can be used.

Finally, please be aware, the Options property is available for arguments of type Switch as well. But for sure, it doesn’t make any sense to use this feature together with Switch arguments.

Limitations

Custom Types

Custom types do not support default values. In other words, the value of property DefaultValue of attribute OptionParameter is ignored when custom types are processed.

More information about custom type processing can be found in sub‑section Custom Types.

Usage of args

The usage of the args array, that is provided as parameter of method Main(), causes some problems if it contains combinations of backslash and double‑quote. Against this background, it is strictly recommended to use extension method Extract() to re‑parse the complete string of command line arguments. See next code snippet for an example of how to safely re‑parse all command line arguments.

static void Main(String[] args)
{
    try
    {
        String[] options = Environment.CommandLine.Extract(true);
        if (options != null && options.Length > 0)
        {
            CmdLineArgs cmdLineArgs = new CmdLineArgs();
            cmdLineArgs.Process(options);
            cmdLineArgs.Validate();
        }
    }
    catch (Exception exception)
    {
        Console.WriteLine(exception.Message);
    }
}

More information about this issue can be found in section Problem.