# 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<int> 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<string> 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<T>`, `List<T>`, `HashSet<T>`, etc.

### Custom Converter

```csharp
public class VectorConverter : BindingConverter<Vector2>
{
    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<MyService>();
        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<ConcatCommand>()
    .UseConsole(console)
    .Build();

var args = new[] { "--left", "foo", "--right", "bar" };
var envVars = new Dictionary<string, string>();

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<int> Main` requires `System.Threading.Tasks` using directive on net48
