Benday.CommandsFramework
4.18.0
See the version list below for details.
dotnet add package Benday.CommandsFramework --version 4.18.0
NuGet\Install-Package Benday.CommandsFramework -Version 4.18.0
<PackageReference Include="Benday.CommandsFramework" Version="4.18.0" />
<PackageVersion Include="Benday.CommandsFramework" Version="4.18.0" />
<PackageReference Include="Benday.CommandsFramework" />
paket add Benday.CommandsFramework --version 4.18.0
#r "nuget: Benday.CommandsFramework, 4.18.0"
#:package Benday.CommandsFramework@4.18.0
#addin nuget:?package=Benday.CommandsFramework&version=4.18.0
#tool nuget:?package=Benday.CommandsFramework&version=4.18.0
Benday.CommandsFramework
A .NET framework for building command-line interface (CLI) utilities. Define named commands with typed, validated arguments using a fluent API, and wire up dependency injection and configuration with minimal boilerplate.
About
Written by Benjamin Day<br>
Pluralsight Author | Microsoft MVP | Scrum.org Professional Scrum Trainer<br>
https://www.benday.com
https://www.honestcheetah.com
info@benday.com
Got ideas for features you'd like to see? Found a bug? Let us know by submitting an issue. Want to contribute? Submit a pull request.
Source code
API Documentation
NuGet Package
Features
- Named commands with descriptions and categories
- Typed arguments:
String,Boolean,Int32,DateTime,File,Directory - Fluent argument definition API with required/optional, default values, and allowed values
- Automatic argument parsing and validation
- Built-in
--helpusage display that reports each argument's default value - Command name aliases, including aliases that supply preset argument values
- Reuse command logic by running one command from inside another
- Dependency injection support
- Configuration from JSON files, environment variables, and custom sources
- Arguments that pull values from configuration via
FromConfig() - Async command support
--jsonschema output for tooling integrationguicommand to launch a web UI via Benday.CommandsFramework.CmdUi
Table of Contents
- Installation
- Getting Started
- Argument Types
- Default Values
- Command Aliases
- Reusing Command Logic
- Configuration
- Dependency Injection
- Async Commands
- Data Formatting Utilities
- CommandsApp Builder Reference
- Built-in Keywords
- About
Installation
dotnet add package Benday.CommandsFramework
Getting Started
1. Create a Command
Commands inherit from SynchronousCommand, AsynchronousCommand, or DependencyInjectionCommand. Use the [Command] attribute to define the command name and description.
using Benday.CommandsFramework;
[Command(Name = "greet",
Description = "Says hello to someone",
Category = "Demo")]
public class GreetCommand : SynchronousCommand
{
public GreetCommand(CommandExecutionInfo info, ITextOutputProvider outputProvider)
: base(info, outputProvider) { }
public override ArgumentCollection GetArguments()
{
var args = new ArgumentCollection();
args.AddString("name").AsRequired().WithDescription("Name of the person to greet");
args.AddBoolean("loud").AsNotRequired().AllowEmptyValue().WithDescription("Greet loudly");
return args;
}
protected override void OnExecute()
{
var name = Arguments.GetStringValue("name");
var loud = Arguments.GetBooleanValue("loud");
var message = $"Hello, {name}!";
WriteLine(loud ? message.ToUpper() : message);
}
}
2. Set Up Program.cs
Use the CommandsApp builder to configure and run your CLI app. Pass any command type from your assembly to Create<T>() — the framework discovers all [Command]-attributed classes in that assembly.
using Benday.CommandsFramework;
CommandsApp
.Create<GreetCommand>(args)
.WithAppInfo("My CLI Tool", "https://www.example.com")
.WithVersionFromAssembly()
.Run();
3. Run It
dotnet run -- greet /name:World
# Output: Hello, World!
dotnet run -- greet /name:World /loud
# Output: HELLO, WORLD!
dotnet run -- greet --help
# Output: Usage information for the greet command
Argument Types
Define arguments using the fluent API in GetArguments():
public override ArgumentCollection GetArguments()
{
var args = new ArgumentCollection();
args.AddString("name").AsRequired().WithDescription("Your name");
args.AddInt32("count").AsRequired().WithDescription("Number of times");
args.AddBoolean("verbose").AsNotRequired().AllowEmptyValue();
args.AddDateTime("start-date").AsRequired().WithDescription("Start date");
args.AddFile("input").AsRequired().WithDescription("Input file path");
args.AddDirectory("output-dir").AsNotRequired().WithDescription("Output directory");
// Restrict to specific values
args.AddString("format").AsRequired().WithAllowedValues("json", "xml", "csv");
// Set a default value
args.AddString("env").AsNotRequired().WithDefaultValue("production");
return args;
}
Arguments are passed on the command line using /name:value syntax. Boolean flags with AllowEmptyValue() can be passed as just /name (presence means true).
Argument names are matched without regard to case, so /verbose, /Verbose, and /VERBOSE all reach the same argument. Argument values keep their case — only names are case-insensitive.
Positional Arguments
Use FromPositionalArgument(n) to read a value from its position on the command line instead of making the user type the argument name. Positions start at 1 and count the bare values that follow the command name:
public override ArgumentCollection GetArguments()
{
var args = new ArgumentCollection();
args.AddString("source").AsRequired()
.WithDescription("Source file")
.FromPositionalArgument(1);
args.AddString("destination").AsNotRequired()
.WithDescription("Destination file")
.FromPositionalArgument(2);
args.AddBoolean("overwrite").AsNotRequired().AllowEmptyValue()
.WithDescription("Overwrite the destination");
return args;
}
mytool copy input.txt output.txt
mytool copy input.txt output.txt /overwrite
Named arguments do not consume positions, so they can appear anywhere in the command line without shifting the positional values:
mytool copy /overwrite input.txt output.txt # source=input.txt, destination=output.txt
Unix style paths are handled correctly. A value like /home/user/data.txt contains more than one slash and no colon, so it is treated as a positional value rather than as an argument name.
Positional arguments show up in usage output wrapped in braces rather than with a leading slash:
{source:String} - Source file
[{destination:String}] - Destination file
Argument Aliases
Use WithAlias() to give an argument a second name, which is handy for offering a short form:
args.AddString("environment").AsRequired()
.WithAlias("env")
.WithDescription("Target environment");
mytool deploy /environment:production
mytool deploy /env:production # same thing
The real argument name is matched first, so an alias can never shadow another argument's name.
WithAlias() and FromPositionalArgument() both write to the same underlying alias slot — FromPositionalArgument(n) works by setting the alias to POSITION_n. Use one or the other on any given argument, not both, since the second call overwrites the first.
Friendly Names
Use WithFriendlyName() to give an argument a human readable label. This does not change the console usage output — it is carried in the --json schema and is used as the field label when the command is rendered in cmdui:
args.AddString("api-key").AsRequired()
.WithFriendlyName("API Key")
.WithDescription("Your API key");
File and Directory Existence
MustExist() and ExistenceOptional() apply to AddFile() and AddDirectory() arguments only. Calling either one on any other argument type throws an InvalidOperationException.
Existence is optional by default, so MustExist() is the one you normally reach for. When set, validation fails if the file or directory is not there:
args.AddFile("input").AsRequired()
.MustExist()
.WithDescription("Input file, must already exist");
args.AddDirectory("output-dir").AsNotRequired()
.ExistenceOptional()
.WithDescription("Output directory, created if missing");
Relative paths are resolved against the current working directory before the existence check. Read the resolved path back with the argument's AbsolutePath property, or with the GetPathToFile() / GetPathToDirectory() helpers:
var inputPath = Arguments.GetPathToFile("input", mustExist: true, fullyQualifiedPath: true);
var outputPath = Arguments.GetPathToDirectory("output-dir");
Default Values
WithDefaultValue() sets the value an argument falls back to when nothing is supplied. The default is reported in the --help output on a line of its own, so users can see what a command will do before they run it:
args.AddString("thing").AsNotRequired()
.WithDescription("thing to deploy")
.WithDefaultValue("the-usual-thing");
deploy --help
** USAGE **
deploy
/environment:String - environment to deploy to
[/thing:String] - thing to deploy
(default: the-usual-thing)
The default is also reported when a command fails validation, and it always shows the configured default rather than whatever was typed on the command line. Defaults are exposed on IArgument.DefaultValue and IArgument.HasDefaultValue, and are included in the --json schema output.
Command Aliases
Short Names
Use Aliases on the [Command] attribute to give a command extra names. This is handy for offering a short form of a long command name:
[Command(Name = "generate-project-scaffolding",
Aliases = new[] { "gps", "scaffold" },
Description = "Generates project scaffolding")]
public class GenerateScaffoldingCommand : SynchronousCommand
mytool gps # same as: mytool generate-project-scaffolding
Aliases are resolved to the real command name before the command runs, so ExecutionInfo.CommandName is always the real name. A real command name always wins over an alias, so an alias can never shadow an actual command. Aliases appear next to the command in the available commands list:
generate-project-scaffolding (gps, scaffold) - Generates project scaffolding
Aliases That Supply Argument Values
Use [CommandAlias] to create a shortcut for a command that is usually run with the same set of arguments. Each entry is in name=value form; an entry with no = is treated as a flag style argument:
[Command(Name = "deploy", Description = "Deploys a thing to an environment")]
[CommandAlias("deploy-prod", "environment=production", "verbose",
Description = "Deploy to production with verbose output")]
[CommandAlias("deploy-dev", "environment=development",
Description = "Deploy to development")]
public class DeployCommand : SynchronousCommand
mytool deploy-prod # environment=production, verbose=true
mytool deploy-prod /environment:staging # environment=staging, verbose=true
The values are applied as though they had been typed on the command line, so anything actually supplied on the command line wins over them. The full order of precedence is:
command line → alias → configuration (FromConfig()) → default value
A command can have as many [CommandAlias] attributes as you like. They are listed in their own section of the usage output:
Command aliases:
deploy-dev - Deploy to development (deploy /environment:development)
deploy-prod - Deploy to production with verbose output (deploy
/environment:production /verbose)
Nothing validates aliases automatically. Call CommandAttributeUtility.GetCommandNameProblems() from a unit test to catch duplicate command names, aliases that collide with a command name or with a reserved keyword, aliases claimed by two commands, and empty aliases:
[Fact]
public void NoCommandNameProblems()
{
var util = new CommandAttributeUtility(new DefaultProgramOptions());
Assert.Empty(util.GetCommandNameProblems(typeof(MyCommand).Assembly));
}
Reusing Command Logic
A command can run another command in process rather than shelling out to the command line. Use ExecuteCommand<T>() for synchronous commands and ExecuteCommandAsync<T>() for async ones. Both return the command instance so you can read results back off it.
Expose whatever the caller needs as public properties set in OnExecute():
[Command(Name = "greeting", Description = "Builds a greeting for a person")]
public class GreetingCommand : SynchronousCommand
{
public GreetingCommand(CommandExecutionInfo info, ITextOutputProvider outputProvider)
: base(info, outputProvider) { }
public string Greeting { get; private set; } = string.Empty;
public override ArgumentCollection GetArguments()
{
var args = new ArgumentCollection();
args.AddString("name").AsRequired().WithDescription("Name of the person to greet");
return args;
}
protected override void OnExecute()
{
Greeting = $"Hello, {Arguments.GetStringValue("name")}!";
WriteLine(Greeting);
}
}
[Command(Name = "greet-everybody", Description = "Greets several people")]
public class GreetEverybodyCommand : SynchronousCommand
{
public GreetEverybodyCommand(CommandExecutionInfo info, ITextOutputProvider outputProvider)
: base(info, outputProvider) { }
public override ArgumentCollection GetArguments()
{
var args = new ArgumentCollection();
args.AddString("names").AsRequired().WithDescription("Comma separated list of names");
return args;
}
protected override void OnExecute()
{
var names = Arguments.GetStringValue("names")
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
foreach (var name in names)
{
var command = ExecuteCommand<GreetingCommand>(args => args["name"] = name);
WriteLine(command.Greeting);
}
}
}
Things worth knowing:
- The command that gets run shares the calling command's program options, configuration, and output provider.
- It runs in quiet mode by default, which suppresses its
WriteLine()output so it does not write over the calling command's output. Passquiet: falseto let it write. - A validation failure throws a
KnownExceptioninstead of printing usage information. Running a command from the command line prints usage and returns, which would leave the calling command with no way of knowing that the command never ran. - The process exit code is left alone. A command that gets run this way cannot decide the exit code for the process.
CreateCommand<T>()builds the command without running it, if you need to inspect or configure it first.- Commands nested more than
CommandFrameworkConstants.MaxCommandNestingDepthlevels deep throw, so an accidental "A calls B calls A" loop produces a clear error rather than a stack overflow.
Configuration
JSON Files and Environment Variables
CommandsApp
.Create<MyCommand>(args)
.WithAppSettings() // loads appsettings.json + env vars
.WithConfigFile("appsettings.local.json", optional: true) // additional JSON file
.WithEnvironmentVariables() // add env vars explicitly
.Run();
Custom Configuration Sources
Use ConfigureConfiguration() to add any configuration source supported by IConfigurationBuilder, such as in-memory collections:
CommandsApp
.Create<MyCommand>(args)
.WithAppSettings()
.ConfigureConfiguration(config =>
{
config.AddInMemoryCollection(new[]
{
new KeyValuePair<string, string?>("MySection:MyKey", "MyValue")
});
})
.Run();
Config-Backed Arguments
Arguments can pull their values from configuration using FromConfig(). Command-line values take precedence over config values.
public override ArgumentCollection GetArguments()
{
var args = new ArgumentCollection();
args.AddString("api-key")
.FromConfig()
.AsRequired()
.WithDescription("Your API key");
args.AddString("base-url")
.FromConfig()
.AsNotRequired()
.WithDefaultValue("https://api.example.com")
.WithDescription("API base URL");
return args;
}
Dependency Injection
Register services with ConfigureServices() and use them in commands that inherit from DependencyInjectionCommand:
// Program.cs
CommandsApp
.Create<GreetCommand>(args)
.WithAppInfo("My Tool", "https://www.example.com")
.ConfigureServices(services =>
{
services.AddSingleton<IGreetingService, GreetingService>();
})
.Run();
// Or with access to configuration:
CommandsApp
.Create<GreetCommand>(args)
.WithAppSettings()
.ConfigureServices((services, config) =>
{
services.Configure<MyOptions>(config.GetSection("MyOptions"));
services.AddSingleton<IGreetingService, GreetingService>();
})
.Run();
// Command using DI
[Command(Name = "greet", Description = "Greet with DI", IsAsync = true)]
public class GreetCommand : DependencyInjectionCommand
{
public GreetCommand(CommandExecutionInfo info, ITextOutputProvider outputProvider)
: base(info, outputProvider) { }
public override ArgumentCollection GetArguments()
{
var args = new ArgumentCollection();
args.AddString("name").AsRequired().WithDescription("Name to greet");
return args;
}
protected override Task OnExecute()
{
var service = GetRequiredService<IGreetingService>();
WriteLine(service.GetGreeting(Arguments.GetStringValue("name")));
return Task.CompletedTask;
}
}
Async Commands
For commands that need async operations, inherit from AsynchronousCommand:
[Command(Name = "fetch", Description = "Fetch data from API", IsAsync = true)]
public class FetchCommand : AsynchronousCommand
{
public FetchCommand(CommandExecutionInfo info, ITextOutputProvider outputProvider)
: base(info, outputProvider) { }
public override ArgumentCollection GetArguments()
{
var args = new ArgumentCollection();
args.AddString("url").AsRequired().WithDescription("URL to fetch");
return args;
}
protected override async Task OnExecute()
{
var url = Arguments.GetStringValue("url");
// async work here
await Task.CompletedTask;
}
}
CommandsApp Builder Reference
| Method | Description |
|---|---|
Create<TCommand>(args) |
Create builder, discover commands from the assembly containing TCommand |
Create(args, assembly) |
Create builder with explicit assembly |
WithAppInfo(name, website) |
Set application name and website |
WithAppInfo(name, version, website) |
Set application name, version, and website |
WithVersion(version) |
Set version string |
WithVersionFromAssembly() |
Auto-detect version from assembly file version |
WithAppSettings(optional) |
Load appsettings.json and environment variables |
WithConfigFile(filename, optional) |
Load an additional JSON config file |
WithEnvironmentVariables() |
Add environment variables to configuration |
ConfigureConfiguration(action) |
Direct access to IConfigurationBuilder for custom sources |
ConfigureServices(action) |
Register services for dependency injection |
ConfigureServices(action<services, config>) |
Register services with access to IConfiguration |
ConfigureOptions(action) |
Configure DefaultProgramOptions directly |
ConfigureUsageDisplay(action) |
Configure how usage/help is displayed |
UsesConfiguration(bool) |
Enable/disable built-in configuration storage |
Run() |
Build and run the application |
RunAsync() |
Build and run the application asynchronously |
Data Formatting Utilities
The framework includes utility classes in Benday.CommandsFramework.DataFormatting for working with tabular and CSV data inside your commands.
TableFormatter
Format data as aligned, column-padded tables for console output. Supports optional row filtering.
using Benday.CommandsFramework.DataFormatting;
var formatter = new TableFormatter();
formatter.AddColumn("Name");
formatter.AddColumn("Role");
formatter.AddColumn("Location");
formatter.AddData("Alice", "Developer", "Seattle");
formatter.AddData("Bob", "Designer", "Portland");
formatter.AddData("Carol", "Manager", "Denver");
WriteLine(formatter.FormatTable());
Output:
Name Role Location
Alice Developer Seattle
Bob Designer Portland
Carol Manager Denver
Use AddDataWithFilter() to only include rows where any column value contains a search string (case-insensitive):
formatter.AddDataWithFilter("port", "Bob", "Designer", "Portland"); // included
formatter.AddDataWithFilter("port", "Alice", "Developer", "Seattle"); // excluded
CsvReader
Read and iterate over CSV files or strings. Supports header rows, quoted values with embedded commas and newlines, and column access by name or index.
using Benday.CommandsFramework.DataFormatting;
// From a file
var reader = CsvReader.FromFile("/path/to/data.csv");
// Or from a string
var reader = new CsvReader("Name,Age,City\nAlice,30,Seattle\nBob,25,Portland");
foreach (var row in reader)
{
// Access by column name
var name = row["Name"];
var age = row["Age"];
// Or by index
var city = row[2];
Console.WriteLine($"{name} is {age} years old and lives in {city}");
}
CsvWriter
Build CSV data in memory, edit existing CSV content, and write to file or string. Handles quoting of values that contain commas, newlines, or quotes.
using Benday.CommandsFramework.DataFormatting;
// Create from scratch
var writer = new CsvWriter();
writer.AddColumns("Name", "Age", "City");
writer.AddRow("Alice", "30", "Seattle");
writer.AddRow("Bob", "25", "Portland");
// Save to file
writer.SaveToFile("/path/to/output.csv");
// Or get as string
var csvString = writer.ToCsvString();
Edit existing CSV data by loading from a CsvReader:
var reader = CsvReader.FromFile("/path/to/data.csv");
var writer = new CsvWriter(reader);
// Modify a value
writer.SetValue(0, "City", "Tacoma");
// Add a new row
writer.AddRow("Carol", "35", "Denver");
// Remove a row
writer.RemoveRow(1);
writer.SaveToFile("/path/to/updated.csv");
Built-in Keywords
--help— Display usage information for a command--json— Output the full command schema as JSON (used by tooling)gui— Launch the CmdUi web interface for this toolquiet— Suppress a command'sWriteLine()output. Applied automatically to commands that are run by another command.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.8)
- Microsoft.Extensions.Hosting (>= 10.0.8)
- Microsoft.Extensions.Logging (>= 10.0.8)
-
net8.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.8)
- Microsoft.Extensions.Hosting (>= 10.0.8)
- Microsoft.Extensions.Logging (>= 10.0.8)
-
net9.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.8)
- Microsoft.Extensions.Hosting (>= 10.0.8)
- Microsoft.Extensions.Logging (>= 10.0.8)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on Benday.CommandsFramework:
| Package | Downloads |
|---|---|
|
Benday.CommandsFramework.Tui
An in-process terminal UI for any tool built on Benday.CommandsFramework. Reference this package and call .WithTui() and the tool gains a 'tui' keyword that browses its commands, fills in a form and runs it without leaving the terminal. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 5.1.0 | 104 | 8/29/2026 |
| 4.18.0 | 112 | 8/15/2026 |
| 4.17.0 | 226 | 4/17/2026 |
| 4.16.0 | 192 | 3/13/2026 |
| 4.15.0 | 145 | 3/7/2026 |
| 4.14.1 | 147 | 3/5/2026 |
| 4.14.0 | 161 | 3/4/2026 |
| 4.13.0 | 375 | 11/5/2025 |
| 4.12.0 | 222 | 9/27/2025 |
| 4.11.0 | 205 | 9/26/2025 |
| 4.10.0 | 323 | 8/9/2025 |
| 4.9.0 | 383 | 6/4/2025 |
| 4.8.0 | 262 | 5/11/2025 |
| 4.7.0 | 372 | 4/16/2025 |
| 4.6.0 | 259 | 3/21/2025 |
| 4.5.0 | 222 | 3/15/2025 |
| 4.4.0 | 418 | 11/20/2024 |
| 4.3.0 | 317 | 8/19/2024 |
| 4.2.0 | 301 | 8/16/2024 |
| 4.1.1 | 267 | 8/8/2024 |
v4.18 - Argument names are now matched without regard to case, which fixes a bug where a flag style argument whose definition contained uppercase letters could never be set from the command line; Usage output now shows the configured default value for an argument, and IArgument exposes DefaultValue and HasDefaultValue; Added command name aliases via CommandAttribute.Aliases; Added CommandAliasAttribute for aliases that also supply argument values; Added CreateCommand/ExecuteCommand/ExecuteCommandAsync so commands can reuse other commands in process; The dependency injection service provider is now built once and shared by all commands instead of being rebuilt per command; The reserved 'quiet' argument now suppresses WriteLine() output; The --json schema now includes DefaultValue and HasDefaultValue on each argument plus Aliases and CommandAliases on each command; Fixed GetAvailableCommandNames() and GetAvailableCommandAttributes() returning the built-in configuration commands twice when asked about the framework assembly itself;
v4.17 - Modified CsvWriter's SaveToFile() method to write a byte order marker by default with an option to skip the BOM;
v4.16 - Added ConfigureConfiguration() method to CommandsApp builder for adding custom configuration sources (in-memory collections, additional JSON files, environment variables, etc.);
v4.15 - Added StrictArgumentValidation option to control whether unknown arguments cause validation failures (defaults to false for backward compatibility);
v4.14 - Added support for .NET 10.0; Added support for 'gui' command that launches a blazor UI for the tool using the Benday.CommandsFramework.CmdUi package; Added fluent config methods and simplified DI configuration; Added support for defining allowed argument values; Added support for defining args that pull their values from a config value;
v4.13 - Added support for writing CSV files via CsvWriter utility class;
v4.12 - Improved dependency injection support for commands;
v4.11 - Added option to get file and directory arguments as fully qualified paths;
v4.10 - Added support for .NET 9.0; Added CsvReader utility class for parsing CSV files;
v4.9 - Added ability to pass in an IServiceCollection to the commands framework via IProgramOptions in order to optionally allow commands to use dependency injection;
v4.8 - Changed visibility of runtime args collection; Added utility method to pull value from args collection or config collection;
v4.7 - Fixed bug getting --help for commands with no parameters.
v4.6 - Added support for .NET Core 9.
v4.5 - Added TableFormatter to help with formatting tabular data and filtered tabular data.
v4.4 - Added FileArgument and DirectoryArgument types.
v4.3 - Added support for GetDate -Format FileDateUniversal datetime parsing.
v4.2 - Added support for more datetime files in the DateTime argument type.
v4.1.1 - Fixed bug in DefaultProgram where it was not setting the ExitCode to 1 when there was an error.
v4.1 - Added '--help' option to default program implementation in order to display usages.
v4.0 - Refactored argument base classes to remove unnecessary constructors. Added option to get all usages in JSON format using '--json' option.
v3.4 - Changed 'display usage' on commands to return ExitCode of 1 if there is an invalid/missing parameter.
v3.3 - Fixed 'display usage' formatting bug when argument does not have a description. Added logic to DefaultProgram to set ExitCode to 1 automatically on error.
v3.2 - Bug fixes.
v3.1 - Bug fixes. Added ability to inject an instance of ITextOutputProvider for testability.
v3.0 - Upgraded to .NET 8.0. Added default commands for managing basic string configuration values. Added extension methods for working with relative paths as arguments.