dotnet / command-line-api

Command line parsing, invocation, and rendering of terminal output.
https://github.com/dotnet/command-line-api/wiki
MIT License
3.37k stars 378 forks source link

Examples of program usage #400

Open WojciechNagorski opened 5 years ago

WojciechNagorski commented 5 years ago

CommandLineParser supports of printing examples of program usage: There is examples ot this feature in BenchmarkDotNet:

https://github.com/dotnet/BenchmarkDotNet/blob/adde64cbbcde01938d6b2772066852c4f6c0e88d/src/BenchmarkDotNet/ConsoleArguments/CommandLineOptions.cs#L151-L178

Is there an equivalent of this function in `System.Command Line'?

jonsequitur commented 5 years ago

There isn't an equivalent of the UnParser that this leverages, although we should be able to build one using the upcoming binding infrastructure (#390).

Would you imagine a common way to model examples? A common way to ask a program for examples (analogous to / related to -h)?

WojciechNagorski commented 5 years ago

A common way to ask a program for examples is -h or --help. It should be presented like this:

  Program description.

    Usage:
      naval_fate.exe ship new <name>...
      naval_fate.exe ship <name> move <x> <y> [--speed=<kn>]
      naval_fate.exe ship shoot <x> <y>
      naval_fate.exe mine (set|remove) <x> <y> [--moored | --drifting]
      naval_fate.exe (-h | --help)
      naval_fate.exe --version

    Options:
      -h --help     Show this screen.
      --version     Show version.
      --speed=<kn>  Speed in knots [default: 10].
      --moored      Moored (anchored) mine.
      --drifting    Drifting mine.

docopt did great job. You have to see video about docopt on the page http://docopt.org/ if you didn't see yet. In docopt you just write help message (in string) like above and then this text is parsed.

namespace NavalFate
{
    internal class Program
    {
        private const string usage = @"Naval Fate.

    Usage:
      naval_fate.exe ship new <name>...
      naval_fate.exe ship <name> move <x> <y> [--speed=<kn>]
      naval_fate.exe ship shoot <x> <y>
      naval_fate.exe mine (set|remove) <x> <y> [--moored | --drifting]
      naval_fate.exe (-h | --help)
      naval_fate.exe --version

    Options:
      -h --help     Show this screen.
      --version     Show version.
      --speed=<kn>  Speed in knots [default: 10].
      --moored      Moored (anchored) mine.
      --drifting    Drifting mine.

    ";

        private static void Main(string[] args)
        {
            var arguments = new Docopt().Apply(usage, args, version: "Naval Fate 2.0", exit: true);
            foreach (var argument in arguments)
            {
                Console.WriteLine("{0} = {1}", argument.Key, argument.Value);
            }
        }
    }
}

I think that this is simple and great idea. System.CommandLine should have the same layer:

namespace NavalFate
{
    internal class Program
    {
        private const string usage = @"Naval Fate.

    Usage:
      naval_fate.exe ship new <name>...
      naval_fate.exe ship <name> move <x> <y> [--speed=<kn>]
      naval_fate.exe ship shoot <x> <y>
      naval_fate.exe mine (set|remove) <x> <y> [--moored | --drifting]
      naval_fate.exe (-h | --help)
      naval_fate.exe --version

    Options:
      -h --help     Show this screen.
      --version     Show version.
      --speed=<kn>  Speed in knots [default: 10].
      --moored      Moored (anchored) mine.
      --drifting    Drifting mine.

    ";

        private static void Main(string[] args)
        {
var parser = new CommandLineBuilder()
                .ConfigureFromUsage(usage)

                .UseParseDirective()
                .UseDebugDirective()
                .UseSuggestDirective()
                .RegisterWithDotnetSuggest()
                .UseTypoCorrections()
                .UseParseErrorReporting()
                .UseExceptionHandler()
                .CancelOnProcessTermination()

                .Build();
            var arguments = new parser.Parse(args);
            foreach (var argument in arguments)
            {
                Console.WriteLine("{0} = {1}", argument.Key, argument.Value);
            }
        }
    }
}

Or/And even strong type approach, like @KathleenDollard did in Jackfruit https://github.com/dotnet/command-line-api/pull/363 :

namespace NavalFate
{
    public class CommandLineHandler
    {
        public class Ship
        {
            public async Task<int> New(string name) => await Task.FromResult(42); // actually do stuff
            public async Task<int> Move(string name, int x, int y, dobule speed) => await Task.FromResult(42); // actually do stuff
            public async Task<int> Shoot(string name, int x, int y) => await Task.FromResult(42); // actually do stuff
        }
        public class Mine 
        {
        [...]
        }
        [...]
    }

    internal class Program
    {
        private const string usage = @"Naval Fate.

    Usage:
      naval_fate.exe ship new <name>...
      naval_fate.exe ship <name> move <x> <y> [--speed=<kn>]
      naval_fate.exe ship shoot <x> <y>
      naval_fate.exe mine (set|remove) <x> <y> [--moored | --drifting]
      naval_fate.exe (-h | --help)
      naval_fate.exe --version

    Options:
      -h --help     Show this screen.
      --version     Show version.
      --speed=<kn>  Speed in knots [default: 10].
      --moored      Moored (anchored) mine.
      --drifting    Drifting mine.

    ";

        private static Task<int> Main(string[] args)
        {
var parser = new CommandLineBuilder()
                .ConfigureFromUsage(usage)

                .UseParseDirective()
                .UseDebugDirective()
                .UseSuggestDirective()
                .RegisterWithDotnetSuggest()
                .UseTypoCorrections()
                .UseParseErrorReporting()
                .UseExceptionHandler()
                .CancelOnProcessTermination()

                .Build();
            return await parser.Parse<CommandLineHandler>(args);
        }
    }
}
AraHaan commented 5 years ago

Hmm nice, also does nuget.exe use a version of this yet? Also I provide my own command parser for my newsmake however I would like to see if I can in fact move it over to this and not loose anything that my custom one provides (like how I register commands) But how I can register command groups should in fact be different completely.

jonsequitur commented 5 years ago

@AraHaan If you try switching your parser, please let us know how the experience goes and what missing features you might run into.

totollygeek commented 3 years ago

I am also switching my parser currently from CommandLineParser to this one, mostly because I need an easy way to have multiple layers of subcommands, which is not easily supported in CommandLineParser. The biggest thing I am missing here is exactly the Example/Usage support for the help. Since I see there is not progress in that since the past two years, I guess it won't be coming soon?

Don't get me wrong, I am not complaining, your library is great. I am just honestly asking if there is some plan to add this.

AraHaan commented 3 years ago

@jonsequitur Welp I moved my newsmake to System.CommandLine, however it seems currently that a few things are missing:

I usually take security diagnostics on the analyzer seriously so I wold like a way for these above to happen would be nice, or at least a place where we can set in the case of if an exception handler gets triggered the error code we want and if the library sees it as non-zero return that code instead of the code returned from the command handler(s).

jonsequitur commented 3 years ago

To get help out directly, you can do this:

using System.CommandLine;
using System.CommandLine.IO;
using System.CommandLine.Help;

var command = new RootCommand
{
    new Option<int>("-i", "An option"),
    new Option<string[]>("some-args", "An argument"),
};

var console = new TestConsole();

new HelpBuilder(console).Write(command);

console.Out.ToString()

which outputs, e.g.:

Microsoft.DotNet.Interactive.App:
  myapp

Usage:
  Microsoft.DotNet.Interactive.App [options]

Options:
  -i <i>                   An option
  some-args <some-args>    An argument
AraHaan commented 3 years ago

To get help out directly, you can do this:

using System.CommandLine;
using System.CommandLine.IO;
using System.CommandLine.Help;

var command = new RootCommand
{
    new Option<int>("-i", "An option"),
    new Option<string[]>("some-args", "An argument"),
};

var console = new TestConsole();

new HelpBuilder(console).Write(command);

console.Out.ToString()

which outputs, e.g.:

Microsoft.DotNet.Interactive.App:
  myapp

Usage:
  Microsoft.DotNet.Interactive.App [options]

Options:
  -i <i>                   An option
  some-args <some-args>    An argument

From within the commad's handler?

jonsequitur commented 3 years ago

Yes. And in the handler you would probably want to use an IConsole parameter if you don't want to have to use the TestConsole.

AraHaan commented 3 years ago

Yes. And in the handler you would probably want to use an IConsole parameter if you don't want to have to use the TestConsole.

And I can get the command instance of the command that the handler is invoked for too?

jonsequitur commented 3 years ago

Yes.

using System.CommandLine;
using System.CommandLine.Help;
using System.CommandLine.Parsing;
using System.CommandLine.Invocation;

var subcommandOne = new Command("one");
var subcommandTwo = new Command("two");

var rootCommand = new RootCommand
{
    subcommandOne,
    subcommandTwo
};

var handler = CommandHandler.Create<InvocationContext, IConsole>((ctx, console) => {
    var helpBuilder = new HelpBuilder(console);  
    helpBuilder.Write(ctx.ParseResult.CommandResult.Command);
});

subcommandOne.Handler = handler;
subcommandTwo.Handler = handler;

await rootCommand.InvokeAsync("one");