Rework the readme

This commit is contained in:
Tyrrrz
2023-03-02 11:27:43 +02:00
parent 1c331df4b1
commit a09818d452
3 changed files with 121 additions and 177 deletions

View File

@@ -31,7 +31,7 @@ internal class CommandBinder
return converter.Convert(rawValue); return converter.Convert(rawValue);
} }
// Assignable from string (e.g. string itself, object, etc) // Assignable from a string (e.g. string itself, object, etc)
if (targetType.IsAssignableFrom(typeof(string))) if (targetType.IsAssignableFrom(typeof(string)))
{ {
return rawValue; return rawValue;
@@ -77,7 +77,7 @@ internal class CommandBinder
: null; : null;
} }
// String-constructible (FileInfo, etc) // String-constructable (FileInfo, etc)
var stringConstructor = targetType.GetConstructor(new[] { typeof(string) }); var stringConstructor = targetType.GetConstructor(new[] { typeof(string) });
if (stringConstructor is not null) if (stringConstructor is not null)
{ {
@@ -125,7 +125,7 @@ internal class CommandBinder
return array; return array;
} }
// Array-constructible (List<T>, HashSet<T>, etc) // Array-constructable (List<T>, HashSet<T>, etc)
var arrayConstructor = targetEnumerableType.GetConstructor(new[] { arrayType }); var arrayConstructor = targetEnumerableType.GetConstructor(new[] { arrayType });
if (arrayConstructor is not null) if (arrayConstructor is not null)
{ {
@@ -307,7 +307,7 @@ internal class CommandBinder
BindMember(optionSchema, commandInstance, rawValues); BindMember(optionSchema, commandInstance, rawValues);
// Required options require at least one value to be set // Required options need at least one value to be set
if (rawValues.Any()) if (rawValues.Any())
remainingRequiredOptionSchemas.Remove(optionSchema); remainingRequiredOptionSchemas.Remove(optionSchema);
} }
@@ -320,7 +320,7 @@ internal class CommandBinder
BindMember(optionSchema, commandInstance, rawValues); BindMember(optionSchema, commandInstance, rawValues);
// Required options require at least one value to be set // Required options need at least one value to be set
if (rawValues.Any()) if (rawValues.Any())
remainingRequiredOptionSchemas.Remove(optionSchema); remainingRequiredOptionSchemas.Remove(optionSchema);
} }

View File

@@ -70,8 +70,7 @@ internal partial class CommandSchema
public static bool IsCommandType(Type type) => public static bool IsCommandType(Type type) =>
type.Implements(typeof(ICommand)) && type.Implements(typeof(ICommand)) &&
type.IsDefined(typeof(CommandAttribute)) && type.IsDefined(typeof(CommandAttribute)) &&
!type.IsAbstract && type is { IsAbstract: false, IsInterface: false };
!type.IsInterface;
public static CommandSchema? TryResolve(Type type) public static CommandSchema? TryResolve(Type type)
{ {
@@ -88,7 +87,7 @@ internal partial class CommandSchema
: new[] {OptionSchema.HelpOption}; : new[] {OptionSchema.HelpOption};
var properties = type var properties = type
// Get properties directly on command type // Get properties directly on the command type
.GetProperties() .GetProperties()
// Get non-abstract properties on interfaces (to support default interfaces members) // Get non-abstract properties on interfaces (to support default interfaces members)
.Union(type .Union(type

283
Readme.md
View File

@@ -49,12 +49,14 @@ To learn more about the war and how you can help, [click here](https://tyrrrz.me
## Usage ## Usage
### Application and commands ### Quick overview
To turn your program into a command-line interface, modify your `Main` method so that it delegates execution to `CliApplication`. To turn your program into a command-line interface, modify the `Main()` method so that it delegates the execution to an instance of `CliApplication`.
You can use `CliApplicationBuilder` to fluently create and configure an instance of `CliApplication`: You can use `CliApplicationBuilder` to simplify the process of creating and configuring an application:
```csharp ```csharp
using CliFx;
public static class Program public static class Program
{ {
public static async Task<int> Main() => public static async Task<int> Main() =>
@@ -71,74 +73,18 @@ public static class Program
> **Note**: > **Note**:
> When calling `CliApplication.RunAsync()`, **CliFx** resolves command-line arguments and environment variables from `Environment.GetCommandLineArgs()` and `Environment.GetEnvironmentVariables()` respectively. > When calling `CliApplication.RunAsync()`, **CliFx** resolves command-line arguments and environment variables from `Environment.GetCommandLineArgs()` and `Environment.GetEnvironmentVariables()` respectively.
> You can also provide them manually using one of the alternative overloads.
The code above uses `AddCommandsFromThisAssembly()` to detect command types defined within the current assembly. The code above uses `AddCommandsFromThisAssembly()` to detect command types defined within the current project and register them on the application.
Commands are entry points, through which the user can interact with your application. Commands are independent entry points, through which the user can interact with your program.
To define a command, create a new class by implementing the `ICommand` interface and annotate it with the `[Command]` attribute: To define a command, create a class that implements the `ICommand` interface and annotate it with the `[Command]` attribute:
```csharp ```csharp
[Command] using CliFx;
public class HelloWorldCommand : ICommand using CliFx.Attributes;
{
public ValueTask ExecuteAsync(IConsole console)
{
console.Output.WriteLine("Hello world!");
// Return default task if the command is not asynchronous [Command(Description = "Calculates the logarithm of a value.")]
return default;
}
}
```
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.
As the only 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.
With the basic setup above, the user can now run the application and get a greeting in return:
```powershell
> dotnet myapp.dll
Hello world!
```
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:
```powershell
> dotnet myapp.dll --help
MyApp v1.0
USAGE
dotnet myapp.dll [options]
OPTIONS
-h|--help Shows help text.
--version Shows version information.
```
```powershell
> dotnet myapp.dll --version
v1.0
```
### Parameters and options
Commands can be configured to take input from command-line arguments.
To do that, you need to add properties to the command class and bind them using special attributes.
In **CliFx**, there are two types of argument bindings: **parameters** and **options**.
Parameters are bound from arguments based on the order they appear in, while options are bound by their name.
As an example, here's a command that calculates the logarithm of a value — it uses a parameter binding to specify the input value and an option binding for the logarithm base:
```csharp
[Command]
public class LogCommand : ICommand public class LogCommand : ICommand
{ {
// Order: 0 // Order: 0
@@ -155,54 +101,61 @@ public class LogCommand : ICommand
var result = Math.Log(Value, Base); var result = Math.Log(Value, Base);
console.Output.WriteLine(result); console.Output.WriteLine(result);
// If the execution is not meant to be asynchronous,
// return an empty task at the end of the method.
return default; return default;
} }
} }
``` ```
> **Note**: In order to implement `ICommand`, the class needs to define an `ExecuteAsync(...)` method.
> You can specify whether a parameter or an option is required by setting the `IsRequired` property on the attribute. This is the method that gets called by the framework when the user decides to execute the command.
> Alternatively, you can also use the `required` keyword (introduced in C# 11) on the property to mark the corresponding argument binding as required.
> **Note**: As the only parameter, this method takes an instance of `IConsole`, which is an abstraction around the system console.
> **CliFx** has built-in analyzers that detect common errors in command definitions. Use this abstraction in place of `System.Console` whenever you need to write output, read input, or otherwise interact with the console.
> Your code will not compile if the command contains duplicate options, overlapping parameters, or otherwise invalid configuration.
In order to execute this command, at a minimum, the user needs to provide the input value: In most cases, you will also want to define input bindings, which are properties annotated by the `[CommandParameter]` and `[CommandOption]` attributes.
These bindings provide a way to map command-line arguments into structured data that can be used by the command.
```powershell The command in the above example serves as a simple logarithm calculator and defines two inputs: a positional parameter for the input value and a named option for the logarithm base.
> dotnet myapp.dll 10000 In order to execute this command, at minimum, the user needs to provide the input value:
```sh
$ dotnet myapp.dll 10000
4 4
``` ```
They can also pass the `base` option to override the default logarithm base of 10: They can also pass the `-b|--base` option to override the default logarithm base of `10`:
```powershell ```sh
> dotnet myapp.dll 729 -b 3 $ dotnet myapp.dll 729 -b 3
6 6
``` ```
In case the user forgets to specify the `value` parameter, the application will exit with an error: In case the user forgets to specify the required `value` parameter, the application will instead exit with an error:
```powershell ```sh
> dotnet myapp.dll -b 10 $ dotnet myapp.dll -b 10
Missing required parameter(s): Missing required parameter(s):
<value> <value>
``` ```
Available parameters and options are also listed in the command's help text, which can be accessed by passing the `--help` option: Out of the box, **CliFx** also provides a built-in `--help` option, which generates a help screen that lists all parameters and options available for the command:
```powershell ```sh
> dotnet myapp.dll --help $ dotnet myapp.dll --help
MyApp v1.0 MyApp v1.0
USAGE USAGE
dotnet myapp.dll <value> [options] dotnet myapp.dll <value> [options]
DESCRIPTION
Calculates the logarithm of a value.
PARAMETERS PARAMETERS
* value Value whose logarithm is to be found. * value Value whose logarithm is to be found.
@@ -212,21 +165,9 @@ OPTIONS
--version Shows version information. --version Shows version information.
``` ```
Overall, parameters and options are both used to consume input from the command-line, but they differ in a few important ways:
| | Parameter | Option |
| ------------------ | ------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------- |
| **Identification** | Positional (by relative order). | Nominal (by name or short name). |
| **Requiredness** | Required by default. Only the last parameter can be configured to be optional. | Optional by default. Any option can be configured to be required without limitations. |
| **Arity** | Only the last parameter can be bound to a non-scalar property (i.e. an array). | Any option can be bound to a non-scalar property without limitations. |
| **Fallback** | — | Can be configured to use an environment variable as fallback if the value isn't explicitly provided. |
As a general guideline, 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 ### Argument syntax
This library employs the POSIX argument syntax, which is used in most modern command-line tools. This library employs a variation of the POSIX argument syntax, which is used in most modern command-line tools.
Here are some examples of how it works: Here are some examples of how it works:
- `myapp --foo bar` sets option `"foo"` to value `"bar"` - `myapp --foo bar` sets option `"foo"` to value `"bar"`
@@ -248,10 +189,67 @@ Similarly, unseparated arguments in the form of `myapp -ofile` will be treated a
These rules also make the order of arguments important — command-line string is expected to follow this pattern: These rules also make the order of arguments important — command-line string is expected to follow this pattern:
```powershell ```sh
> myapp [...directives] [command] [...parameters] [...options] $ myapp [...directives] [command] [...parameters] [...options]
``` ```
### Parameters and options
**CliFx** supports two types of argument bindings: **parameters** and **options**.
Parameters are bound from arguments based on the order they appear in, while options are bound by their name.
Besides that, they also differ in the following ways:
- Parameters are required by default, while options are not.
- You can make an option required by setting `IsRequired = true` on the corresponding attribute or by adding the `required` keyword to the property declaration (introduced in C# 11):
```csharp
// Any option can be required or optional without restrictions
[CommandOption("foo")]
public required string RequiredOption { get; init; }
```
- To make a parameter optional, you can set `IsRequired = false`, but only the last parameter (by order) can be configured in such way:
```csharp
// Only the last parameter can be optional
[CommandParameter(0, IsRequired = false)]
public string? OptionalParameter { get; init; }
```
- Parameters are primarily used for scalar (non-enumerable) properties, while options can be used for both scalar and non-scalar properties.
- You can bind an option to a property of a non-scalar type, such as `IReadOnlyList<T>`:
```csharp
// Any option can be non-scalar
[CommandOption("foo")]
public required IReadOnlyList<string> NonScalarOption { get; init; }
```
- You can bind a parameter to a non-scalar property, but only if it's the last parameter in the command:
```csharp
// Only the last parameter can be non-scalar
[CommandParameter(0)]
public required IReadOnlyList<string> NonScalarParameter { get; init; }
```
- Options can rely on an environment variable for fallback, while parameters cannot:
```csharp
// If the value is not provided directly, it will be read
// from the environment variable instead.
// This works for both scalar and non-scalar properties.
[CommandOption("foo", EnvironmentVariable = "ENV_FOO")]
public required string OptionWithFallback { get; init; }
```
> **Note**:
> **CliFx** has a set of built-in analyzers that detect common errors in command definitions.
> Your code will not compile if a command contains duplicate options, overlapping parameters, or otherwise invalid configuration.
### Value conversion ### Value conversion
Parameters and options can be bound to properties with the following underlying types: Parameters and options can be bound to properties with the following underlying types:
@@ -294,34 +292,8 @@ public class FileSizeCalculatorCommand : ICommand
} }
``` ```
```powershell ```sh
> dotnet myapp.dll 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:
```csharp
[Command]
public class FileSizeCalculatorCommand : ICommand
{
[CommandOption("files")]
public required IReadOnlyList<FileInfo> Files { get; init; }
public ValueTask ExecuteAsync(IConsole console)
{
var totalSize = Files.Sum(f => f.Length);
console.Output.WriteLine($"Total file size: {totalSize} bytes");
return default;
}
}
```
```powershell
> dotnet myapp.dll --files file1.bin file2.exe
Total file size: 186368 bytes Total file size: 186368 bytes
``` ```
@@ -376,38 +348,12 @@ public class SurfaceCalculatorCommand : ICommand
} }
``` ```
### Environment variables ```sh
$ dotnet myapp.dll 0x0 0x10 10x0
An option can be configured to use a specific environment variable as fallback. Triangle surface area: 50
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.
```csharp
[Command]
public class AuthCommand : ICommand
{
[CommandOption("token", EnvironmentVariable = "AUTH_TOKEN")]
public required string AuthToken { get; init; }
public ValueTask ExecuteAsync(IConsole console)
{
console.Output.WriteLine(AuthToken);
return default;
}
}
``` ```
```powershell
> $env:AUTH_TOKEN="test"
> dotnet myapp.dll
test
```
Environment variables can be configured for options of non-scalar types (arrays, lists, etc.) as well.
In such case, the value of the environment variable will be split by `Path.PathSeparator` (`;` on Windows, `:` on Unix systems).
### Multiple commands ### Multiple commands
In order to facilitate a variety of different workflows, command-line applications may provide the user with more than just a single command. In order to facilitate a variety of different workflows, command-line applications may provide the user with more than just a single command.
@@ -451,8 +397,8 @@ For example, running `dotnet myapp.dll cmd1 arg1 -p 42` will execute `FirstComma
Requesting help will show direct subcommands of the current command: Requesting help will show direct subcommands of the current command:
```powershell ```sh
> dotnet myapp.dll --help $ dotnet myapp.dll --help
MyApp v1.0 MyApp v1.0
@@ -473,8 +419,8 @@ You can run `dotnet myapp.dll [command] --help` to show help on a specific comma
The user can also refine their help request by querying it on a specific command: The user can also refine their help request by querying it on a specific command:
```powershell ```sh
> dotnet myapp.dll cmd1 --help $ dotnet myapp.dll cmd1 --help
USAGE USAGE
dotnet myapp.dll cmd1 [options] dotnet myapp.dll cmd1 [options]
@@ -524,13 +470,12 @@ public class DivideCommand : ICommand
} }
``` ```
```powershell ```sh
> dotnet myapp.dll --dividend 10 --divisor 0 $ dotnet myapp.dll --dividend 10 --divisor 0
Division by zero is not supported. Division by zero is not supported.
$ echo $?
> $LastExitCode
133 133
``` ```
@@ -697,8 +642,8 @@ To do that, you need to pass the corresponding directive before any other argume
In order to run the application in debug mode, use the `[debug]` directive. 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: This will cause the program to launch in a suspended state, waiting for debugger to be attached to the process:
```powershell ```sh
> dotnet myapp.dll [debug] cmd -o $ dotnet myapp.dll [debug] cmd -o
Attach debugger to PID 3148 to continue. Attach debugger to PID 3148 to continue.
``` ```
@@ -706,8 +651,8 @@ Attach debugger to PID 3148 to continue.
To run the application in preview mode, use the `[preview]` directive. 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: This will short-circuit the execution and instead print the consumed command-line arguments as they were parsed, along with resolved environment variables:
```powershell ```sh
> dotnet myapp.dll [preview] cmd arg1 arg2 -o foo --option bar1 bar2 $ dotnet myapp.dll [preview] cmd arg1 arg2 -o foo --option bar1 bar2
Command-line: Command-line:
cmd <arg1> <arg2> [-o foo] [--option bar1 bar2] cmd <arg1> <arg2> [-o foo] [--option bar1 bar2]
@@ -730,4 +675,4 @@ var app = new CliApplicationBuilder()
## Etymology ## Etymology
**CliFx** is made out of "Cli" for "Command-line Interface" and "Fx" for "Framework". **CliFx** is made out of "Cli" for "Command-line Interface" and "Fx" for "Framework".
It's pronounced as "cliff ex". It's pronounced as "cliff ex".