CliWrap 3.3.3

Library for interacting with external command line interfaces

Install-Package CliWrap -Version 3.3.3
dotnet add package CliWrap --version 3.3.3
<PackageReference Include="CliWrap" Version="3.3.3" />
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add CliWrap --version 3.3.3
The NuGet Team does not provide support for this client. Please contact its maintainers for support.
#r "nuget: CliWrap, 3.3.3"
#r directive can be used in F# Interactive, C# scripting and .NET Interactive. Copy this into the interactive tool or source code of the script to reference the package.
// Install CliWrap as a Cake Addin
#addin nuget:?package=CliWrap&version=3.3.3

// Install CliWrap as a Cake Tool
#tool nuget:?package=CliWrap&version=3.3.3
The NuGet Team does not provide support for this client. Please contact its maintainers for support.

CliWrap

Build Coverage Version Downloads Discord Donate

✅ Project status: active.

CliWrap is a library for interacting with external command line interfaces. It provides a convenient model for launching processes, redirecting input and output streams, awaiting completion, handling cancellation, and more.

💬 If you want to chat, join my Discord server.

Download

📦 NuGet: dotnet add package CliWrap

Features

  • Airtight abstraction over System.Diagnostics.Process
  • Fluent configuration interface
  • Flexible support for piping
  • Fully asynchronous and cancellation-aware API
  • Designed with strict immutability in mind
  • Provides safety against typical deadlock scenarios
  • Tested on Windows, Linux, and macOS
  • Targets .NET Standard 2.0+, .NET Core 3.0+, .NET Framework 4.6.1+
  • No external dependencies

Usage

Quick overview

Similarly to a shell, CliWrap's base unit of work is a command -- an object that encodes instructions for running a process. To build a command, start by calling Cli.Wrap(...) with the executable path, and then use the provided fluent interface to configure arguments, working directory, or other options. Once the command is configured, you can run it by calling ExecuteAsync():

using CliWrap;

var result = await Cli.Wrap("path/to/exe")
    .WithArguments("--foo bar")
    .WithWorkingDirectory("work/dir/path")
    .ExecuteAsync();
    
// Result contains:
// -- result.ExitCode        (int)
// -- result.StartTime       (DateTimeOffset)
// -- result.ExitTime        (DateTimeOffset)
// -- result.RunTime         (TimeSpan)

The code above spawns a child process with the configured command line arguments and working directory, and then asynchronously waits for it to exit. After the task has completed, it resolves a CommandResult object that contains the process exit code and other related information.

⚠️ Note that CliWrap will throw an exception if the underlying process returns a non-zero exit code, as it usually indicates an error. You can override this behavior by disabling result validation using WithValidation(CommandResultValidation.None).

By default, the process's standard input, output and error streams are routed to CliWrap's equivalent of the null device, which represents an empty source and a target that discards all data. You can change this by calling WithStandardInputPipe(...), WithStandardOutputPipe(...), or WithStandardErrorPipe(...) to configure pipes for the corresponding streams:

var stdOutBuffer = new StringBuilder();
var stdErrBuffer = new StringBuilder();

var result = await Cli.Wrap("path/to/exe")
    .WithArguments("--foo bar")
    .WithWorkingDirectory("work/dir/path")
    .WithStandardOutputPipe(PipeTarget.ToStringBuilder(stdOutBuffer))
    .WithStandardErrorPipe(PipeTarget.ToStringBuilder(stdErrBuffer))
    .ExecuteAsync();
    
// Contains stdOut/stdErr buffered in-memory as string
var stdOut = stdOutBuffer.ToString(); 
var stdErr = stdErrBuffer.ToString();

In this example, the data pushed to standard output and error streams is decoded as text and written to separate StringBuilder buffers. After the command has finished executing, you can inspect the contents of these buffers to see what the process has printed to the console during its runtime.

Handling command output is a very common use case, so CliWrap offers a few high-level execution models to make these scenarios simpler. In particular, the same thing shown above can also be achieved more succinctly with the ExecuteBufferedAsync() extension method:

using CliWrap;
using CliWrap.Buffered;

// Calling `ExecuteBufferedAsync()` instead of `ExecuteAsync()`
// implicitly configures pipes that write to in-memory buffers.
var result = await Cli.Wrap("path/to/exe")
    .WithArguments("--foo bar")
    .WithWorkingDirectory("work/dir/path")
    .ExecuteBufferedAsync();

// Result contains:
// -- result.StandardOutput  (string)
// -- result.StandardError   (string)
// -- result.ExitCode        (int)
// -- result.StartTime       (DateTimeOffset)
// -- result.ExitTime        (DateTimeOffset)
// -- result.RunTime         (TimeSpan)

⚠️ Note that standard streams are not limited to text and can contain raw binary data. Additionally, the size of the data may make it inefficient to store in-memory. For more complex scenarios, CliWrap also provides other piping options, which are covered in the Piping section.

Command configuration

The fluent interface provided by the command object allows you to configure various options related to its execution. Below list covers all available configuration methods and their usage.

⚠️ Note that Command is an immutable object, meaning that all configuration methods listed here return a new instance instead of modifying the existing one.

WithArguments(...)

Sets the command line arguments that will be passed to the child process.

Default: empty.

Set arguments directly from a string:

var cmd = Cli.Wrap("git")
    .WithArguments("commit -m \"my commit\"");

Set arguments from a list (each element is treated as a separate argument; spaces are escaped automatically):

var cmd = Cli.Wrap("git")
    .WithArguments(new[] {"commit", "-m", "my commit"});

Set arguments using a builder (same as above, but also automatically converts certain values to their string representations):

var cmd = Cli.Wrap("git")
    .WithArguments(args => args
        .Add("clone")
        .Add("https://github.com/Tyrrrz/CliWrap")
        .Add("--depth")
        .Add(20)); // <- formatted to a string
WithWorkingDirectory(...)

Sets the working directory of the child process.

Default: current working directory, i.e. Directory.GetCurrentDirectory().

var cmd = Cli.Wrap("git")
    .WithWorkingDirectory("c:/projects/my project/");
WithEnvironmentVariables(...)

Sets additional environment variables that will be exposed to the child process.

Default: empty.

Set environment variables from a dictionary:

var cmd = Cli.Wrap("git")
    .WithEnvironmentVariables(new Dictionary<string, string?>
    {
        ["GIT_AUTHOR_NAME"] = "John",
        ["GIT_AUTHOR_EMAIL"] = "john@email.com"
    });

Set environment variables using a builder:

var cmd = Cli.Wrap("git")
    .WithEnvironmentVariables(env => env
        .Set("GIT_AUTHOR_NAME", "John")
        .Set("GIT_AUTHOR_EMAIL", "john@email.com"));

⚠️ Note that these environment variables are set on top of the default environment variables inherited from the parent process. If you provide a variable with the same name as one of the inherited variables, the provided value will take precedence. Additionally, you can also remove an inherited variable by setting its value to null.

WithCredentials(...)

Sets domain, name and password of the user, under whom the child process will be started.

Default: no credentials.

Set credentials directly:

var cmd = Cli.Wrap("git")
    .WithCredentials(new Credentials(
        "some_workspace",
        "johndoe",
        "securepassword123"
    ));

Set credentials using a builder:

var cmd = Cli.Wrap("git")
    .WithCredentials(creds => creds
       .SetDomain("some_workspace")
       .SetUserName("johndoe")
       .SetPassword("securepassword123"));

⚠️ Note that specifying domain and password is only supported on Windows and will result in an exception on other operating systems. Specifying username, on the other hand, is supported across all platforms.

WithValidation(...)

Sets the strategy for validating the result of an execution.

The following modes are available:

  • CommandResultValidation.None -- no validation
  • CommandResultValidation.ZeroExitCode -- ensures zero exit code when the process exits

Default: CommandResultValidation.ZeroExitCode.

Disable validation:

var cmd = Cli.Wrap("git")
    .WithValidation(CommandResultValidation.None);

Enable validation:

// Ensure that exit code is zero after the process exits (otherwise throw an exception)
var cmd = Cli.Wrap("git")
    .WithValidation(CommandResultValidation.ZeroExitCode);
WithStandardInputPipe(...)

Sets the pipe source that will be used for the standard input stream of the process.

Default: PipeSource.Null.

Read more about this method in the Piping section.

WithStandardOutputPipe(...)

Sets the pipe target that will be used for the standard output stream of the process.

Default: PipeTarget.Null.

Read more about this method in the Piping section.

WithStandardErrorPipe(...)

Sets the pipe target that will be used for the standard error stream of the process.

Default: PipeTarget.Null.

Read more about this method in the Piping section.

Piping

CliWrap provides a very powerful and flexible piping model that allows you to redirect process's streams, transform input and output data, and even chain multiple commands together with minimal effort. At its core, it's based on two abstractions: PipeSource which provides data for standard input stream, and PipeTarget which reads data coming from standard output or standard error streams.

By default, command's input pipe is set to PipeSource.Null and the output and error pipes are set to PipeTarget.Null. These objects effectively represent no-op stubs that provide empty input and discard all output respectively.

You can specify your own PipeSource and PipeTarget instances by calling the corresponding configuration methods on the command:

await using var input = File.OpenRead("input.txt");
await using var output = File.Create("output.txt");

await Cli.Wrap("foo")
    .WithStandardInputPipe(PipeSource.FromStream(input))
    .WithStandardOutputPipe(PipeTarget.ToStream(output))
    .ExecuteAsync();

Alternatively, pipes can also be configured in a slightly terser way by using pipe operators:

await using var input = File.OpenRead("input.txt");
await using var output = File.Create("output.txt");

await (input | Cli.Wrap("foo") | output).ExecuteAsync();

Both PipeSource and PipeTarget have many factory methods that let you create pipe implementations for different scenarios:

  • PipeSource:
    • PipeSource.Null -- represents an empty pipe source
    • PipeSource.FromStream(...) -- pipes data from any readable stream
    • PipeSource.FromFile(...) -- pipes data from a file
    • PipeSource.FromBytes(...) -- pipes data from a byte array
    • PipeSource.FromString(...) -- pipes from a text string
    • PipeSource.FromCommand(...) -- pipes data from standard output of another command
  • PipeTarget:
    • PipeTarget.Null -- represents a pipe target that discards all data
    • PipeTarget.ToStream(...) -- pipes data into any writeable stream
    • PipeTarget.ToFile(...) -- pipes data into a file
    • PipeTarget.ToStringBuilder(...) -- pipes data as text into StringBuilder
    • PipeTarget.ToDelegate(...) -- pipes data as text, line-by-line, into Action<string> or Func<string, Task>
    • PipeTarget.Merge(...) -- merges multiple outbound pipes by replicating the same data across all of them

Below you can see some examples of what you can achieve with the help of CliWrap's piping feature.

Pipe a string into stdin
var cmd = "Hello world" | Cli.Wrap("foo");
await cmd.ExecuteAsync();
Pipe stdout as text into a StringBuilder
var stdOutBuffer = new StringBuilder();

var cmd = Cli.Wrap("foo") | stdOutBuffer;
await cmd.ExecuteAsync();
Pipe a binary HTTP stream into stdin
using var httpClient = new HttpClient();
await using var input = await httpClient.GetStreamAsync("https://example.com/image.png");

var cmd = input | Cli.Wrap("foo");
await cmd.ExecuteAsync();
Pipe stdout of one command into stdin of another
var cmd = Cli.Wrap("foo") | Cli.Wrap("bar") | Cli.Wrap("baz");
await cmd.ExecuteAsync();
Pipe stdout and stderr into parent process
await using var stdOut = Console.OpenStandardOutput();
await using var stdErr = Console.OpenStandardError();

var cmd = Cli.Wrap("foo") | (stdOut, stdErr);
await cmd.ExecuteAsync();
Pipe stdout to a delegate
var cmd = Cli.Wrap("foo") | Debug.WriteLine;
await cmd.ExecuteAsync();
Pipe stdout into a file and stderr into a StringBuilder
var buffer = new StringBuilder();

var cmd = Cli.Wrap("foo") |
    (PipeTarget.ToFile("output.txt"), PipeTarget.ToStringBuilder(buffer));

await cmd.ExecuteAsync();
Pipe stdout into multiple files simultaneously
var target = PipeTarget.Merge(
    PipeTarget.ToFile("file1.txt"),
    PipeTarget.ToFile("file2.txt"),
    PipeTarget.ToFile("file3.txt")
);

var cmd = Cli.Wrap("foo") | target;
await cmd.ExecuteAsync();
Pipe a chain of commands
var cmd = "Hello world" | Cli.Wrap("foo")
    .WithArguments("print random") | Cli.Wrap("bar")
    .WithArguments("reverse") | (Console.WriteLine, Console.Error.WriteLine);

await cmd.ExecuteAsync();

Execution models

CliWrap provides a few high-level execution models, which are essentially just extension methods that offer alternative ways to reason about command execution. Under the hood, they are all built by leveraging the piping feature shown earlier.

Buffered execution

This execution model lets you run a process while buffering its standard output and error streams in-memory. The buffered data can then be accessed after the command finishes executing.

In order to execute a command with buffering, call the ExecuteBufferedAsync() extension method:

using CliWrap;
using CliWrap.Buffered;

var result = await Cli.Wrap("path/to/exe")
    .WithArguments("--foo bar")
    .ExecuteBufferedAsync();

var exitCode = result.ExitCode;    
var stdOut = result.StandardOutput;
var stdErr = result.StandardError;

By default, ExecuteBufferedAsync() assumes that the underlying process uses default encoding (Console.OutputEncoding) for writing text to the console. To override this, specify the encoding explicitly by using one of the available overloads:

// Treat both stdout and stderr as UTF8-encoded text streams
var result = await Cli.Wrap("path/to/exe")
    .WithArguments("--foo bar")
    .ExecuteBufferedAsync(Encoding.UTF8);

// Treat stdout as ASCII-encoded and stderr as UTF8-encoded
var result = await Cli.Wrap("path/to/exe")
    .WithArguments("--foo bar")
    .ExecuteBufferedAsync(Encoding.ASCII, Encoding.UTF8);
Pull-based event stream

Besides executing a command as a task, CliWrap also supports an alternative model, in which the execution is represented as an event stream. This lets you start a command and react to the events it produces in real-time.

Those events are:

  • StartedCommandEvent -- received just once, when the command starts executing (contains process ID)
  • StandardOutputCommandEvent -- received every time the underlying process writes a new line to the output stream (contains the text as string)
  • StandardErrorCommandEvent -- received every time the underlying process writes a new line to the error stream (contains the text as string)
  • ExitedCommandEvent -- received just once, when the command finishes executing (contains exit code)

To execute a command as a pull-based event stream, use the ListenAsync() extension method:

using CliWrap;
using CliWrap.EventStream;

var cmd = Cli.Wrap("foo").WithArguments("bar");

await foreach (var cmdEvent in cmd.ListenAsync())
{
    switch (cmdEvent)
    {
        case StartedCommandEvent started:
            _output.WriteLine($"Process started; ID: {started.ProcessId}");
            break;
        case StandardOutputCommandEvent stdOut:
            _output.WriteLine($"Out> {stdOut.Text}");
            break;
        case StandardErrorCommandEvent stdErr:
            _output.WriteLine($"Err> {stdErr.Text}");
            break;
        case ExitedCommandEvent exited:
            _output.WriteLine($"Process exited; Code: {exited.ExitCode}");
            break;
    }
}

The ListenAsync() method starts the command and returns an object of type IAsyncEnumerable<CommandEvent>, which you can iterate using the await foreach construct introduced in C# 8. When using this execution model, back pressure is facilitated by locking the pipes between each iteration of the loop, preventing unnecessary buffering of data in-memory.

If you also need to specify custom encoding, you can use one of the available overloads:

await foreach (var cmdEvent in cmd.ListenAsync(Encoding.UTF8))
{
    // ...
}

await foreach (var cmdEvent in cmd.ListenAsync(Encoding.ASCII, Encoding.UTF8))
{
    // ...
}
Push-based event stream

Similarly to the pull-based stream, you can also execute a command as a push-based event stream instead:

using CliWrap;
using CliWrap.EventStream;
using System.Reactive;

await cmd.Observe().ForEachAsync(cmdEvent =>
{
    switch (cmdEvent)
    {
        case StartedCommandEvent started:
            _output.WriteLine($"Process started; ID: {started.ProcessId}");
            break;
        case StandardOutputCommandEvent stdOut:
            _output.WriteLine($"Out> {stdOut.Text}");
            break;
        case StandardErrorCommandEvent stdErr:
            _output.WriteLine($"Err> {stdErr.Text}");
            break;
        case ExitedCommandEvent exited:
            _output.WriteLine($"Process exited; Code: {exited.ExitCode}");
            break;
    }
});

In this case, Observe() returns a cold IObservable<CommandEvent> that represents an observable stream of command events. You can use the set of extensions provided by Rx.NET to transform, filter, throttle, or otherwise manipulate this stream.

Unlike with the pull-based event stream, this execution model does not involve any back pressure, meaning that the data is pushed to the observer at the rate it becomes available.

Likewise, if you also need to specify custom encoding, you can use one of the available overloads:

var cmdEvents = cmd.Observe(Encoding.UTF8);

// ...

var cmdEvents = cmd.Observe(Encoding.ASCII, Encoding.UTF8);

// ...
Combining execution models with custom pipes

The different execution models shown above are based on the piping model, but those two concepts are not mutually exclusive. That's because internally they all rely on PipeTarget.Merge(), which allows them to wire new pipes while still preserving those configured earlier.

This means that, for example, you can create a piped command and also execute it as an event stream:

var cmd =
    PipeSource.FromFile("input.txt") |
    Cli.Wrap("foo") |
    PipeSource.ToFile("output.txt");

// Iterate as an event stream and pipe to file at the same time
// (pipes are combined, not overriden)
await foreach (var cmdEvent in cmd.ListenAsync())
{
    // ...
}

Timeout and cancellation

Command execution is asynchronous in nature as it involves a completely separate process. In many cases, it may be useful to implement an abortion mechanism to stop the execution before it finishes, either through a manual trigger or a timeout.

To do that, just pass the corresponding CancellationToken when calling ExecuteAsync():

using var cts = new CancellationTokenSource();

// Cancel automatically after a timeout of 10 seconds
cts.CancelAfter(TimeSpan.FromSeconds(10));

var result = await Cli.Wrap("path/to/exe").ExecuteAsync(cts.Token);

In the event that the cancellation is requested, the underlying process will get killed and the ExecuteAsync() will throw an exception of type OperationCanceledException (or its derivative, TaskCanceledException). You will need to catch this exception in your code to recover from cancellation.

Similarly to ExecuteAsync(), cancellation is also supported by ExecuteBufferedAsync(), ListenAsync(), and Observe():

// Cancellation with buffered execution
var result = await Cli.Wrap("path/to/exe").ExecuteBufferedAsync(cts.Token);

// Cancellation with pull-based event stream
await foreach (Cli.Wrap("path/to/exe").ListenAsync(cts.Token))
{
    // ...
}

// Cancellation with push-based event stream
var cmdEvents = Cli.Wrap("path/to/exe").Observe(cts.Token);

Retrieving process ID

The task returned by ExecuteAsync() and ExecuteBufferedAsync() is in fact not a regular Task<T>, but an instance of CommandTask<T>. This is a special awaitable object that contains additional information related to a command which is currently executing.

You can inspect the task while it's running to get the ID of the process that was started by the associated command:

var task = Cli.Wrap("path/to/exe")
    .WithArguments("--foo bar")
    .ExecuteAsync();

// Get the process ID (for example, for logging purposes)
var processId = task.ProcessId;

// Wait for the task to complete
await task;

CliWrap

Build Coverage Version Downloads Discord Donate

✅ Project status: active.

CliWrap is a library for interacting with external command line interfaces. It provides a convenient model for launching processes, redirecting input and output streams, awaiting completion, handling cancellation, and more.

💬 If you want to chat, join my Discord server.

Download

📦 NuGet: dotnet add package CliWrap

Features

  • Airtight abstraction over System.Diagnostics.Process
  • Fluent configuration interface
  • Flexible support for piping
  • Fully asynchronous and cancellation-aware API
  • Designed with strict immutability in mind
  • Provides safety against typical deadlock scenarios
  • Tested on Windows, Linux, and macOS
  • Targets .NET Standard 2.0+, .NET Core 3.0+, .NET Framework 4.6.1+
  • No external dependencies

Usage

Quick overview

Similarly to a shell, CliWrap's base unit of work is a command -- an object that encodes instructions for running a process. To build a command, start by calling Cli.Wrap(...) with the executable path, and then use the provided fluent interface to configure arguments, working directory, or other options. Once the command is configured, you can run it by calling ExecuteAsync():

using CliWrap;

var result = await Cli.Wrap("path/to/exe")
    .WithArguments("--foo bar")
    .WithWorkingDirectory("work/dir/path")
    .ExecuteAsync();
    
// Result contains:
// -- result.ExitCode        (int)
// -- result.StartTime       (DateTimeOffset)
// -- result.ExitTime        (DateTimeOffset)
// -- result.RunTime         (TimeSpan)

The code above spawns a child process with the configured command line arguments and working directory, and then asynchronously waits for it to exit. After the task has completed, it resolves a CommandResult object that contains the process exit code and other related information.

⚠️ Note that CliWrap will throw an exception if the underlying process returns a non-zero exit code, as it usually indicates an error. You can override this behavior by disabling result validation using WithValidation(CommandResultValidation.None).

By default, the process's standard input, output and error streams are routed to CliWrap's equivalent of the null device, which represents an empty source and a target that discards all data. You can change this by calling WithStandardInputPipe(...), WithStandardOutputPipe(...), or WithStandardErrorPipe(...) to configure pipes for the corresponding streams:

var stdOutBuffer = new StringBuilder();
var stdErrBuffer = new StringBuilder();

var result = await Cli.Wrap("path/to/exe")
    .WithArguments("--foo bar")
    .WithWorkingDirectory("work/dir/path")
    .WithStandardOutputPipe(PipeTarget.ToStringBuilder(stdOutBuffer))
    .WithStandardErrorPipe(PipeTarget.ToStringBuilder(stdErrBuffer))
    .ExecuteAsync();
    
// Contains stdOut/stdErr buffered in-memory as string
var stdOut = stdOutBuffer.ToString(); 
var stdErr = stdErrBuffer.ToString();

In this example, the data pushed to standard output and error streams is decoded as text and written to separate StringBuilder buffers. After the command has finished executing, you can inspect the contents of these buffers to see what the process has printed to the console during its runtime.

Handling command output is a very common use case, so CliWrap offers a few high-level execution models to make these scenarios simpler. In particular, the same thing shown above can also be achieved more succinctly with the ExecuteBufferedAsync() extension method:

using CliWrap;
using CliWrap.Buffered;

// Calling `ExecuteBufferedAsync()` instead of `ExecuteAsync()`
// implicitly configures pipes that write to in-memory buffers.
var result = await Cli.Wrap("path/to/exe")
    .WithArguments("--foo bar")
    .WithWorkingDirectory("work/dir/path")
    .ExecuteBufferedAsync();

// Result contains:
// -- result.StandardOutput  (string)
// -- result.StandardError   (string)
// -- result.ExitCode        (int)
// -- result.StartTime       (DateTimeOffset)
// -- result.ExitTime        (DateTimeOffset)
// -- result.RunTime         (TimeSpan)

⚠️ Note that standard streams are not limited to text and can contain raw binary data. Additionally, the size of the data may make it inefficient to store in-memory. For more complex scenarios, CliWrap also provides other piping options, which are covered in the Piping section.

Command configuration

The fluent interface provided by the command object allows you to configure various options related to its execution. Below list covers all available configuration methods and their usage.

⚠️ Note that Command is an immutable object, meaning that all configuration methods listed here return a new instance instead of modifying the existing one.

WithArguments(...)

Sets the command line arguments that will be passed to the child process.

Default: empty.

Set arguments directly from a string:

var cmd = Cli.Wrap("git")
    .WithArguments("commit -m \"my commit\"");

Set arguments from a list (each element is treated as a separate argument; spaces are escaped automatically):

var cmd = Cli.Wrap("git")
    .WithArguments(new[] {"commit", "-m", "my commit"});

Set arguments using a builder (same as above, but also automatically converts certain values to their string representations):

var cmd = Cli.Wrap("git")
    .WithArguments(args => args
        .Add("clone")
        .Add("https://github.com/Tyrrrz/CliWrap")
        .Add("--depth")
        .Add(20)); // <- formatted to a string
WithWorkingDirectory(...)

Sets the working directory of the child process.

Default: current working directory, i.e. Directory.GetCurrentDirectory().

var cmd = Cli.Wrap("git")
    .WithWorkingDirectory("c:/projects/my project/");
WithEnvironmentVariables(...)

Sets additional environment variables that will be exposed to the child process.

Default: empty.

Set environment variables from a dictionary:

var cmd = Cli.Wrap("git")
    .WithEnvironmentVariables(new Dictionary<string, string?>
    {
        ["GIT_AUTHOR_NAME"] = "John",
        ["GIT_AUTHOR_EMAIL"] = "john@email.com"
    });

Set environment variables using a builder:

var cmd = Cli.Wrap("git")
    .WithEnvironmentVariables(env => env
        .Set("GIT_AUTHOR_NAME", "John")
        .Set("GIT_AUTHOR_EMAIL", "john@email.com"));

⚠️ Note that these environment variables are set on top of the default environment variables inherited from the parent process. If you provide a variable with the same name as one of the inherited variables, the provided value will take precedence. Additionally, you can also remove an inherited variable by setting its value to null.

WithCredentials(...)

Sets domain, name and password of the user, under whom the child process will be started.

Default: no credentials.

Set credentials directly:

var cmd = Cli.Wrap("git")
    .WithCredentials(new Credentials(
        "some_workspace",
        "johndoe",
        "securepassword123"
    ));

Set credentials using a builder:

var cmd = Cli.Wrap("git")
    .WithCredentials(creds => creds
       .SetDomain("some_workspace")
       .SetUserName("johndoe")
       .SetPassword("securepassword123"));

⚠️ Note that specifying domain and password is only supported on Windows and will result in an exception on other operating systems. Specifying username, on the other hand, is supported across all platforms.

WithValidation(...)

Sets the strategy for validating the result of an execution.

The following modes are available:

  • CommandResultValidation.None -- no validation
  • CommandResultValidation.ZeroExitCode -- ensures zero exit code when the process exits

Default: CommandResultValidation.ZeroExitCode.

Disable validation:

var cmd = Cli.Wrap("git")
    .WithValidation(CommandResultValidation.None);

Enable validation:

// Ensure that exit code is zero after the process exits (otherwise throw an exception)
var cmd = Cli.Wrap("git")
    .WithValidation(CommandResultValidation.ZeroExitCode);
WithStandardInputPipe(...)

Sets the pipe source that will be used for the standard input stream of the process.

Default: PipeSource.Null.

Read more about this method in the Piping section.

WithStandardOutputPipe(...)

Sets the pipe target that will be used for the standard output stream of the process.

Default: PipeTarget.Null.

Read more about this method in the Piping section.

WithStandardErrorPipe(...)

Sets the pipe target that will be used for the standard error stream of the process.

Default: PipeTarget.Null.

Read more about this method in the Piping section.

Piping

CliWrap provides a very powerful and flexible piping model that allows you to redirect process's streams, transform input and output data, and even chain multiple commands together with minimal effort. At its core, it's based on two abstractions: PipeSource which provides data for standard input stream, and PipeTarget which reads data coming from standard output or standard error streams.

By default, command's input pipe is set to PipeSource.Null and the output and error pipes are set to PipeTarget.Null. These objects effectively represent no-op stubs that provide empty input and discard all output respectively.

You can specify your own PipeSource and PipeTarget instances by calling the corresponding configuration methods on the command:

await using var input = File.OpenRead("input.txt");
await using var output = File.Create("output.txt");

await Cli.Wrap("foo")
    .WithStandardInputPipe(PipeSource.FromStream(input))
    .WithStandardOutputPipe(PipeTarget.ToStream(output))
    .ExecuteAsync();

Alternatively, pipes can also be configured in a slightly terser way by using pipe operators:

await using var input = File.OpenRead("input.txt");
await using var output = File.Create("output.txt");

await (input | Cli.Wrap("foo") | output).ExecuteAsync();

Both PipeSource and PipeTarget have many factory methods that let you create pipe implementations for different scenarios:

  • PipeSource:
    • PipeSource.Null -- represents an empty pipe source
    • PipeSource.FromStream(...) -- pipes data from any readable stream
    • PipeSource.FromFile(...) -- pipes data from a file
    • PipeSource.FromBytes(...) -- pipes data from a byte array
    • PipeSource.FromString(...) -- pipes from a text string
    • PipeSource.FromCommand(...) -- pipes data from standard output of another command
  • PipeTarget:
    • PipeTarget.Null -- represents a pipe target that discards all data
    • PipeTarget.ToStream(...) -- pipes data into any writeable stream
    • PipeTarget.ToFile(...) -- pipes data into a file
    • PipeTarget.ToStringBuilder(...) -- pipes data as text into StringBuilder
    • PipeTarget.ToDelegate(...) -- pipes data as text, line-by-line, into Action<string> or Func<string, Task>
    • PipeTarget.Merge(...) -- merges multiple outbound pipes by replicating the same data across all of them

Below you can see some examples of what you can achieve with the help of CliWrap's piping feature.

Pipe a string into stdin
var cmd = "Hello world" | Cli.Wrap("foo");
await cmd.ExecuteAsync();
Pipe stdout as text into a StringBuilder
var stdOutBuffer = new StringBuilder();

var cmd = Cli.Wrap("foo") | stdOutBuffer;
await cmd.ExecuteAsync();
Pipe a binary HTTP stream into stdin
using var httpClient = new HttpClient();
await using var input = await httpClient.GetStreamAsync("https://example.com/image.png");

var cmd = input | Cli.Wrap("foo");
await cmd.ExecuteAsync();
Pipe stdout of one command into stdin of another
var cmd = Cli.Wrap("foo") | Cli.Wrap("bar") | Cli.Wrap("baz");
await cmd.ExecuteAsync();
Pipe stdout and stderr into parent process
await using var stdOut = Console.OpenStandardOutput();
await using var stdErr = Console.OpenStandardError();

var cmd = Cli.Wrap("foo") | (stdOut, stdErr);
await cmd.ExecuteAsync();
Pipe stdout to a delegate
var cmd = Cli.Wrap("foo") | Debug.WriteLine;
await cmd.ExecuteAsync();
Pipe stdout into a file and stderr into a StringBuilder
var buffer = new StringBuilder();

var cmd = Cli.Wrap("foo") |
    (PipeTarget.ToFile("output.txt"), PipeTarget.ToStringBuilder(buffer));

await cmd.ExecuteAsync();
Pipe stdout into multiple files simultaneously
var target = PipeTarget.Merge(
    PipeTarget.ToFile("file1.txt"),
    PipeTarget.ToFile("file2.txt"),
    PipeTarget.ToFile("file3.txt")
);

var cmd = Cli.Wrap("foo") | target;
await cmd.ExecuteAsync();
Pipe a chain of commands
var cmd = "Hello world" | Cli.Wrap("foo")
    .WithArguments("print random") | Cli.Wrap("bar")
    .WithArguments("reverse") | (Console.WriteLine, Console.Error.WriteLine);

await cmd.ExecuteAsync();

Execution models

CliWrap provides a few high-level execution models, which are essentially just extension methods that offer alternative ways to reason about command execution. Under the hood, they are all built by leveraging the piping feature shown earlier.

Buffered execution

This execution model lets you run a process while buffering its standard output and error streams in-memory. The buffered data can then be accessed after the command finishes executing.

In order to execute a command with buffering, call the ExecuteBufferedAsync() extension method:

using CliWrap;
using CliWrap.Buffered;

var result = await Cli.Wrap("path/to/exe")
    .WithArguments("--foo bar")
    .ExecuteBufferedAsync();

var exitCode = result.ExitCode;    
var stdOut = result.StandardOutput;
var stdErr = result.StandardError;

By default, ExecuteBufferedAsync() assumes that the underlying process uses default encoding (Console.OutputEncoding) for writing text to the console. To override this, specify the encoding explicitly by using one of the available overloads:

// Treat both stdout and stderr as UTF8-encoded text streams
var result = await Cli.Wrap("path/to/exe")
    .WithArguments("--foo bar")
    .ExecuteBufferedAsync(Encoding.UTF8);

// Treat stdout as ASCII-encoded and stderr as UTF8-encoded
var result = await Cli.Wrap("path/to/exe")
    .WithArguments("--foo bar")
    .ExecuteBufferedAsync(Encoding.ASCII, Encoding.UTF8);
Pull-based event stream

Besides executing a command as a task, CliWrap also supports an alternative model, in which the execution is represented as an event stream. This lets you start a command and react to the events it produces in real-time.

Those events are:

  • StartedCommandEvent -- received just once, when the command starts executing (contains process ID)
  • StandardOutputCommandEvent -- received every time the underlying process writes a new line to the output stream (contains the text as string)
  • StandardErrorCommandEvent -- received every time the underlying process writes a new line to the error stream (contains the text as string)
  • ExitedCommandEvent -- received just once, when the command finishes executing (contains exit code)

To execute a command as a pull-based event stream, use the ListenAsync() extension method:

using CliWrap;
using CliWrap.EventStream;

var cmd = Cli.Wrap("foo").WithArguments("bar");

await foreach (var cmdEvent in cmd.ListenAsync())
{
    switch (cmdEvent)
    {
        case StartedCommandEvent started:
            _output.WriteLine($"Process started; ID: {started.ProcessId}");
            break;
        case StandardOutputCommandEvent stdOut:
            _output.WriteLine($"Out> {stdOut.Text}");
            break;
        case StandardErrorCommandEvent stdErr:
            _output.WriteLine($"Err> {stdErr.Text}");
            break;
        case ExitedCommandEvent exited:
            _output.WriteLine($"Process exited; Code: {exited.ExitCode}");
            break;
    }
}

The ListenAsync() method starts the command and returns an object of type IAsyncEnumerable<CommandEvent>, which you can iterate using the await foreach construct introduced in C# 8. When using this execution model, back pressure is facilitated by locking the pipes between each iteration of the loop, preventing unnecessary buffering of data in-memory.

If you also need to specify custom encoding, you can use one of the available overloads:

await foreach (var cmdEvent in cmd.ListenAsync(Encoding.UTF8))
{
    // ...
}

await foreach (var cmdEvent in cmd.ListenAsync(Encoding.ASCII, Encoding.UTF8))
{
    // ...
}
Push-based event stream

Similarly to the pull-based stream, you can also execute a command as a push-based event stream instead:

using CliWrap;
using CliWrap.EventStream;
using System.Reactive;

await cmd.Observe().ForEachAsync(cmdEvent =>
{
    switch (cmdEvent)
    {
        case StartedCommandEvent started:
            _output.WriteLine($"Process started; ID: {started.ProcessId}");
            break;
        case StandardOutputCommandEvent stdOut:
            _output.WriteLine($"Out> {stdOut.Text}");
            break;
        case StandardErrorCommandEvent stdErr:
            _output.WriteLine($"Err> {stdErr.Text}");
            break;
        case ExitedCommandEvent exited:
            _output.WriteLine($"Process exited; Code: {exited.ExitCode}");
            break;
    }
});

In this case, Observe() returns a cold IObservable<CommandEvent> that represents an observable stream of command events. You can use the set of extensions provided by Rx.NET to transform, filter, throttle, or otherwise manipulate this stream.

Unlike with the pull-based event stream, this execution model does not involve any back pressure, meaning that the data is pushed to the observer at the rate it becomes available.

Likewise, if you also need to specify custom encoding, you can use one of the available overloads:

var cmdEvents = cmd.Observe(Encoding.UTF8);

// ...

var cmdEvents = cmd.Observe(Encoding.ASCII, Encoding.UTF8);

// ...
Combining execution models with custom pipes

The different execution models shown above are based on the piping model, but those two concepts are not mutually exclusive. That's because internally they all rely on PipeTarget.Merge(), which allows them to wire new pipes while still preserving those configured earlier.

This means that, for example, you can create a piped command and also execute it as an event stream:

var cmd =
    PipeSource.FromFile("input.txt") |
    Cli.Wrap("foo") |
    PipeSource.ToFile("output.txt");

// Iterate as an event stream and pipe to file at the same time
// (pipes are combined, not overriden)
await foreach (var cmdEvent in cmd.ListenAsync())
{
    // ...
}

Timeout and cancellation

Command execution is asynchronous in nature as it involves a completely separate process. In many cases, it may be useful to implement an abortion mechanism to stop the execution before it finishes, either through a manual trigger or a timeout.

To do that, just pass the corresponding CancellationToken when calling ExecuteAsync():

using var cts = new CancellationTokenSource();

// Cancel automatically after a timeout of 10 seconds
cts.CancelAfter(TimeSpan.FromSeconds(10));

var result = await Cli.Wrap("path/to/exe").ExecuteAsync(cts.Token);

In the event that the cancellation is requested, the underlying process will get killed and the ExecuteAsync() will throw an exception of type OperationCanceledException (or its derivative, TaskCanceledException). You will need to catch this exception in your code to recover from cancellation.

Similarly to ExecuteAsync(), cancellation is also supported by ExecuteBufferedAsync(), ListenAsync(), and Observe():

// Cancellation with buffered execution
var result = await Cli.Wrap("path/to/exe").ExecuteBufferedAsync(cts.Token);

// Cancellation with pull-based event stream
await foreach (Cli.Wrap("path/to/exe").ListenAsync(cts.Token))
{
    // ...
}

// Cancellation with push-based event stream
var cmdEvents = Cli.Wrap("path/to/exe").Observe(cts.Token);

Retrieving process ID

The task returned by ExecuteAsync() and ExecuteBufferedAsync() is in fact not a regular Task<T>, but an instance of CommandTask<T>. This is a special awaitable object that contains additional information related to a command which is currently executing.

You can inspect the task while it's running to get the ID of the process that was started by the associated command:

var task = Cli.Wrap("path/to/exe")
    .WithArguments("--foo bar")
    .ExecuteAsync();

// Get the process ID (for example, for logging purposes)
var processId = task.ProcessId;

// Wait for the task to complete
await task;

Release Notes

https://github.com/Tyrrrz/CliWrap/blob/master/Changelog.md

NuGet packages (24)

Showing the top 5 NuGet packages that depend on CliWrap:

Package Downloads
YoutubeExplode.Converter
Extension for YoutubeExplode that provides an interface to download and convert videos using FFmpeg
TinyFx
TinyFx基础组件
Pulumi.Automation
Pulumi Automation API, the programmatic interface for driving Pulumi programs without the CLI.
HttpRequester
Multiple http clients
Stl.CommandLine
A set of abstractions helping to construct typed wrappers for command line tools.

GitHub repositories (12)

Showing the top 5 popular GitHub repositories that depend on CliWrap:

Repository Stars
Tyrrrz/YoutubeExplode
The ultimate dirty YouTube library
MonkSoul/Furion
让 .NET 开发更简单,更通用,更流行。
Azure/iotedge
The IoT Edge OSS project
servicetitan/Stl.Fusion
Build real-time apps (Blazor included) with less than 1% of extra code responsible for real-time updates. Host 10-1000x faster APIs relying on transparent and nearly 100% consistent caching. We call it DREAM, or Distributed REActive Memoization, and it's here to turn real-time on!
Bililive/BililiveRecorder
B站录播姬 | BiliBili Stream Recorder | 哔哩哔哩直播录制

Version History

Version Downloads Last updated
3.3.3 1,866 8/31/2021
3.3.2 244,137 4/1/2021
3.3.1 12,323 2/21/2021
3.3.0 14,590 12/29/2020
3.2.4 7,124 12/5/2020
3.2.3 54,957 11/4/2020
3.2.2 5,400 10/14/2020
3.2.1 2,019 10/13/2020
3.2.0 44,523 9/17/2020
3.1.1 12,732 9/6/2020
3.1.0 19,978 6/27/2020
3.0.3 1,976 6/22/2020
3.0.2 167,024 4/28/2020
3.0.1 421 4/26/2020
3.0.0 50,835 2/27/2020
3.0.0-alpha3 373 2/23/2020
3.0.0-alpha2 262 2/22/2020
3.0.0-alpha 293 2/21/2020
2.5.0 25,310 10/30/2019
2.4.0 41,385 10/3/2019
2.3.1 18,269 7/10/2019
2.3.0 4,817 5/28/2019
2.2.2 2,525 5/10/2019
2.2.1 4,015 4/1/2019
2.2.0 14,095 12/20/2018
2.1.0 3,398 10/14/2018
2.0.1 1,019 9/17/2018
2.0.0 903 9/12/2018
1.8.5 4,513 6/9/2018
1.8.4 2,901 3/9/2018
1.8.3 977 3/3/2018
1.8.2 1,267 2/2/2018
1.8.1 1,045 1/25/2018
1.8.0 993 1/25/2018
1.7.6-alpha 932 1/24/2018
1.7.5 1,370 1/21/2018
1.7.4 1,125 1/10/2018
1.7.3 1,101 12/19/2017
1.7.2 1,049 12/19/2017
1.7.1 979 12/5/2017
1.7.0 905 12/4/2017
1.6.1 1,237 11/25/2017
1.6.0 1,216 10/18/2017
1.5.3 1,142 10/7/2017
1.5.2 1,190 10/2/2017
1.5.1 1,234 9/8/2017
1.5.0 991 7/25/2017
1.4.0 942 7/25/2017
1.3.0 983 6/30/2017
1.2.3 980 6/1/2017
1.2.2 960 5/4/2017
1.2.1 932 4/27/2017
1.2.0 935 4/27/2017
1.1.0 908 4/26/2017
1.0.0 1,070 4/11/2017