dotnet / dotnet/command-line-api
Reading files from (piped) standard input, with correct cancellation support
- 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
Assessment
This issue has not been assessed yet.