Skip to content

Instantly share code, notes, and snippets.

@Tyrrrz
Created September 22, 2021 21:42
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Tyrrrz/4cf67feea50cfdbbd871f217a10427c4 to your computer and use it in GitHub Desktop.
Save Tyrrrz/4cf67feea50cfdbbd871f217a10427c4 to your computer and use it in GitHub Desktop.
CliWrap examples for JetBrains OSS Power-ups webinar

Basic execution

await Cli.Wrap("docker")
    .WithArguments("run --detach -e POSTGRES_PASSWORD=password postgres")
    .ExecuteAsync();
  • Show contents of command result
  • Show how to get process ID

Buffered execution

var result = await Cli.Wrap("docker")
    .WithArguments("run --detach -e POSTGRES_PASSWORD=password postgres")
    .ExecuteBufferedAsync();
  • Show contents of buffered command result

Execution that fails

await Cli.Wrap("docker")
    .WithArguments("run --detach -e POSTGRES_PASSWORD=password postgresss")
    .ExecuteAsync();
  • Show how ExecuteBufferedAsync() affects the error message

Passing arguments in different ways

var password = "hello world";

var result = await Cli.Wrap("docker")
    .WithArguments($"run --detach -e POSTGRES_PASSWORD={password} postgres")
    .ExecuteBufferedAsync();
  • Show that the password is not escaped and the arguments end up malformed
  • Instead of password, it can be anything, e.g. file path
var password = "hello world";

var result = await Cli.Wrap("docker")
    .WithArguments(new[]
    {
        "run",
        "--detach",
        "-e", $"POSTGRES_PASSWORD={password}",
        "postgres"
    })
    .ExecuteBufferedAsync();
  • Show that arguments are escaped correctly
var password = "hello world";

var result = await Cli.Wrap("docker")
    .WithArguments(args => args
        .Add("run")
        .Add("--detach")
        .Add("-e").Add($"POSTGRES_PASSWORD={password}")
        .Add("postgres")
    )
    .ExecuteBufferedAsync();
  • Show usage with IFormattable

Custom arguments builder extensions

public static class CliExtensions
{
    public static ArgumentsBuilder AddOption(
        this ArgumentsBuilder args,
        string name,
        string? value)
    {
        if (string.IsNullOrWhiteSpace(value))
            return args;

        return args.Add(name).Add(value);
    }
}
var network = "mynet";

var result = await Cli.Wrap("docker")
    .WithArguments(args => args
        .Add("run")
        .Add("--detach")
        .Add("-e").Add("POSTGRES_PASSWORD=hello world")
        .AddOption("--network", network)
        .Add("postgres")
    )
    .ExecuteBufferedAsync();
  • Show how changing network to empty string does not pass the option

Event stream execution models

var cmd = Cli.Wrap("docker")
    .WithArguments(args => args
        .Add("run")
        .Add("-e").Add("POSTGRES_PASSWORD=hello world")
        .Add("postgres")
    );

await foreach (var cmdEvent in cmd.ListenAsync())
{
    switch (cmdEvent)
    {
        case StartedCommandEvent started:
        {
            Console.WriteLine("Process started. PID: " + started.ProcessId);
            break;
        }

        case StandardOutputCommandEvent stdOut:
        {
            Console.ForegroundColor = ConsoleColor.White;
            Console.WriteLine("OUT> " + stdOut.Text);
            Console.ResetColor();
            break;
        }

        case StandardErrorCommandEvent stdErr:
        {
            Console.ForegroundColor = ConsoleColor.DarkRed;
            Console.WriteLine("ERR> " + stdErr.Text);
            Console.ResetColor();
            break;
        }

        case ExitedCommandEvent exited:
        {
            Console.WriteLine("Process exited. Code: " + exited.ExitCode);
            break;
        }
    }
}
  • Show how the events are handled as the process is running
  • Explain backpressure and single-threaded event flow
  • Show observable stream execution model
  • Mention that you can easily create your own execution models

Output and error piping

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

await Cli.Wrap("docker")
    .WithArguments(args => args
        .Add("run")
        .Add("--detach")
        .Add("-e").Add("POSTGRES_PASSWORD=hello world")
        .Add("postgres")
    )
    .WithStandardOutputPipe(PipeTarget.ToStringBuilder(stdOutBuffer))
    .WithStandardErrorPipe(PipeTarget.ToStringBuilder(stdErrBuffer))
    .ExecuteAsync();

Console.WriteLine(stdOutBuffer);
Console.WriteLine(stdErrBuffer);
  • Explain that this is functionally equivalent to ExecuteBufferedAsync() (exception non-zero exit code exception message)
  • Show PipeTarget contract
await Cli.Wrap("docker")
    .WithArguments(args => args
        .Add("run")
        .Add("--detach")
        .Add("-e").Add("POSTGRES_PASSWORD=hello world")
        .Add("postgres")
    )
    .WithStandardOutputPipe(PipeTarget.ToDelegate(Console.WriteLine))
    .WithStandardErrorPipe(PipeTarget.ToDelegate(Console.WriteLine))
    .ExecuteAsync();
  • Explain that ListenAsync() and ObserveAsync() are using a simlar setup under the hood
  • Show that this supports async delegates too (Func<string, Task>)
await Cli.Wrap("docker")
    .WithArguments(args => args
        .Add("run")
        .Add("--detach")
        .Add("-e").Add("POSTGRES_PASSWORD=hello world")
        .Add("postgres")
    )
    .WithStandardOutputPipe(PipeTarget.ToFile("stdout.txt"))
    .WithStandardErrorPipe(PipeTarget.ToFile("stderr.txt"))
    .ExecuteAsync();
  • Mention other targets
var result = await Cli.Wrap("docker")
    .WithArguments(args => args
        .Add("run")
        .Add("--detach")
        .Add("-e").Add("POSTGRES_PASSWORD=hello world")
        .Add("postgres")
    )
    .WithStandardOutputPipe(PipeTarget.ToDelegate(Console.WriteLine))
    .WithStandardErrorPipe(PipeTarget.ToDelegate(Console.WriteLine))
    .ExecuteBufferedAsync();

Console.WriteLine(result.StandardOutput);
  • Explain how execution models work without overriding pipes
var cmd = Cli.Wrap("docker")
    .WithArguments(args => args
        .Add("run")
        .Add("-e").Add("POSTGRES_PASSWORD=hello world")
        .Add("postgres")
    ) | (Console.WriteLine, Console.Error.WriteLine);

await cmd.ExecuteAsync();
  • Explain pipe operators
  • Explain how pipe operators work with tuples

Cancellation

using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(6));

try
{
    await Cli.Wrap("ping")
        .WithArguments("-t google.com")
        .WithStandardOutputPipe(PipeTarget.ToDelegate(Console.WriteLine))
        .ExecuteAsync(cts.Token);
}
catch (OperationCanceledException)
{
    Console.WriteLine("Cancelled!");
}
  • Explain how CliWrap kills the process on cancellation

Input piping

var cmd = PipeSource.FromFile("video.mp4") | Cli.Wrap("ffmpeg")
    .WithArguments(args => args
        .Add("-i").Add("-")
        .Add("video_out.webm")
    ) | (Console.WriteLine, Console.Error.WriteLine);

await cmd.ExecuteAsync();
  • Mention other sources
  • Remind that the same can be done using WithStandardInputPipe(...) instead
var youtube = new YoutubeClient();
var streamManifest = await youtube.Videos.Streams.GetManifestAsync("https://www.youtube.com/watch?v=-Sf9NPQeZOQ");
var streamInfo = streamManifest.GetVideoStreams().GetWithHighestVideoQuality();
var stream = await youtube.Videos.Streams.GetAsync(streamInfo);

var cmd = stream | Cli.Wrap("ffmpeg")
    .WithArguments(args => args
        .Add("-i").Add("-")
        .Add("-preset").Add("ultrafast")
        .Add("youtube_video.mp4")
    ) | (Console.WriteLine, Console.Error.WriteLine);

await cmd.ExecuteAsync();

Process-to-process piping

var cmd =
    // Take first 5 seconds of the video and convert to webm
    Cli.Wrap("ffmpeg")
        .WithArguments(args => args
            .Add("-i").Add("video.mp4")
            .Add("-t").Add(5)
            .Add("-f").Add("webm")
            .Add("-")) |

    // Reverse the stream and write to file
    Cli.Wrap("ffmpeg")
        .WithArguments(args => args
            .Add("-i").Add("-")
            .Add("-vf").Add("reverse")
            .Add("video_altered.webm")) |

    (Console.WriteLine, Console.Error.WriteLine);

await cmd.ExecuteAsync();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment