Using a Custom IConsole with System.CommandLine
Introduction
System.CommandLine is an open source .net command line parser.
As of this writing the package is in pre-release with version 2.0.0-beta1.21216.1. This means that to install it you will need to include the --prerelease
flag when installing with dotnet add package
.
See the Resources for links to resources referenced in this post.
There are a number of features in this tool and I encourage you to visit the NuGet page or the GitHub site for the latest information.
Once you have configured the command line with the available commands, options, arguments, directives and delimiters you can run your application and pass in the arguments. CommandLine will parse the tokens provided on the command line and evaluate them against the configured command line.
If there are any errors CommandLine will intercept the execution and will output some usage text. This text is a standard output that provides usage information. If you were to initialize a command line without any additional options you would have two built in options available --help
and --version
. Using the --help
option would provide you output similar to below.
Usage:
ConsoleApp [options]
Options:
--version Show version information
-?, -h, --help Show help and usage information
If you are running a console application the usage information or any other output from CommandLine will be output to the standard Console output. This is generally an intended behavior. If you are writing a unit test you might want to be able to capture that output for inspection. Or perhaps you want to save the output to a file instead. You can redirect the output to another source by implementing your own IConsole.
The IConsole interface
The IConsole interface defines how output is directed. The default implementation of IConsole simply redirects the output to the standard input, output and error locations. By implementing your own IConsole you can change where the output is directed.
We can start by creating a new class that uses the IConsole interface.
public class CustomConsole : IConsole
{
public IStandardStreamWriter Out => throw new NotImplementedException();
public bool IsOutputRedirected => throw new NotImplementedException();
public IStandardStreamWriter Error => throw new NotImplementedException();
public bool IsErrorRedirected => throw new NotImplementedException();
public bool IsInputRedirected => throw new NotImplementedException();
}
There are 2 methods and 3 properties that need to be implmented. Out
is where standard output will be directed. Error
is where error output will be directed. The remaining properties are used to indicate if the othe streams have been redirected.
The flags are used to alter the rendering of output that is generated. If the output is redirected then some color coding of output will not be done. We can safely set these values to false
.
public bool IsOutputRedirected => false;
public bool IsErrorRedirected => false;
public bool IsInputRedirected => false;
We need to initialize the Out
and Error
to a new IStandardStreamWriter value. To accomplish this we will create an constructor.
public CustomConsole(IStandardStreamWriter stdOut = null, IStandardStreamWriter stdError = Console)
{
}
The constructor takes two arguments. Both will default to null if not provided. In the body of the constructor we can setup a simple check to see if a value was passed for either of the paramemters. If a non-null value is passed in then we can set the relevant values.
if (stdOut != null)
{
Out = stdOut;
}
if (stdError != null)
{
Error = stdError;
}
What if we only pass in a value for stdOut? What happens if we do not assign a value to Error
? If there are no values assigned to Out
or Error
then the application will throw an error. To ensure that a valid value is assigned we can use an alternate path if no value is provided in the constructor.
Ideally we would just assign the standard output and standard error if new values are not provided. For error this would be Console.Error
. Console.Error
returns a TextWriter and we need an IStandardStreamWriter. There is a simple way to get what we need. Using the StandardStreamWriter.Create()
method we can pass a TextWriter and get back an IStandardStreamWriter. We can do the same for the Console.Out which is the standard output.
if (stdOut != null)
{
Out = stdOut;
} else {
Out = StandardStreamWriter.Create(Console.Out);
}
if (stdError != null)
{
Error = stdError;
} else {
Error = StandardStreamWriter.Create(Console.Error);
}
After all of that our new class should look like this.
public class CustomConsole : IConsole
{
public CustomConsole(IStandardStreamWriter stdOut = null, IStandardStreamWriter stdError = null)
{
if (stdOut != null)
{
Out = stdOut;
} else {
Out = StandardStreamWriter.Create(Console.Out);
}
if (stdError != null)
{
Error = stdError;
} else {
Error = StandardStreamWriter.Create(Console.Error);
}
}
public IStandardStreamWriter Out { get; }
public IStandardStreamWriter Error { get; }
public bool IsOutputRedirected { get; } = false;
public bool IsErrorRedirected { get; } = false;
public bool IsInputRedirected { get; } = false;
}
Redirecting Out
With our new class defined we are ready to direct the standard output to a new output. This could be a file or any other stream that can be a TextWriter.
We will start by instantiating the new CustomConsole
;
IConsole console = new CustomConsole(StandardStreamWriter.Create());
The Create()
method is missing a parameter which is a TextWriter. So now we need to create a text writer.
TextWriter tw = new StreamWriter();
The TextWriter is abstract and cannot be instantiated directly. It must have an instance that implements a Stream. We need to create an implementation of a Stream that we can write to. We can use a MemoryStream. Here is the code after creating a MemoryStream and TextWriter.
MemoryStream ms = new MemoryStream();
TextWriter tw = new StreamWriter(ms);
IConsole console = new CustomConsole(StandardStreamWriter.Create(tw));
With a new console created we now need to get the CommandLine to use it. This is done by passing it as an argument to the Invoke
or InvokeAsync
method.
rootCommand.InvokeAsync(args, console);
CommandLine will now use the CustomConsole to output instead of the standard output.
Resources
Links to resources referenced
You must be logged in to see the comments. Log in now!