Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up

Library for running command-line processes

License

NotificationsYou must be signed in to change notification settings

Tyrrrz/CliWrap

Repository files navigation

StatusMade in UkraineBuildCoverageVersionDownloadsDiscordFuck Russia

Development of this project is entirely funded by the community.Consider donating to support!

Icon

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.

Terms of use[?]

By using this project or its source code, for any purpose and in any shape or form, you grant yourimplicit agreement to all the following statements:

  • Youcondemn Russia and its military aggression against Ukraine
  • Yourecognize that Russia is an occupant that unlawfully invaded a sovereign state
  • Yousupport Ukraine's territorial integrity, including its claims over temporarily occupied territories of Crimea and Donbas
  • Youreject false narratives perpetuated by Russian state propaganda

To learn more about the war and how you can help,click here. Glory to Ukraine! 🇺🇦

Install

  • 📦NuGet:dotnet add package CliWrap

Features

  • Airtight abstraction overSystem.Diagnostics.Process
  • Fluent configuration interface
  • Flexible support for piping
  • Fully asynchronous and cancellation-aware API
  • Graceful cancellation using interrupt signals
  • 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.2+
  • No external dependencies

Usage

Video guides

You can watch one of these videos to learn how to use the library:

Intro to CliWrap

Stop using the Process class for CLI interactions in .NET

Quick overview

Similarly to a shell,CliWrap's base unit of work is acommand — an object that encapsulates instructions for running a process.To build a command, start by callingCli.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 callingExecuteAsync():

usingCliWrap;varresult=awaitCli.Wrap("path/to/exe").WithArguments(["--foo","bar"]).WithWorkingDirectory("work/dir/path").ExecuteAsync();// Result contains:// -- result.IsSuccess       (bool)// -- 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 to aCommandResult object that contains the process exit code and other relevant information.

Warning:CliWrap will throw an exception if the underlying process returns a non-zero exit code, as it usually indicates an error.You canoverride this behavior by disabling result validation usingWithValidation(CommandResultValidation.None).

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

usingCliWrap;varstdOutBuffer=newStringBuilder();varstdErrBuffer=newStringBuilder();varresult=awaitCli.Wrap("path/to/exe").WithArguments(["--foo","bar"]).WithWorkingDirectory("work/dir/path")// This can be simplified with `ExecuteBufferedAsync()`.WithStandardOutputPipe(PipeTarget.ToStringBuilder(stdOutBuffer)).WithStandardErrorPipe(PipeTarget.ToStringBuilder(stdErrBuffer)).ExecuteAsync();// Access stdout & stderr buffered in-memory as stringsvarstdOut=stdOutBuffer.ToString();varstdErr=stdErrBuffer.ToString();

This example command is configured to decode the data written to the standard output and error streams as text, and append it to the correspondingStringBuilder buffers.Once the execution is complete, these buffers can be inspected to see what the process has printed to the console.

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

usingCliWrap;usingCliWrap.Buffered;// Calling `ExecuteBufferedAsync()` instead of `ExecuteAsync()`// implicitly configures pipes that write to in-memory buffers.varresult=awaitCli.Wrap("path/to/exe").WithArguments(["--foo","bar"]).WithWorkingDirectory("work/dir/path").ExecuteBufferedAsync();// Result contains:// -- result.IsSuccess       (bool)// -- result.StandardOutput  (string)// -- result.StandardError   (string)// -- result.ExitCode        (int)// -- result.StartTime       (DateTimeOffset)// -- result.ExitTime        (DateTimeOffset)// -- result.RunTime         (TimeSpan)

Warning:Be mindful when usingExecuteBufferedAsync().Programs can write arbitrary data (including binary) to the output and error streams, and storing it in-memory may be impractical.For more advanced scenarios,CliWrap also provides other piping options, which are covered in thepiping section.

Command configuration

The fluent interface provided by the command object allows you to configure various aspects of its execution.This section covers all available configuration methods and their usage.

Note:Command is an immutable object — all configuration methods listed here create a new instance instead of modifying the existing one.

WithArguments(...)

Sets the command-line arguments passed to the child process.

Default: empty.

Examples:

  • Set arguments using an array:
varcmd=Cli.Wrap("git")// Each element is formatted as a separate argument.// Equivalent to: `git commit -m "my commit"`.WithArguments(["commit","-m","my commit"]);
  • Set arguments using a builder:
varcmd=Cli.Wrap("git")// Each Add(...) call takes care of formatting automatically.// Equivalent to: `git clone https://github.com/Tyrrrz/CliWrap --depth 20`.WithArguments(args=>args.Add("clone").Add("https://github.com/Tyrrrz/CliWrap").Add("--depth").Add(20));
varforcePush=true;varcmd=Cli.Wrap("git")// Arguments can also be constructed in an imperative fashion.// Equivalent to: `git push --force`.WithArguments(args=>{args.Add("push");if(forcePush)args.Add("--force");});

Note:The builder overload allows you to define custom extension methods for reusable argument patterns.Learn more.

  • Set arguments directly:
varcmd=Cli.Wrap("git")// Avoid using this overload unless you really have to.// Equivalent to: `git commit -m "my commit"`.WithArguments("commit -m\"my commit\"");

Warning:Unless you absolutely have to, avoid setting command-line arguments directly from a string.This method expects all arguments to be correctly escaped and formatted ahead of time — which can be cumbersome to do yourself.Formatting errors may result in unexpected bugs and security vulnerabilities.

Note:There are someobscure scenarios, where you may need to assemble the command-line arguments yourself.In such cases, you can use theArgumentsBuilder.Escape(...) method to escape individual arguments manually.

WithWorkingDirectory(...)

Sets the working directory of the child process.

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

Example:

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

WithEnvironmentVariables(...)

Sets additional environment variables exposed to the child process.

Default: empty.

Examples:

  • Set environment variables using a builder:
varcmd=Cli.Wrap("git").WithEnvironmentVariables(env=>env.Set("GIT_AUTHOR_NAME","John").Set("GIT_AUTHOR_EMAIL","john@email.com"));
  • Set environment variables directly:
varcmd=Cli.Wrap("git").WithEnvironmentVariables(newDictionary<string,string?>{["GIT_AUTHOR_NAME"]="John",["GIT_AUTHOR_EMAIL"]="john@email.com"});

Note:Environment variables configured usingWithEnvironmentVariables(...) are applied on top of those inherited from the parent process.If you need to remove an inherited variable, set the corresponding value tonull.

WithResourcePolicy(...)

Sets the system resource management policy for the child process.

Default: default policy.

Examples:

  • Set resource policy using a builder:
varcmd=Cli.Wrap("git").WithResourcePolicy(policy=>policy.SetPriority(ProcessPriorityClass.High).SetAffinity(0b1010).SetMinWorkingSet(1024).SetMaxWorkingSet(4096));
  • Set resource policy directly:
varcmd=Cli.Wrap("git").WithResourcePolicy(newResourcePolicy(priority:ProcessPriorityClass.High,affinity:0b1010,minWorkingSet:1024,maxWorkingSet:4096));

Warning:Resource policy options have varying support across different platforms.

WithCredentials(...)

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

Default: no credentials.

Examples:

  • Set credentials using a builder:
varcmd=Cli.Wrap("git").WithCredentials(creds=>creds.SetDomain("some_workspace").SetUserName("johndoe").SetPassword("securepassword123").LoadUserProfile());
  • Set credentials directly:
varcmd=Cli.Wrap("git").WithCredentials(newCredentials(domain:"some_workspace",userName:"johndoe",password:"securepassword123",loadUserProfile:true));

Warning:Running a process under a different username is supported across all platforms, but other options are only available on Windows.

WithValidation(...)

Sets the strategy for validating the result of an execution.

Accepted values:

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

Default:CommandResultValidation.ZeroExitCode.

Examples:

  • Enable validation:
varcmd=Cli.Wrap("git").WithValidation(CommandResultValidation.ZeroExitCode);
  • Disable validation:
varcmd=Cli.Wrap("git").WithValidation(CommandResultValidation.None);

If you want to throw a custom exception when the process exits with a non-zero exit code, don't disable result validation, but instead catch the defaultCommandExecutionException and re-throw it inside your own exception.This way you can preserve the information provided by the original exception, while extending it with additional context:

try{awaitCli.Wrap("git").ExecuteAsync();}catch(CommandExecutionExceptionex){// Re-throw the original exception to preserve additional information// about the command that failed (exit code, arguments, etc.).thrownewMyException("Failed to run the git command-line tool.",ex);}

WithStandardInputPipe(...)

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

Default:PipeSource.Null.

Read more about this method in thepiping section.

WithStandardOutputPipe(...)

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

Default:PipeTarget.Null.

Read more about this method in thepiping section.

WithStandardErrorPipe(...)

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

Default:PipeTarget.Null.

Read more about this method in thepiping 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 the standard input stream, andPipeTarget which reads data coming from the standard output stream or the standard error stream.

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

You can specify your ownPipeSource andPipeTarget instances by calling the corresponding configuration methods on the command:

awaitusingvarinput=File.OpenRead("input.txt");awaitusingvaroutput=File.Create("output.txt");awaitCli.Wrap("foo").WithStandardInputPipe(PipeSource.FromStream(input)).WithStandardOutputPipe(PipeTarget.ToStream(output)).ExecuteAsync();

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

awaitusingvarinput=File.OpenRead("input.txt");awaitusingvaroutput=File.Create("output.txt");await(input|Cli.Wrap("foo")|output).ExecuteAsync();

BothPipeSource andPipeTarget 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 data from a text string
    • PipeSource.FromCommand(...) — pipes data from the standard output of another command
  • PipeTarget:
    • PipeTarget.Null — represents a pipe target that discards all data
    • PipeTarget.ToStream(...) — pipes data to any writable stream
    • PipeTarget.ToFile(...) — pipes data to a file
    • PipeTarget.ToStringBuilder(...) — pipes data as text into aStringBuilder
    • PipeTarget.ToDelegate(...) — pipes data as text, line-by-line, into anAction<string>, or aFunc<string, Task>, or aFunc<string, CancellationToken, Task> delegate
    • PipeTarget.Merge(...) — merges multiple outbound pipes by replicating the same data across all of them

Warning:UsingPipeTarget.Null results in the corresponding stream (stdout or stderr) not being opened for the underlying process at all.In the vast majority of cases, this behavior should be functionally equivalent to piping to a null stream, but without the performance overhead of consuming and discarding unneeded data.This may be undesirable incertain situations — in which case it's recommended to pipe to a null stream explicitly usingPipeTarget.ToStream(Stream.Null).

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

  • Pipe a string into stdin:
varcmd="Hello world"|Cli.Wrap("foo");awaitcmd.ExecuteAsync();
  • Pipe stdout as text into aStringBuilder:
varstdOutBuffer=newStringBuilder();varcmd=Cli.Wrap("foo")|stdOutBuffer;awaitcmd.ExecuteAsync();
  • Pipe a binary HTTP stream into stdin:
usingvarhttpClient=newHttpClient();awaitusingvarinput=awaithttpClient.GetStreamAsync("https://example.com/image.png");varcmd=input|Cli.Wrap("foo");awaitcmd.ExecuteAsync();
  • Pipe stdout of one command into stdin of another:
varcmd=Cli.Wrap("foo")|Cli.Wrap("bar")|Cli.Wrap("baz");awaitcmd.ExecuteAsync();
  • Pipe stdout and stderr into stdout and stderr of the parent process:
awaitusingvarstdOut=Console.OpenStandardOutput();awaitusingvarstdErr=Console.OpenStandardError();varcmd=Cli.Wrap("foo")|(stdOut,stdErr);awaitcmd.ExecuteAsync();
  • Pipe stdout into a delegate:
varcmd=Cli.Wrap("foo")|Debug.WriteLine;awaitcmd.ExecuteAsync();
  • Pipe stdout into a file and stderr into aStringBuilder:
varbuffer=newStringBuilder();varcmd=Cli.Wrap("foo")|(PipeTarget.ToFile("output.txt"),PipeTarget.ToStringBuilder(buffer));awaitcmd.ExecuteAsync();
  • Pipe stdout into multiple files simultaneously:
vartarget=PipeTarget.Merge(PipeTarget.ToFile("file1.txt"),PipeTarget.ToFile("file2.txt"),PipeTarget.ToFile("file3.txt"));varcmd=Cli.Wrap("foo")|target;awaitcmd.ExecuteAsync();
  • Pipe a string into stdin of one command, stdout of that command into stdin of another command, and then stdout and stderr of the last command into stdout and stderr of the parent process:
varcmd="Hello world"|Cli.Wrap("foo").WithArguments(["aaa"])|Cli.Wrap("bar").WithArguments(["bbb"])|(Console.WriteLine,Console.Error.WriteLine);awaitcmd.ExecuteAsync();

Execution models

CliWrap provides a few high-level execution models that offer alternative ways to reason about commands.These are essentially just extension methods that work by leveraging thepiping feature shown earlier.

Buffered execution

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

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

usingCliWrap;usingCliWrap.Buffered;varresult=awaitCli.Wrap("foo").WithArguments(["bar"]).ExecuteBufferedAsync();varexitCode=result.ExitCode;varstdOut=result.StandardOutput;varstdErr=result.StandardError;

By default,ExecuteBufferedAsync() assumes that the underlying process uses the default encoding (Encoding.Default) 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 streamsvarresult=awaitCli.Wrap("foo").WithArguments(["bar"]).ExecuteBufferedAsync(Encoding.UTF8);// Treat stdout as ASCII-encoded and stderr as UTF8-encodedvarresult=awaitCli.Wrap("foo").WithArguments(["bar"]).ExecuteBufferedAsync(Encoding.ASCII,Encoding.UTF8);

Note:If the underlying process returns a non-zero exit code,ExecuteBufferedAsync() will throw an exception similarly toExecuteAsync(), but the exception message will also include the standard error data.

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 process and react to the events it produces in real-time.

Those events are:

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

To execute a command as apull-based event stream, use theListenAsync() extension method:

usingCliWrap;usingCliWrap.EventStream;varcmd=Cli.Wrap("foo").WithArguments(["bar"]);awaitforeach(varcmdEventincmd.ListenAsync()){switch(cmdEvent){caseStartedCommandEventstarted:_output.WriteLine($"Process started; ID:{started.ProcessId}");break;caseStandardOutputCommandEventstdOut:_output.WriteLine($"Out>{stdOut.Text}");break;caseStandardErrorCommandEventstdErr:_output.WriteLine($"Err>{stdErr.Text}");break;caseExitedCommandEventexited:_output.WriteLine($"Process exited; Code:{exited.ExitCode}");break;}}

TheListenAsync() method starts the command and returns an object of typeIAsyncEnumerable<CommandEvent>, which you can iterate using theawait 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.

Note:Just like withExecuteBufferedAsync(), you can specify custom encoding forListenAsync() using one of its overloads.

Push-based event stream

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

usingSystem.Reactive;usingCliWrap;usingCliWrap.EventStream;varcmd=Cli.Wrap("foo").WithArguments(["bar"]);awaitcmd.Observe().ForEachAsync(cmdEvent=>{switch(cmdEvent){caseStartedCommandEventstarted:_output.WriteLine($"Process started; ID:{started.ProcessId}");break;caseStandardOutputCommandEventstdOut:_output.WriteLine($"Out>{stdOut.Text}");break;caseStandardErrorCommandEventstdErr:_output.WriteLine($"Err>{stdErr.Text}");break;caseExitedCommandEventexited:_output.WriteLine($"Process exited; Code:{exited.ExitCode}");break;}});

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

Unlike 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 that it becomes available.

Note:Similarly toExecuteBufferedAsync(), you can specify custom encoding forObserve() using one of its overloads.

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.When running a command using one of the built-in execution models, existing pipe configurations are preserved and extended usingPipeTarget.Merge(...).

This means that you can, for example, pipe a command to a file and simultaneously execute it as an event stream:

varcmd=PipeSource.FromFile("input.txt")|Cli.Wrap("foo")|PipeTarget.ToFile("output.txt");// Iterate as an event stream and pipe to a file at the same time// (execution models preserve configured pipes)awaitforeach(varcmdEventincmd.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, issue the correspondingCancellationToken and include it when callingExecuteAsync():

usingSystem.Threading;usingCliWrap;usingvarcts=newCancellationTokenSource();// Cancel after a timeout of 10 secondscts.CancelAfter(TimeSpan.FromSeconds(10));varresult=awaitCli.Wrap("foo").ExecuteAsync(cts.Token);

In the event of a cancellation request, the underlying process will be killed andExecuteAsync() will throw an exception of typeOperationCanceledException (or its derivative,TaskCanceledException).You will need to catch this exception in your code to recover from cancellation:

try{awaitCli.Wrap("foo").ExecuteAsync(cts.Token);}catch(OperationCanceledException){// Command was canceled}

Besides outright killing the process, you can also request cancellation in a more graceful way by sending an interrupt signal.To do that, pass an additional cancellation token toExecuteAsync() that corresponds to that request:

usingvarforcefulCts=newCancellationTokenSource();usingvargracefulCts=newCancellationTokenSource();// Cancel forcefully after a timeout of 10 seconds.// This serves as a fallback in case graceful cancellation// takes too long.forcefulCts.CancelAfter(TimeSpan.FromSeconds(10));// Cancel gracefully after a timeout of 7 seconds.// If the process takes too long to respond to graceful// cancellation, it will get killed by forceful cancellation// 3 seconds later (as configured above).gracefulCts.CancelAfter(TimeSpan.FromSeconds(7));varresult=awaitCli.Wrap("foo").ExecuteAsync(forcefulCts.Token,gracefulCts.Token);

Requesting graceful cancellation inCliWrap is functionally equivalent to pressingCtrl+C in the console window.The underlying process may handle this signal to perform last-minute critical work before finally exiting on its own terms.

Graceful cancellation is inherently cooperative, so it's possible that the process may take too long to fulfill the request or choose to ignore it altogether.In the above example, this risk is mitigated by additionally scheduling a delayed forceful cancellation that prevents the command from hanging.

If you are executing a command inside a method and don't want to expose those implementation details to the caller, you can rely on the following pattern to use the provided token for graceful cancellation and extend it with a forceful fallback:

publicasyncTaskGitPushAsync(CancellationTokencancellationToken=default){usingvarforcefulCts=newCancellationTokenSource();// When the cancellation token is triggered,// schedule forceful cancellation as a fallback.awaitusingvarlink=cancellationToken.Register(()=>forcefulCts.CancelAfter(TimeSpan.FromSeconds(3)));awaitCli.Wrap("git").WithArguments(["push"]).ExecuteAsync(forcefulCts.Token,cancellationToken);}

Note:Similarly toExecuteAsync(), cancellation is also supported byExecuteBufferedAsync(),ListenAsync(), andObserve().

Retrieving process-related information

The task returned byExecuteAsync() andExecuteBufferedAsync() is, in fact, not a regularTask<T>, but an instance ofCommandTask<T>.This is a specialized awaitable object that contains additional information about the process associated with the executing command:

vartask=Cli.Wrap("foo").WithArguments(["bar"]).ExecuteAsync();// Get the process IDvarprocessId=task.ProcessId;// Wait for the task to completeawaittask;

[8]ページ先頭

©2009-2025 Movatter.jp