mirror of
https://github.com/Tyrrrz/CliFx.git
synced 2025-10-25 15:19:17 +00:00
Add usage to readme
This commit is contained in:
248
Readme.md
248
Readme.md
@@ -23,7 +23,253 @@ CliFx is a powerful framework for building command line applications.
|
||||
|
||||
## Usage
|
||||
|
||||
To be added with a stable release...
|
||||
### Configuring application
|
||||
|
||||
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`.
|
||||
|
||||
```c#
|
||||
public static class Program
|
||||
{
|
||||
public static Task<int> Main(string[] args) =>
|
||||
new CliApplicationBuilder()
|
||||
.WithCommandsFromThisAssembly()
|
||||
.Build()
|
||||
.RunAsync(args);
|
||||
}
|
||||
```
|
||||
|
||||
The above code will create and run a default `CliApplication` that will resolve commands defined in the calling assembly.
|
||||
If you want to configure different aspects of your application, you can use the fluent interface provided by the `CliApplicationBuilder`.
|
||||
|
||||
### Defining a command
|
||||
|
||||
To define a command, you need to create a class that implements `ICommand` and decorate it with a `Command` attribute.
|
||||
To define options, the corresponding properties need to be decorated with a `CommandOption` attribute.
|
||||
|
||||
```c#
|
||||
[Command("log", Description = "Calculates the logarithm of a value.")]
|
||||
public class LogCommand : ICommand
|
||||
{
|
||||
[CommandOption("value", 'v', IsRequired = true, Description = "Value whose logarithm is to be found.")]
|
||||
public double Value { get; set; }
|
||||
|
||||
[CommandOption("base", 'b', Description = "Logarithm base.")]
|
||||
public double Base { get; set; } = 10;
|
||||
|
||||
public Task ExecuteAsync(IConsole console)
|
||||
{
|
||||
var result = Math.Log(Value, Base);
|
||||
console.Output.WriteLine(result);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Commands may or may not have a name (which is `"log"` in this case). Command that doesn't have a name is the default command, which is executed when the user doesn't specify a command. If your application doesn't define a default command, a stub will be provided at runtime automatically by CliFx.
|
||||
|
||||
Commands usually also have options, each with a name (`"value"`, `"base"`), a short name (`'v'`, `'b'`), or both. Properties marked with `CommandOption` attribute need to be public and have an accessible setter. If you want to set a default value for an option, simply set the default value for the corresponding property.
|
||||
|
||||
By implementing `ICommand`, you're required to provide an `ExecuteAsync` method. This is the method that will be called when the command is invoked. Its return type is `Task` in order to facilitate asynchronous execution, but if your command runs synchronously you can simply return `Task.CompletedTask` at the end.
|
||||
|
||||
When interacting with the console, the command is expected to use the `IConsole` instance provided as a parameter to `ExecuteAsync` instead of using `System.Console`. This makes testing easier because you will be able to substitute or mock `IConsole` in your tests.
|
||||
|
||||
Finally, the command defined above can be executed from the command line in one of the following ways:
|
||||
|
||||
- `myapp log -v 1000`
|
||||
- `myapp log --value 8 --base 2`
|
||||
- `myapp log -v 81 -b 3`
|
||||
|
||||
### Dependency injection
|
||||
|
||||
CliFx uses an implementation of `ICommandFactory` to initialize commands and by default it only works with types that have parameterless constructors.
|
||||
In real-life scenarios your commands will most likely have dependencies which need to be injected. CliFx makes it really easy to plug in any dependency container of your choice.
|
||||
|
||||
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).
|
||||
|
||||
```c#
|
||||
public static class Program
|
||||
{
|
||||
public static Task<int> Main(string[] args)
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
|
||||
// Register services
|
||||
services.AddSingleton<MyService>();
|
||||
|
||||
// Register commands
|
||||
services.AddTransient<MyCommand>();
|
||||
|
||||
var serviceProvider = new DefaultServiceProviderFactory().CreateServiceProvider(services);
|
||||
|
||||
return new CliApplicationBuilder()
|
||||
.WithCommandsFromThisAssembly()
|
||||
.UseCommandFactory(type => (ICommand) serviceProvider.GetRequiredService(type))
|
||||
.Build()
|
||||
.RunAsync(args);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Resolve specific commands or commands from other assemblies
|
||||
|
||||
In most cases, your commands will probably be defined in your main assembly, which is where CliFx will look if you initialize the application using the following code.
|
||||
|
||||
```c#
|
||||
var app = new CliApplicationBuilder().WithCommandsFromThisAssembly().Build();
|
||||
```
|
||||
|
||||
If you want to resolve specific commands or commands from another assembly you can use `WithCommand` and `WithCommandsFrom` methods to do that.
|
||||
|
||||
```c#
|
||||
var app = new CliApplicationBuilder()
|
||||
.WithCommand(typeof(CommandA)) // include CommandA specifically
|
||||
.WithCommand(typeof(CommandB)) // include CommandB specifically
|
||||
.WithCommandsFrom(typeof(CommandC).Assembly) // include all commands from assembly that contains CommandC
|
||||
.Build();
|
||||
```
|
||||
|
||||
### Child commands
|
||||
|
||||
In a more complex application you may need to build a hierarchy of commands. CliFx takes the approach of resolving hierarchy at runtime using command names so that you don't have to explicitly specify any child-parent relationships.
|
||||
|
||||
If you have a command `"cmd"` and you want to define commands `"sub1"` and `"sub2"` as its children, simply name them accordingly.
|
||||
|
||||
```c#
|
||||
[Command("cmd")]
|
||||
public class ParentCommand : ICommand
|
||||
{
|
||||
// ...
|
||||
}
|
||||
|
||||
[Command("cmd sub1")]
|
||||
public class FirstSubCommand : ICommand
|
||||
{
|
||||
// ...
|
||||
}
|
||||
|
||||
[Command("cmd sub2")]
|
||||
public class SecondSubCommand : ICommand
|
||||
{
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### Reporting errors
|
||||
|
||||
You may have noticed that commands in CliFx don't return exit codes. This is by design, exit codes are handled by `CliApplication`, not by individual commands.
|
||||
|
||||
Commands can report execution failure simply by throwing an exception, just like in any other C# code. The exit code will be automatically set to a non-zero value to indicate failure to the calling process.
|
||||
|
||||
If you want to communicate a specific error through exit code, you can throw an instance of `CommandErrorException` which takes exit code as a constructor parameter.
|
||||
|
||||
```c#
|
||||
[Command]
|
||||
public class DivideCommand : ICommand
|
||||
{
|
||||
[CommandOption("dividend", IsRequired = true)]
|
||||
public double Dividend { get; set; }
|
||||
|
||||
[CommandOption("divisor", IsRequired = true)]
|
||||
public double Divisor { get; set; }
|
||||
|
||||
public Task ExecuteAsync(IConsole console)
|
||||
{
|
||||
if (Math.Abs(Divisor) < double.Epsilon)
|
||||
{
|
||||
// Exit code will be 1337
|
||||
throw new CommandErrorException(1337, "Division by zero is not supported");
|
||||
}
|
||||
|
||||
var result = Dividend / Divisor;
|
||||
console.Output.WriteLine(result);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
Testing of commands is enabled by the `IConsole` interface.
|
||||
|
||||
The easiest way to substitute custom stdin, stdout, stderr is by using an instance of `TestConsole` class. It has multiple constructor overloads allowing you to specify the exact set of streams that you want. Streams that are not provided are treated as empty, i.e. `TestConsole` doesn't leak to `System.Console` in any way.
|
||||
|
||||
Let's assume you have a simple command such as this one.
|
||||
|
||||
```c#
|
||||
[Command]
|
||||
public class ConcatCommand : ICommand
|
||||
{
|
||||
[CommandOption("left")]
|
||||
public string Left { get; set; } = "Hello";
|
||||
|
||||
[CommandOption("right")]
|
||||
public string Right { get; set; } = "world";
|
||||
|
||||
public Task ExecuteAsync(IConsole console)
|
||||
{
|
||||
console.Output.Write(Left);
|
||||
console.Output.Write(' ');
|
||||
console.Output.Write(Right);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
By substituting `IConsole` you can write your test cases like this.
|
||||
|
||||
```c#
|
||||
[Test]
|
||||
public async Task ConcatCommand_Test()
|
||||
{
|
||||
// Arrange
|
||||
using (var stdout = new StringWriter())
|
||||
{
|
||||
var console = new TestConsole(stdout);
|
||||
|
||||
var command = new ConcatCommand
|
||||
{
|
||||
Left = "foo",
|
||||
Right = "bar"
|
||||
};
|
||||
|
||||
// Act
|
||||
await command.ExecuteAsync(console);
|
||||
|
||||
// Assert
|
||||
Assert.That(stdout.ToString(), Is.EqualTo("foo bar"));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
And if you want, you can even test the whole application in a similar fashion.
|
||||
|
||||
```c#
|
||||
[Test]
|
||||
public async Task ConcatCommand_Test()
|
||||
{
|
||||
// Arrange
|
||||
using (var stdout = new StringWriter())
|
||||
{
|
||||
var console = new TestConsole(stdout);
|
||||
|
||||
var app = new CliApplicationBuilder()
|
||||
.WithCommand(typeof(ConcatCommand))
|
||||
.UseConsole(console)
|
||||
.Build();
|
||||
|
||||
var args = new[] {"--left", "foo", "--right", "bar"};
|
||||
|
||||
// Act
|
||||
await app.RunAsync(args);
|
||||
|
||||
// Assert
|
||||
Assert.That(stdout.ToString(), Is.EqualTo("foo bar"));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Libraries used
|
||||
|
||||
|
||||
Reference in New Issue
Block a user