Ajna_

Your next CLI is a class.

Ajna is a REPL framework for .NET. Design the command line in plain C#.

$ dotnet add package Ajna
net10.0 v0.9.10 MIT licensed one package, generator included
Program.cs — the whole program
using Ajna;

CLI.From<Toolbox>( ).Run( );

public class Toolbox
{
  /// <summary>Greets a person by name.</summary>
  /// <param name="name">The name to greet.</param>
  /// <returns>A greeting line.</returns>
  public string Greet ( string name ) => $"Hello, {name}!";

  /// <summary>Adds two integers together.</summary>
  /// <param name="left">First addend.</param>
  /// <param name="right">Second addend.</param>
  /// <returns>The sum.</returns>
  public int AddNumbers ( int left, int right ) => left + right;
}
Taiji@demo> greet --name world
Hello, world!
Taiji@demo> add numbers --left 20 --right 22
42
Taiji@demo> add numbers -h

Adds two integers together.

add numbers (
  [ Required ]

  --left : Int32

    First addend.

  [ Required ]

  --right : Int32

    Second addend.

) -> Int32

The sum.

Taiji@demo> 

Conventions

Spelled the way you named it.

Each capital starts a word. Commands join the words with spaces, flags with hyphens, and a flag's first letter is its short form.

The projection is exact, and its few rules are the shape of a command line: flags rather than overloads, at most 25 of them, names in ASCII letters. The reference lists them.

Greet→greet
AddNumbers→add numbers
ListActiveUsers→list active users
name→--name  /  -n
maxRetries→--max-retries  /  -m

Four spellings of AddNumbers( 20, 22 )

add numbers 20 22                    // positional
add numbers --left 20 --right 22     // long flags, any order
add numbers -l 20 -r 22              // short flags
add numbers 20 --right 22            // mixed

Your class

The class stays plain.

Write it as you would for any caller. Everything the prompt needs is in the signature.

Program.cs
using Ajna;

CLI.From<Deploy>( ).Run( );

public class Deploy
{
  // Ctrl+C cancels this
  CancellationToken Cancel { get; set; }

  /// <summary>Starts replicas in a region.</summary>
  public IEnumerable<string> Rollout ( Region region, int count = 3 )
  {
    for ( int i = 1; i <= count; i++ )
    {
      if ( Cancel.IsCancellationRequested )
        throw new CLICancellationException( "stopped early" );

      Thread.Sleep( 500 );
      // printed as it is produced
      yield return $"{region}: replica {i} up";
    }
  }

  /// <summary>Points the service at an upstream.</summary>
  // Uri parses itself
  public Uri Upstream ( Uri address )
    => address.Scheme == "https"
     ? address
     : throw new CLICustomCommandException( "https only" );
}

public enum Region { Eu, Us, Ap }
recorded session
Taiji@demo> rollout eu --count 2
Eu: replica 1 up
Eu: replica 2 up
Taiji@demo> rollout us -c 100
Us: replica 1 up
Us: replica 2 up
Us: replica 3 up
^C
rollout: stopped early
Taiji@demo> upstream http://origin.local
upstream: https only
Taiji@demo> upstream https://origin.local
https://origin.local/
Taiji@demo> rollout ap
Ap: replica 1 up
Ap: replica 2 up
Ap: replica 3 up
Taiji@demo> 

Any surface

Bring your own terminal.

System.Console is the default. With<> binds the prompt to whatever your app has instead.

The playground on this site is this library, unchanged, typing into a web page.

Program.cs — a DOM-backed prompt
CLI.From<Toolbox>( )
   .With<ITerminalSurface, DomSurface>( new DomSurface( ) )
   .With<IKeyInputSource, DomKeys>( new DomKeys( ) )
   .Run( );

For the curious

What the prompt is made of.

All of it is internal. It is why the API above can be this small.

Command invocationReflection runs once, when the command table is built. From then on, commands invoke through arity-specialized delegates.
Command lookupA trie stored in one flat array. Tab completion is the same walk, stopped early.
Line editingA gap buffer, so insertion at the caret is constant-time whatever the line length.
Key dispatchKey and modifiers pack into one integer that indexes a flat handler table.
Help textAn incremental source generator reads the /// comments at compile time and emits them into your assembly as string literals. The project needs GenerateDocumentationFile switched on.

Next

Read on.