Fable icon indicating copy to clipboard operation
Fable copied to clipboard

CLI Rewrite with System.CommandLine

Open shayanhabibi opened this issue 4 months ago • 3 comments

This pull uses the community & microsoft driven library System.CommandLine which is currently in v2-beta5.

When compared to Spectre.Console.Cli, this means no requirements for interfaces/classes and attributes to derive the CLI interface.

We miss out on aesthetics. But I am fine to be patient with this, as it's simply a matter of the library becoming polished enough to allow more hooks and customisation with the help message rendering.

We can still use Spectre.Console to render everything outside the help messages.

Completions?

Using dotnet-suggest, System.CommandLine supports completions for zsh, bash and powershell.

Progress

After tinkering and testing some elements of the package, I've implemented a skeleton structure to show how the CLI interface could be logically reasoned and derived.

I have again implemented the CLI options as a interface hierarchy, as this simplifies consuming the args imo.

Nothing has actually been hooked in yet, as some validation has to be added (there should be architecture already existing that supports multiple validation error message reporting like I did with Spectre.Console.Cli).

It's important before this progresses to know whether this API for creating the CLI is acceptable.

Some example option creation and manipulation

    let projPath =
        Argument.create<string voption> "PATH"
        |> Mutate.Argument.defaultValue (System.Environment.CurrentDirectory |> ValueSome)
        |> Mutate.description $"{dim}Path containing project files{dimOff}"
        |> Utils.addArgToCommand root
    // extension argument for clean only
    let extensionArg =
        Argument.create<string voption> "EXT"
        |> Mutate.Argument.defaultValue (".fs.js" |> ValueSome)
        |> Mutate.description $"{dim}Path extension for cleaning{dimOff}"
        |> Utils.addArgToCommand cleanCommand

    let workingDirectory =
        CommandOption.create<string> "--cwd"
        |> Mutate.description $"{dim}Set the working directory{dimOff}"
        |> Mutate.CommandOption.filePathsOnly
        |> Mutate.CommandOption.defaultValue System.Environment.CurrentDirectory
        |> Utils.addToCommands rootAndCommands

    let verbosity =
        CommandOption.create<Verbosity> "--verbosity"
        |> Mutate.description $"{dim}Set the logging volume{dimOff}"
        |> Mutate.CommandOption.addAlias "-v"
        |> Mutate.CommandOption.valueOneOfStrings Utils.Unions.getAllCaseStringOrInitials<Verbosity>
        |> Utils.Unions.addCustomParser (
            function
            | Verbosity.Normal -> [ "n"; "normal" ]
            | Verbosity.Silent -> [ "s"; "silent" ]
            | Verbosity.Verbose -> [ "v"; "verbose" ]
        )
        |> Mutate.CommandOption.defaultValue Verbosity.Normal
        |> Mutate.CommandOption.helpName $"{dim}n|normal|s|silent|v|verbose{dimOff}"
        |> Utils.addToCommands rootAndCommands

Images

As a proof of concept and test, I used ANSI escape sequences to color what is readily available dim. All descriptions can be safely manipulated.

[!NOTE] There is a way to color everything, except the headers, but it requires a hack.

The method would be to render a option which uses escape sequences, and then the proper option which is hidden. This is because it is impossible to match the options if they have escape sequences.

image image

shayanhabibi avatar Aug 29 '25 17:08 shayanhabibi

@shayanhabibi Would using FSharp.SystemCommandLine benefit us?

MangelMaxime avatar Sep 01 '25 15:09 MangelMaxime

Personally, I would wait until the dependency itself is stable before we have a dependency on the wrapper

shayanhabibi avatar Sep 07 '25 02:09 shayanhabibi

Need to know whether we should continue with this or not.

shayanhabibi avatar Sep 12 '25 03:09 shayanhabibi