{"id":13414883,"url":"https://github.com/Tyrrrz/CliWrap","last_synced_at":"2025-03-14T22:32:22.378Z","repository":{"id":37579991,"uuid":"86743880","full_name":"Tyrrrz/CliWrap","owner":"Tyrrrz","description":"Library for running command-line processes","archived":false,"fork":false,"pushed_at":"2025-03-06T19:14:02.000Z","size":914,"stargazers_count":4522,"open_issues_count":2,"forks_count":276,"subscribers_count":46,"default_branch":"master","last_synced_at":"2025-03-11T22:01:01.640Z","etag":null,"topics":["cli","command-line","dotnet","dotnet-core","dotnet-standard","event-stream","pipe-stderr","pipe-stdin","pipe-stdout","piping","process","shell","stderr","stdin","stdout"],"latest_commit_sha":null,"homepage":"","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Tyrrrz.png","metadata":{"files":{"readme":"Readme.md","changelog":null,"contributing":null,"funding":null,"license":"License.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null},"funding":{"github":"Tyrrrz","patreon":"Tyrrrz","custom":["tyrrrz.me/donate"]}},"created_at":"2017-03-30T20:05:06.000Z","updated_at":"2025-03-11T14:55:11.000Z","dependencies_parsed_at":"2022-07-13T15:29:35.316Z","dependency_job_id":"27959291-85d5-43f0-92bd-b726f4cc810a","html_url":"https://github.com/Tyrrrz/CliWrap","commit_stats":{"total_commits":638,"total_committers":32,"mean_commits":19.9375,"dds":0.5940438871473355,"last_synced_commit":"5e0bb8e12d2355330630f0ea0455fc9f02f84504"},"previous_names":[],"tags_count":70,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Tyrrrz%2FCliWrap","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Tyrrrz%2FCliWrap/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Tyrrrz%2FCliWrap/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Tyrrrz%2FCliWrap/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Tyrrrz","download_url":"https://codeload.github.com/Tyrrrz/CliWrap/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":243658055,"owners_count":20326459,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cli","command-line","dotnet","dotnet-core","dotnet-standard","event-stream","pipe-stderr","pipe-stdin","pipe-stdout","piping","process","shell","stderr","stdin","stdout"],"created_at":"2024-07-30T21:00:39.200Z","updated_at":"2025-03-14T22:32:22.364Z","avatar_url":"https://github.com/Tyrrrz.png","language":"C#","funding_links":["https://github.com/sponsors/Tyrrrz","https://patreon.com/Tyrrrz","tyrrrz.me/donate"],"categories":["Frameworks, Libraries and Tools","CLI","C\\#","cli","框架, 库和工具","C# #","C#","shell","\u003e 1K ⭐️","[Dotnet](https://dotnet.microsoft.com/)"],"sub_categories":["Misc","大杂烩"],"readme":"# CliWrap\n\n[![Status](https://img.shields.io/badge/status-active-47c219.svg)](https://github.com/Tyrrrz/.github/blob/master/docs/project-status.md)\n[![Made in Ukraine](https://img.shields.io/badge/made_in-ukraine-ffd700.svg?labelColor=0057b7)](https://tyrrrz.me/ukraine)\n[![Build](https://img.shields.io/github/actions/workflow/status/Tyrrrz/CliWrap/main.yml?branch=master)](https://github.com/Tyrrrz/CliWrap/actions)\n[![Coverage](https://img.shields.io/codecov/c/github/Tyrrrz/CliWrap/master)](https://codecov.io/gh/Tyrrrz/CliWrap)\n[![Version](https://img.shields.io/nuget/v/CliWrap.svg)](https://nuget.org/packages/CliWrap)\n[![Downloads](https://img.shields.io/nuget/dt/CliWrap.svg)](https://nuget.org/packages/CliWrap)\n[![Discord](https://img.shields.io/discord/869237470565392384?label=discord)](https://discord.gg/2SUWKFnHSm)\n[![Fuck Russia](https://img.shields.io/badge/fuck-russia-e4181c.svg?labelColor=000000)](https://twitter.com/tyrrrz/status/1495972128977571848)\n\n\u003ctable\u003e\n    \u003ctr\u003e\n        \u003ctd width=\"99999\" align=\"center\"\u003eDevelopment of this project is entirely funded by the community. \u003cb\u003e\u003ca href=\"https://tyrrrz.me/donate\"\u003eConsider donating to support!\u003c/a\u003e\u003c/b\u003e\u003c/td\u003e\n    \u003c/tr\u003e\n\u003c/table\u003e\n\n\u003cp align=\"center\"\u003e\n    \u003cimg src=\"favicon.png\" alt=\"Icon\" /\u003e\n\u003c/p\u003e\n\n**CliWrap** is a library for interacting with external command-line interfaces.\nIt provides a convenient model for launching processes, redirecting input and output streams, awaiting completion, handling cancellation, and more.\n\n## Terms of use\u003csup\u003e[[?]](https://github.com/Tyrrrz/.github/blob/master/docs/why-so-political.md)\u003c/sup\u003e\n\nBy using this project or its source code, for any purpose and in any shape or form, you grant your **implicit agreement** to all the following statements:\n\n- You **condemn Russia and its military aggression against Ukraine**\n- You **recognize that Russia is an occupant that unlawfully invaded a sovereign state**\n- You **support Ukraine's territorial integrity, including its claims over temporarily occupied territories of Crimea and Donbas**\n- You **reject false narratives perpetuated by Russian state propaganda**\n\nTo learn more about the war and how you can help, [click here](https://tyrrrz.me/ukraine). Glory to Ukraine! 🇺🇦\n\n## Install\n\n- 📦 [NuGet](https://nuget.org/packages/CliWrap): `dotnet add package CliWrap`\n\n## Features\n\n- Airtight abstraction over `System.Diagnostics.Process`\n- Fluent configuration interface\n- Flexible support for piping\n- Fully asynchronous and cancellation-aware API\n- Graceful cancellation using interrupt signals\n- Designed with strict immutability in mind\n- Provides safety against typical deadlock scenarios\n- Tested on Windows, Linux, and macOS\n- Targets .NET Standard 2.0+, .NET Core 3.0+, .NET Framework 4.6.2+\n- No external dependencies\n\n## Usage\n\n### Video guides\n\nYou can watch one of these videos to learn how to use the library:\n\n- [**OSS Power-Ups: CliWrap**](https://youtube.com/watch?v=3_Ucw3Fflmo) by [Oleksii Holub](https://twitter.com/tyrrrz)\n\n[![Intro to CliWrap](.assets/video-guide-oss-powerups.jpg)](https://youtube.com/watch?v=3_Ucw3Fflmo)\n\n- [**Stop using the Process class for CLI interactions in .NET**](https://youtube.com/watch?v=Pt-0KM5SxmI) by [Nick Chapsas](https://twitter.com/nickchapsas)\n\n[![Stop using the Process class for CLI interactions in .NET](.assets/video-guide-nick-chapsas.jpg)](https://youtube.com/watch?v=Pt-0KM5SxmI)\n\n### Quick overview\n\nSimilarly to a shell, **CliWrap**'s base unit of work is a **command** — an object that encapsulates instructions for running a process.\nTo 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.\nOnce the command is configured, you can run it by calling `ExecuteAsync()`:\n\n```csharp\nusing CliWrap;\n\nvar result = await Cli.Wrap(\"path/to/exe\")\n    .WithArguments([\"--foo\", \"bar\"])\n    .WithWorkingDirectory(\"work/dir/path\")\n    .ExecuteAsync();\n\n// Result contains:\n// -- result.IsSuccess       (bool)\n// -- result.ExitCode        (int)\n// -- result.StartTime       (DateTimeOffset)\n// -- result.ExitTime        (DateTimeOffset)\n// -- result.RunTime         (TimeSpan)\n```\n\nThe code above spawns a child process with the configured command-line arguments and working directory, and then asynchronously waits for it to exit.\nAfter the task has completed, it resolves to a `CommandResult` object that contains the process exit code and other relevant information.\n\n\u003e **Warning**:\n\u003e **CliWrap** will throw an exception if the underlying process returns a non-zero exit code, as it usually indicates an error.\n\u003e You can [override this behavior](#withvalidation) by disabling result validation using `WithValidation(CommandResultValidation.None)`.\n\nBy default, the process's standard input, output and error streams are routed to **CliWrap**'s equivalent of a [_null device_](https://en.wikipedia.org/wiki/Null_device), which represents an empty source and a target that discards all data.\nYou can change this by calling `WithStandardInputPipe(...)`, `WithStandardOutputPipe(...)`, or `WithStandardErrorPipe(...)` to configure pipes for the corresponding streams:\n\n```csharp\nusing CliWrap;\n\nvar stdOutBuffer = new StringBuilder();\nvar stdErrBuffer = new StringBuilder();\n\nvar result = await Cli.Wrap(\"path/to/exe\")\n    .WithArguments([\"--foo\", \"bar\"])\n    .WithWorkingDirectory(\"work/dir/path\")\n    // This can be simplified with `ExecuteBufferedAsync()`\n    .WithStandardOutputPipe(PipeTarget.ToStringBuilder(stdOutBuffer))\n    .WithStandardErrorPipe(PipeTarget.ToStringBuilder(stdErrBuffer))\n    .ExecuteAsync();\n\n// Access stdout \u0026 stderr buffered in-memory as strings\nvar stdOut = stdOutBuffer.ToString();\nvar stdErr = stdErrBuffer.ToString();\n```\n\nThis example command is configured to decode the data written to the standard output and error streams as text, and append it to the corresponding `StringBuilder` buffers.\nOnce the execution is complete, these buffers can be inspected to see what the process has printed to the console.\n\nHandling command output is a very common use case, so **CliWrap** offers a few high-level [execution models](#execution-models) to make these scenarios simpler.\nIn particular, the same thing shown above can also be achieved more succinctly with the `ExecuteBufferedAsync()` extension method:\n\n```csharp\nusing CliWrap;\nusing CliWrap.Buffered;\n\n// Calling `ExecuteBufferedAsync()` instead of `ExecuteAsync()`\n// implicitly configures pipes that write to in-memory buffers.\nvar result = await Cli.Wrap(\"path/to/exe\")\n    .WithArguments([\"--foo\", \"bar\"])\n    .WithWorkingDirectory(\"work/dir/path\")\n    .ExecuteBufferedAsync();\n\n// Result contains:\n// -- result.IsSuccess       (bool)\n// -- result.StandardOutput  (string)\n// -- result.StandardError   (string)\n// -- result.ExitCode        (int)\n// -- result.StartTime       (DateTimeOffset)\n// -- result.ExitTime        (DateTimeOffset)\n// -- result.RunTime         (TimeSpan)\n```\n\n\u003e **Warning**:\n\u003e Be mindful when using `ExecuteBufferedAsync()`.\n\u003e Programs can write arbitrary data (including binary) to the output and error streams, and storing it in-memory may be impractical.\n\u003e For more advanced scenarios, **CliWrap** also provides other piping options, which are covered in the [piping section](#piping).\n\n### Command configuration\n\nThe fluent interface provided by the command object allows you to configure various aspects of its execution.\nThis section covers all available configuration methods and their usage.\n\n\u003e **Note**:\n\u003e `Command` is an immutable object — all configuration methods listed here create a new instance instead of modifying the existing one.\n\n#### `WithArguments(...)`\n\nSets the command-line arguments passed to the child process.\n\n**Default**: empty.\n\n**Examples**:\n\n- Set arguments using an array:\n\n```csharp\nvar cmd = Cli.Wrap(\"git\")\n    // Each element is formatted as a separate argument.\n    // Equivalent to: `git commit -m \"my commit\"`\n    .WithArguments([\"commit\", \"-m\", \"my commit\"]);\n```\n\n- Set arguments using a builder:\n\n```csharp\nvar cmd = Cli.Wrap(\"git\")\n    // Each Add(...) call takes care of formatting automatically.\n    // Equivalent to: `git clone https://github.com/Tyrrrz/CliWrap --depth 20`\n    .WithArguments(args =\u003e args\n        .Add(\"clone\")\n        .Add(\"https://github.com/Tyrrrz/CliWrap\")\n        .Add(\"--depth\")\n        .Add(20)\n    );\n```\n\n```csharp\nvar forcePush = true;\n\nvar cmd = Cli.Wrap(\"git\")\n    // Arguments can also be constructed in an imperative fashion.\n    // Equivalent to: `git push --force`\n    .WithArguments(args =\u003e \n    {\n        args.Add(\"push\");\n\n        if (forcePush)\n            args.Add(\"--force\");\n    });\n```\n\n\u003e **Note**:\n\u003e The builder overload allows you to define custom extension methods for reusable argument patterns.\n\u003e [Learn more](https://twitter.com/Tyrrrz/status/1409104223753605121).\n\n- Set arguments directly:\n\n```csharp\nvar cmd = Cli.Wrap(\"git\")\n    // Avoid using this overload unless you really have to.\n    // Equivalent to: `git commit -m \"my commit\"`\n    .WithArguments(\"commit -m \\\"my commit\\\"\");\n```\n\n\u003e **Warning**:\n\u003e Unless you absolutely have to, avoid setting command-line arguments directly from a string.\n\u003e This method expects all arguments to be correctly escaped and formatted ahead of time — which can be cumbersome to do yourself.\n\u003e Formatting errors may result in unexpected bugs and security vulnerabilities.\n\n\u003e **Note**:\n\u003e There are some [obscure scenarios](https://github.com/Tyrrrz/CliWrap/issues/263), where you may need to assemble the command-line arguments yourself.\n\u003e In such cases, you can use the `ArgumentsBuilder.Escape(...)` method to escape individual arguments manually.\n\n#### `WithWorkingDirectory(...)`\n\nSets the working directory of the child process.\n\n**Default**: current working directory, i.e. `Directory.GetCurrentDirectory()`.\n\n**Example**:\n\n```csharp\nvar cmd = Cli.Wrap(\"git\")\n    .WithWorkingDirectory(\"c:/projects/my project/\");\n```\n\n#### `WithEnvironmentVariables(...)`\n\nSets additional environment variables exposed to the child process.\n\n**Default**: empty.\n\n**Examples**:\n\n- Set environment variables using a builder:\n\n```csharp\nvar cmd = Cli.Wrap(\"git\")\n    .WithEnvironmentVariables(env =\u003e env\n        .Set(\"GIT_AUTHOR_NAME\", \"John\")\n        .Set(\"GIT_AUTHOR_EMAIL\", \"john@email.com\")\n    );\n```\n\n- Set environment variables directly:\n\n```csharp\nvar cmd = Cli.Wrap(\"git\")\n    .WithEnvironmentVariables(new Dictionary\u003cstring, string?\u003e\n    {\n        [\"GIT_AUTHOR_NAME\"] = \"John\",\n        [\"GIT_AUTHOR_EMAIL\"] = \"john@email.com\"\n    });\n```\n\n\u003e **Note**:\n\u003e Environment variables configured using `WithEnvironmentVariables(...)` are applied on top of those inherited from the parent process.\n\u003e If you need to remove an inherited variable, set the corresponding value to `null`.\n\n#### `WithResourcePolicy(...)`\n\nSets the system resource management policy for the child process.\n\n**Default**: default policy.\n\n**Examples**:\n\n- Set resource policy using a builder:\n\n```csharp\nvar cmd = Cli.Wrap(\"git\")\n    .WithResourcePolicy(policy =\u003e policy\n        .SetPriority(ProcessPriorityClass.High)\n        .SetAffinity(0b1010)\n        .SetMinWorkingSet(1024)\n        .SetMaxWorkingSet(4096)\n    );\n```\n\n- Set resource policy directly:\n\n```csharp\nvar cmd = Cli.Wrap(\"git\")\n    .WithResourcePolicy(new ResourcePolicy(\n        priority: ProcessPriorityClass.High,\n        affinity: 0b1010,\n        minWorkingSet: 1024,\n        maxWorkingSet: 4096\n    ));\n```\n\n\u003e **Warning**:\n\u003e Resource policy options have varying support across different platforms.\n\n#### `WithCredentials(...)`\n\nSets domain, name and password of the user, under whom the child process should be started.\n\n**Default**: no credentials.\n\n**Examples**:\n\n- Set credentials using a builder:\n\n```csharp\nvar cmd = Cli.Wrap(\"git\")\n    .WithCredentials(creds =\u003e creds\n       .SetDomain(\"some_workspace\")\n       .SetUserName(\"johndoe\")\n       .SetPassword(\"securepassword123\")\n       .LoadUserProfile()\n    );\n```\n\n- Set credentials directly:\n\n```csharp\nvar cmd = Cli.Wrap(\"git\")\n    .WithCredentials(new Credentials(\n        domain: \"some_workspace\",\n        userName: \"johndoe\",\n        password: \"securepassword123\",\n        loadUserProfile: true\n    ));\n```\n\n\u003e **Warning**:\n\u003e Running a process under a different username is supported across all platforms, but other options are only available on Windows.\n\n#### `WithValidation(...)`\n\nSets the strategy for validating the result of an execution.\n\n**Accepted values**:\n\n- `CommandResultValidation.None` — no validation\n- `CommandResultValidation.ZeroExitCode` — ensures zero exit code when the process exits\n\n**Default**: `CommandResultValidation.ZeroExitCode`.\n\n**Examples**:\n\n- Enable validation:\n\n```csharp\nvar cmd = Cli.Wrap(\"git\")\n    .WithValidation(CommandResultValidation.ZeroExitCode);\n```\n\n- Disable validation:\n\n```csharp\nvar cmd = Cli.Wrap(\"git\")\n    .WithValidation(CommandResultValidation.None);\n```\n\nIf 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 default `CommandExecutionException` and re-throw it inside your own exception.\nThis way you can preserve the information provided by the original exception, while extending it with additional context:\n\n```csharp\ntry\n{\n    await Cli.Wrap(\"git\").ExecuteAsync();\n}\ncatch (CommandExecutionException ex)\n{\n    // Re-throw the original exception to preserve additional information\n    // about the command that failed (exit code, arguments, etc.).\n    throw new MyException(\"Failed to run the git command-line tool.\", ex);\n}\n```\n\n#### `WithStandardInputPipe(...)`\n\nSets the pipe source that will be used for the standard _input_ stream of the process.\n\n**Default**: `PipeSource.Null`.\n\nRead more about this method in the [piping section](#piping).\n\n#### `WithStandardOutputPipe(...)`\n\nSets the pipe target that will be used for the standard _output_ stream of the process.\n\n**Default**: `PipeTarget.Null`.\n\nRead more about this method in the [piping section](#piping).\n\n#### `WithStandardErrorPipe(...)`\n\nSets the pipe target that will be used for the standard _error_ stream of the process.\n\n**Default**: `PipeTarget.Null`.\n\nRead more about this method in the [piping section](#piping).\n\n### Piping\n\n**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.\nAt its core, it's based on two abstractions: `PipeSource` which provides data for the standard input stream, and `PipeTarget` which reads data coming from the standard output stream or the standard error stream.\n\nBy default, a command's input pipe is set to `PipeSource.Null` and the output and error pipes are set to `PipeTarget.Null`.\nThese objects effectively represent no-op stubs that provide empty input and discard all output respectively.\n\nYou can specify your own `PipeSource` and `PipeTarget` instances by calling the corresponding configuration methods on the command:\n\n```csharp\nawait using var input = File.OpenRead(\"input.txt\");\nawait using var output = File.Create(\"output.txt\");\n\nawait Cli.Wrap(\"foo\")\n    .WithStandardInputPipe(PipeSource.FromStream(input))\n    .WithStandardOutputPipe(PipeTarget.ToStream(output))\n    .ExecuteAsync();\n```\n\nAlternatively, pipes can also be configured in a slightly terser way using pipe operators:\n\n```csharp\nawait using var input = File.OpenRead(\"input.txt\");\nawait using var output = File.Create(\"output.txt\");\n\nawait (input | Cli.Wrap(\"foo\") | output).ExecuteAsync();\n```\n\nBoth `PipeSource` and `PipeTarget` have many factory methods that let you create pipe implementations for different scenarios:\n\n- `PipeSource`:\n  - `PipeSource.Null` — represents an empty pipe source\n  - `PipeSource.FromStream(...)` — pipes data from any readable stream\n  - `PipeSource.FromFile(...)` — pipes data from a file\n  - `PipeSource.FromBytes(...)` — pipes data from a byte array\n  - `PipeSource.FromString(...)` — pipes data from a text string\n  - `PipeSource.FromCommand(...)` — pipes data from the standard output of another command\n- `PipeTarget`:\n  - `PipeTarget.Null` — represents a pipe target that discards all data\n  - `PipeTarget.ToStream(...)` — pipes data to any writable stream\n  - `PipeTarget.ToFile(...)` — pipes data to a file\n  - `PipeTarget.ToStringBuilder(...)` — pipes data as text into a `StringBuilder`\n  - `PipeTarget.ToDelegate(...)` — pipes data as text, line-by-line, into an `Action\u003cstring\u003e`, or a `Func\u003cstring, Task\u003e`, or a `Func\u003cstring, CancellationToken, Task\u003e` delegate\n  - `PipeTarget.Merge(...)` — merges multiple outbound pipes by replicating the same data across all of them\n\n\u003e **Warning**:\n\u003e Using `PipeTarget.Null` results in the corresponding stream (stdout or stderr) not being opened for the underlying process at all.\n\u003e 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.\n\u003e This may be undesirable in [certain situations](https://github.com/Tyrrrz/CliWrap/issues/145#issuecomment-1100680547) — in which case it's recommended to pipe to a null stream explicitly using `PipeTarget.ToStream(Stream.Null)`.\n\nBelow you can see some examples of what you can achieve with the help of **CliWrap**'s piping feature:\n\n- Pipe a string into stdin:\n\n```csharp\nvar cmd = \"Hello world\" | Cli.Wrap(\"foo\");\nawait cmd.ExecuteAsync();\n```\n\n- Pipe stdout as text into a `StringBuilder`:\n\n```csharp\nvar stdOutBuffer = new StringBuilder();\n\nvar cmd = Cli.Wrap(\"foo\") | stdOutBuffer;\nawait cmd.ExecuteAsync();\n```\n\n- Pipe a binary HTTP stream into stdin:\n\n```csharp\nusing var httpClient = new HttpClient();\nawait using var input = await httpClient.GetStreamAsync(\"https://example.com/image.png\");\n\nvar cmd = input | Cli.Wrap(\"foo\");\nawait cmd.ExecuteAsync();\n```\n\n- Pipe stdout of one command into stdin of another:\n\n```csharp\nvar cmd = Cli.Wrap(\"foo\") | Cli.Wrap(\"bar\") | Cli.Wrap(\"baz\");\nawait cmd.ExecuteAsync();\n```\n\n- Pipe stdout and stderr into stdout and stderr of the parent process:\n\n```csharp\nawait using var stdOut = Console.OpenStandardOutput();\nawait using var stdErr = Console.OpenStandardError();\n\nvar cmd = Cli.Wrap(\"foo\") | (stdOut, stdErr);\nawait cmd.ExecuteAsync();\n```\n\n- Pipe stdout into a delegate:\n\n```csharp\nvar cmd = Cli.Wrap(\"foo\") | Debug.WriteLine;\nawait cmd.ExecuteAsync();\n```\n\n- Pipe stdout into a file and stderr into a `StringBuilder`:\n\n```csharp\nvar buffer = new StringBuilder();\n\nvar cmd = Cli.Wrap(\"foo\") |\n    (PipeTarget.ToFile(\"output.txt\"), PipeTarget.ToStringBuilder(buffer));\n\nawait cmd.ExecuteAsync();\n```\n\n- Pipe stdout into multiple files simultaneously:\n\n```csharp\nvar target = PipeTarget.Merge(\n    PipeTarget.ToFile(\"file1.txt\"),\n    PipeTarget.ToFile(\"file2.txt\"),\n    PipeTarget.ToFile(\"file3.txt\")\n);\n\nvar cmd = Cli.Wrap(\"foo\") | target;\nawait cmd.ExecuteAsync();\n```\n\n- 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:\n\n```csharp\nvar cmd =\n    \"Hello world\" |\n    Cli.Wrap(\"foo\").WithArguments([\"aaa\"]) |\n    Cli.Wrap(\"bar\").WithArguments([\"bbb\"]) |\n    (Console.WriteLine, Console.Error.WriteLine);\n\nawait cmd.ExecuteAsync();\n```\n\n### Execution models\n\n**CliWrap** provides a few high-level execution models that offer alternative ways to reason about commands.\nThese are essentially just extension methods that work by leveraging the [piping feature](#piping) shown earlier.\n\n#### Buffered execution\n\nThis execution model lets you run a process while buffering its standard output and error streams in-memory as text.\nThe buffered data can then be accessed after the command finishes executing.\n\nIn order to execute a command with buffering, call the `ExecuteBufferedAsync()` extension method:\n\n```csharp\nusing CliWrap;\nusing CliWrap.Buffered;\n\nvar result = await Cli.Wrap(\"foo\")\n    .WithArguments([\"bar\"])\n    .ExecuteBufferedAsync();\n\nvar exitCode = result.ExitCode;\nvar stdOut = result.StandardOutput;\nvar stdErr = result.StandardError;\n```\n\nBy default, `ExecuteBufferedAsync()` assumes that the underlying process uses the default encoding (`Encoding.Default`) for writing text to the console.\nTo override this, specify the encoding explicitly by using one of the available overloads:\n\n```csharp\n// Treat both stdout and stderr as UTF8-encoded text streams\nvar result = await Cli.Wrap(\"foo\")\n    .WithArguments([\"bar\"])\n    .ExecuteBufferedAsync(Encoding.UTF8);\n\n// Treat stdout as ASCII-encoded and stderr as UTF8-encoded\nvar result = await Cli.Wrap(\"foo\")\n    .WithArguments([\"bar\"])\n    .ExecuteBufferedAsync(Encoding.ASCII, Encoding.UTF8);\n```\n\n\u003e **Note**:\n\u003e If the underlying process returns a non-zero exit code, `ExecuteBufferedAsync()` will throw an exception similarly to `ExecuteAsync()`, but the exception message will also include the standard error data.\n\n#### Pull-based event stream\n\nBesides executing a command as a task, **CliWrap** also supports an alternative model, in which the execution is represented as an event stream.\nThis lets you start a process and react to the events it produces in real-time.\n\nThose events are:\n\n- `StartedCommandEvent` — received just once, when the command starts executing (contains the process ID)\n- `StandardOutputCommandEvent` — received every time the underlying process writes a new line to the output stream (contains the text as a string)\n- `StandardErrorCommandEvent` — received every time the underlying process writes a new line to the error stream (contains the text as a string)\n- `ExitedCommandEvent` — received just once, when the command finishes executing (contains the exit code)\n\nTo execute a command as a _pull-based_ event stream, use the `ListenAsync()` extension method:\n\n```csharp\nusing CliWrap;\nusing CliWrap.EventStream;\n\nvar cmd = Cli.Wrap(\"foo\").WithArguments([\"bar\"]);\n\nawait foreach (var cmdEvent in cmd.ListenAsync())\n{\n    switch (cmdEvent)\n    {\n        case StartedCommandEvent started:\n            _output.WriteLine($\"Process started; ID: {started.ProcessId}\");\n            break;\n        case StandardOutputCommandEvent stdOut:\n            _output.WriteLine($\"Out\u003e {stdOut.Text}\");\n            break;\n        case StandardErrorCommandEvent stdErr:\n            _output.WriteLine($\"Err\u003e {stdErr.Text}\");\n            break;\n        case ExitedCommandEvent exited:\n            _output.WriteLine($\"Process exited; Code: {exited.ExitCode}\");\n            break;\n    }\n}\n```\n\nThe `ListenAsync()` method starts the command and returns an object of type `IAsyncEnumerable\u003cCommandEvent\u003e`, which you can iterate using the `await foreach` construct introduced in C# 8.\nWhen 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.\n\n\u003e **Note**:\n\u003e Just like with `ExecuteBufferedAsync()`, you can specify custom encoding for `ListenAsync()` using one of its overloads.\n\n#### Push-based event stream\n\nSimilarly to the pull-based stream, you can also execute a command as a _push-based_ event stream instead:\n\n```csharp\nusing System.Reactive;\nusing CliWrap;\nusing CliWrap.EventStream;\n\nvar cmd = Cli.Wrap(\"foo\").WithArguments([\"bar\"]);\n\nawait cmd.Observe().ForEachAsync(cmdEvent =\u003e\n{\n    switch (cmdEvent)\n    {\n        case StartedCommandEvent started:\n            _output.WriteLine($\"Process started; ID: {started.ProcessId}\");\n            break;\n        case StandardOutputCommandEvent stdOut:\n            _output.WriteLine($\"Out\u003e {stdOut.Text}\");\n            break;\n        case StandardErrorCommandEvent stdErr:\n            _output.WriteLine($\"Err\u003e {stdErr.Text}\");\n            break;\n        case ExitedCommandEvent exited:\n            _output.WriteLine($\"Process exited; Code: {exited.ExitCode}\");\n            break;\n    }\n});\n```\n\nIn this case, `Observe()` returns a cold `IObservable\u003cCommandEvent\u003e` that represents an observable stream of command events.\nYou can use the set of extensions provided by [Rx.NET](https://github.com/dotnet/reactive) to transform, filter, throttle, or otherwise manipulate this stream.\n\nUnlike 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.\n\n\u003e **Note**:\n\u003e Similarly to `ExecuteBufferedAsync()`, you can specify custom encoding for `Observe()` using one of its overloads.\n\n#### Combining execution models with custom pipes\n\nThe different execution models shown above are based on the piping model, but those two concepts are not mutually exclusive.\nWhen running a command using one of the built-in execution models, existing pipe configurations are preserved and extended using `PipeTarget.Merge(...)`.\n\nThis means that you can, for example, pipe a command to a file and simultaneously execute it as an event stream:\n\n```csharp\nvar cmd =\n    PipeSource.FromFile(\"input.txt\") |\n    Cli.Wrap(\"foo\") |\n    PipeTarget.ToFile(\"output.txt\");\n\n// Iterate as an event stream and pipe to a file at the same time\n// (execution models preserve configured pipes)\nawait foreach (var cmdEvent in cmd.ListenAsync())\n{\n    // ...\n}\n```\n\n### Timeout and cancellation\n\nCommand execution is asynchronous in nature as it involves a completely separate process.\nIn 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.\n\nTo do that, issue the corresponding [`CancellationToken`](https://learn.microsoft.com/en-us/dotnet/api/system.threading.cancellationtoken) and include it when calling `ExecuteAsync()`:\n\n```csharp\nusing System.Threading;\nusing CliWrap;\n\nusing var cts = new CancellationTokenSource();\n\n// Cancel after a timeout of 10 seconds\ncts.CancelAfter(TimeSpan.FromSeconds(10));\n\nvar result = await Cli.Wrap(\"foo\").ExecuteAsync(cts.Token);\n```\n\nIn the event of a cancellation request, the underlying process will be killed and `ExecuteAsync()` will throw an exception of type `OperationCanceledException` (or its derivative, `TaskCanceledException`).\nYou will need to catch this exception in your code to recover from cancellation:\n\n```csharp\ntry\n{\n    await Cli.Wrap(\"foo\").ExecuteAsync(cts.Token);\n}\ncatch (OperationCanceledException)\n{\n    // Command was canceled\n}\n```\n\nBesides outright killing the process, you can also request cancellation in a more graceful way by sending an interrupt signal.\nTo do that, pass an additional cancellation token to `ExecuteAsync()` that corresponds to that request:\n\n```csharp\nusing var forcefulCts = new CancellationTokenSource();\nusing var gracefulCts = new CancellationTokenSource();\n\n// Cancel forcefully after a timeout of 10 seconds.\n// This serves as a fallback in case graceful cancellation\n// takes too long.\nforcefulCts.CancelAfter(TimeSpan.FromSeconds(10));\n\n// Cancel gracefully after a timeout of 7 seconds.\n// If the process takes too long to respond to graceful\n// cancellation, it will get killed by forceful cancellation\n// 3 seconds later (as configured above).\ngracefulCts.CancelAfter(TimeSpan.FromSeconds(7));\n\nvar result = await Cli.Wrap(\"foo\").ExecuteAsync(forcefulCts.Token, gracefulCts.Token);\n```\n\nRequesting graceful cancellation in **CliWrap** is functionally equivalent to pressing `Ctrl+C` in the console window.\nThe underlying process may handle this signal to perform last-minute critical work before finally exiting on its own terms.\n\nGraceful 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.\nIn the above example, this risk is mitigated by additionally scheduling a delayed forceful cancellation that prevents the command from hanging.\n\nIf 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:\n\n```csharp\npublic async Task GitPushAsync(CancellationToken cancellationToken = default)\n{\n    using var forcefulCts = new CancellationTokenSource();\n\n    // When the cancellation token is triggered,\n    // schedule forceful cancellation as a fallback.\n    await using var link = cancellationToken.Register(() =\u003e\n        forcefulCts.CancelAfter(TimeSpan.FromSeconds(3))\n    );\n\n    await Cli.Wrap(\"git\")\n        .WithArguments([\"push\"])\n        .ExecuteAsync(forcefulCts.Token, cancellationToken);\n}\n```\n\n\u003e **Note**:\n\u003e Similarly to `ExecuteAsync()`, cancellation is also supported by `ExecuteBufferedAsync()`, `ListenAsync()`, and `Observe()`.\n\n### Retrieving process-related information\n\nThe task returned by `ExecuteAsync()` and `ExecuteBufferedAsync()` is, in fact, not a regular `Task\u003cT\u003e`, but an instance of `CommandTask\u003cT\u003e`.\nThis is a specialized awaitable object that contains additional information about the process associated with the executing command:\n\n```csharp\nvar task = Cli.Wrap(\"foo\")\n    .WithArguments([\"bar\"])\n    .ExecuteAsync();\n\n// Get the process ID\nvar processId = task.ProcessId;\n\n// Wait for the task to complete\nawait task;\n```","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FTyrrrz%2FCliWrap","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FTyrrrz%2FCliWrap","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FTyrrrz%2FCliWrap/lists"}