dotnet / command-line-api

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

[`Question`] Develop Console Prompt Screen #2422

Closed cschuchardt88 closed 2 weeks ago

cschuchardt88 commented 1 month ago

I am building a custom console prompt; however I keep getting error when a command is found or not. As you will see in the image below.

How my app works:

I'm running my app as MyApp.exe connect \\.\pipe\HelloWorld then screen refreshes to appear as terminal on that computer. This would be my implementation of IPC for the program.

        public static async Task<int> RunConsolePrompt(InvocationContext context, CancellationToken cancellationToken)
        {
            context.Console.Clear();

            var rootCommand = new DefaultRemoteCommand();
            var parser = new CommandLineBuilder(rootCommand)
                .UseParseErrorReporting()
                .Build();

            while (cancellationToken.IsCancellationRequested == false)
            {
                PrintPrompt(context.Console);

                var line = context.Console.ReadLine()?.Trim() ?? string.Empty;

                if (string.IsNullOrEmpty(line) || string.IsNullOrWhiteSpace(line))
                    continue;

                if (line.Equals("exit", StringComparison.OrdinalIgnoreCase) || line.Equals("quit", StringComparison.OrdinalIgnoreCase))
                    break;

                _ = await parser.InvokeAsync(line, context.Console);
            }

            return 0;
        }
internal class ConnectCommand : Command
{
    public ConnectCommand() : base("connect", "Connect to local Neo service")
    {
        var pipeNameArgument = new Argument<string>("PIPE_NAME", "Name of the named pipe to connect to");

        AddArgument(pipeNameArgument);
    }

    public new class Handler : ICommandHandler
    {
        private static readonly string s_computerName = Environment.MachineName;
        private static readonly string s_userName = Environment.UserName;

        private NamedPipeEndPoint? _pipeEndPoint;

        public async Task<int> InvokeAsync(InvocationContext context)
        {
            var stopping = context.GetCancellationToken();

            if (EnvironmentUtility.TryGetServicePipeName(out var pipeName))
            {
                _pipeEndPoint = new NamedPipeEndPoint(pipeName);
                var pipeStream = NamedPipeTransportFactory.CreateClientStream(_pipeEndPoint);

                context.Console.SetTerminalForegroundColor(ConsoleColor.DarkMagenta);
                context.Console.WriteLine($"Connecting to {_pipeEndPoint}...");
                context.Console.ResetTerminalForegroundColor();

                try
                {
                    await pipeStream.ConnectAsync(stopping).DefaultTimeout();
                    await RunConsolePrompt(context, stopping);
                }
                catch (TimeoutException)
                {
                    context.Console.WriteLine(string.Empty);
                    context.Console.ErrorMessage($"Failed to connect! Try again Later!");

                    context.Console.SetTerminalForegroundColor(ConsoleColor.DarkCyan);
                    context.Console.WriteLine("Note: Make sure service is running.");
                    context.Console.ResetTerminalForegroundColor();
                }

                return 0;
            }

            return -1;
        }

        public int Invoke(InvocationContext context)
        {
            throw new NotImplementedException();
        }

        private static void PrintPrompt(IConsole console)
        {
            console.SetTerminalForegroundColor(ConsoleColor.DarkBlue);
            console.Write($"{s_userName}@{s_computerName}");
            console.SetTerminalForegroundColor(ConsoleColor.White);
            console.Write(":~$ ");
            console.SetTerminalForegroundColor(ConsoleColor.DarkCyan);
            console.ResetTerminalForegroundColor();
        }

        public static async Task<int> RunConsolePrompt(InvocationContext context, CancellationToken cancellationToken)
        {
            context.Console.Clear();

            var rootCommand = new DefaultRemoteCommand();
            var parser = new CommandLineBuilder(rootCommand)
                .UseParseErrorReporting()
                .Build();

            while (cancellationToken.IsCancellationRequested == false)
            {
                PrintPrompt(context.Console);

                var line = context.Console.ReadLine()?.Trim() ?? string.Empty;

                if (string.IsNullOrEmpty(line) || string.IsNullOrWhiteSpace(line))
                    continue;

                if (line.Equals("exit", StringComparison.OrdinalIgnoreCase) || line.Equals("quit", StringComparison.OrdinalIgnoreCase))
                    break;

                _ = await parser.InvokeAsync(line, context.Console);
            }

            return 0;
        }
    }
}

Now this is the prompt (image) its not my powershell prompt; even though it looks like it. But still in my terminal.

image

elgonzo commented 1 month ago

Your attempt with --help:

You are using a CommandLineBuilder. To use the built-in help options, call either UseHelp() or UseDefaults() (which itself calls UseHelp) on the CommandLineBuilder before building the parser from it.

Your attempt with ?:

Note that ? is different from -?. -? is one of the built-in help option aliases, but ? is not. Depending on what you want, you can declare ? either as a help option alias, or as a custom command with the name ?. If you want ? just as another help option alias, pass it together with all other desired help option aliases as parameter to the UseHelp() method. (Attention: The aliases passed to UseHelp() replace the existing help option aliases, they are not added to them.)

cschuchardt88 commented 1 month ago

Problem that I am having when I don't use RootCommand It can't not even parse --help or -?; with UseHelp or UseDefaults. I believe the reason is because of RootCommand.ExecutableName and RootCommand.ExecutablePath is empty or if used isn't on the line that I am parsing. What I would like to know is how to display Help info with the Command class; I just want to add a new class HelpCommand : ICommandHandler to able to type help and display all the commands; also only show red text in terminal image above (not the rest of the text that is displayed). I have tried looking through the source and couldn't find an easy way to do so.

elgonzo commented 1 month ago

Unfortunately, i am unable to follow your explanations :-( You say that "Problem that I am having when I don't use RootCommand It can't not even parse --help or -?". But System.CommandLine doesn't need a RootCommand to successfully parse the help options (demo: https://dotnetfiddle.net/XNX628)

I believe the reason is because of RootCommand.ExecutableName and RootCommand.ExecutablePath is empty or if used isn't on the line that I am parsing.

While i am not 100% certain, i doubt RootCommand.ExecutableName and RootCommand.ExecutablePath would be related to SCL being (un)able to parse the input string "--help" or "-?". The parser built from the CommandLineBuilder should successfully parse either input string, assuming the built-in help options have been actived via UseHelp() or UseDefaults(). But without being able to easily reproduce this on my end, i can't say for sure either way...

What I would like to know is how to display Help info with the Command class

I see two ways to do it. One way is to let the handler for the help command simply parse and invoke on a bespoke string featuring a help option. If your existing parser instance has these built-in help options activated, you could reuse it, i believe. Otherwise, let the help command create a new parser based off a new CommandLineBuilder that has the built-in help options activated.

The other way i see is letting the handler for the Help command do something similar to to what the SCL middleware for the built-in help options is doing: https://github.com/dotnet/command-line-api/blob/3e0db9e5830f2722645b9a576f80e3650b50f23a/src/System.CommandLine/Help/HelpResult.cs#L12-L23

also only show red text in terminal image above (not the rest of the text that is displayed).

Don't use UseParseErrorReporting() then, as it printing the help text is unfortunately hard-coded. Instead, inspect the parse result before calling Invoke/Async on it. Specifically look at its ParseResult.Errors list. Each of the parse errors in this list provides the error message for that error in its ParseError.Message property, which you can print out.