# CliFx Reference (v2.3.6) Local reference for the CliFx CLI framework. Source: https://github.com/Tyrrrz/CliFx ## Setup ```csharp using CliFx; public static class Program { public static async Task Main() => await new CliApplicationBuilder() .AddCommandsFromThisAssembly() .Build() .RunAsync(); } ``` **Important:** `Main()` must return the integer exit code from `RunAsync()`. `RunAsync()` resolves args from `Environment.GetCommandLineArgs()` and env vars from `Environment.GetEnvironmentVariables()`. You can also provide them manually via overloads. ## Defining Commands Commands implement `ICommand` and are decorated with `[Command]`: ```csharp using CliFx; using CliFx.Attributes; [Command(Description = "Calculates the logarithm of a value.")] public class LogCommand : ICommand { [CommandParameter(0, Description = "Value whose logarithm is to be found.")] public required double Value { get; init; } [CommandOption("base", 'b', Description = "Logarithm base.")] public double Base { get; init; } = 10; public ValueTask ExecuteAsync(IConsole console) { var result = Math.Log(Value, Base); console.Output.WriteLine(result); return default; } } ``` Use `IConsole` (not `System.Console`) for all console I/O — this enables testability. ## Parameters vs Options | Aspect | `[CommandParameter]` | `[CommandOption]` | |---|---|---| | Binding | By position order | By name (`--foo`) or short name (`-f`) | | Required by default | Yes | No | | Make optional | Omit `required` (last param only) | Omit `required` | | Make required | Use `required` keyword | Use `required` keyword | | Non-scalar (collections) | Only last parameter | Any option | | Env var fallback | No | Yes (`EnvironmentVariable = "ENV_FOO"`) | ```csharp // Required option [CommandOption("foo")] public required string Foo { get; init; } // Optional parameter (must be last) [CommandParameter(0)] public string? OptionalParam { get; init; } // Option with env var fallback [CommandOption("foo", EnvironmentVariable = "ENV_FOO")] public required string FooWithFallback { get; init; } // Non-scalar option [CommandOption("items")] public required IReadOnlyList Items { get; init; } ``` ## Argument Syntax (POSIX-style) ``` myapp [...directives] [command] [...parameters] [...options] ``` - `--foo bar` → option "foo" = "bar" - `-f bar` → option 'f' = "bar" - `--switch` → option "switch" (no value / boolean) - `-abc` → options 'a', 'b', 'c' (no value) - `-i file1.txt file2.txt` → option 'i' = ["file1.txt", "file2.txt"] - `-i file1.txt -i file2.txt` → same as above ## Value Conversion Supports out of the box: - **Primitives**: `int`, `bool`, `double`, `ulong`, `char`, etc. - **Date/time**: `DateTime`, `DateTimeOffset`, `TimeSpan` - **Enums**: by name or numeric value - **String-initializable**: types with `ctor(string)` or static `Parse(string)` — e.g. `FileInfo`, `Guid`, `BigInteger` - **Nullable** versions of all above - **Collections**: `T[]`, `IReadOnlyList`, `List`, `HashSet`, etc. ### Custom Converter ```csharp public class VectorConverter : BindingConverter { public override Vector2 Convert(string? rawValue) { 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); } } [CommandParameter(0, Converter = typeof(VectorConverter))] public required Vector2 Point { get; init; } ``` ## Multiple / Nested Commands Give each command a unique name. Common name segments create hierarchy: ```csharp [Command] // Default (unnamed) command public class DefaultCommand : ICommand { ... } [Command("cmd1")] // Child of default public class FirstCommand : ICommand { ... } [Command("cmd1 sub")] // Child of cmd1 public class SubCommand : ICommand { ... } ``` Usage: `myapp cmd1 sub arg1 --opt value` Default command is optional. If absent, running without a command shows root help text. ## Error Reporting Use `CommandException` to report errors with specific exit codes: ```csharp throw new CommandException("Division by zero is not supported.", 133); ``` Exit codes should be 1–255 (8-bit unsigned) to avoid overflow on Unix. ## Graceful Cancellation ```csharp public async ValueTask ExecuteAsync(IConsole console) { var cancellation = console.RegisterCancellationHandler(); await DoSomethingAsync(cancellation); } ``` First interrupt signal triggers the token. Second interrupt force-kills. ## Dependency Injection Use `UseTypeActivator(...)` with `Microsoft.Extensions.DependencyInjection`: ```csharp await new CliApplicationBuilder() .AddCommandsFromThisAssembly() .UseTypeActivator(commandTypes => { var services = new ServiceCollection(); services.AddSingleton(); foreach (var commandType in commandTypes) services.AddTransient(commandType); return services.BuildServiceProvider(); }) .Build() .RunAsync(); ``` ## Testing Use `FakeInMemoryConsole` to test commands in isolation: ### Command-level test ```csharp using var console = new FakeInMemoryConsole(); var command = new ConcatCommand { Left = "foo", Right = "bar" }; await command.ExecuteAsync(console); var stdOut = console.ReadOutputString(); Assert.Equal("foo bar", stdOut); ``` ### Application-level test ```csharp using var console = new FakeInMemoryConsole(); var app = new CliApplicationBuilder() .AddCommand() .UseConsole(console) .Build(); var args = new[] { "--left", "foo", "--right", "bar" }; var envVars = new Dictionary(); await app.RunAsync(args, envVars); var stdOut = console.ReadOutputString(); Assert.Equal("foo bar", stdOut); ``` ## Debug & Preview Directives ```bash # Suspend until debugger attaches myapp [debug] cmd -o # Print parsed args without executing myapp [preview] cmd arg1 -o foo ``` Disable in production: ```csharp new CliApplicationBuilder() .AllowDebugMode(false) .AllowPreviewMode(false) .Build(); ``` ## .NET Framework 4.8 Notes - CliFx targets .NET Standard 2.0+, fully compatible with net48 - `required` keyword requires C# 11+ — on net48 (LangVersion 9.0), use properties with default values and validate in `ExecuteAsync` instead - `init` setters require C# 9+ (supported on net48 with LangVersion 9.0) - `async Task Main` requires `System.Threading.Tasks` using directive on net48