#
Command Line Parsing
The base classes CliCommand
and CliApplicationWithParams
give you access to typed and named command line parameters. You no longer get access to string[] args
in your main method - instead you create instances CliParam<T>
(and, optionally CliVerb
) to define your command line interface. This also allows for automatic help page generation.
A command line interface definition consists of parameters and verbs (which are either named command or groups of verbs).
Side note: For additional details, see DESIGN-NOTES.md.
#
Quick Start
In its simplest form, an application can look like this:
internal sealed class Program : CliApplicationWithParams
{
// Defines main method
protected override CliCommandExecutor Executor => new(Execute);
// A positional parameter
private CliParam<string> ServerNameParam { get; } = new("server-name", positionIndex: 0)
{
HelpText = "The hostname or ip address of the metal-init server.",
};
// An optional named parameter
private CliParam<FileInfo?> CertFileParam { get; } = new("--server-cert")
{
DefaultValue = null,
HelpText = "The server certificate (.cer file) to use.",
};
private static int Main(string[] args)
{
// Creates an instance of "Program" and runs it.
return Run<Program>(args);
}
// The main method
private void Execute()
{
// Access to a parameter
var baseUri = new Uri($"http://{this.ServerNameParam.Value}");
// Main code here
}
}
This program takes a required server name parameter and an optional parameter --server-cert
.
If you need commands/verbs (like the git
command), an application can look like this:
internal static class Program
{
private static int Main(string[] args)
{
var app = new CliApplicationWithVerbs()
{
Verbs = new[]
{
new CliVerb("add", new AddCommand()),
new CliVerb("remove", new RemoveCommand()),
},
};
return app.Run(args);
}
}
or:
return CliApplication.Run(
args,
new CliVerb("add", new AddCommand()),
new CliVerb("remove", new RemoveCommand())
);
The AddCommand
can look like this:
internal sealed class AddCommand : CliCommand
{
public override string? HelpText => "Adds an item";
protected override CliCommandExecutor Executor => new(Execute);
private CliParam<FileInfo> ItemParam { get; } = new("item", positionIndex: 0)
{
HelpText = "The item to add.",
};
private void Execute()
{
// Command code here
}
}
#
Parameters
Parameters come in two variants: named and positional.
Named parameters are something like --no-verify
or -m
in git commit
. Named parameters can either have a value (like with -m
) or be standalone flags (like --no-verify
).
Positional parameters are defined by the order in which they are specified on the command line. For example, in git mv
the first parameter is always the source and the second is always the destination; both are positional parameters.
Parameters are represented by the CliParam<T>
class. The following C# types are supported:
- number types (
int
,double
, ...) bool
string
- anything that has a constructor that takes a single
string
argument
Parameters must be defined in a "container" - either within a CliCommand
or CliApplicationWithParams
. Parameters are (usually) defined as instance (i.e. non-static
) property or field. These properties or fields can have any visibility (including private
).
You can define a named parameter like this:
private CliParam<int> ValueParam { get; } = new("--value");
This creates a named parameter with the name --value
that takes an int
as value.
To create a positional parameter, define it like this:
private CliParam<string> FromParam { get; } = new("from", positionIndex: 0);
Note that the name from
is just used for generating the help page for this parameter - it's not specified on the command line by the end user.
You can also define alias names for named parameters, default values (which makes the parameter optional) and help texts:
private CliParam<int> ValueParam { get; } = new("--value", "--val")
{
DefaultValue = 42,
HelpText = "The value to work on.",
};
To access the value of a parameter, simply use the Value
property:
private void Execute()
{
int theValue = this.ValueParam.Value;
}
Note that the value is only available from within an "executor" method (see below).
#
Optional Parameters
By default, parameters are considered "required". Parameters become "optional" if the DefaultValue
property is set.
For example, this parameter is required:
private CliParam<int> ValueParam { get; } = new("--value");
While this parameter is optional:
private CliParam<int> ValueParam { get; } = new("--value")
{
DefaultValue = 42,
};
To make a reference type parameter optional, set its default value to null
:
private CliParam<FileInfo?> ValueParam { get; } = new("--value")
{
DefaultValue = null,
};
Note that the type of the parameter needs to be nullable (e.g. FileInfo?
- not FileInfo
) for this to work.
There are a few parameter types that are optional by default:
- Named parameters of type
CliParam<bool>
: the default value is set tofalse
; these parameters usually represent "flags" (e.g.--verbose
) - Parameters with a nullable value type (e.g.
CliParam<int?>
): the default value is set tonull
#
Verbs and Commands
Verbs let you have multiple functions within your application. For example, in git add myfile.cs
the word add
is such a verb.
Verbs - represented by the CliVerb
class - always have a name and usually a command:
var verb = new CliVerb("benchmark", new BenchmarkCommand());
Commands are implemented as classes that inherit from CliCommand
. Commands have an executor (think: main method) and usually parameters:
public class BenchmarkCommand : CliCommand
{
protected override CliCommandExecutor Executor => new(Execute);
private CliParam<int> DurationParam { get; } = new("--duration", "-d")
{
DefaultValue = 10,
HelpText = "How long to run this benchmark (in seconds).",
};
private void Execute()
{
TimeSpan duration = TimeSpan.FromSeconds(this.DurationParam.Value);
...
}
}
The user would execute this command via something like this:
myapp benchmark --duration 20
Verbs can also have alias names (like named parameters), a help text, and sub/child verbs.
#
Verb Groups (Verbs without Command)
Verb groups are simply verbs that just group other verbs under a name.
For example, if you have a CLI api like this:
ssh-env keys create
ssh-env keys install
ssh-env keys delete
... then keys
would be a verb group (i.e. it doesn't do anything on its own)
Verb groups are simply verbs without a command:
var verbGroup = new CliVerb("keys")
{
SubVerbs = new[]
{
new CliVerb("create", CreateKeysCommand()),
new CliVerb("install", new InstallKeysCommand()),
new CliVerb("delete", new DeleteKeysCommand()),
},
};
#
Putting Everything Together
To make your commands, verbs, and parameters accessible to the end user, you must create an instance of one of the following classes:
- An application with multiple functions:
CliApplicationWithVerbs
- An application with a single function:
CliApplicationWithParams
CliApplicationWithCommand
#
Application with Multiple Functions
As the name suggests, CliApplicationWithVerbs
provides access to verbs. It provides multiple functions under one application. It cannot have parameters on its own. Examples for this application type are git
or dotnet
.
var gitApplication = new CliApplicationWithVerbs()
{
Verbs = new[]
{
new CliVerb("init", new GitInitCommand()),
new CliVerb("commit", new GitCommitCommand()),
new CliVerb("add", new GitAddCommand()),
},
};
#
Application with a Single Function
CliApplicationWithParams
on the other hand cannot have verbs but only parameters. These applications only provide one function. Examples for this application type are cd
, rm
, or dir/
ls`. As such, they require an "executor" method and usually have parameters (you can think of them as single-command applications).
internal sealed class MoveApplication : CliApplicationWithParams
{
protected override CliCommandExecutor Executor => new(Execute);
private CliParam<string> SourceParam { get; } = new("from", positionIndex: 0);
private CliParam<string> DestParam { get; } = new("dest", positionIndex: 1);
private void Execute()
{
File.Move(this.SourceParam.Value, this.DestParam.Value);
}
}
The same application can be written as an application with a command (with CliApplicationWithCommand
):
internal sealed class MoveCommand : CliCommand
{
protected override CliCommandExecutor Executor => new(Execute);
private CliParam<string> SourceParam { get; } = new("from", positionIndex: 0);
private CliParam<string> DestParam { get; } = new("dest", positionIndex: 1);
private void Execute()
{
File.Move(this.SourceParam.Value, this.DestParam.Value);
}
}
var app = new CliApplicationWithCommand(new MoveCommand());
This form has the advantage that the same MoveCommand
can be used with both CliApplicationWithCommand
(single function) and CliApplicationWithVerbs
(multiple functions).
#
Running the Application
To run either application type, simply invoke Run()
or RunAsync()
:
internal static class Program
{
public int Main(string[] args)
{
var app = new MoveApplication();
return app.Run(args);
}
}
Or:
internal static class Program : CliApplicationWithParams
{
// Other code here
public int Main(string[] args)
{
return Run<Program>(args);
}
}
Or use one of the static Run()
/RunAsync()
convenience methods:
return CliApplication.Run(args, new MyCliCommand());