dotnet / dotnet/command-line-api

Reading files from (piped) standard input, with correct cancellation support

Open
#1,073 3 comments 0 reactions 0 assignees View on GitHub
enhancement
Dominant language
C#
Stars
3.7k
Forks
428
PR merge metrics
No merged PRs in 30d

Description

Hi, thanks for the great work so far!

I'm still struggling with one concept though. I want to create a CLI app that can receive input from Console.In, so it can be combined with other cli tools. It should also support proper cancellation. This combination (reading from standard input + cancellation) is where I'm stuck.

Main usage
------------

Example preferred usage (from powershell, but that doesn't really matter)

```powershell
gci *.txt | MyApp.exe
```

Fallback 1
-----------
If possible, I'd also like to provide an alternative so that the files can optionally be passed in as a positional argument, or if that's not possible, as a named argument. Something like this:

```powershell
MyApp.exe C:\file1.txt C:\file2.txt C:\file3.txt
```

or perhaps

```powershell
MyApp.exe --files=C:/file1.txt C:/file2.txt C:\file3.txt
```

Fallback 2
-----------

Furthermore, if MyApp.exe is called without any arguments, it should read the files from standard input with support for cancellation.

```powershell
> MyApp.exe
> C:/file1.txt
> C:/file2.txt
> C:/file3.txt
> CTRL + C (app should immediately close)
```

Using just System.CommandLine (not Dragonfruit), what would MyApp look like? Can I use any of the existing Option infrastructure to implement this?

I've tried the following so far:

```csharp
public class Program
{
public static Task Main(string[] args)
{
var filesArgument = new Argument("files")
{
Arity = ArgumentArity.ZeroOrMore,
ArgumentType = typeof(IEnumerable),
};
var filesOption = new Option(new[] {"-f", "--files"}, "Process these files")
{
Argument = filesArgument,
};

var rootCommand = new RootCommand("Process files")
{
// Make positional argument work
filesArgument,

// Make named argument work
filesOption,
};

rootCommand.Handler =
CommandHandler.Create?, CancellationToken>(ProcessFiles);

return rootCommand.InvokeAsync(args);
}

private static void ProcessFiles(IEnumerable? files, CancellationToken cancellationToken)
{
files ??= ReadFilesFromStandardInput(cancellationToken);

foreach (var file in files)
{
cancellationToken.ThrowIfCancellationRequested();

Console.WriteLine("Hello this is a file: " + file);
}
}

private static IEnumerable ReadFilesFromStandardInput(CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();

string? line;
while ((line = Console.In.ReadLine()) != null)
{
cancellationToken.ThrowIfCancellationRequested();

if (File.Exists(line))
yield return new FileInfo(line);
}
}
}
```

The above implementation gets very close to what I want, but I cannot seem to get cancellation support working correctly, because the process is blocked at "Console.In.ReadLine".

A simple summary:

- [x] Support for piped input via standard input
- [x] Support for manually typing file names one by one via standard input
- [x] Support for named argument "files"
- [x] Support for positional argument
- [ ] Support for proper cancellation when pressing CTRL + C

I've tried searching for issues or anything in the docs, but I couldn't find anything that answers my questions.

Thanks for your time.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.