Skip to content

Migration guide (from v2.5 to v3.0)

Alexey Golub edited this page May 17, 2020 · 12 revisions

CliWrap v3.0 introduced a lot of breaking changes. This guide should cover most of the common scenarios with suggestions on how to migrate your code to the latest version of CliWrap. If you still have problems or questions, create a new issue.

Basic execution scenarios

CliWrap v2.5

var result = await Cli.Wrap("cli.exe")
    .SetArguments("Hello world!")
    .ExecuteAsync();

var exitCode = result.ExitCode;
var stdOut = result.StandardOutput;
var stdErr = result.StandardError;
var startTime = result.StartTime;
var exitTime = result.ExitTime;
var runTime = result.RunTime;

CliWrap v3.0

var result = await Cli.Wrap("cli.exe")
    .WithArguments("Hello world!")
    .ExecuteBufferedAsync();

var exitCode = result.ExitCode;
var stdOut = result.StandardOutput;
var stdErr = result.StandardError;
var startTime = result.StartTime;
var exitTime = result.ExitTime;
var runTime = result.RunTime;

If you're not interested in buffering stdout and stderr, you can now also do the following:

var result = await Cli.Wrap("cli.exe")
    .WithArguments("Hello world!")
    .ExecuteAsync();

* Note, synchronous Execute() is now gone. Use ExecuteAsync() or, if you really can't, resort to ExecuteAsync().GetAwaiter().GetResult().

** Note, ExecuteAndForget() is now gone. If you really need to, use ExecuteAsync() without awaiting it.

Command configuration

Setting arguments

CliWrap v2.5

Cli.Wrap("foo").SetArguments("bar baz");

Or

Cli.Wrap("foo").SetArguments(new[] {"bar", "baz"});

CliWrap v3.0

Cli.Wrap("foo").WithArguments("bar baz");

Or

Cli.Wrap("foo").WithArguments(new[] {"bar", "baz"});

Or

Cli.Wrap("foo").WithArguments(a => a.Add("bar").Add("baz"));

Setting working directory

CliWrap v2.5

Cli.Wrap("foo").SetWorkingDirectory("path")

CliWrap v3.0

Cli.Wrap("foo").WithWorkingDirectory("path")

Setting environment variables

CliWrap v2.5

Cli.Wrap("foo")
    .SetEnvironmentVariable("var1", "value1")
    .SetEnvironmentVariable("var2", "value2")

CliWrap v3.0

Cli.Wrap("foo").WithEnvironmentVariables(e => e
    .Set("var1", "value1")
    .Set("var2", "value2"));

Configuring validation

CliWrap v2.5

Cli.Wrap("foo")
    .EnableExitCodeValidation(true)
    .EnableStandardErrorValidation(true)

CliWrap v3.0

Cli.Wrap("foo")
    .WithValidation(CommandResultValidation.ZeroExitCode);

Or

Cli.Wrap("foo")
    .WithValidation(CommandResultValidation.None);

* Note, standard error validation has been removed entirely.

** Note, other configuration options (encoding, callbacks, etc) no longer exist and have been replaced with different execution models.

*** Note, in addition to the changes listed above, WithXyz() methods now return new immutable objects, instead of modifying the original instance.

Setting encoding

CliWrap v2.5

await result = Cli.Wrap("foo")
    .SetStandardOutputEncoding(Encoding.UTF8)
    .SetStandardErrorEncoding(Encoding.UTF8)
    .ExecuteAsync();

CliWrap v3.0

await result = Cli.Wrap("foo")
    .ExecuteBufferedAsync(Encoding.UTF8, Encoding.UTF8);

Or

await result = Cli.Wrap("foo")
    .ExecuteBufferedAsync(Encoding.UTF8);

Callbacks

Callbacks have been removed, but you can achieve the same result through piping or event streams.

CliWrap v2.5

var result = await Cli.Wrap("cli.exe")
    .SetStandardOutputCallback(l => Console.WriteLine($"StdOut> {l}"))
    .SetStandardErrorCallback(l => Console.WriteLine($"StdErr> {l}"))
    .ExecuteAsync();

CliWrap v3.0

With piping:

var result = await Cli.Wrap("cli.exe")
    .WithStandardOutputPipe(PipeTarget.ToDelegate(l => Console.WriteLine($"StdOut> {l}")))
    .WithStandardErrorPipe(PipeTarget.ToDelegate(l => Console.WriteLine($"StdErr> {l}")))
    .ExecuteAsync(); // await cmd.ExecuteBufferedAsync();

Or

void StdOutHandler(string l) => Console.WriteLine($"StdOut> {l}");
void StdErrHandler(string l) => Console.WriteLine($"StdErr> {l}");

var cmd = Cli.Wrap("cli.exe") | (StdOutHandler, StdErrHandler);
var result = await cmd.ExecuteAsync(); // await cmd.ExecuteBufferedAsync();

With async event streams:

await foreach (var cmdEvent in Cli.Wrap("cli.exe").ListenAsync())
{
    switch (cmdEvent)
    {
        case StandardOutputCommandEvent stdOut:
            Console.WriteLine($"StdOut> {stdOut.Text}");
            break;
        case StandardErrorCommandEvent stdErr:
            Console.WriteLine($"StdErr> {stdErr.Text}");
            break;
    }
}

With observable event streams:

await Cli.Wrap("cli.exe").Observe().ForEachAsync(cmdEvent =>
{
    switch (cmdEvent)
    {
        case StandardOutputCommandEvent stdOut:
            Console.WriteLine($"StdOut> {stdOut.Text}");
            break;
        case StandardErrorCommandEvent stdErr:
            Console.WriteLine($"StdErr> {stdErr.Text}");
            break;
    }
});

Cancellation

CliWrap v2.5

using var cts = new CancellationTokenSource();

cts.CancelAfter(TimeSpan.FromSeconds(5));

var result = await Cli.Wrap("cli.exe")
    .SetCancellationToken(cts.Token)
    .ExecuteAsync();

CliWrap v3.0

using var cts = new CancellationTokenSource();

cts.CancelAfter(TimeSpan.FromSeconds(5));

var result = await Cli.Wrap("cli.exe")
    .ExecuteAsync(cts.Token);

Piping stdin

CliWrap v3.0 has support for a very wide range of piping scenarios while older versions of CliWrap only supported piping stdin from a regular stream or a string.

CliWrap v2.5

var result = await Cli.Wrap("cli.exe")
    .SetStandardInput("Hello world from stdin!")
    .ExecuteAsync();

CliWrap v3.0

var result = await Cli.Wrap("cli.exe")
    .WithStandardInputPipe(PipeSource.FromString("Hello world from stdin!"))
    .ExecuteAsync();

Or

var result = await ("Hello world from stdin!" | Cli.Wrap("cli.exe")).ExecuteAsync();

Check out readme to see what else you can do with piping in v3.0!

Getting process ID

CliWrap v2.5

var cmd = Cli.Wrap("cli.exe");
var task = cmd.ExecuteAsync();

var processId = cmd.ProcessId.Value;

await task;

CliWrap v3.0

var task = Cli.Wrap("cli.exe").ExecuteAsync();

var processId = task.ProcessId;

await task;

Removal of ICli

With v3.0 and going forward, CliWrap will not be providing autotelic interfaces for its public classes. This is done to discourage testing anti-patterns. When following dependency inversion principle correctly, the consumer must own the abstraction, instead of the other way around.

From "Excerpt from Agile Principles, Patterns, and Practices in C#":

Excerpt from Agile Principles, Patterns, and Practices in C#

Extrapolating from that, if you want to mock the dependency represented by an external CLI or a command, you need to put an abstraction in place at the system boundary that makes sense for your particular use case. For example, if you are using CliWrap to call docker-compose, you would have an IDockerCompose abstraction that has methods like Up(...), Down(...) etc. The default implementation of IDockerCompose would use CliWrap while your tests can use a mock instead.

If you were previously using ICli to inject spies to ensure that your consuming code was calling it correctly, that's an anti-pattern as well. Doing so binds your interface to its implementation. If you decide to change or refactor how any of your methods work, the tests will fail and won't provide a safety net for you.