CliWrap

repository·prime·Indexed 26 days ago

https://github.com/tyrrrz/cliwrap

A .NET library for interacting with command-line interfaces (CLIs) providing a fluent, asynchronous, and cancellation-aware abstraction over System.Diagnostics.Process. It supports piping, cross-platform execution (Windows, Linux, macOS), and targets .NET Standard 2.0+, .NET Core 3.0+, and .NET Framework 4.6.2+ with no external dependencies.

Tokens
3.2K
Snippets
13
Records
16
Agent score
40%

What's inside CliWrap

  1. Overview of CliWrap features

    prime

    CliWrap is a library for interacting with command-line interfaces, providing an abstraction over System.Diagnostics.Process.

    Key features include:

    • Fluent configuration interface
    • Flexible support for piping
    • Fully asynchronous and cancellation-aware API
    • Graceful cancellation using interrupt signals
    • Strict immutability design
    • Safety against typical deadlock scenarios
    • Cross-platform support (Windows, Linux, and macOS)
    • Targets .NET Standard 2.0+, .NET Core 3.0+, and .NET Framework 4.6.2+
    • No external dependencies
  2. Run a basic command with Cli.Wrap

    prime

    To run a process, use Cli.Wrap(executablePath) to create a command object. You can configure arguments and the working directory using a fluent interface. Call ExecuteAsync() to run the command asynchronously. By default, CliWrap throws an exception if the process returns a non-zero exit code.

    using CliWrap;
    
    var result = await Cli.Wrap("path/to/exe")
        .WithArguments(["--foo", "bar"])
        .WithWorkingDirectory("work/dir/path")
        .ExecuteAsync();
    
    // Result contains:
    // -- result.ExitCode        (int)
    // -- result.IsSuccess       (bool)
    // -- result.StartTime       (DateTimeOffset)
    // -- result.ExitTime        (DateTimeOffset)
    // -- result.RunTime         (TimeSpan)
  3. Use the piping model for stream redirection

    prime

    CliWrap uses PipeSource for standard input and PipeTarget for standard output/error. You can use pipe operators (|) for a concise syntax.

    Common PipeSources:

    • PipeSource.FromFile(path)
    • PipeSource.FromString(text)
    • PipeSource.FromStream(stream)
    • PipeSource.FromCommand(command)

    Common PipeTargets:

    • PipeTarget.ToFile(path)
    • PipeTarget.ToStringBuilder(builder)
    • PipeTarget.ToDelegate(action)
    • PipeTarget.Merge(targets...) (Replicates data to multiple targets)
    • PipeTarget.Null (Discards data)
    // Pipe stdout of one command into stdin of another
    var cmd = Cli.Wrap("cat").WithArguments(["access.log"])
        | Cli.Wrap("grep").WithArguments(["ERROR"])
        | Cli.Wrap("sort");
    
    await cmd.ExecuteAsync();
  4. Handle command cancellation and timeouts

    prime

    Pass a CancellationToken to ExecuteAsync() to abort a process.

    Graceful vs Forceful Cancellation:

    • Forceful Cancellation: Passing a token to ExecuteAsync(forcefulToken) will kill the process immediately.
    • Graceful Cancellation: Passing a token to ExecuteAsync(forcefulToken, gracefulToken) sends an interrupt signal (like Ctrl+C) to the process, allowing it to perform cleanup before exiting.
    using System.Threading;
    
    using var cts = new CancellationTokenSource();
    cts.CancelAfter(TimeSpan.FromSeconds(10)); // Timeout after 10s
    
    try
    {
        await Cli.Wrap("ffmpeg")
            .WithArguments(["-i", "input.mp4", "output.webm"])
            .ExecuteAsync(cts.Token);
    }
    catch (OperationCanceledException)
    {
        // Handle cancellation
    }
  5. Listen to command events with ListenAsync (Pull-based)

    prime

    Use ListenAsync() to treat a command execution as an IAsyncEnumerable<CommandEvent>. This allows you to react to process events (start, stdout, stderr, exit) in real-time with back pressure support.

    using CliWrap.EventStream;
    
    var cmd = Cli.Wrap("dotnet").WithArguments(["build"]);
    
    await foreach (var cmdEvent in cmd.ListenAsync())
    {
        switch (cmdEvent)
        {
            case StartedCommandEvent started: 
                Console.WriteLine($"Started: {started.ProcessId}"); break;
            case StandardOutputCommandEvent stdOut: 
                Console.WriteLine($"Out: {stdOut.Text}"); break;
            case StandardErrorCommandEvent stdErr: 
                Console.WriteLine($"Err: {stdErr.Text}"); break;
            case ExitedCommandEvent exited: 
                Console.WriteLine($"Exited: {exited.ExitCode}"); break;
        }
    }
  6. Configure command arguments with WithArguments

    prime

    Use WithArguments(...) to set the command-line arguments. Command objects are immutable; each configuration method returns a new instance.

    • Array: WithArguments(["arg1", "arg2"])
    • Builder: WithArguments(args => args.Add("arg1").Add(20))
    • Direct String: WithArguments("arg1 arg2") (Use with caution: requires manual escaping and is prone to security vulnerabilities).
    // Using a builder for automatic formatting
    var cmd = Cli.Wrap("git")
        .WithArguments(args => args
            .Add("clone")
            .Add("https://github.com/Tyrrrz/CliWrap")
            .Add("--depth")
            .Add(20)
        );
  7. Configure process resource policy with WithResourcePolicy

    prime

    Use WithResourcePolicy(...) to manage system resources like priority, affinity, and working set size. Note that support for these options varies across platforms.

    var cmd = Cli.Wrap("git")
        .WithResourcePolicy(policy => policy
            .SetPriority(ProcessPriorityClass.High)
            .SetAffinity(0b1010)
            .SetMinWorkingSet(1024)
            .SetMaxWorkingSet(4096)
        );
  8. Execute commands with Buffered execution

    prime

    The ExecuteBufferedAsync() extension method runs a process and buffers its standard output and error streams in-memory as text. This is a high-level model for scenarios where you need to capture output strings easily.

    Note: Be careful with large binary outputs as they are stored in memory.

    using CliWrap.Buffered;
    
    // Get full result object
    var result = await Cli.Wrap("python")
        .WithArguments(["script.py"])
        .ExecuteBufferedAsync();
    
    // Use tuple deconstruction
    var (exitCode, stdOut, stdErr) = await Cli.Wrap("python")
        .WithArguments(["script.py"])
        .ExecuteBufferedAsync();
    
    // Implicit conversion to string (StandardOutput only)
    string stdOut = await Cli.Wrap("python")
        .WithArguments(["script.py"])
        .ExecuteBufferedAsync();
  9. Configure environment variables with WithEnvironmentVariables

    prime

    Use WithEnvironmentVariables(...) to set additional environment variables. These are applied on top of the variables inherited from the parent process. To remove an inherited variable, set its value to null.

    // Using a builder
    var cmd = Cli.Wrap("git")
        .WithEnvironmentVariables(env => env
            .Set("GIT_AUTHOR_NAME", "John")
            .Set("GIT_AUTHOR_EMAIL", "john@email.com")
        );
    
    // Using a Dictionary
    var cmd2 = Cli.Wrap("git")
        .WithEnvironmentVariables(new Dictionary<string, string?>
        {
            ["GIT_AUTHOR_NAME"] = "John",
            ["GIT_AUTHOR_EMAIL"] = "john@email.com"
        });
  10. Observe command events with Observe (Push-based)

    prime

    Use Observe() to treat a command execution as an IObservable<CommandEvent>. This is a push-based model using Rx.NET, which does not involve back pressure and pushes data at the rate it becomes available.

    using System.Reactive;
    using CliWrap.EventStream;
    
    var cmd = Cli.Wrap("dotnet").WithArguments(["build"]);
    
    await cmd.Observe().ForEachAsync(cmdEvent =>
    {
        // Handle events (StartedCommandEvent, StandardOutputCommandEvent, etc.)
    });
  11. Configure process credentials with WithCredentials

    prime

    Use WithCredentials(...) to specify the user identity under which the process should run. Running under a different username is supported on all platforms, but other options (like domain) are only available on Windows.

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