ArgParser is a declarative CLI argument parsing library for .NET.
- Parse custom types
- Support for plain arguments
- Define multiple short and long option names
- Define custom validators for an individual option or the whole argument class
- Mark options as required
- Define option dependencies
- Mark flags as terminating
- Automatically generate the help message using additional informational attributes
- Compile time validation of attribute usage using Roslyn
- Typed positional arguments
dotnet add package JANECEA.ArgParser# Clone repository
git clone https://gitlab.mff.cuni.cz/teaching/nprg043/2026-summer/task-1/t21-api-design.git
# Install in your project
cd YourProject/
dotnet add reference <path to t21-api-design>/src/ArgParser/ArgParser.csproj
dotnet buildWhen installing from NuGet, the analyzer is included automatically. For a local project reference, add this to .csproj:
<ItemGroup>
<ProjectReference
Include="<path to t21-api-design>/src/analyzers/ArgParser.Analyzers/ArgParser.Analyzers.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false"
/>
</ItemGroup>Build examples, run tests, or compile documentation
# Build the examples
dotnet build ./src/examples/<example>/<example>.csproj
# Test the project
dotnet test ./src/ArgParser.sln
# Compile documentation
cd docs
doxygen DoxyfileTo define your CLI structure, create a class that inherits from BaseArgs. The parser inspects all public properties and maps them to command-line inputs based on their type and attributes.
To avoid confusion, we will define the basic terms:
Option is a named parameter that requires an associated value.
- Identification: Must have at least one short name or long name defined.
- Supported types:
string,Enumor any type implementingIParsable<T>(excludingbool) - Usage:
--option=value,--option value,-o=valueor-o value - Declaration:
[
ShortNames('s'),
]
public string Option { get; set; }Flag is a named parameter. It does not accept a value, its presence alone evaluates to true.
- Identification: Must have at least one short name or long name defined.
- Supported types:
bool - Usage:
--verboseor-v - Declaration:
[
LongNames("flag"),
]
public bool Flag { get; set; }Positional Arguments are values mapped by their order in the command line rather than a name.
- Identification: Properties that do not have any name attributes and are explicitly listed in the
[PositionalArgs]class attribute. - Supported types:
string,Enum, or any type implementingIParsable<T> - Order: The sequence defined in the
[PositionalArgs]attribute determines the mapping, regardless of the declaration order. - Declaration:
[PositionalArgs(nameof(First), nameof(Second))]
internal sealed class SimpleArgs : BaseArgs
{
public bool Second { get; set; }
public int First { get; set; }
}Plain Arguments are the "leftovers" of the parsing process. These include:
- Any tokens that do not match a defined Option, Flag, or Positional Argument.
- Any values provided after the
--(double-dash) delimiter.
Any property that does not meet the criteria for an Option, Flag or Positional Argument is ignored by the parser.
The following snippet of code shows declaration of simple class inheriting from BaseArgs. Two options that are expecting some value and one flag are defined.
using ArgParser;
using ArgParser.Attributes;
using ArgParser.Exceptions;
[ExampleUsage("Usage: myProgram [options]")]
internal sealed class SimpleArgs : BaseArgs
{
[
ShortNames('i'),
LongNames("int"),
Help("Example of int option."),
MetaVarName("INT_VALUE"),
]
public int? IntOption { get; set; }
[
ShortNames('s'),
LongNames("string"),
Help("Example of string option."),
MetaVarName("STR_VALUE"),
]
public string? StringOption { get; set; }
[
ShortNames('f'),
LongNames("flag"),
Help("Example of flag."),
]
public bool Flag { get; set; }
public override string[] PlainArguments { get; set; } = [];
}During the creation of ArgParser<SimpleArgs>, SimpleArgs class is validated, including the usage of attributes.
Now SimpleArgs class can be used in the program. If creating of ArgParser and the parsing of arguments was successful, the values can be accessed directly from the created SimpleArgs object by their defined property names.
internal class SimpleExampleProgram
{
private static void Main(string[] args)
{
ArgParser<SimpleArgs> simpleArgsParser =
ArgParserFactory.FromType<SimpleArgs>();
try
{
SimpleArgs simpleArguments = simpleArgsParser.Parse(args);
Run(simpleArguments);
}
catch (CommandLineParsingException ex)
{
Console.WriteLine(ex.Message);
}
catch (HelpCalledException helpEx)
{
Console.WriteLine(simpleArgsParser.GenerateHelpMessage());
}
}
private static void Run(SimpleArgs args)
{
if (args.Flag)
{
// Do desired functionality
}
if (args.IntOption is int intVal)
{
// Do desired functionality with the given value
}
if (args.StringOption is string strVal)
{
// Do desired functionality with the given value
}
if (args.PlainArguments.Length > 0)
{
// Do desired functionality with given plain arguments
}
}
}-o value -o=value --option value --option=valuemyapp.exe -i 10 --string="Hello World" -f plainArgumentBased on the class SimpleArgs, the following help message would be generated:
> myapp.exe --help
Usage: myProgram [options]
Options:
-h, --help
Prints help message and exits.
-f, --flag
Example of flag.
-i, --int INT_VALUE
Example of int option.
-s, --string STR_VALUE
Example of string option.Now we will look into advanced usage of the library.
First we will show how the user can define his own classes and attributes that will next be used in AdvancedArgs.
Any type that implements the IParsable<T> interface is supported for the option values (string and enum options are supported as well).
internal enum MyEnum
{
First,
Second,
Third,
}
internal class MyClass : IParsable<MyClass>
{
public static MyClass Parse(string s, IFormatProvider? provider)
{
if (TryParse(s, provider, out MyClass result))
return result;
else
throw new ArgumentException("arg is null");
}
public static bool TryParse(
[NotNullWhen(true)] string? s,
IFormatProvider? provider,
[MaybeNullWhen(false)] out MyClass result
)
{
if (s is not null)
{
result = new MyClass();
return true;
}
else
{
result = null;
return false;
}
}
}Custom validators for the whole class can be defined. Here we created example of implementing mutual exclusivity.
internal sealed class MutuallyExclusiveEnumEmailAttribute : ClassValidatorAttribute<AdvancedArgs>
{
public override bool Validate(AdvancedArgs args, out string? errorMessage)
{
if (args.Email is not null && args.Enum is not null)
{
errorMessage = "Mutually exclusive attributes Enum and Email were given.";
return false;
}
errorMessage = null;
return true;
}
}Define custom validator for options. Here we define an example validator that checks if string contains a given substring.
public sealed class MustContainAttribute : OptionValidatorAttribute<string>
{
private readonly string _required;
public MustContainAttribute(string required)
{
_required = required;
}
public override bool Validate(string arg, out string? errorMessage)
{
if (!arg.Contains(_required))
{
errorMessage = $"The argument {arg} must contain {_required}";
return false;
}
errorMessage = null;
return true;
}
}Flags can be marked as terminating using the TerminatingFlag attribute. This attribute accepts a type parameter, which must derive from the base Exception class and must provide a parameterless constructor.
internal class FlagCalledException : Exception {}
...
[
ShortNames('f'),
TerminatingFlag<FlagCalledException>
]
public bool Flag {get; set;}If this flag is present in the command-line arguments the specified exception is thrown in order to skip parsing and validation.
...
try
{
Args Arguments = ArgsParser.Parse(args);
}
catch (FlagCalledException flagEx)
{
// Handle terminating flag
}In this class advanced usage is shown using advanced attributes and the showcased examples. Default values for options can be defined. These default values will not be overridden during parsing in case the option is not present.
using ArgParser;
using ArgParser.Attributes;
using ArgParser.Exceptions;
[
ExampleUsage("myProgram -c <COUNT> [options]"),
MutuallyExclusiveEnumEmail,
AllowPlainArguments(true),
PositionalArgs(nameof(Command), nameof(Number)),
]
internal sealed class AdvancedArgs : BaseArgs
{
[
Required,
Requires(nameof(Number)),
MetaVarName("COMMAND"),
Help("Command to execute"),
]
public string? Command { get; set; }
[
MetaVarName("NUMBER"),
Help("Number of runs"),
]
public string? Number { get; set; }
[
ShortNames('c', 'x'),
LongNames("count", "ct"),
Range<int>(0, int.MaxValue),
Required,
MetaVarName("COUNT"),
Help("Help for count option.")
]
public int? Count { get; set; }
[
LongNames("email"),
MustContain("@")
]
public string? Email { get; set; }
[
ShortNames('e'),
EnumCasePolicy(EnumCase.AllUpperCase)
]
public MyEnum Enum { get; set; } = MyEnum.Second;
[
ShortNames('f'),
LongNames("flag"),
TerminatingFlag<FlagCalledException>,
]
public bool Flag { set; get; }
[
ShortNames('l'),
LongNames("class"),
Requires(nameof(Email)),
MetaVarName("CLASS"),
]
public MyClass Class { get; set; } = new();
public override string[] PlainArguments { get; set; } = [];
}Lastly, the use of created AdvancedArgs.
internal class AdvancedExample
{
internal static void Main(string[] args)
{
ArgParser<AdvancedArgs> advancedArgsParser =
ArgParserFactory.FromType<AdvancedArgs>();
try
{
AdvancedArgs AdvArguments = advancedArgsParser.Parse(args);
}
catch (CommandLineParsingException ex)
{
Console.WriteLine(ex.Message);
}
catch (FlagCalledException flagEx)
{
// Handle terminating flag
}
catch (HelpCalledException helpEx)
{
Console.WriteLine(advancedArgsParser.GenerateHelpMessage());
}
}
private static void Run(AdvancedArgs args)
{
if (args.Enum is MyEnum.Second)
{
// Do desired functionality
}
// access all properties by their defined names
}
}myapp.exe -c=10 positionalCommand --class myclass -e FIRST -- 13 PlainArgument2 -PlainArgument3Based on the class AdvancedArgs, the following help message would be generated:
> myapp.exe --help
myProgram command number -c <COUNT> [options]
Arguments:
<command>
Command to execute
<number>
Number of runs
Options:
-f, --flag
-h, --help
Prints help message and exits.
-c, -x, --count, --ct COUNT
Help for count option.
--email Email
-e Enum
-l, --class CLASS