Tyrrrz / CliWrap

Library for running command-line processes
MIT License
4.32k stars 264 forks source link

Input prompt is not captured #191

Closed YouKnowThem closed 1 year ago

YouKnowThem commented 1 year ago

Version

3.6.0

Details

I have an external CLI software, that ouputs information and prompts the user for imput. I wanted to automate this and using the CliWrap in C# to wait for the prompts and then dynamically output the right user inputs (so a script is not intended). The output of the CLI contains information used to decide on what to feed the CLI prompt.

Steps to reproduce

Tyrrrz commented 1 year ago

Hey there.

In general, responding to prompts is done by piping standard input data, as that's where the underlying console application reads the input from (usually separated by new lines).

If you know all the prompts and the respective inputs ahead of time, you can just pre-compute the corresponding stdin string and pipe it to the command.

If you need to react to prompts dynamically, the best solution is to create your own PipeSource and use a synchronization primitive to write data. Something like this:

using var semaphore = new SemaphoreSlim(0, 1);
var buffer = new StringBuilder();

var stdin = PipeSource.Create(async (destination, cancellationToken) =>
{
    while (!cancellationToken.IsCancellationRequested)
    {
        await semaphore.WaitAsync(cancellationToken);
        var data = Encoding.UTF8.GetBytes(buffer.ToString());
        await destination.WriteAsync(data, 0, data.Length, cancellationToken);
    }
});

var cmd = stdin | Cli.Wrap("my cmd");

await foreach (var cmdEvent in cmd.ListenAsync())
{
    if (cmdEvent is StandardOutputEvent stdOutEvent)
    {
        // Detect if it's a prompt
        if (stdOutEvent.Text.Contains("Prompt"))
        {
            // Write the response
            buffer.Clear();
            buffer.AppendLine("Hello world");
            semaphore.Release();
        }
    } 
}
YouKnowThem commented 1 year ago

Thanks for the clarification. It's just that the await ListenAsync does not return on this JLinkSTM32.exe's output. I don't know why it doesn't. If it did, your example code would do exactly what I need.

Tyrrrz commented 1 year ago

What do you mean by "does not return"?

YouKnowThem commented 1 year ago

The await ListenAsync waits indefinetly. Although when opening the CLI software JLinkSTM32.exe manually shows an immediate output with a promt waiting for user input.

That particular output is not fetched by await ListenAsync. My C# software waits for the JLinkSTM32 output forever. Although I expect it to be already there.

Tyrrrz commented 1 year ago

I don't know what JLinkSTM32 does, but it might be because it detects that the standard input is already redirected at the start and switches to some alternative execution mode.

YouKnowThem commented 1 year ago

I did not think about such behavior. Also did not know such a thing would exist. But thinking about it feels like it makes sense. I have tried piping an input in. In this case I do not get any output also the CLI does seem to never exit. A screenshot from task manager. I guess this is the JLinkSTM32.exe's fault? grafik

Tyrrrz commented 1 year ago

It's weird that it would never exit. In the worst case, you can probably fool it by wrapping cmd and using it to launch JLinkSTM32. Most likely it will behave the same as if it were launched from a terminal.

YouKnowThem commented 1 year ago

Good idea. I tried that.

var jLinkSTM32 = await (
            "JLinkSTM32.exe\r\n" |
            Cli.Wrap("cmd")
            .WithWorkingDirectory(@"C:\Program Files\SEGGER\JLink")
            .WithStandardOutputPipe(PipeTarget.ToDelegate(line => Console.WriteLine(line)))
            .WithStandardErrorPipe(PipeTarget.ToDelegate(line => Console.WriteLine(line)))
            )
            .ExecuteBufferedAsync();

But without success. CMD did actually output its contents. JLinkSTM32 kept quiet again. So all I saw was this: grafik

vpenades commented 2 months ago

I've been having a similar problem and, adding to the discussion, I would like to suggest this feature:

In the same way that the TargetPipe has a ToDelegate(Action<string> delegate) , it could be useful to have a FromDelegate(Func<string> delegate) in the PipeSource

Tyrrrz commented 2 months ago

@vpenades it's a good idea but not super straightforward. Please make a new issue for it because this one is old.