mirror of
https://github.com/Tyrrrz/CliFx.git
synced 2025-10-25 15:19:17 +00:00
Update readme
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 14 KiB |
441
Readme.md
441
Readme.md
@@ -8,7 +8,8 @@
|
||||
|
||||
✅ **Project status: active**.
|
||||
|
||||
CliFx is a simple to use, yet powerful framework for building command line applications. Its primary goal is to completely take over the user input layer, letting you forget about the infrastructure and instead focus on writing your application. This framework uses a declarative class-first approach for defining commands, avoiding excessive boilerplate code and complex configurations.
|
||||
CliFx is a simple to use, yet powerful framework for building command line applications.
|
||||
Its primary goal is to completely take over the user input layer, allowing you to forget about infrastructural concerns and instead focus on writing your application.
|
||||
|
||||
## Download
|
||||
|
||||
@@ -17,17 +18,15 @@ CliFx is a simple to use, yet powerful framework for building command line appli
|
||||
## Features
|
||||
|
||||
- Complete application framework, not just an argument parser
|
||||
- Requires minimal amount of code to get started
|
||||
- Configuration via attributes
|
||||
- Handles conversions to various types, including custom types
|
||||
- Supports multi-level command hierarchies
|
||||
- Exposes raw input, output, error streams to handle binary data
|
||||
- Allows graceful command cancellation
|
||||
- Prints errors and routes exit codes on exceptions
|
||||
- Provides comprehensive and colorful auto-generated help text
|
||||
- Highly testable and easy to debug
|
||||
- Comes with built-in analyzers to help catch common mistakes
|
||||
- Works with .NET Standard 2.0+, .NET Core 2.0+, .NET Framework 4.6.1+
|
||||
- Minimum boilerplate and easy to get started
|
||||
- Class-first configuration via attributes
|
||||
- Comprehensive autogenerated help text
|
||||
- Support for deeply nested command hierarchies
|
||||
- Graceful cancellation via interrupt signals
|
||||
- Support for reading and writing binary data
|
||||
- Testable console interaction layer
|
||||
- Built-in analyzers to catch configuration issues
|
||||
- Targets .NET Standard 2.0+
|
||||
- No external dependencies
|
||||
|
||||
## Screenshots
|
||||
@@ -36,25 +35,14 @@ CliFx is a simple to use, yet powerful framework for building command line appli
|
||||
|
||||
## Usage
|
||||
|
||||
- [Quick start](#quick-start)
|
||||
- [Binding arguments](#binding-arguments)
|
||||
- [Argument syntax](#argument-syntax)
|
||||
- [Value conversion](#value-conversion)
|
||||
- [Multiple commands](#multiple-commands)
|
||||
- [Reporting errors](#reporting-errors)
|
||||
- [Graceful cancellation](#graceful-cancellation)
|
||||
- [Dependency injection](#dependency-injection)
|
||||
- [Testing](#testing)
|
||||
- [Debug and preview mode](#debug-and-preview-mode)
|
||||
- [Environment variables](#environment-variables)
|
||||
|
||||
### Quick start
|
||||
|
||||

|
||||
|
||||
To turn your application into a command line interface you need to change your program's `Main` method so that it delegates execution to `CliApplication`. This is how to do it:
|
||||
To turn your program into a command line interface, modify your `Main` method so that it delegates execution to `CliApplication`.
|
||||
You can use `CliApplicationBuilder` to fluently create and configure an instance of `CliApplication`:
|
||||
|
||||
```c#
|
||||
```csharp
|
||||
public static class Program
|
||||
{
|
||||
public static async Task<int> Main() =>
|
||||
@@ -65,13 +53,15 @@ public static class Program
|
||||
}
|
||||
```
|
||||
|
||||
The above code will create and run a `CliApplication` that will resolve commands defined in the calling assembly. Using fluent interface provided by `CliApplicationBuilder` you can also easily configure other aspects of your application.
|
||||
> Note: ensure that your `Main()` method returns the integer exit code provided by `CliApplication.RunAsync()`, as shown in the above example.
|
||||
Exit code is used to communicate execution result to the parent process, so it's important that your program returns it.
|
||||
|
||||
In order to add functionality, however, you need to define at least one command. Commands are essentially entry points through which the user can interact with your application. You can think of them as something similar to controllers in ASP.NET Core.
|
||||
The code above calls `AddCommandsFromThisAssembly()` to scan and resolve command types defined within the current assembly.
|
||||
Commands are entry points, through which the user can interact with your application.
|
||||
|
||||
To define a command, just create a new class that implements the `ICommand` interface and annotate it with `[Command]` attribute:
|
||||
To define a command, create a new class by implementing the `ICommand` interface and annotate it with the `[Command]` attribute:
|
||||
|
||||
```c#
|
||||
```csharp
|
||||
[Command]
|
||||
public class HelloWorldCommand : ICommand
|
||||
{
|
||||
@@ -79,35 +69,36 @@ public class HelloWorldCommand : ICommand
|
||||
{
|
||||
console.Output.WriteLine("Hello world!");
|
||||
|
||||
// Return empty task because our command executes synchronously
|
||||
// Return default task if the command is not asynchronous
|
||||
return default;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
To implement `ICommand`, the class needs to define an `ExecuteAsync()` method. This is the method that gets called by CliFx when the user runs the application.
|
||||
In order to implement `ICommand`, the class needs to define an `ExecuteAsync(...)` method.
|
||||
This is the method that gets called by the framework when the user decides to execute this command.
|
||||
|
||||
To facilitate both asynchronous and synchronous execution, this method returns a `ValueTask`. Since the simple command above executes synchronously, we can just put `return default` at the end. In an asynchronous command, however, we would use the `async`/`await` keywords instead.
|
||||
As a parameter, this method takes an instance of `IConsole`, which is an abstraction around the system console.
|
||||
Use this abstraction in place of `System.Console` whenever you need to write output, read input, or otherwise interact with the console.
|
||||
|
||||
As a parameter, this method takes an instance of `IConsole`, an abstraction around the system console. You should use this abstraction in places where you would normally interact with `System.Console`, in order to make your command testable.
|
||||
|
||||
With this basic setup, the user can execute your application and get a greeting in return:
|
||||
With the basic setup above, the user can now run the application and get a greeting in return:
|
||||
|
||||
```sh
|
||||
> myapp.exe
|
||||
> dotnet myapp.dll
|
||||
|
||||
Hello world!
|
||||
```
|
||||
|
||||
Out of the box, your application now also supports the built-in help and version options:
|
||||
Out of the box, the application also comes with built-in `--help` and `--version` options.
|
||||
They can be used to show help text or application version respectively:
|
||||
|
||||
```sh
|
||||
> myapp.exe --help
|
||||
> dotnet myapp.dll --help
|
||||
|
||||
MyApp v1.0
|
||||
|
||||
Usage
|
||||
myapp.exe
|
||||
dotnet myapp.dll [options]
|
||||
|
||||
Options
|
||||
-h|--help Shows help text.
|
||||
@@ -115,28 +106,32 @@ Options
|
||||
```
|
||||
|
||||
```sh
|
||||
> myapp.exe --version
|
||||
> dotnet myapp.dll --version
|
||||
|
||||
v1.0
|
||||
```
|
||||
|
||||
### Binding arguments
|
||||
### Parameters and options
|
||||
|
||||
Commands can be configured to take input from command line arguments. To do that, we need to add properties to the command and annotate them with special attributes.
|
||||
Commands can be configured to take input from command line arguments.
|
||||
To do that, you need to add properties to the command class and annotate them with special attributes.
|
||||
|
||||
In CliFx, there are two types of argument bindings: **parameters** and **options**. Parameters are positional arguments that are identified by the order they appear in, while options are arguments identified by their names.
|
||||
In CliFx, there are two types of argument bindings: **parameters** and **options**.
|
||||
Parameters are positional arguments identified by the order they appear in, while options represent sets of arguments identified by their name.
|
||||
|
||||
Here's an example command that calculates a logarithm of a value, which uses both a parameter and an option:
|
||||
As an example, here's a command that calculates the logarithm of a value, using a parameter binding to specify the input and an option binding to configure the logarithm base:
|
||||
|
||||
```c#
|
||||
```csharp
|
||||
[Command]
|
||||
public class LogCommand : ICommand
|
||||
{
|
||||
// Order: 0
|
||||
[CommandParameter(0, Description = "Value whose logarithm is to be found.")]
|
||||
public double Value { get; set; }
|
||||
public double Value { get; init; }
|
||||
|
||||
// Name: --base | Short name: -b
|
||||
[CommandOption("base", 'b', Description = "Logarithm base.")]
|
||||
public double Base { get; set; } = 10;
|
||||
public double Base { get; init; } = 10;
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
@@ -148,20 +143,40 @@ public class LogCommand : ICommand
|
||||
}
|
||||
```
|
||||
|
||||
The above command has two inputs:
|
||||
|
||||
- `Value` which is a parameter with order `0`.
|
||||
- `Base` which is an option with name `base` and short name `b`.
|
||||
|
||||
Let's try running `--help` to see how this command is supposed to be used:
|
||||
In order to execute this command, at a minimum, the user needs to provide the input value:
|
||||
|
||||
```sh
|
||||
> myapp.exe --help
|
||||
> dotnet myapp.dll 10000
|
||||
|
||||
4
|
||||
```
|
||||
|
||||
They can also pass the `base` option to override the default logarithm base of 10:
|
||||
|
||||
```sh
|
||||
> dotnet myapp.dll 729 -b 3
|
||||
|
||||
6
|
||||
```
|
||||
|
||||
In case the user forgets to specify the `value` parameter, the application will exit with an error:
|
||||
|
||||
```sh
|
||||
> dotnet myapp.dll -b 10
|
||||
|
||||
Missing parameter(s):
|
||||
<value>
|
||||
```
|
||||
|
||||
Available parameters and options are also listed in the command's help text, which can be accessed by passing the `--help` option:
|
||||
|
||||
```sh
|
||||
> dotnet myapp.dll --help
|
||||
|
||||
MyApp v1.0
|
||||
|
||||
Usage
|
||||
myapp.exe <value> [options]
|
||||
dotnet myapp.dll <value> [options]
|
||||
|
||||
Parameters
|
||||
* value Value whose logarithm is to be found.
|
||||
@@ -172,51 +187,21 @@ Options
|
||||
--version Shows version information.
|
||||
```
|
||||
|
||||
As we can see, in order to execute this command, at a minimum, the user has to supply a value:
|
||||
Overall, parameters and options are both used to consume input from the command line, but they differ in a few important ways:
|
||||
|
||||
```sh
|
||||
> myapp.exe 10000
|
||||
- Parameters are identified by their relative order. Options are identified by their name or a single-character short name.
|
||||
- Parameters technically also have a name, but it's only used in the help text.
|
||||
- Parameters are always required. Options are normally optional, but can also be configured to require a value.
|
||||
- Options can be configured to use an environment variable as a fallback.
|
||||
- Both parameters and options can take multiple values, but there can only be one such parameter in a command and it must be the last in order. Options are not limited in this regard.
|
||||
|
||||
4
|
||||
```
|
||||
|
||||
They can also set the non-required `base` option to override the default logarithm base of 10:
|
||||
|
||||
```sh
|
||||
> myapp.exe 729 -b 3
|
||||
|
||||
6
|
||||
```
|
||||
|
||||
```sh
|
||||
> myapp.exe 123 --base 4.5
|
||||
|
||||
3.199426017362198
|
||||
```
|
||||
|
||||
On the other hand, if the user fails to provide the parameter, they will get an error, as parameters are always required:
|
||||
|
||||
```sh
|
||||
> myapp.exe -b 10
|
||||
|
||||
Missing value for parameter <value>.
|
||||
```
|
||||
|
||||
Overall, the difference between parameters and options is as follows:
|
||||
|
||||
- Parameters are identified by their relative order. Options are identified by two dashes followed by their name, or a single dash followed by their short name (single character).
|
||||
- Parameters can't be optional. Options are usually optional (as evident by the name), but can be configured to be required as well.
|
||||
- Parameters technically have a name, but it's only used in the help text.
|
||||
- Options can be configured to use the value of an environment variable as a fallback.
|
||||
- Both parameters and options can take multiple values, but such a parameter must be last in order to avoid ambiguity. Options are not limited in this aspect.
|
||||
|
||||
As a general guideline, prefer to use parameters for required inputs that the command can't work without. Use options for non-required inputs, or when the command has too many required inputs, or when specifying the option name explicitly provides a better user experience.
|
||||
As a general guideline, it's recommended to use parameters for required inputs that the command can't function without.
|
||||
Use options for all other non-required inputs or when specifying the name explicitly makes the usage clearer.
|
||||
|
||||
### Argument syntax
|
||||
|
||||
This library supports an argument syntax which is based on the POSIX standard. To be fair, nobody really knows what the standard is about and very few tools actually follow it to the letter, so for the purpose of having dashes and spaces, CliFx is using the "standard command line syntax".
|
||||
|
||||
More specifically, the following examples are all valid:
|
||||
This library employs the POSIX argument syntax, which is used in most modern command line tools.
|
||||
Here are some examples of how it works:
|
||||
|
||||
- `myapp --foo bar` sets option `"foo"` to value `"bar"`
|
||||
- `myapp -f bar` sets option `'f'` to value `"bar"`
|
||||
@@ -226,51 +211,54 @@ More specifically, the following examples are all valid:
|
||||
- `myapp -xqf bar` sets options `'x'` and `'q'` without value, and option `'f'` to value `"bar"`
|
||||
- `myapp -i file1.txt file2.txt` sets option `'i'` to a sequence of values `"file1.txt"` and `"file2.txt"`
|
||||
- `myapp -i file1.txt -i file2.txt` sets option `'i'` to a sequence of values `"file1.txt"` and `"file2.txt"`
|
||||
- `myapp cmd abc -o` routes to command `cmd` (assuming it exists) with parameter `abc` and sets option `'o'` without value
|
||||
- `myapp cmd abc -o` routes to command `cmd` (assuming it's an existing command) with parameter `abc` and sets option `'o'` without value
|
||||
|
||||
Argument parsing in CliFx aims to be as deterministic as possible, ideally yielding the same result no matter the context. The only context-sensitive part in the parser is the command name resolution which needs to know what commands are available in order to discern between arguments that correspond to the command name and arguments which are parameters.
|
||||
Additionally, argument parsing in CliFx aims to be as deterministic as possible, ideally yielding the same result regardless of the application configuration.
|
||||
In fact, the only context-sensitive part in the parser is the command name resolution, which needs to know the list of available commands in order to discern between arguments that correspond to command name and arguments which map as parameters.
|
||||
|
||||
An option is always parsed the same way, regardless of the arity of the actual property it's bound to. This means that `myapp -i file1.txt file2.txt` will _always_ be parsed as an option set to multiple values, even if the underlying property is not enumerable. For the same reason, unseparated arguments such as `myapp -ofile` will be treated as five distinct options `'o'`, `'f'`, `'i'`, `'l'`, `'e'`, instead of `'o'` being set to `"file"`.
|
||||
The parser's context-free nature has several implications on how it consumes arguments.
|
||||
For example, passing `myapp -i file1.txt file2.txt` will always be parsed as an option with multiple values, regardless of the arity of the underlying property it's bound to.
|
||||
Similarly, unseparated arguments in the form of `myapp -ofile` will be treated as five distinct options `'o'`, `'f'`, `'i'`, `'l'`, `'e'`, instead of `'o'` being set to value `"file"`.
|
||||
|
||||
Because of these rules, order of arguments is semantically important and it always goes like this:
|
||||
Because of these rules, order of arguments is semantically important and must always follow this pattern:
|
||||
|
||||
```ini
|
||||
{directives} {command name} {parameters} {options}
|
||||
[directives] [command name] [parameters] [options]
|
||||
```
|
||||
|
||||
The above design makes the usage of your applications a lot more intuitive and predictable, providing a better end-user experience.
|
||||
|
||||
### Value conversion
|
||||
|
||||
Parameters and options can have different underlying types:
|
||||
Parameters and options can have the following underlying types:
|
||||
|
||||
- Standard types
|
||||
- Primitive types (`int`, `bool`, `double`, `ulong`, `char`, etc.)
|
||||
- Date and time types (`DateTime`, `DateTimeOffset`, `TimeSpan`)
|
||||
- Enum types (converted from either name or value)
|
||||
- Enum types (converted from either name or numeric value)
|
||||
- String-initializable types
|
||||
- Types with a constructor that accepts a single `string` parameter (`FileInfo`, `DirectoryInfo`, etc.)
|
||||
- Types with a static method `Parse` that accepts a single `string` parameter (and optionally `IFormatProvider`)
|
||||
- Any other type if a custom converter is specified
|
||||
- Types with a constructor accepting `string` (`FileInfo`, `DirectoryInfo`, etc.)
|
||||
- Types with a static `Parse(...)` method accepting `string` and optionally `IFormatProvider` (`Guid`, `Uri`, etc.)
|
||||
- Nullable versions of all above types (`decimal?`, `TimeSpan?`, etc.)
|
||||
- Any other type if a custom converter is specified
|
||||
- Collections of all above types
|
||||
- Array types (`T[]`)
|
||||
- Types that are assignable from arrays (`IReadOnlyList<T>`, `ICollection<T>`, etc.)
|
||||
- Types with a constructor that accepts a single `T[]` parameter (`HashSet<T>`, `List<T>`, etc.)
|
||||
|
||||
When defining a parameter of an enumerable type, keep in mind that it has to be the only such parameter and it must be the last in order. Options, on the other hand, don't have this limitation.
|
||||
- Types with a constructor accepting an array (`List<T>`, `HashSet<T>`, etc.)
|
||||
|
||||
- Example command with a custom converter:
|
||||
|
||||
```c#
|
||||
```csharp
|
||||
// Maps 2D vectors from AxB notation
|
||||
public class VectorConverter : ArgumentValueConverter<Vector2>
|
||||
public class VectorConverter : BindingConverter<Vector2>
|
||||
{
|
||||
public override Vector2 ConvertFrom(string value)
|
||||
public override Vector2 Convert(string? rawValue)
|
||||
{
|
||||
var components = value.Split('x', 'X', ';');
|
||||
if (string.IsNullOrWhiteSpace(rawValue))
|
||||
return default;
|
||||
|
||||
var components = rawValue.Split('x', 'X', ';');
|
||||
var x = int.Parse(components[0], CultureInfo.InvariantCulture);
|
||||
var y = int.Parse(components[1], CultureInfo.InvariantCulture);
|
||||
|
||||
return new Vector2(x, y);
|
||||
}
|
||||
}
|
||||
@@ -280,13 +268,13 @@ public class SurfaceCalculatorCommand : ICommand
|
||||
{
|
||||
// Custom converter is used to map raw argument values
|
||||
[CommandParameter(0, Converter = typeof(VectorConverter))]
|
||||
public Vector2 PointA { get; set; }
|
||||
public Vector2 PointA { get; init; }
|
||||
|
||||
[CommandParameter(1, Converter = typeof(VectorConverter))]
|
||||
public Vector2 PointB { get; set; }
|
||||
public Vector2 PointB { get; init; }
|
||||
|
||||
[CommandParameter(2, Converter = typeof(VectorConverter))]
|
||||
public Vector2 PointC { get; set; }
|
||||
public Vector2 PointC { get; init; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
@@ -294,7 +282,6 @@ public class SurfaceCalculatorCommand : ICommand
|
||||
var b = (PointC - PointB).Length();
|
||||
var c = (PointA - PointC).Length();
|
||||
|
||||
// Heron's formula
|
||||
var p = (a + b + c) / 2;
|
||||
var surface = Math.Sqrt(p * (p - a) * (p - b) * (p - c));
|
||||
|
||||
@@ -306,21 +293,21 @@ public class SurfaceCalculatorCommand : ICommand
|
||||
```
|
||||
|
||||
```sh
|
||||
> myapp.exe 0x0 0x18 24x0
|
||||
> dotnet myapp.dll 0x0 0x18 24x0
|
||||
|
||||
Triangle surface area: 216
|
||||
```
|
||||
|
||||
- Example command with an array-backed parameter:
|
||||
|
||||
```c#
|
||||
```csharp
|
||||
[Command]
|
||||
public class FileSizeCalculatorCommand : ICommand
|
||||
{
|
||||
// FileInfo is string-initializable and IReadOnlyList<T> can be assgined from an array,
|
||||
// so the value of this property can be mapped from a sequence of arguments.
|
||||
[CommandParameter(0)]
|
||||
public IReadOnlyList<FileInfo> Files { get; set; }
|
||||
public IReadOnlyList<FileInfo> Files { get; init; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
@@ -334,19 +321,19 @@ public class FileSizeCalculatorCommand : ICommand
|
||||
```
|
||||
|
||||
```sh
|
||||
> myapp.exe file1.bin file2.exe
|
||||
> dotnet myapp.dll file1.bin file2.exe
|
||||
|
||||
Total file size: 186368 bytes
|
||||
```
|
||||
|
||||
Same command, but using an option for the list of files instead:
|
||||
|
||||
```c#
|
||||
```csharp
|
||||
[Command]
|
||||
public class FileSizeCalculatorCommand : ICommand
|
||||
{
|
||||
[CommandOption("files")]
|
||||
public IReadOnlyList<FileInfo> Files { get; set; }
|
||||
public IReadOnlyList<FileInfo> Files { get; init; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
@@ -360,18 +347,20 @@ public class FileSizeCalculatorCommand : ICommand
|
||||
```
|
||||
|
||||
```sh
|
||||
> myapp.exe --files file1.bin file2.exe
|
||||
> dotnet myapp.dll --files file1.bin file2.exe
|
||||
|
||||
Total file size: 186368 bytes
|
||||
```
|
||||
|
||||
### Multiple commands
|
||||
|
||||
Complex command line applications may have more than a single command in order to facilitate different workflows. In even more complex applications there may be multiple levels of commands, forming a hierarchy.
|
||||
In order to facilitate a variety of different workflows, command line applications may provide the user with more than just a single command.
|
||||
Complex applications may also nest commands within each other, employing a multi-level hierarchical structure.
|
||||
|
||||
Whichever case it is, CliFx takes care of everything for you. All you need to do is specify appropriate command names in the attributes:
|
||||
With CliFx, this is achieved by simply giving each command a unique name through the `[Command]` attribute.
|
||||
Commands that have common name segments are considered to be hierarchically related, which affects how they're listed in the help text.
|
||||
|
||||
```c#
|
||||
```csharp
|
||||
// Default command, i.e. command without a name
|
||||
[Command]
|
||||
public class DefaultCommand : ICommand
|
||||
@@ -401,36 +390,39 @@ public class SubCommand : ICommand
|
||||
}
|
||||
```
|
||||
|
||||
There is no limit to the number of commands or the level of their nesting. Once configured, the user can execute a specific command by typing its name before any other arguments, e.g. `myapp.exe cmd1 arg1 -p 42`.
|
||||
Once configured, the user can execute a specific command by including its name in the passed arguments.
|
||||
For example, running `dotnet myapp.dll cmd1 arg1 -p 42` will execute `FirstCommand` in the above example.
|
||||
|
||||
Requesting help on the application above will show:
|
||||
Requesting help will show direct subcommands of the current command:
|
||||
|
||||
```sh
|
||||
> myapp.exe --help
|
||||
> dotnet myapp.dll --help
|
||||
|
||||
MyApp v1.0
|
||||
|
||||
Usage
|
||||
myapp.exe [command]
|
||||
dotnet myapp.dll [options]
|
||||
dotnet myapp.dll [command] [...]
|
||||
|
||||
Options
|
||||
-h|--help Shows help text.
|
||||
--version Shows version information.
|
||||
|
||||
Commands
|
||||
cmd1
|
||||
cmd1 Subcommands: cmd1 sub.
|
||||
cmd2
|
||||
|
||||
You can run `myapp.exe [command] --help` to show help on a specific command.
|
||||
You can run `dotnet myapp.dll [command] --help` to show help on a specific command.
|
||||
```
|
||||
|
||||
As you can see, only two commands are listed here because `cmd1 sub` is not an immediate child of the default command. We can further refine our help query to get information on `cmd1`:
|
||||
The user can also refine their help request by querying it on a specific command:
|
||||
|
||||
```sh
|
||||
> myapp.exe cmd1 --help
|
||||
> dotnet myapp.dll cmd1 --help
|
||||
|
||||
Usage
|
||||
myapp.exe cmd1 [command]
|
||||
dotnet myapp.dll cmd1 [options]
|
||||
dotnet myapp.dll cmd1 [command] [...]
|
||||
|
||||
Options
|
||||
-h|--help Shows help text.
|
||||
@@ -438,28 +430,26 @@ Options
|
||||
Commands
|
||||
sub
|
||||
|
||||
You can run `myapp.exe cmd1 [command] --help` to show help on a specific command.
|
||||
You can run `dotnet myapp.dll cmd1 [command] --help` to show help on a specific command.
|
||||
```
|
||||
|
||||
In a multi-command application you may also choose to not have a default command and only use named commands. If that's the case, running an application without parameters will simply print help text.
|
||||
> Note that defining a default (unnamed) command is not required.
|
||||
In the even of its absence, running the application without specifying a command will just show root level help text.
|
||||
|
||||
### Reporting errors
|
||||
|
||||
You may have noticed that commands in CliFx don't return exit codes. This is by design as exit codes are considered an infrastructural concern and thus handled by `CliApplication`, not by individual commands.
|
||||
Commands in CliFx do not directly return exit codes, but can instead communicate execution errors by throwing `CommandException`.
|
||||
This special exception can be used to print an error message to the console, return a specific exit code, and also optionally show help text for the current command:
|
||||
|
||||
Commands can instead report execution failure by throwing an instance of `CommandException`. Using this exception, you can specify the message printed to stderr and the returned exit code.
|
||||
|
||||
Here is an example:
|
||||
|
||||
```c#
|
||||
```csharp
|
||||
[Command]
|
||||
public class DivideCommand : ICommand
|
||||
{
|
||||
[CommandOption("dividend", IsRequired = true)]
|
||||
public double Dividend { get; set; }
|
||||
public double Dividend { get; init; }
|
||||
|
||||
[CommandOption("divisor", IsRequired = true)]
|
||||
public double Divisor { get; set; }
|
||||
public double Divisor { get; init; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
@@ -478,7 +468,7 @@ public class DivideCommand : ICommand
|
||||
```
|
||||
|
||||
```sh
|
||||
> myapp.exe --dividend 10 --divisor 0
|
||||
> dotnet myapp.dll --dividend 10 --divisor 0
|
||||
|
||||
Division by zero is not supported.
|
||||
|
||||
@@ -488,33 +478,18 @@ Division by zero is not supported.
|
||||
133
|
||||
```
|
||||
|
||||
You can also specify the `showHelp` parameter to instruct whether to show the help text for the current command after printing the error:
|
||||
|
||||
```c#
|
||||
[Command]
|
||||
public class ExampleCommand : ICommand
|
||||
{
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
throw new CommandException("Something went wrong.", showHelp: true);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> Note: Unix systems rely on 8-bit unsigned integers for exit codes, so it's strongly recommended to use values between `1` and `255` when specifying exit code, in order to avoid potential overflow issues.
|
||||
> Note that Unix systems rely on 8-bit unsigned integers to represent exit codes, which means that you can only use values between `1` and `255` to indicate an unsuccessful execution result.
|
||||
|
||||
### Graceful cancellation
|
||||
|
||||
The user may abort execution by sending an interrupt signal (Ctrl+C or Ctrl+Break). If your command has critical disposable resources, you can intercept this signal to perform cleanup before exiting.
|
||||
Console applications support the concept of interrupt signals, which can be issued by the user to abort the currently ongoing operation.
|
||||
If your command performs critical work, you can intercept these signals to handle cancellation requests in a graceful way.
|
||||
|
||||
In order to make a command cancellation-aware, all you need to do is call `console.GetCancellationToken()`. This method returns a `CancellationToken` that will trigger when the user issues an interrupt signal.
|
||||
To make command cancellation-aware, call `console.RegisterCancellationHandler()`.
|
||||
This method configures a handler that listens for interrupt signals on the console and returns a `CancellationToken` that represents a potential cancellation request.
|
||||
|
||||
Note that any operation which precedes `console.GetCancellationToken()` will not be cancellation-aware and as such will not delay the process termination. Calling this method multiple times is fine, as it will always return the same token.
|
||||
|
||||
Here's an example of a command that supports cancellation:
|
||||
|
||||
```c#
|
||||
[Command("cancel")]
|
||||
```csharp
|
||||
[Command]
|
||||
public class CancellableCommand : ICommand
|
||||
{
|
||||
private async ValueTask DoSomethingAsync(CancellationToken cancellation)
|
||||
@@ -525,7 +500,7 @@ public class CancellableCommand : ICommand
|
||||
public async ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
// Make the command cancellation-aware
|
||||
var cancellation = console.GetCancellationToken();
|
||||
var cancellation = console.RegisterCancellationHandler();
|
||||
|
||||
// Execute some long-running cancellable operation
|
||||
await DoSomethingAsync(cancellation);
|
||||
@@ -535,17 +510,20 @@ public class CancellableCommand : ICommand
|
||||
}
|
||||
```
|
||||
|
||||
Keep in mind that a command may delay cancellation only once. If the user decides to send an interrupt signal for the second time, the execution will be terminated immediately.
|
||||
> Note that a command may use this approach to delay cancellation only once.
|
||||
If the user issues a second interrupt signal, the application will be immediately terminated.
|
||||
|
||||
### Dependency injection
|
||||
### Type activation
|
||||
|
||||
CliFx uses an implementation of `ITypeActivator` to initialize commands and by default it only works with types that have parameter-less constructors. This is sufficient for majority of scenarios.
|
||||
Because CliFx takes responsibility for the application's entire lifecycle, it needs to be capable of instantiating various user-defined types at runtime.
|
||||
To facilitate that, it uses an interface called `ITypeActivator` that determines how to create a new instance of a given type.
|
||||
|
||||
However, in some cases you may also want to initialize commands dynamically with the help of a dependency injection container. CliFx makes it really easy to integrate with any DI framework of your choice.
|
||||
The default implementation of `ITypeActivator` only supports types that have public parameterless constructors, which is sufficient for majority of scenarios.
|
||||
However, in some cases you may also want to define a custom initializer, for example when integrating with an external dependency container.
|
||||
|
||||
For example, here is how you would configure your application to use [`Microsoft.Extensions.DependencyInjection`](https://nuget.org/packages/Microsoft.Extensions.DependencyInjection) (aka the built-in dependency container in ASP.NET Core):
|
||||
The following snippet shows how to configure your application to use [`Microsoft.Extensions.DependencyInjection`](https://nuget.org/packages/Microsoft.Extensions.DependencyInjection) as the type activator:
|
||||
|
||||
```c#
|
||||
```csharp
|
||||
public static class Program
|
||||
{
|
||||
public static async Task<int> Main()
|
||||
@@ -571,40 +549,20 @@ public static class Program
|
||||
|
||||
### Testing
|
||||
|
||||
CliFx provides a convenient way to write functional tests for your applications, thanks to the `IConsole` interface. While a command running in production uses `SystemConsole` for console interactions, you can rely on `VirtualConsole` in your tests to validate these interactions in a simulated environment.
|
||||
Thanks to the `IConsole` abstraction, CliFx commands can be easily tested in isolation.
|
||||
While an application running in production would rely on `SystemConsole` to interact with the real console, you can use `FakeConsole` and `FakeInMemoryConsole` in your tests to execute your commands in a simulated environment.
|
||||
|
||||
When you initialize an instance of `VirtualConsole`, you can supply your own streams which will be used as the application's stdin, stdout, and stderr. You don't have to supply all of them, however, and any remaining streams will be substituted with a no-op stub.
|
||||
For example, imagine you have the following command:
|
||||
|
||||
```c#
|
||||
var console = new VirtualConsole(
|
||||
input: stdIn,
|
||||
output: stdOut,
|
||||
error: stdErr
|
||||
);
|
||||
```
|
||||
|
||||
Although `VirtualConsole` can be constructed with all kinds of streams, most of the time you will want to test against in-memory stores. To simplify setup in such scenarios, CliFx also provides a `CreateBuffered` factory method that returns an instance of `IConsole` along with in-memory streams that you can later read from:
|
||||
|
||||
```c#
|
||||
var (console, stdOut, stdErr) = VirtualConsole.CreateBuffered();
|
||||
|
||||
// ...
|
||||
|
||||
// Get the text that was written so far
|
||||
var stdOutData = stdOut.GetString();
|
||||
```
|
||||
|
||||
To illustrate how to use this, let's look at an example. Assume you want to test a simple command such as this one:
|
||||
|
||||
```c#
|
||||
```csharp
|
||||
[Command]
|
||||
public class ConcatCommand : ICommand
|
||||
{
|
||||
[CommandOption("left")]
|
||||
public string Left { get; set; } = "Hello";
|
||||
public string Left { get; init; } = "Hello";
|
||||
|
||||
[CommandOption("right")]
|
||||
public string Right { get; set; } = "world";
|
||||
public string Right { get; init; } = "world";
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
@@ -617,15 +575,15 @@ public class ConcatCommand : ICommand
|
||||
}
|
||||
```
|
||||
|
||||
By substituting `IConsole` you can write your test cases like so:
|
||||
To test it, you can instantiate the command in code with the required values, and then pass an instance of `FakeInMemoryConsole` as parameter to `ExecuteAsync(...)`:
|
||||
|
||||
```c#
|
||||
```csharp
|
||||
// Integration test at the command level
|
||||
[Test]
|
||||
public async Task ConcatCommand_executes_successfully()
|
||||
{
|
||||
// Arrange
|
||||
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
||||
using var console = new FakeInMemoryConsole();
|
||||
|
||||
var command = new ConcatCommand
|
||||
{
|
||||
@@ -636,20 +594,22 @@ public async Task ConcatCommand_executes_successfully()
|
||||
// Act
|
||||
await command.ExecuteAsync(console);
|
||||
|
||||
var stdOut = console.ReadOutputString();
|
||||
|
||||
// Assert
|
||||
Assert.That(stdOut.GetString(), Is.EqualTo("foo bar"));
|
||||
Assert.That(stdOut, Is.EqualTo("foo bar"));
|
||||
}
|
||||
```
|
||||
|
||||
Similarly, you can also test the entire execution end-to-end like so:
|
||||
Similarly, you can also test your command at a higher level like so:
|
||||
|
||||
```c#
|
||||
```csharp
|
||||
// End-to-end test at the application level
|
||||
[Test]
|
||||
public async Task ConcatCommand_executes_successfully()
|
||||
{
|
||||
// Arrange
|
||||
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
||||
using var console = new FakeInMemoryConsole();
|
||||
|
||||
var app = new CliApplicationBuilder()
|
||||
.AddCommand<ConcatCommand>()
|
||||
@@ -662,49 +622,44 @@ public async Task ConcatCommand_executes_successfully()
|
||||
// Act
|
||||
await app.RunAsync(args, envVars);
|
||||
|
||||
var stdOut = console.ReadOutputString();
|
||||
|
||||
// Assert
|
||||
Assert.That(stdOut.GetString(), Is.EqualTo("foo bar"));
|
||||
Assert.That(stdOut, Is.EqualTo("foo bar"));
|
||||
}
|
||||
```
|
||||
|
||||
As a general recommendation, it's always more preferable to test at the application level. While you can validate your command's execution adequately simply by testing its `ExecuteAsync()` method, testing end-to-end also helps you catch bugs related to configuration, such as incorrect option names, parameter order, environment variable names, etc.
|
||||
|
||||
Additionally, it's important to remember that commands in CliFx are not constrained to text and can produce binary data. In such cases, you can still use the above setup but call `GetBytes()` instead of `GetString()`:
|
||||
|
||||
```c#
|
||||
// Act
|
||||
await app.RunAsync(args, envVars);
|
||||
|
||||
// Assert
|
||||
Assert.That(stdOut.GetBytes(), Is.EqualTo(new byte[] {1, 2, 3, 4, 5}));
|
||||
```
|
||||
|
||||
In some scenarios the binary data may be too large to load in-memory. If that's the case, it's recommended to use `VirtualConsole` directly with custom streams.
|
||||
|
||||
### Debug and preview mode
|
||||
|
||||
When troubleshooting issues, you may find it useful to run your app in debug or preview mode. To do it, simply pass the corresponding directive to your app along with any other command line arguments.
|
||||
When troubleshooting issues, you may find it useful to run your app in debug or preview mode.
|
||||
To do that, you need to pass pass the corresponding directive before any other arguments.
|
||||
|
||||
If your application is ran in debug mode (using the `[debug]` directive), it will wait for debugger to be attached before proceeding. This is useful for debugging apps that were ran outside of the IDE.
|
||||
In order to run the application in debug mode, use the `[debug]` directive.
|
||||
This will cause the program to launch in a suspended state, waiting for debugger to be attached to the process:
|
||||
|
||||
```sh
|
||||
> myapp.exe [debug] cmd -o
|
||||
> dotnet myapp.dll [debug] cmd -o
|
||||
|
||||
Attach debugger to PID 3148 to continue.
|
||||
```
|
||||
|
||||
If preview mode is specified (using the `[preview]` directive), the app will short-circuit by printing consumed command line arguments as they were parsed. This is useful when troubleshooting issues related to command routing and argument binding.
|
||||
To run the application in preview mode, use the `[preview]` directive.
|
||||
This will short-circuit the execution and instead print the consumed command line arguments as they were parsed, along with resolved environment variables:
|
||||
|
||||
```sh
|
||||
> myapp.exe [preview] cmd arg1 arg2 -o foo --option bar1 bar2
|
||||
> dotnet myapp.dll [preview] cmd arg1 arg2 -o foo --option bar1 bar2
|
||||
|
||||
Parser preview:
|
||||
cmd <arg1> <arg2> [-o foo] [--option bar1 bar2]
|
||||
Command line:
|
||||
cmd <arg1> <arg2> [-o foo] [--option bar1 bar2]
|
||||
|
||||
Environment:
|
||||
FOO="123"
|
||||
BAR="xyz"
|
||||
```
|
||||
|
||||
You can also disallow these directives, e.g. when running in production, by calling `AllowDebugMode` and `AllowPreviewMode` methods on `CliApplicationBuilder`.
|
||||
You can also disallow these directives, e.g. when running in production, by calling `AllowDebugMode(...)` and `AllowPreviewMode(...)` methods on `CliApplicationBuilder`:
|
||||
|
||||
```c#
|
||||
```csharp
|
||||
var app = new CliApplicationBuilder()
|
||||
.AddCommandsFromThisAssembly()
|
||||
.AllowDebugMode(true) // allow debug mode
|
||||
@@ -714,16 +669,15 @@ var app = new CliApplicationBuilder()
|
||||
|
||||
### Environment variables
|
||||
|
||||
An option can be configured to use the value of an environment variable as a fallback. If the value for such an option is not directly specified in the arguments, it will be extracted from that environment variable instead.
|
||||
An option can be configured to use a specific environment variable as fallback.
|
||||
If the user does not provide value for such option through command line arguments, the current value of the environment variable will be used instead.
|
||||
|
||||
Here's an example of a required option that can be either provided directly or extracted from the environment:
|
||||
|
||||
```c#
|
||||
```csharp
|
||||
[Command]
|
||||
public class AuthCommand : ICommand
|
||||
{
|
||||
[CommandOption("token", IsRequired = true, EnvironmentVariableName = "AUTH_TOKEN")]
|
||||
public string AuthToken { get; set; }
|
||||
[CommandOption("token", IsRequired = true, EnvironmentVariable = "AUTH_TOKEN")]
|
||||
public string AuthToken { get; init; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
@@ -737,12 +691,13 @@ public class AuthCommand : ICommand
|
||||
```sh
|
||||
> $env:AUTH_TOKEN="test"
|
||||
|
||||
> myapp.exe
|
||||
> dotnet myapp.dll
|
||||
|
||||
test
|
||||
```
|
||||
|
||||
Environment variables can be used as fallback for options of enumerable types too. In this case, the value of the variable will be split by `Path.PathSeparator` (which is `;` on Windows, `:` on Linux).
|
||||
Environment variables can be configured for options of non-scalar types as well.
|
||||
In such case, the values of the environment variable will be split by `Path.PathSeparator` (`;` on Windows, `:` on Linux).
|
||||
|
||||
## Etymology
|
||||
|
||||
|
||||
Reference in New Issue
Block a user