Repository files navigation

ktsu.RunCommand

A .NET library for executing external commands and handling their output through delegates, with synchronous and asynchronous APIs, cancellation, and control over the spawned process.

LicenseNuGet VersionNuGet VersionNuGet DownloadsGitHub commit activityGitHub contributorsGitHub Actions Workflow Status

Introduction

ktsu.RunCommand runs an external command and hands you its output as it arrives, instead of making you assemble Process, ProcessStartInfo, redirected streams and exit-code plumbing yourself. Output is delivered through delegates — either as raw chunks exactly as the process emits them, or buffered into complete lines — and every method returns the process exit code.

Arguments are passed as a vector rather than as one string, so a path containing spaces needs no manual quoting and cannot be mis-split. The process itself can be shaped through a working directory and an environment variable overlay, run elevated on Windows, and terminated along with its children through a cancellation token.

Features

  • Delegate-based output: Receive standard output and standard error through Action<string> delegates as the process produces them, rather than waiting for it to exit.
  • Raw or line-buffered: OutputHandler delivers undelimited chunks exactly as they arrive; LineOutputHandler buffers across chunks and raises one call per complete line.
  • Synchronous and asynchronous: Every operation is available as both Execute and ExecuteAsync, with the asynchronous implementation as the single source of truth.
  • Quote-free arguments: Pass the executable and each argument separately, so spaces in paths and arguments are handled by the platform rather than by string concatenation.
  • Working directory: Start the process in a specific directory without mutating the process-global current directory.
  • Environment variables: Apply an overlay over the inherited environment for a single call, adding, overriding, or removing individual variables.
  • Cancellation: A signalled CancellationToken terminates the process and always surfaces as an OperationCanceledException, never as a synthetic exit code.
  • Windows elevation: Launch through the runas verb for a UAC-elevated process.
  • Custom encoding: Decode the output streams with any Encoding; defaults to UTF-8.
  • Broad target support: .NET Standard 2.0 and 2.1 through .NET 10.

Installation

Package Manager Console

Install-Package ktsu.RunCommand

.NET CLI

dotnet add package ktsu.RunCommand

Package Reference

<PackageReferenceInclude="ktsu.RunCommand"Version="1.5.0" />

Usage Examples

Basic Example

Pass the executable and its arguments separately. All methods return the process exit code:

usingktsu.RunCommand;classProgram{staticvoidMain(){intexitCode=RunCommand.Execute("dotnet",["--version"]);if(exitCode==0){Console.WriteLine("Command executed successfully!");}else{Console.WriteLine($"Command failed with exit code: {exitCode}");}}}

Custom Output Handling

To handle the output of the command, provide delegates to the OutputHandler class:

usingktsu.RunCommand;classProgram{staticvoidMain(){intexitCode=RunCommand.Execute(fileName:"dotnet",arguments:["--version"],outputHandler:new(onStandardOutput:Console.Write,onStandardError:Console.Write));Console.WriteLine($"Process exited with code: {exitCode}");}}

NOTE:When using the default OutputHandler, the delegates receive undelimited chunks of output. This gives you exactly what the command produces, including whitespace and non-printable characters, to handle as you see fit.

Line-by-Line Output Handling

To handle the output one line at a time, use the LineOutputHandler class:

usingktsu.RunCommand;classProgram{staticvoidMain(){intexitCode=RunCommand.Execute(fileName:"dotnet",arguments:["--version"],outputHandler:newLineOutputHandler(onStandardOutput: line =>Console.WriteLine($"Output: {line}"),onStandardError: line =>Console.WriteLine($"Error: {line}")));Console.WriteLine($"Process exited with code: {exitCode}");}}

Asynchronous Execution

All of the above examples can be run asynchronously with ExecuteAsync:

usingktsu.RunCommand;classProgram{staticasyncTaskMain(){intexitCode=awaitRunCommand.ExecuteAsync("dotnet",["--version"]);Console.WriteLine($"Process exited with code: {exitCode}");}}

Cancellation

Passing a CancellationToken terminates the process when the token is signalled:

usingktsu.RunCommand;classProgram{staticasyncTaskMain(){usingCancellationTokenSourcecancellation=new(TimeSpan.FromSeconds(30));try{intexitCode=awaitRunCommand.ExecuteAsync(fileName:"dotnet",arguments:["build"],outputHandler:newLineOutputHandler(onStandardOutput:Console.WriteLine),cancellationToken:cancellation.Token);Console.WriteLine($"Process exited with code: {exitCode}");}catch(OperationCanceledException){Console.WriteLine("The command was cancelled.");}}}

A cancelled call always throws OperationCanceledException — it never returns the killed process's exit code — so cancellation cannot be mistaken for a genuine failure of the command.

On .NET Core 3.0 and later the entire process tree is terminated. On .NET Standard 2.0 and 2.1 only the process itself can be terminated, so any grandchildren it spawned are left running.

Process Options

CommandOptions shapes the process a command runs in. Pass it alongside an executable and its arguments:

usingktsu.RunCommand;usingktsu.Semantics.Paths;classProgram{staticasyncTaskMain(){intexitCode=awaitRunCommand.ExecuteAsync(fileName:"git",arguments:["status","--short"],outputHandler:newLineOutputHandler(onStandardOutput:Console.WriteLine),options:new(){WorkingDirectory=AbsoluteDirectoryPath.Create(@"C:\repos\my project"),EnvironmentVariables=newDictionary<string,string?>{["GIT_TERMINAL_PROMPT"]="0",["LC_ALL"]="C",},});Console.WriteLine($"Process exited with code: {exitCode}");}}

CommandOptions.Elevation carries the privilege level too, so a single options object replaces the separate Elevation argument.

Working Directory

Without a WorkingDirectory the process inherits the current directory of the calling process, which is what commands did before this option existed.

The type is AbsoluteDirectoryPath rather than a string on purpose. A relative directory would have to be resolved against the caller's current directory — the process-global state this option exists to avoid depending on, since it is shared by every thread and races with concurrent calls.

Environment Variables

EnvironmentVariables is an overlay on the inherited environment, not a replacement: a name you do not list keeps whatever the calling process had. A null value removes a variable, which is how you unset something the parent had set:

EnvironmentVariables=newDictionary<string,string?>{["GIT_DIR"]=null,}

Environment variables are the only control surface some tools expose, so this covers behaviour with no command-line equivalent — GIT_TERMINAL_PROMPT=0 to make an authenticating git fetch fail rather than block forever on a prompt no terminal will answer, GIT_ASKPASS/SSH_ASKPASS to supply credentials without putting them on a command line where any process listing can read them, and LC_ALL=C to force stable, machine-parseable output rather than whatever the host locale produces.

NOTE:EnvironmentVariables cannot be combined with Elevation.Elevated on Windows. Elevation requires UseShellExecute, which offers nowhere to pass an environment, so the call throws ArgumentException rather than silently dropping the variables.

Elevation (Windows)

To run a command with elevated privileges, set Elevation.Elevated. On Windows this launches the process with the runas verb, which triggers a UAC prompt:

usingktsu.RunCommand;classProgram{staticvoidMain(){intexitCode=RunCommand.Execute(fileName:"powershell",arguments:["-Command","Get-Service"],outputHandler:new(),options:new(){Elevation=Elevation.Elevated});Console.WriteLine($"Process exited with code: {exitCode}");}}

NOTE:Output redirection is incompatible with runas, so an OutputHandler passed alongside Elevation.Elevated will not be invoked. You still get the process exit code.

On non-Windows platforms Elevation.Elevated is a no-op — prefix your command with sudo yourself if you need elevation there.

Encoding

By default the library decodes the output streams as UTF-8. To use a different encoding, specify it in the OutputHandler or LineOutputHandler constructor:

usingSystem.Text;usingktsu.RunCommand;classProgram{staticvoidMain(){intexitCode=RunCommand.Execute(fileName:"dotnet",arguments:["--version"],outputHandler:new(onStandardOutput:Console.Write,onStandardError:Console.Write,encoding:Encoding.ASCII));}}

Deprecated: Single Command Strings

The overloads taking one command string are obsolete. They separate the executable from its arguments by splitting on the first space, which cannot represent an executable path that itself contains a space — on Windows that includes anything under C:\Program Files\:

// Obsolete, and broken: splits into "C:\Program" plus "Files\Git\bin\git.exe --version"awaitRunCommand.ExecuteAsync(@"C:\Program Files\Git\bin\git.exe --version");// CorrectawaitRunCommand.ExecuteAsync(@"C:\Program Files\Git\bin\git.exe",["--version"]);

Quoting does not rescue it, because the split happens before any quote handling. The string form is inherently ambiguous — no parse handles every combination of spaces and quotes without adopting a shell's full grammar — so rather than grow a half-grammar that moves the surprise elsewhere, these overloads are deprecated in favour of the argument-list ones, which have no such ambiguity because the executable is passed separately.

Migration is mechanical: split the string yourself at the boundaries you meant.

ObsoleteReplacement
Execute(command)Execute(fileName, arguments)
Execute(command, outputHandler)Execute(fileName, arguments, outputHandler)
Execute(command, elevation)Execute(fileName, arguments, outputHandler, options)
ExecuteAsync(command)ExecuteAsync(fileName, arguments)
ExecuteAsync(command, outputHandler)ExecuteAsync(fileName, arguments, outputHandler)
ExecuteAsync(command, cancellationToken)ExecuteAsync(fileName, arguments, outputHandler, cancellationToken)
ExecuteAsync(command, outputHandler, elevation, cancellationToken)ExecuteAsync(fileName, arguments, outputHandler, options, cancellationToken)

API Reference

RunCommand

Static class providing the command execution API. Every method returns the process exit code.

Methods

NameReturn TypeDescription
Execute(string fileName, IEnumerable<string> arguments)intExecutes a command synchronously.
Execute(string fileName, IEnumerable<string> arguments, OutputHandler outputHandler)intExecutes a command synchronously with custom output handling.
Execute(string fileName, IEnumerable<string> arguments, OutputHandler outputHandler, CommandOptions options)intExecutes a command synchronously with the given process options.
ExecuteAsync(string fileName, IEnumerable<string> arguments)Task<int>Executes a command asynchronously.
ExecuteAsync(string fileName, IEnumerable<string> arguments, OutputHandler outputHandler)Task<int>Executes a command asynchronously with custom output handling.
ExecuteAsync(string fileName, IEnumerable<string> arguments, OutputHandler outputHandler, CancellationToken cancellationToken)Task<int>As above, terminating the process and its children if the token is signalled.
ExecuteAsync(string fileName, IEnumerable<string> arguments, OutputHandler outputHandler, Elevation elevation, CancellationToken cancellationToken)Task<int>As above, at the given elevation level.
ExecuteAsync(string fileName, IEnumerable<string> arguments, OutputHandler outputHandler, CommandOptions options)Task<int>Executes a command asynchronously with the given process options.
ExecuteAsync(string fileName, IEnumerable<string> arguments, OutputHandler outputHandler, CommandOptions options, CancellationToken cancellationToken)Task<int>As above, terminating the process and its children if the token is signalled.

The overloads taking a single command string — four Execute and seven ExecuteAsync — are obsolete. See Deprecated: Single Command Strings for the migration table.

CommandOptions

Record describing how to shape the process a command runs in. Every member defaults to the behaviour commands had before the type existed, so an instance with nothing set is equivalent to not passing one at all.

Properties

NameTypeDescription
WorkingDirectoryAbsoluteDirectoryPath?The directory the process starts in, or null to inherit the caller's current directory.
EnvironmentVariablesIReadOnlyDictionary<string, string?>?Variables applied over the inherited environment, or null to inherit it unchanged. A null value removes a variable.
ElevationElevationThe privilege level under which to run the command. Defaults to Elevation.Default.

OutputHandler

Processes output in raw, undelimited chunks as they arrive from the process.

Constructor

NameDescription
OutputHandler(Action<string>? onStandardOutput = null, Action<string>? onStandardError = null, Encoding? encoding = null)Creates a handler with delegates for the output and error streams. encoding defaults to UTF-8.

Properties

NameTypeDescription
EncodingEncodingThe encoding used to decode the process's output streams.

LineOutputHandler

Inherits from OutputHandler and buffers incoming chunks, invoking the delegates once per complete line. Incomplete trailing data is held until the rest of the line arrives.

Constructor

NameDescription
LineOutputHandler(Action<string>? onStandardOutput = null, Action<string>? onStandardError = null, Encoding? encoding = null)Creates a line-buffering handler with delegates for the output and error streams.

Elevation

Enum specifying the privilege level under which a command runs.

NameDescription
DefaultRun with the current process's privileges. Standard output and standard error are captured.
ElevatedOn Windows, launch through the runas verb, prompting for UAC consent; output is not captured. No effect on non-Windows platforms.

Contributing

Contributions are welcome! Feel free to open issues or submit pull requests.

License

This project is licensed under the MIT License. See the LICENSE.md file for details.

About

A library that provides an easy way to execute shell commands and handle the output via delegates with both synchronous and asynchronous support.

Topics

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

ktsu.RunCommand

A .NET library for executing external commands and handling their output through delegates, with synchronous and asynchronous APIs, cancellation, and control over the spawned process.

LicenseNuGet VersionNuGet VersionNuGet DownloadsGitHub commit activityGitHub contributorsGitHub Actions Workflow Status

Introduction

ktsu.RunCommand runs an external command and hands you its output as it arrives, instead of making you assemble Process, ProcessStartInfo, redirected streams and exit-code plumbing yourself. Output is delivered through delegates — either as raw chunks exactly as the process emits them, or buffered into complete lines — and every method returns the process exit code.

Arguments are passed as a vector rather than as one string, so a path containing spaces needs no manual quoting and cannot be mis-split. The process itself can be shaped through a working directory and an environment variable overlay, run elevated on Windows, and terminated along with its children through a cancellation token.

Features

  • Delegate-based output: Receive standard output and standard error through Action<string> delegates as the process produces them, rather than waiting for it to exit.
  • Raw or line-buffered: OutputHandler delivers undelimited chunks exactly as they arrive; LineOutputHandler buffers across chunks and raises one call per complete line.
  • Synchronous and asynchronous: Every operation is available as both Execute and ExecuteAsync, with the asynchronous implementation as the single source of truth.
  • Quote-free arguments: Pass the executable and each argument separately, so spaces in paths and arguments are handled by the platform rather than by string concatenation.
  • Working directory: Start the process in a specific directory without mutating the process-global current directory.
  • Environment variables: Apply an overlay over the inherited environment for a single call, adding, overriding, or removing individual variables.
  • Cancellation: A signalled CancellationToken terminates the process and always surfaces as an OperationCanceledException, never as a synthetic exit code.
  • Windows elevation: Launch through the runas verb for a UAC-elevated process.
  • Custom encoding: Decode the output streams with any Encoding; defaults to UTF-8.
  • Broad target support: .NET Standard 2.0 and 2.1 through .NET 10.

Installation

Package Manager Console

Install-Package ktsu.RunCommand

.NET CLI

dotnet add package ktsu.RunCommand

Package Reference

<PackageReferenceInclude="ktsu.RunCommand"Version="1.5.0" />

Usage Examples

Basic Example

Pass the executable and its arguments separately. All methods return the process exit code:

usingktsu.RunCommand;classProgram{staticvoidMain(){intexitCode=RunCommand.Execute("dotnet",["--version"]);if(exitCode==0){Console.WriteLine("Command executed successfully!");}else{Console.WriteLine($"Command failed with exit code: {exitCode}");}}}

Custom Output Handling

To handle the output of the command, provide delegates to the OutputHandler class:

usingktsu.RunCommand;classProgram{staticvoidMain(){intexitCode=RunCommand.Execute(fileName:"dotnet",arguments:["--version"],outputHandler:new(onStandardOutput:Console.Write,onStandardError:Console.Write));Console.WriteLine($"Process exited with code: {exitCode}");}}

NOTE:When using the default OutputHandler, the delegates receive undelimited chunks of output. This gives you exactly what the command produces, including whitespace and non-printable characters, to handle as you see fit.

Line-by-Line Output Handling

To handle the output one line at a time, use the LineOutputHandler class:

usingktsu.RunCommand;classProgram{staticvoidMain(){intexitCode=RunCommand.Execute(fileName:"dotnet",arguments:["--version"],outputHandler:newLineOutputHandler(onStandardOutput: line =>Console.WriteLine($"Output: {line}"),onStandardError: line =>Console.WriteLine($"Error: {line}")));Console.WriteLine($"Process exited with code: {exitCode}");}}

Asynchronous Execution

All of the above examples can be run asynchronously with ExecuteAsync:

usingktsu.RunCommand;classProgram{staticasyncTaskMain(){intexitCode=awaitRunCommand.ExecuteAsync("dotnet",["--version"]);Console.WriteLine($"Process exited with code: {exitCode}");}}

Cancellation

Passing a CancellationToken terminates the process when the token is signalled:

usingktsu.RunCommand;classProgram{staticasyncTaskMain(){usingCancellationTokenSourcecancellation=new(TimeSpan.FromSeconds(30));try{intexitCode=awaitRunCommand.ExecuteAsync(fileName:"dotnet",arguments:["build"],outputHandler:newLineOutputHandler(onStandardOutput:Console.WriteLine),cancellationToken:cancellation.Token);Console.WriteLine($"Process exited with code: {exitCode}");}catch(OperationCanceledException){Console.WriteLine("The command was cancelled.");}}}

A cancelled call always throws OperationCanceledException — it never returns the killed process's exit code — so cancellation cannot be mistaken for a genuine failure of the command.

On .NET Core 3.0 and later the entire process tree is terminated. On .NET Standard 2.0 and 2.1 only the process itself can be terminated, so any grandchildren it spawned are left running.

Process Options

CommandOptions shapes the process a command runs in. Pass it alongside an executable and its arguments:

usingktsu.RunCommand;usingktsu.Semantics.Paths;classProgram{staticasyncTaskMain(){intexitCode=awaitRunCommand.ExecuteAsync(fileName:"git",arguments:["status","--short"],outputHandler:newLineOutputHandler(onStandardOutput:Console.WriteLine),options:new(){WorkingDirectory=AbsoluteDirectoryPath.Create(@"C:\repos\my project"),EnvironmentVariables=newDictionary<string,string?>{["GIT_TERMINAL_PROMPT"]="0",["LC_ALL"]="C",},});Console.WriteLine($"Process exited with code: {exitCode}");}}

CommandOptions.Elevation carries the privilege level too, so a single options object replaces the separate Elevation argument.

Working Directory

Without a WorkingDirectory the process inherits the current directory of the calling process, which is what commands did before this option existed.

The type is AbsoluteDirectoryPath rather than a string on purpose. A relative directory would have to be resolved against the caller's current directory — the process-global state this option exists to avoid depending on, since it is shared by every thread and races with concurrent calls.

Environment Variables

EnvironmentVariables is an overlay on the inherited environment, not a replacement: a name you do not list keeps whatever the calling process had. A null value removes a variable, which is how you unset something the parent had set:

EnvironmentVariables=newDictionary<string,string?>{["GIT_DIR"]=null,}

Environment variables are the only control surface some tools expose, so this covers behaviour with no command-line equivalent — GIT_TERMINAL_PROMPT=0 to make an authenticating git fetch fail rather than block forever on a prompt no terminal will answer, GIT_ASKPASS/SSH_ASKPASS to supply credentials without putting them on a command line where any process listing can read them, and LC_ALL=C to force stable, machine-parseable output rather than whatever the host locale produces.

NOTE:EnvironmentVariables cannot be combined with Elevation.Elevated on Windows. Elevation requires UseShellExecute, which offers nowhere to pass an environment, so the call throws ArgumentException rather than silently dropping the variables.

Elevation (Windows)

To run a command with elevated privileges, set Elevation.Elevated. On Windows this launches the process with the runas verb, which triggers a UAC prompt:

usingktsu.RunCommand;classProgram{staticvoidMain(){intexitCode=RunCommand.Execute(fileName:"powershell",arguments:["-Command","Get-Service"],outputHandler:new(),options:new(){Elevation=Elevation.Elevated});Console.WriteLine($"Process exited with code: {exitCode}");}}

NOTE:Output redirection is incompatible with runas, so an OutputHandler passed alongside Elevation.Elevated will not be invoked. You still get the process exit code.

On non-Windows platforms Elevation.Elevated is a no-op — prefix your command with sudo yourself if you need elevation there.

Encoding

By default the library decodes the output streams as UTF-8. To use a different encoding, specify it in the OutputHandler or LineOutputHandler constructor:

usingSystem.Text;usingktsu.RunCommand;classProgram{staticvoidMain(){intexitCode=RunCommand.Execute(fileName:"dotnet",arguments:["--version"],outputHandler:new(onStandardOutput:Console.Write,onStandardError:Console.Write,encoding:Encoding.ASCII));}}

Deprecated: Single Command Strings

The overloads taking one command string are obsolete. They separate the executable from its arguments by splitting on the first space, which cannot represent an executable path that itself contains a space — on Windows that includes anything under C:\Program Files\:

// Obsolete, and broken: splits into "C:\Program" plus "Files\Git\bin\git.exe --version"awaitRunCommand.ExecuteAsync(@"C:\Program Files\Git\bin\git.exe --version");// CorrectawaitRunCommand.ExecuteAsync(@"C:\Program Files\Git\bin\git.exe",["--version"]);

Quoting does not rescue it, because the split happens before any quote handling. The string form is inherently ambiguous — no parse handles every combination of spaces and quotes without adopting a shell's full grammar — so rather than grow a half-grammar that moves the surprise elsewhere, these overloads are deprecated in favour of the argument-list ones, which have no such ambiguity because the executable is passed separately.

Migration is mechanical: split the string yourself at the boundaries you meant.

ObsoleteReplacement
Execute(command)Execute(fileName, arguments)
Execute(command, outputHandler)Execute(fileName, arguments, outputHandler)
Execute(command, elevation)Execute(fileName, arguments, outputHandler, options)
ExecuteAsync(command)ExecuteAsync(fileName, arguments)
ExecuteAsync(command, outputHandler)ExecuteAsync(fileName, arguments, outputHandler)
ExecuteAsync(command, cancellationToken)ExecuteAsync(fileName, arguments, outputHandler, cancellationToken)
ExecuteAsync(command, outputHandler, elevation, cancellationToken)ExecuteAsync(fileName, arguments, outputHandler, options, cancellationToken)

API Reference

RunCommand

Static class providing the command execution API. Every method returns the process exit code.

Methods

NameReturn TypeDescription
Execute(string fileName, IEnumerable<string> arguments)intExecutes a command synchronously.
Execute(string fileName, IEnumerable<string> arguments, OutputHandler outputHandler)intExecutes a command synchronously with custom output handling.
Execute(string fileName, IEnumerable<string> arguments, OutputHandler outputHandler, CommandOptions options)intExecutes a command synchronously with the given process options.
ExecuteAsync(string fileName, IEnumerable<string> arguments)Task<int>Executes a command asynchronously.
ExecuteAsync(string fileName, IEnumerable<string> arguments, OutputHandler outputHandler)Task<int>Executes a command asynchronously with custom output handling.
ExecuteAsync(string fileName, IEnumerable<string> arguments, OutputHandler outputHandler, CancellationToken cancellationToken)Task<int>As above, terminating the process and its children if the token is signalled.
ExecuteAsync(string fileName, IEnumerable<string> arguments, OutputHandler outputHandler, Elevation elevation, CancellationToken cancellationToken)Task<int>As above, at the given elevation level.
ExecuteAsync(string fileName, IEnumerable<string> arguments, OutputHandler outputHandler, CommandOptions options)Task<int>Executes a command asynchronously with the given process options.
ExecuteAsync(string fileName, IEnumerable<string> arguments, OutputHandler outputHandler, CommandOptions options, CancellationToken cancellationToken)Task<int>As above, terminating the process and its children if the token is signalled.

The overloads taking a single command string — four Execute and seven ExecuteAsync — are obsolete. See Deprecated: Single Command Strings for the migration table.

CommandOptions

Record describing how to shape the process a command runs in. Every member defaults to the behaviour commands had before the type existed, so an instance with nothing set is equivalent to not passing one at all.

Properties

NameTypeDescription
WorkingDirectoryAbsoluteDirectoryPath?The directory the process starts in, or null to inherit the caller's current directory.
EnvironmentVariablesIReadOnlyDictionary<string, string?>?Variables applied over the inherited environment, or null to inherit it unchanged. A null value removes a variable.
ElevationElevationThe privilege level under which to run the command. Defaults to Elevation.Default.

OutputHandler

Processes output in raw, undelimited chunks as they arrive from the process.

Constructor

NameDescription
OutputHandler(Action<string>? onStandardOutput = null, Action<string>? onStandardError = null, Encoding? encoding = null)Creates a handler with delegates for the output and error streams. encoding defaults to UTF-8.

Properties

NameTypeDescription
EncodingEncodingThe encoding used to decode the process's output streams.

LineOutputHandler

Inherits from OutputHandler and buffers incoming chunks, invoking the delegates once per complete line. Incomplete trailing data is held until the rest of the line arrives.

Constructor

NameDescription
LineOutputHandler(Action<string>? onStandardOutput = null, Action<string>? onStandardError = null, Encoding? encoding = null)Creates a line-buffering handler with delegates for the output and error streams.

Elevation

Enum specifying the privilege level under which a command runs.

NameDescription
DefaultRun with the current process's privileges. Standard output and standard error are captured.
ElevatedOn Windows, launch through the runas verb, prompting for UAC consent; output is not captured. No effect on non-Windows platforms.

Contributing

Contributions are welcome! Feel free to open issues or submit pull requests.

License

This project is licensed under the MIT License. See the LICENSE.md file for details.

About

A library that provides an easy way to execute shell commands and handle the output via delegates with both synchronous and asynchronous support.

Topics

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

ktsu.RunCommand

A .NET library for executing external commands and handling their output through delegates, with synchronous and asynchronous APIs, cancellation, and control over the spawned process.

LicenseNuGet VersionNuGet VersionNuGet DownloadsGitHub commit activityGitHub contributorsGitHub Actions Workflow Status

Introduction

ktsu.RunCommand runs an external command and hands you its output as it arrives, instead of making you assemble Process, ProcessStartInfo, redirected streams and exit-code plumbing yourself. Output is delivered through delegates — either as raw chunks exactly as the process emits them, or buffered into complete lines — and every method returns the process exit code.

Arguments are passed as a vector rather than as one string, so a path containing spaces needs no manual quoting and cannot be mis-split. The process itself can be shaped through a working directory and an environment variable overlay, run elevated on Windows, and terminated along with its children through a cancellation token.

Features

  • Delegate-based output: Receive standard output and standard error through Action<string> delegates as the process produces them, rather than waiting for it to exit.
  • Raw or line-buffered: OutputHandler delivers undelimited chunks exactly as they arrive; LineOutputHandler buffers across chunks and raises one call per complete line.
  • Synchronous and asynchronous: Every operation is available as both Execute and ExecuteAsync, with the asynchronous implementation as the single source of truth.
  • Quote-free arguments: Pass the executable and each argument separately, so spaces in paths and arguments are handled by the platform rather than by string concatenation.
  • Working directory: Start the process in a specific directory without mutating the process-global current directory.
  • Environment variables: Apply an overlay over the inherited environment for a single call, adding, overriding, or removing individual variables.
  • Cancellation: A signalled CancellationToken terminates the process and always surfaces as an OperationCanceledException, never as a synthetic exit code.
  • Windows elevation: Launch through the runas verb for a UAC-elevated process.
  • Custom encoding: Decode the output streams with any Encoding; defaults to UTF-8.
  • Broad target support: .NET Standard 2.0 and 2.1 through .NET 10.

Installation

Package Manager Console

Install-Package ktsu.RunCommand

.NET CLI

dotnet add package ktsu.RunCommand

Package Reference

<PackageReferenceInclude="ktsu.RunCommand"Version="1.5.0" />

Usage Examples

Basic Example

Pass the executable and its arguments separately. All methods return the process exit code:

usingktsu.RunCommand;classProgram{staticvoidMain(){intexitCode=RunCommand.Execute("dotnet",["--version"]);if(exitCode==0){Console.WriteLine("Command executed successfully!");}else{Console.WriteLine($"Command failed with exit code: {exitCode}");}}}

Custom Output Handling

To handle the output of the command, provide delegates to the OutputHandler class:

usingktsu.RunCommand;classProgram{staticvoidMain(){intexitCode=RunCommand.Execute(fileName:"dotnet",arguments:["--version"],outputHandler:new(onStandardOutput:Console.Write,onStandardError:Console.Write));Console.WriteLine($"Process exited with code: {exitCode}");}}

NOTE:When using the default OutputHandler, the delegates receive undelimited chunks of output. This gives you exactly what the command produces, including whitespace and non-printable characters, to handle as you see fit.

Line-by-Line Output Handling

To handle the output one line at a time, use the LineOutputHandler class:

usingktsu.RunCommand;classProgram{staticvoidMain(){intexitCode=RunCommand.Execute(fileName:"dotnet",arguments:["--version"],outputHandler:newLineOutputHandler(onStandardOutput: line =>Console.WriteLine($"Output: {line}"),onStandardError: line =>Console.WriteLine($"Error: {line}")));Console.WriteLine($"Process exited with code: {exitCode}");}}

Asynchronous Execution

All of the above examples can be run asynchronously with ExecuteAsync:

usingktsu.RunCommand;classProgram{staticasyncTaskMain(){intexitCode=awaitRunCommand.ExecuteAsync("dotnet",["--version"]);Console.WriteLine($"Process exited with code: {exitCode}");}}

Cancellation

Passing a CancellationToken terminates the process when the token is signalled:

usingktsu.RunCommand;classProgram{staticasyncTaskMain(){usingCancellationTokenSourcecancellation=new(TimeSpan.FromSeconds(30));try{intexitCode=awaitRunCommand.ExecuteAsync(fileName:"dotnet",arguments:["build"],outputHandler:newLineOutputHandler(onStandardOutput:Console.WriteLine),cancellationToken:cancellation.Token);Console.WriteLine($"Process exited with code: {exitCode}");}catch(OperationCanceledException){Console.WriteLine("The command was cancelled.");}}}

A cancelled call always throws OperationCanceledException — it never returns the killed process's exit code — so cancellation cannot be mistaken for a genuine failure of the command.

On .NET Core 3.0 and later the entire process tree is terminated. On .NET Standard 2.0 and 2.1 only the process itself can be terminated, so any grandchildren it spawned are left running.

Process Options

CommandOptions shapes the process a command runs in. Pass it alongside an executable and its arguments:

usingktsu.RunCommand;usingktsu.Semantics.Paths;classProgram{staticasyncTaskMain(){intexitCode=awaitRunCommand.ExecuteAsync(fileName:"git",arguments:["status","--short"],outputHandler:newLineOutputHandler(onStandardOutput:Console.WriteLine),options:new(){WorkingDirectory=AbsoluteDirectoryPath.Create(@"C:\repos\my project"),EnvironmentVariables=newDictionary<string,string?>{["GIT_TERMINAL_PROMPT"]="0",["LC_ALL"]="C",},});Console.WriteLine($"Process exited with code: {exitCode}");}}

CommandOptions.Elevation carries the privilege level too, so a single options object replaces the separate Elevation argument.

Working Directory

Without a WorkingDirectory the process inherits the current directory of the calling process, which is what commands did before this option existed.

The type is AbsoluteDirectoryPath rather than a string on purpose. A relative directory would have to be resolved against the caller's current directory — the process-global state this option exists to avoid depending on, since it is shared by every thread and races with concurrent calls.

Environment Variables

EnvironmentVariables is an overlay on the inherited environment, not a replacement: a name you do not list keeps whatever the calling process had. A null value removes a variable, which is how you unset something the parent had set:

EnvironmentVariables=newDictionary<string,string?>{["GIT_DIR"]=null,}

Environment variables are the only control surface some tools expose, so this covers behaviour with no command-line equivalent — GIT_TERMINAL_PROMPT=0 to make an authenticating git fetch fail rather than block forever on a prompt no terminal will answer, GIT_ASKPASS/SSH_ASKPASS to supply credentials without putting them on a command line where any process listing can read them, and LC_ALL=C to force stable, machine-parseable output rather than whatever the host locale produces.

NOTE:EnvironmentVariables cannot be combined with Elevation.Elevated on Windows. Elevation requires UseShellExecute, which offers nowhere to pass an environment, so the call throws ArgumentException rather than silently dropping the variables.

Elevation (Windows)

To run a command with elevated privileges, set Elevation.Elevated. On Windows this launches the process with the runas verb, which triggers a UAC prompt:

usingktsu.RunCommand;classProgram{staticvoidMain(){intexitCode=RunCommand.Execute(fileName:"powershell",arguments:["-Command","Get-Service"],outputHandler:new(),options:new(){Elevation=Elevation.Elevated});Console.WriteLine($"Process exited with code: {exitCode}");}}

NOTE:Output redirection is incompatible with runas, so an OutputHandler passed alongside Elevation.Elevated will not be invoked. You still get the process exit code.

On non-Windows platforms Elevation.Elevated is a no-op — prefix your command with sudo yourself if you need elevation there.

Encoding

By default the library decodes the output streams as UTF-8. To use a different encoding, specify it in the OutputHandler or LineOutputHandler constructor:

usingSystem.Text;usingktsu.RunCommand;classProgram{staticvoidMain(){intexitCode=RunCommand.Execute(fileName:"dotnet",arguments:["--version"],outputHandler:new(onStandardOutput:Console.Write,onStandardError:Console.Write,encoding:Encoding.ASCII));}}

Deprecated: Single Command Strings

The overloads taking one command string are obsolete. They separate the executable from its arguments by splitting on the first space, which cannot represent an executable path that itself contains a space — on Windows that includes anything under C:\Program Files\:

// Obsolete, and broken: splits into "C:\Program" plus "Files\Git\bin\git.exe --version"awaitRunCommand.ExecuteAsync(@"C:\Program Files\Git\bin\git.exe --version");// CorrectawaitRunCommand.ExecuteAsync(@"C:\Program Files\Git\bin\git.exe",["--version"]);

Quoting does not rescue it, because the split happens before any quote handling. The string form is inherently ambiguous — no parse handles every combination of spaces and quotes without adopting a shell's full grammar — so rather than grow a half-grammar that moves the surprise elsewhere, these overloads are deprecated in favour of the argument-list ones, which have no such ambiguity because the executable is passed separately.

Migration is mechanical: split the string yourself at the boundaries you meant.

ObsoleteReplacement
Execute(command)Execute(fileName, arguments)
Execute(command, outputHandler)Execute(fileName, arguments, outputHandler)
Execute(command, elevation)Execute(fileName, arguments, outputHandler, options)
ExecuteAsync(command)ExecuteAsync(fileName, arguments)
ExecuteAsync(command, outputHandler)ExecuteAsync(fileName, arguments, outputHandler)
ExecuteAsync(command, cancellationToken)ExecuteAsync(fileName, arguments, outputHandler, cancellationToken)
ExecuteAsync(command, outputHandler, elevation, cancellationToken)ExecuteAsync(fileName, arguments, outputHandler, options, cancellationToken)

API Reference

RunCommand

Static class providing the command execution API. Every method returns the process exit code.

Methods

NameReturn TypeDescription
Execute(string fileName, IEnumerable<string> arguments)intExecutes a command synchronously.
Execute(string fileName, IEnumerable<string> arguments, OutputHandler outputHandler)intExecutes a command synchronously with custom output handling.
Execute(string fileName, IEnumerable<string> arguments, OutputHandler outputHandler, CommandOptions options)intExecutes a command synchronously with the given process options.
ExecuteAsync(string fileName, IEnumerable<string> arguments)Task<int>Executes a command asynchronously.
ExecuteAsync(string fileName, IEnumerable<string> arguments, OutputHandler outputHandler)Task<int>Executes a command asynchronously with custom output handling.
ExecuteAsync(string fileName, IEnumerable<string> arguments, OutputHandler outputHandler, CancellationToken cancellationToken)Task<int>As above, terminating the process and its children if the token is signalled.
ExecuteAsync(string fileName, IEnumerable<string> arguments, OutputHandler outputHandler, Elevation elevation, CancellationToken cancellationToken)Task<int>As above, at the given elevation level.
ExecuteAsync(string fileName, IEnumerable<string> arguments, OutputHandler outputHandler, CommandOptions options)Task<int>Executes a command asynchronously with the given process options.
ExecuteAsync(string fileName, IEnumerable<string> arguments, OutputHandler outputHandler, CommandOptions options, CancellationToken cancellationToken)Task<int>As above, terminating the process and its children if the token is signalled.

The overloads taking a single command string — four Execute and seven ExecuteAsync — are obsolete. See Deprecated: Single Command Strings for the migration table.

CommandOptions

Record describing how to shape the process a command runs in. Every member defaults to the behaviour commands had before the type existed, so an instance with nothing set is equivalent to not passing one at all.

Properties

NameTypeDescription
WorkingDirectoryAbsoluteDirectoryPath?The directory the process starts in, or null to inherit the caller's current directory.
EnvironmentVariablesIReadOnlyDictionary<string, string?>?Variables applied over the inherited environment, or null to inherit it unchanged. A null value removes a variable.
ElevationElevationThe privilege level under which to run the command. Defaults to Elevation.Default.

OutputHandler

Processes output in raw, undelimited chunks as they arrive from the process.

Constructor

NameDescription
OutputHandler(Action<string>? onStandardOutput = null, Action<string>? onStandardError = null, Encoding? encoding = null)Creates a handler with delegates for the output and error streams. encoding defaults to UTF-8.

Properties

NameTypeDescription
EncodingEncodingThe encoding used to decode the process's output streams.

LineOutputHandler

Inherits from OutputHandler and buffers incoming chunks, invoking the delegates once per complete line. Incomplete trailing data is held until the rest of the line arrives.

Constructor

NameDescription
LineOutputHandler(Action<string>? onStandardOutput = null, Action<string>? onStandardError = null, Encoding? encoding = null)Creates a line-buffering handler with delegates for the output and error streams.

Elevation

Enum specifying the privilege level under which a command runs.

NameDescription
DefaultRun with the current process's privileges. Standard output and standard error are captured.
ElevatedOn Windows, launch through the runas verb, prompting for UAC consent; output is not captured. No effect on non-Windows platforms.

Contributing

Contributions are welcome! Feel free to open issues or submit pull requests.

License

This project is licensed under the MIT License. See the LICENSE.md file for details.

About

A library that provides an easy way to execute shell commands and handle the output via delegates with both synchronous and asynchronous support.

Topics

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

ktsu.RunCommand

A .NET library for executing external commands and handling their output through delegates, with synchronous and asynchronous APIs, cancellation, and control over the spawned process.

LicenseNuGet VersionNuGet VersionNuGet DownloadsGitHub commit activityGitHub contributorsGitHub Actions Workflow Status

Introduction

ktsu.RunCommand runs an external command and hands you its output as it arrives, instead of making you assemble Process, ProcessStartInfo, redirected streams and exit-code plumbing yourself. Output is delivered through delegates — either as raw chunks exactly as the process emits them, or buffered into complete lines — and every method returns the process exit code.

Arguments are passed as a vector rather than as one string, so a path containing spaces needs no manual quoting and cannot be mis-split. The process itself can be shaped through a working directory and an environment variable overlay, run elevated on Windows, and terminated along with its children through a cancellation token.

Features

  • Delegate-based output: Receive standard output and standard error through Action<string> delegates as the process produces them, rather than waiting for it to exit.
  • Raw or line-buffered: OutputHandler delivers undelimited chunks exactly as they arrive; LineOutputHandler buffers across chunks and raises one call per complete line.
  • Synchronous and asynchronous: Every operation is available as both Execute and ExecuteAsync, with the asynchronous implementation as the single source of truth.
  • Quote-free arguments: Pass the executable and each argument separately, so spaces in paths and arguments are handled by the platform rather than by string concatenation.
  • Working directory: Start the process in a specific directory without mutating the process-global current directory.
  • Environment variables: Apply an overlay over the inherited environment for a single call, adding, overriding, or removing individual variables.
  • Cancellation: A signalled CancellationToken terminates the process and always surfaces as an OperationCanceledException, never as a synthetic exit code.
  • Windows elevation: Launch through the runas verb for a UAC-elevated process.
  • Custom encoding: Decode the output streams with any Encoding; defaults to UTF-8.
  • Broad target support: .NET Standard 2.0 and 2.1 through .NET 10.

Installation

Package Manager Console

Install-Package ktsu.RunCommand

.NET CLI

dotnet add package ktsu.RunCommand

Package Reference

<PackageReferenceInclude="ktsu.RunCommand"Version="1.5.0" />

Usage Examples

Basic Example

Pass the executable and its arguments separately. All methods return the process exit code:

usingktsu.RunCommand;classProgram{staticvoidMain(){intexitCode=RunCommand.Execute("dotnet",["--version"]);if(exitCode==0){Console.WriteLine("Command executed successfully!");}else{Console.WriteLine($"Command failed with exit code: {exitCode}");}}}

Custom Output Handling

To handle the output of the command, provide delegates to the OutputHandler class:

usingktsu.RunCommand;classProgram{staticvoidMain(){intexitCode=RunCommand.Execute(fileName:"dotnet",arguments:["--version"],outputHandler:new(onStandardOutput:Console.Write,onStandardError:Console.Write));Console.WriteLine($"Process exited with code: {exitCode}");}}

NOTE:When using the default OutputHandler, the delegates receive undelimited chunks of output. This gives you exactly what the command produces, including whitespace and non-printable characters, to handle as you see fit.

Line-by-Line Output Handling

To handle the output one line at a time, use the LineOutputHandler class:

usingktsu.RunCommand;classProgram{staticvoidMain(){intexitCode=RunCommand.Execute(fileName:"dotnet",arguments:["--version"],outputHandler:newLineOutputHandler(onStandardOutput: line =>Console.WriteLine($"Output: {line}"),onStandardError: line =>Console.WriteLine($"Error: {line}")));Console.WriteLine($"Process exited with code: {exitCode}");}}

Asynchronous Execution

All of the above examples can be run asynchronously with ExecuteAsync:

usingktsu.RunCommand;classProgram{staticasyncTaskMain(){intexitCode=awaitRunCommand.ExecuteAsync("dotnet",["--version"]);Console.WriteLine($"Process exited with code: {exitCode}");}}

Cancellation

Passing a CancellationToken terminates the process when the token is signalled:

usingktsu.RunCommand;classProgram{staticasyncTaskMain(){usingCancellationTokenSourcecancellation=new(TimeSpan.FromSeconds(30));try{intexitCode=awaitRunCommand.ExecuteAsync(fileName:"dotnet",arguments:["build"],outputHandler:newLineOutputHandler(onStandardOutput:Console.WriteLine),cancellationToken:cancellation.Token);Console.WriteLine($"Process exited with code: {exitCode}");}catch(OperationCanceledException){Console.WriteLine("The command was cancelled.");}}}

A cancelled call always throws OperationCanceledException — it never returns the killed process's exit code — so cancellation cannot be mistaken for a genuine failure of the command.

On .NET Core 3.0 and later the entire process tree is terminated. On .NET Standard 2.0 and 2.1 only the process itself can be terminated, so any grandchildren it spawned are left running.

Process Options

CommandOptions shapes the process a command runs in. Pass it alongside an executable and its arguments:

usingktsu.RunCommand;usingktsu.Semantics.Paths;classProgram{staticasyncTaskMain(){intexitCode=awaitRunCommand.ExecuteAsync(fileName:"git",arguments:["status","--short"],outputHandler:newLineOutputHandler(onStandardOutput:Console.WriteLine),options:new(){WorkingDirectory=AbsoluteDirectoryPath.Create(@"C:\repos\my project"),EnvironmentVariables=newDictionary<string,string?>{["GIT_TERMINAL_PROMPT"]="0",["LC_ALL"]="C",},});Console.WriteLine($"Process exited with code: {exitCode}");}}

CommandOptions.Elevation carries the privilege level too, so a single options object replaces the separate Elevation argument.

Working Directory

Without a WorkingDirectory the process inherits the current directory of the calling process, which is what commands did before this option existed.

The type is AbsoluteDirectoryPath rather than a string on purpose. A relative directory would have to be resolved against the caller's current directory — the process-global state this option exists to avoid depending on, since it is shared by every thread and races with concurrent calls.

Environment Variables

EnvironmentVariables is an overlay on the inherited environment, not a replacement: a name you do not list keeps whatever the calling process had. A null value removes a variable, which is how you unset something the parent had set:

EnvironmentVariables=newDictionary<string,string?>{["GIT_DIR"]=null,}

Environment variables are the only control surface some tools expose, so this covers behaviour with no command-line equivalent — GIT_TERMINAL_PROMPT=0 to make an authenticating git fetch fail rather than block forever on a prompt no terminal will answer, GIT_ASKPASS/SSH_ASKPASS to supply credentials without putting them on a command line where any process listing can read them, and LC_ALL=C to force stable, machine-parseable output rather than whatever the host locale produces.

NOTE:EnvironmentVariables cannot be combined with Elevation.Elevated on Windows. Elevation requires UseShellExecute, which offers nowhere to pass an environment, so the call throws ArgumentException rather than silently dropping the variables.

Elevation (Windows)

To run a command with elevated privileges, set Elevation.Elevated. On Windows this launches the process with the runas verb, which triggers a UAC prompt:

usingktsu.RunCommand;classProgram{staticvoidMain(){intexitCode=RunCommand.Execute(fileName:"powershell",arguments:["-Command","Get-Service"],outputHandler:new(),options:new(){Elevation=Elevation.Elevated});Console.WriteLine($"Process exited with code: {exitCode}");}}

NOTE:Output redirection is incompatible with runas, so an OutputHandler passed alongside Elevation.Elevated will not be invoked. You still get the process exit code.

On non-Windows platforms Elevation.Elevated is a no-op — prefix your command with sudo yourself if you need elevation there.

Encoding

By default the library decodes the output streams as UTF-8. To use a different encoding, specify it in the OutputHandler or LineOutputHandler constructor:

usingSystem.Text;usingktsu.RunCommand;classProgram{staticvoidMain(){intexitCode=RunCommand.Execute(fileName:"dotnet",arguments:["--version"],outputHandler:new(onStandardOutput:Console.Write,onStandardError:Console.Write,encoding:Encoding.ASCII));}}

Deprecated: Single Command Strings

The overloads taking one command string are obsolete. They separate the executable from its arguments by splitting on the first space, which cannot represent an executable path that itself contains a space — on Windows that includes anything under C:\Program Files\:

// Obsolete, and broken: splits into "C:\Program" plus "Files\Git\bin\git.exe --version"awaitRunCommand.ExecuteAsync(@"C:\Program Files\Git\bin\git.exe --version");// CorrectawaitRunCommand.ExecuteAsync(@"C:\Program Files\Git\bin\git.exe",["--version"]);

Quoting does not rescue it, because the split happens before any quote handling. The string form is inherently ambiguous — no parse handles every combination of spaces and quotes without adopting a shell's full grammar — so rather than grow a half-grammar that moves the surprise elsewhere, these overloads are deprecated in favour of the argument-list ones, which have no such ambiguity because the executable is passed separately.

Migration is mechanical: split the string yourself at the boundaries you meant.

ObsoleteReplacement
Execute(command)Execute(fileName, arguments)
Execute(command, outputHandler)Execute(fileName, arguments, outputHandler)
Execute(command, elevation)Execute(fileName, arguments, outputHandler, options)
ExecuteAsync(command)ExecuteAsync(fileName, arguments)
ExecuteAsync(command, outputHandler)ExecuteAsync(fileName, arguments, outputHandler)
ExecuteAsync(command, cancellationToken)ExecuteAsync(fileName, arguments, outputHandler, cancellationToken)
ExecuteAsync(command, outputHandler, elevation, cancellationToken)ExecuteAsync(fileName, arguments, outputHandler, options, cancellationToken)

API Reference

RunCommand

Static class providing the command execution API. Every method returns the process exit code.

Methods

NameReturn TypeDescription
Execute(string fileName, IEnumerable<string> arguments)intExecutes a command synchronously.
Execute(string fileName, IEnumerable<string> arguments, OutputHandler outputHandler)intExecutes a command synchronously with custom output handling.
Execute(string fileName, IEnumerable<string> arguments, OutputHandler outputHandler, CommandOptions options)intExecutes a command synchronously with the given process options.
ExecuteAsync(string fileName, IEnumerable<string> arguments)Task<int>Executes a command asynchronously.
ExecuteAsync(string fileName, IEnumerable<string> arguments, OutputHandler outputHandler)Task<int>Executes a command asynchronously with custom output handling.
ExecuteAsync(string fileName, IEnumerable<string> arguments, OutputHandler outputHandler, CancellationToken cancellationToken)Task<int>As above, terminating the process and its children if the token is signalled.
ExecuteAsync(string fileName, IEnumerable<string> arguments, OutputHandler outputHandler, Elevation elevation, CancellationToken cancellationToken)Task<int>As above, at the given elevation level.
ExecuteAsync(string fileName, IEnumerable<string> arguments, OutputHandler outputHandler, CommandOptions options)Task<int>Executes a command asynchronously with the given process options.
ExecuteAsync(string fileName, IEnumerable<string> arguments, OutputHandler outputHandler, CommandOptions options, CancellationToken cancellationToken)Task<int>As above, terminating the process and its children if the token is signalled.

The overloads taking a single command string — four Execute and seven ExecuteAsync — are obsolete. See Deprecated: Single Command Strings for the migration table.

CommandOptions

Record describing how to shape the process a command runs in. Every member defaults to the behaviour commands had before the type existed, so an instance with nothing set is equivalent to not passing one at all.

Properties

NameTypeDescription
WorkingDirectoryAbsoluteDirectoryPath?The directory the process starts in, or null to inherit the caller's current directory.
EnvironmentVariablesIReadOnlyDictionary<string, string?>?Variables applied over the inherited environment, or null to inherit it unchanged. A null value removes a variable.
ElevationElevationThe privilege level under which to run the command. Defaults to Elevation.Default.

OutputHandler

Processes output in raw, undelimited chunks as they arrive from the process.

Constructor

NameDescription
OutputHandler(Action<string>? onStandardOutput = null, Action<string>? onStandardError = null, Encoding? encoding = null)Creates a handler with delegates for the output and error streams. encoding defaults to UTF-8.

Properties

NameTypeDescription
EncodingEncodingThe encoding used to decode the process's output streams.

LineOutputHandler

Inherits from OutputHandler and buffers incoming chunks, invoking the delegates once per complete line. Incomplete trailing data is held until the rest of the line arrives.

Constructor

NameDescription
LineOutputHandler(Action<string>? onStandardOutput = null, Action<string>? onStandardError = null, Encoding? encoding = null)Creates a line-buffering handler with delegates for the output and error streams.

Elevation

Enum specifying the privilege level under which a command runs.

NameDescription
DefaultRun with the current process's privileges. Standard output and standard error are captured.
ElevatedOn Windows, launch through the runas verb, prompting for UAC consent; output is not captured. No effect on non-Windows platforms.

Contributing

Contributions are welcome! Feel free to open issues or submit pull requests.

License

This project is licensed under the MIT License. See the LICENSE.md file for details.

About

A library that provides an easy way to execute shell commands and handle the output via delegates with both synchronous and asynchronous support.

Topics

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

ktsu.RunCommand

A .NET library for executing external commands and handling their output through delegates, with synchronous and asynchronous APIs, cancellation, and control over the spawned process.

LicenseNuGet VersionNuGet VersionNuGet DownloadsGitHub commit activityGitHub contributorsGitHub Actions Workflow Status

Introduction

ktsu.RunCommand runs an external command and hands you its output as it arrives, instead of making you assemble Process, ProcessStartInfo, redirected streams and exit-code plumbing yourself. Output is delivered through delegates — either as raw chunks exactly as the process emits them, or buffered into complete lines — and every method returns the process exit code.

Arguments are passed as a vector rather than as one string, so a path containing spaces needs no manual quoting and cannot be mis-split. The process itself can be shaped through a working directory and an environment variable overlay, run elevated on Windows, and terminated along with its children through a cancellation token.

Features

  • Delegate-based output: Receive standard output and standard error through Action<string> delegates as the process produces them, rather than waiting for it to exit.
  • Raw or line-buffered: OutputHandler delivers undelimited chunks exactly as they arrive; LineOutputHandler buffers across chunks and raises one call per complete line.
  • Synchronous and asynchronous: Every operation is available as both Execute and ExecuteAsync, with the asynchronous implementation as the single source of truth.
  • Quote-free arguments: Pass the executable and each argument separately, so spaces in paths and arguments are handled by the platform rather than by string concatenation.
  • Working directory: Start the process in a specific directory without mutating the process-global current directory.
  • Environment variables: Apply an overlay over the inherited environment for a single call, adding, overriding, or removing individual variables.
  • Cancellation: A signalled CancellationToken terminates the process and always surfaces as an OperationCanceledException, never as a synthetic exit code.
  • Windows elevation: Launch through the runas verb for a UAC-elevated process.
  • Custom encoding: Decode the output streams with any Encoding; defaults to UTF-8.
  • Broad target support: .NET Standard 2.0 and 2.1 through .NET 10.

Installation

Package Manager Console

Install-Package ktsu.RunCommand

.NET CLI

dotnet add package ktsu.RunCommand

Package Reference

<PackageReferenceInclude="ktsu.RunCommand"Version="1.5.0" />

Usage Examples

Basic Example

Pass the executable and its arguments separately. All methods return the process exit code:

usingktsu.RunCommand;classProgram{staticvoidMain(){intexitCode=RunCommand.Execute("dotnet",["--version"]);if(exitCode==0){Console.WriteLine("Command executed successfully!");}else{Console.WriteLine($"Command failed with exit code: {exitCode}");}}}

Custom Output Handling

To handle the output of the command, provide delegates to the OutputHandler class:

usingktsu.RunCommand;classProgram{staticvoidMain(){intexitCode=RunCommand.Execute(fileName:"dotnet",arguments:["--version"],outputHandler:new(onStandardOutput:Console.Write,onStandardError:Console.Write));Console.WriteLine($"Process exited with code: {exitCode}");}}

NOTE:When using the default OutputHandler, the delegates receive undelimited chunks of output. This gives you exactly what the command produces, including whitespace and non-printable characters, to handle as you see fit.

Line-by-Line Output Handling

To handle the output one line at a time, use the LineOutputHandler class:

usingktsu.RunCommand;classProgram{staticvoidMain(){intexitCode=RunCommand.Execute(fileName:"dotnet",arguments:["--version"],outputHandler:newLineOutputHandler(onStandardOutput: line =>Console.WriteLine($"Output: {line}"),onStandardError: line =>Console.WriteLine($"Error: {line}")));Console.WriteLine($"Process exited with code: {exitCode}");}}

Asynchronous Execution

All of the above examples can be run asynchronously with ExecuteAsync:

usingktsu.RunCommand;classProgram{staticasyncTaskMain(){intexitCode=awaitRunCommand.ExecuteAsync("dotnet",["--version"]);Console.WriteLine($"Process exited with code: {exitCode}");}}

Cancellation

Passing a CancellationToken terminates the process when the token is signalled:

usingktsu.RunCommand;classProgram{staticasyncTaskMain(){usingCancellationTokenSourcecancellation=new(TimeSpan.FromSeconds(30));try{intexitCode=awaitRunCommand.ExecuteAsync(fileName:"dotnet",arguments:["build"],outputHandler:newLineOutputHandler(onStandardOutput:Console.WriteLine),cancellationToken:cancellation.Token);Console.WriteLine($"Process exited with code: {exitCode}");}catch(OperationCanceledException){Console.WriteLine("The command was cancelled.");}}}

A cancelled call always throws OperationCanceledException — it never returns the killed process's exit code — so cancellation cannot be mistaken for a genuine failure of the command.

On .NET Core 3.0 and later the entire process tree is terminated. On .NET Standard 2.0 and 2.1 only the process itself can be terminated, so any grandchildren it spawned are left running.

Process Options

CommandOptions shapes the process a command runs in. Pass it alongside an executable and its arguments:

usingktsu.RunCommand;usingktsu.Semantics.Paths;classProgram{staticasyncTaskMain(){intexitCode=awaitRunCommand.ExecuteAsync(fileName:"git",arguments:["status","--short"],outputHandler:newLineOutputHandler(onStandardOutput:Console.WriteLine),options:new(){WorkingDirectory=AbsoluteDirectoryPath.Create(@"C:\repos\my project"),EnvironmentVariables=newDictionary<string,string?>{["GIT_TERMINAL_PROMPT"]="0",["LC_ALL"]="C",},});Console.WriteLine($"Process exited with code: {exitCode}");}}

CommandOptions.Elevation carries the privilege level too, so a single options object replaces the separate Elevation argument.

Working Directory

Without a WorkingDirectory the process inherits the current directory of the calling process, which is what commands did before this option existed.

The type is AbsoluteDirectoryPath rather than a string on purpose. A relative directory would have to be resolved against the caller's current directory — the process-global state this option exists to avoid depending on, since it is shared by every thread and races with concurrent calls.

Environment Variables

EnvironmentVariables is an overlay on the inherited environment, not a replacement: a name you do not list keeps whatever the calling process had. A null value removes a variable, which is how you unset something the parent had set:

EnvironmentVariables=newDictionary<string,string?>{["GIT_DIR"]=null,}

Environment variables are the only control surface some tools expose, so this covers behaviour with no command-line equivalent — GIT_TERMINAL_PROMPT=0 to make an authenticating git fetch fail rather than block forever on a prompt no terminal will answer, GIT_ASKPASS/SSH_ASKPASS to supply credentials without putting them on a command line where any process listing can read them, and LC_ALL=C to force stable, machine-parseable output rather than whatever the host locale produces.

NOTE:EnvironmentVariables cannot be combined with Elevation.Elevated on Windows. Elevation requires UseShellExecute, which offers nowhere to pass an environment, so the call throws ArgumentException rather than silently dropping the variables.

Elevation (Windows)

To run a command with elevated privileges, set Elevation.Elevated. On Windows this launches the process with the runas verb, which triggers a UAC prompt:

usingktsu.RunCommand;classProgram{staticvoidMain(){intexitCode=RunCommand.Execute(fileName:"powershell",arguments:["-Command","Get-Service"],outputHandler:new(),options:new(){Elevation=Elevation.Elevated});Console.WriteLine($"Process exited with code: {exitCode}");}}

NOTE:Output redirection is incompatible with runas, so an OutputHandler passed alongside Elevation.Elevated will not be invoked. You still get the process exit code.

On non-Windows platforms Elevation.Elevated is a no-op — prefix your command with sudo yourself if you need elevation there.

Encoding

By default the library decodes the output streams as UTF-8. To use a different encoding, specify it in the OutputHandler or LineOutputHandler constructor:

usingSystem.Text;usingktsu.RunCommand;classProgram{staticvoidMain(){intexitCode=RunCommand.Execute(fileName:"dotnet",arguments:["--version"],outputHandler:new(onStandardOutput:Console.Write,onStandardError:Console.Write,encoding:Encoding.ASCII));}}

Deprecated: Single Command Strings

The overloads taking one command string are obsolete. They separate the executable from its arguments by splitting on the first space, which cannot represent an executable path that itself contains a space — on Windows that includes anything under C:\Program Files\:

// Obsolete, and broken: splits into "C:\Program" plus "Files\Git\bin\git.exe --version"awaitRunCommand.ExecuteAsync(@"C:\Program Files\Git\bin\git.exe --version");// CorrectawaitRunCommand.ExecuteAsync(@"C:\Program Files\Git\bin\git.exe",["--version"]);

Quoting does not rescue it, because the split happens before any quote handling. The string form is inherently ambiguous — no parse handles every combination of spaces and quotes without adopting a shell's full grammar — so rather than grow a half-grammar that moves the surprise elsewhere, these overloads are deprecated in favour of the argument-list ones, which have no such ambiguity because the executable is passed separately.

Migration is mechanical: split the string yourself at the boundaries you meant.

ObsoleteReplacement
Execute(command)Execute(fileName, arguments)
Execute(command, outputHandler)Execute(fileName, arguments, outputHandler)
Execute(command, elevation)Execute(fileName, arguments, outputHandler, options)
ExecuteAsync(command)ExecuteAsync(fileName, arguments)
ExecuteAsync(command, outputHandler)ExecuteAsync(fileName, arguments, outputHandler)
ExecuteAsync(command, cancellationToken)ExecuteAsync(fileName, arguments, outputHandler, cancellationToken)
ExecuteAsync(command, outputHandler, elevation, cancellationToken)ExecuteAsync(fileName, arguments, outputHandler, options, cancellationToken)

API Reference

RunCommand

Static class providing the command execution API. Every method returns the process exit code.

Methods

NameReturn TypeDescription
Execute(string fileName, IEnumerable<string> arguments)intExecutes a command synchronously.
Execute(string fileName, IEnumerable<string> arguments, OutputHandler outputHandler)intExecutes a command synchronously with custom output handling.
Execute(string fileName, IEnumerable<string> arguments, OutputHandler outputHandler, CommandOptions options)intExecutes a command synchronously with the given process options.
ExecuteAsync(string fileName, IEnumerable<string> arguments)Task<int>Executes a command asynchronously.
ExecuteAsync(string fileName, IEnumerable<string> arguments, OutputHandler outputHandler)Task<int>Executes a command asynchronously with custom output handling.
ExecuteAsync(string fileName, IEnumerable<string> arguments, OutputHandler outputHandler, CancellationToken cancellationToken)Task<int>As above, terminating the process and its children if the token is signalled.
ExecuteAsync(string fileName, IEnumerable<string> arguments, OutputHandler outputHandler, Elevation elevation, CancellationToken cancellationToken)Task<int>As above, at the given elevation level.
ExecuteAsync(string fileName, IEnumerable<string> arguments, OutputHandler outputHandler, CommandOptions options)Task<int>Executes a command asynchronously with the given process options.
ExecuteAsync(string fileName, IEnumerable<string> arguments, OutputHandler outputHandler, CommandOptions options, CancellationToken cancellationToken)Task<int>As above, terminating the process and its children if the token is signalled.

The overloads taking a single command string — four Execute and seven ExecuteAsync — are obsolete. See Deprecated: Single Command Strings for the migration table.

CommandOptions

Record describing how to shape the process a command runs in. Every member defaults to the behaviour commands had before the type existed, so an instance with nothing set is equivalent to not passing one at all.

Properties

NameTypeDescription
WorkingDirectoryAbsoluteDirectoryPath?The directory the process starts in, or null to inherit the caller's current directory.
EnvironmentVariablesIReadOnlyDictionary<string, string?>?Variables applied over the inherited environment, or null to inherit it unchanged. A null value removes a variable.
ElevationElevationThe privilege level under which to run the command. Defaults to Elevation.Default.

OutputHandler

Processes output in raw, undelimited chunks as they arrive from the process.

Constructor

NameDescription
OutputHandler(Action<string>? onStandardOutput = null, Action<string>? onStandardError = null, Encoding? encoding = null)Creates a handler with delegates for the output and error streams. encoding defaults to UTF-8.

Properties

NameTypeDescription
EncodingEncodingThe encoding used to decode the process's output streams.

LineOutputHandler

Inherits from OutputHandler and buffers incoming chunks, invoking the delegates once per complete line. Incomplete trailing data is held until the rest of the line arrives.

Constructor

NameDescription
LineOutputHandler(Action<string>? onStandardOutput = null, Action<string>? onStandardError = null, Encoding? encoding = null)Creates a line-buffering handler with delegates for the output and error streams.

Elevation

Enum specifying the privilege level under which a command runs.

NameDescription
DefaultRun with the current process's privileges. Standard output and standard error are captured.
ElevatedOn Windows, launch through the runas verb, prompting for UAC consent; output is not captured. No effect on non-Windows platforms.

Contributing

Contributions are welcome! Feel free to open issues or submit pull requests.

License

This project is licensed under the MIT License. See the LICENSE.md file for details.

About

A library that provides an easy way to execute shell commands and handle the output via delegates with both synchronous and asynchronous support.

Topics

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

ktsu.RunCommand

A .NET library for executing external commands and handling their output through delegates, with synchronous and asynchronous APIs, cancellation, and control over the spawned process.

LicenseNuGet VersionNuGet VersionNuGet DownloadsGitHub commit activityGitHub contributorsGitHub Actions Workflow Status

Introduction

ktsu.RunCommand runs an external command and hands you its output as it arrives, instead of making you assemble Process, ProcessStartInfo, redirected streams and exit-code plumbing yourself. Output is delivered through delegates — either as raw chunks exactly as the process emits them, or buffered into complete lines — and every method returns the process exit code.

Arguments are passed as a vector rather than as one string, so a path containing spaces needs no manual quoting and cannot be mis-split. The process itself can be shaped through a working directory and an environment variable overlay, run elevated on Windows, and terminated along with its children through a cancellation token.

Features

  • Delegate-based output: Receive standard output and standard error through Action<string> delegates as the process produces them, rather than waiting for it to exit.
  • Raw or line-buffered: OutputHandler delivers undelimited chunks exactly as they arrive; LineOutputHandler buffers across chunks and raises one call per complete line.
  • Synchronous and asynchronous: Every operation is available as both Execute and ExecuteAsync, with the asynchronous implementation as the single source of truth.
  • Quote-free arguments: Pass the executable and each argument separately, so spaces in paths and arguments are handled by the platform rather than by string concatenation.
  • Working directory: Start the process in a specific directory without mutating the process-global current directory.
  • Environment variables: Apply an overlay over the inherited environment for a single call, adding, overriding, or removing individual variables.
  • Cancellation: A signalled CancellationToken terminates the process and always surfaces as an OperationCanceledException, never as a synthetic exit code.
  • Windows elevation: Launch through the runas verb for a UAC-elevated process.
  • Custom encoding: Decode the output streams with any Encoding; defaults to UTF-8.
  • Broad target support: .NET Standard 2.0 and 2.1 through .NET 10.

Installation

Package Manager Console

Install-Package ktsu.RunCommand

.NET CLI

dotnet add package ktsu.RunCommand

Package Reference

<PackageReferenceInclude="ktsu.RunCommand"Version="1.5.0" />

Usage Examples

Basic Example

Pass the executable and its arguments separately. All methods return the process exit code:

usingktsu.RunCommand;classProgram{staticvoidMain(){intexitCode=RunCommand.Execute("dotnet",["--version"]);if(exitCode==0){Console.WriteLine("Command executed successfully!");}else{Console.WriteLine($"Command failed with exit code: {exitCode}");}}}

Custom Output Handling

To handle the output of the command, provide delegates to the OutputHandler class:

usingktsu.RunCommand;classProgram{staticvoidMain(){intexitCode=RunCommand.Execute(fileName:"dotnet",arguments:["--version"],outputHandler:new(onStandardOutput:Console.Write,onStandardError:Console.Write));Console.WriteLine($"Process exited with code: {exitCode}");}}

NOTE:When using the default OutputHandler, the delegates receive undelimited chunks of output. This gives you exactly what the command produces, including whitespace and non-printable characters, to handle as you see fit.

Line-by-Line Output Handling

To handle the output one line at a time, use the LineOutputHandler class:

usingktsu.RunCommand;classProgram{staticvoidMain(){intexitCode=RunCommand.Execute(fileName:"dotnet",arguments:["--version"],outputHandler:newLineOutputHandler(onStandardOutput: line =>Console.WriteLine($"Output: {line}"),onStandardError: line =>Console.WriteLine($"Error: {line}")));Console.WriteLine($"Process exited with code: {exitCode}");}}

Asynchronous Execution

All of the above examples can be run asynchronously with ExecuteAsync:

usingktsu.RunCommand;classProgram{staticasyncTaskMain(){intexitCode=awaitRunCommand.ExecuteAsync("dotnet",["--version"]);Console.WriteLine($"Process exited with code: {exitCode}");}}

Cancellation

Passing a CancellationToken terminates the process when the token is signalled:

usingktsu.RunCommand;classProgram{staticasyncTaskMain(){usingCancellationTokenSourcecancellation=new(TimeSpan.FromSeconds(30));try{intexitCode=awaitRunCommand.ExecuteAsync(fileName:"dotnet",arguments:["build"],outputHandler:newLineOutputHandler(onStandardOutput:Console.WriteLine),cancellationToken:cancellation.Token);Console.WriteLine($"Process exited with code: {exitCode}");}catch(OperationCanceledException){Console.WriteLine("The command was cancelled.");}}}

A cancelled call always throws OperationCanceledException — it never returns the killed process's exit code — so cancellation cannot be mistaken for a genuine failure of the command.

On .NET Core 3.0 and later the entire process tree is terminated. On .NET Standard 2.0 and 2.1 only the process itself can be terminated, so any grandchildren it spawned are left running.

Process Options

CommandOptions shapes the process a command runs in. Pass it alongside an executable and its arguments:

usingktsu.RunCommand;usingktsu.Semantics.Paths;classProgram{staticasyncTaskMain(){intexitCode=awaitRunCommand.ExecuteAsync(fileName:"git",arguments:["status","--short"],outputHandler:newLineOutputHandler(onStandardOutput:Console.WriteLine),options:new(){WorkingDirectory=AbsoluteDirectoryPath.Create(@"C:\repos\my project"),EnvironmentVariables=newDictionary<string,string?>{["GIT_TERMINAL_PROMPT"]="0",["LC_ALL"]="C",},});Console.WriteLine($"Process exited with code: {exitCode}");}}

CommandOptions.Elevation carries the privilege level too, so a single options object replaces the separate Elevation argument.

Working Directory

Without a WorkingDirectory the process inherits the current directory of the calling process, which is what commands did before this option existed.

The type is AbsoluteDirectoryPath rather than a string on purpose. A relative directory would have to be resolved against the caller's current directory — the process-global state this option exists to avoid depending on, since it is shared by every thread and races with concurrent calls.

Environment Variables

EnvironmentVariables is an overlay on the inherited environment, not a replacement: a name you do not list keeps whatever the calling process had. A null value removes a variable, which is how you unset something the parent had set:

EnvironmentVariables=newDictionary<string,string?>{["GIT_DIR"]=null,}

Environment variables are the only control surface some tools expose, so this covers behaviour with no command-line equivalent — GIT_TERMINAL_PROMPT=0 to make an authenticating git fetch fail rather than block forever on a prompt no terminal will answer, GIT_ASKPASS/SSH_ASKPASS to supply credentials without putting them on a command line where any process listing can read them, and LC_ALL=C to force stable, machine-parseable output rather than whatever the host locale produces.

NOTE:EnvironmentVariables cannot be combined with Elevation.Elevated on Windows. Elevation requires UseShellExecute, which offers nowhere to pass an environment, so the call throws ArgumentException rather than silently dropping the variables.

Elevation (Windows)

To run a command with elevated privileges, set Elevation.Elevated. On Windows this launches the process with the runas verb, which triggers a UAC prompt:

usingktsu.RunCommand;classProgram{staticvoidMain(){intexitCode=RunCommand.Execute(fileName:"powershell",arguments:["-Command","Get-Service"],outputHandler:new(),options:new(){Elevation=Elevation.Elevated});Console.WriteLine($"Process exited with code: {exitCode}");}}

NOTE:Output redirection is incompatible with runas, so an OutputHandler passed alongside Elevation.Elevated will not be invoked. You still get the process exit code.

On non-Windows platforms Elevation.Elevated is a no-op — prefix your command with sudo yourself if you need elevation there.

Encoding

By default the library decodes the output streams as UTF-8. To use a different encoding, specify it in the OutputHandler or LineOutputHandler constructor:

usingSystem.Text;usingktsu.RunCommand;classProgram{staticvoidMain(){intexitCode=RunCommand.Execute(fileName:"dotnet",arguments:["--version"],outputHandler:new(onStandardOutput:Console.Write,onStandardError:Console.Write,encoding:Encoding.ASCII));}}

Deprecated: Single Command Strings

The overloads taking one command string are obsolete. They separate the executable from its arguments by splitting on the first space, which cannot represent an executable path that itself contains a space — on Windows that includes anything under C:\Program Files\:

// Obsolete, and broken: splits into "C:\Program" plus "Files\Git\bin\git.exe --version"awaitRunCommand.ExecuteAsync(@"C:\Program Files\Git\bin\git.exe --version");// CorrectawaitRunCommand.ExecuteAsync(@"C:\Program Files\Git\bin\git.exe",["--version"]);

Quoting does not rescue it, because the split happens before any quote handling. The string form is inherently ambiguous — no parse handles every combination of spaces and quotes without adopting a shell's full grammar — so rather than grow a half-grammar that moves the surprise elsewhere, these overloads are deprecated in favour of the argument-list ones, which have no such ambiguity because the executable is passed separately.

Migration is mechanical: split the string yourself at the boundaries you meant.

ObsoleteReplacement
Execute(command)Execute(fileName, arguments)
Execute(command, outputHandler)Execute(fileName, arguments, outputHandler)
Execute(command, elevation)Execute(fileName, arguments, outputHandler, options)
ExecuteAsync(command)ExecuteAsync(fileName, arguments)
ExecuteAsync(command, outputHandler)ExecuteAsync(fileName, arguments, outputHandler)
ExecuteAsync(command, cancellationToken)ExecuteAsync(fileName, arguments, outputHandler, cancellationToken)
ExecuteAsync(command, outputHandler, elevation, cancellationToken)ExecuteAsync(fileName, arguments, outputHandler, options, cancellationToken)

API Reference

RunCommand

Static class providing the command execution API. Every method returns the process exit code.

Methods

NameReturn TypeDescription
Execute(string fileName, IEnumerable<string> arguments)intExecutes a command synchronously.
Execute(string fileName, IEnumerable<string> arguments, OutputHandler outputHandler)intExecutes a command synchronously with custom output handling.
Execute(string fileName, IEnumerable<string> arguments, OutputHandler outputHandler, CommandOptions options)intExecutes a command synchronously with the given process options.
ExecuteAsync(string fileName, IEnumerable<string> arguments)Task<int>Executes a command asynchronously.
ExecuteAsync(string fileName, IEnumerable<string> arguments, OutputHandler outputHandler)Task<int>Executes a command asynchronously with custom output handling.
ExecuteAsync(string fileName, IEnumerable<string> arguments, OutputHandler outputHandler, CancellationToken cancellationToken)Task<int>As above, terminating the process and its children if the token is signalled.
ExecuteAsync(string fileName, IEnumerable<string> arguments, OutputHandler outputHandler, Elevation elevation, CancellationToken cancellationToken)Task<int>As above, at the given elevation level.
ExecuteAsync(string fileName, IEnumerable<string> arguments, OutputHandler outputHandler, CommandOptions options)Task<int>Executes a command asynchronously with the given process options.
ExecuteAsync(string fileName, IEnumerable<string> arguments, OutputHandler outputHandler, CommandOptions options, CancellationToken cancellationToken)Task<int>As above, terminating the process and its children if the token is signalled.

The overloads taking a single command string — four Execute and seven ExecuteAsync — are obsolete. See Deprecated: Single Command Strings for the migration table.

CommandOptions

Record describing how to shape the process a command runs in. Every member defaults to the behaviour commands had before the type existed, so an instance with nothing set is equivalent to not passing one at all.

Properties

NameTypeDescription
WorkingDirectoryAbsoluteDirectoryPath?The directory the process starts in, or null to inherit the caller's current directory.
EnvironmentVariablesIReadOnlyDictionary<string, string?>?Variables applied over the inherited environment, or null to inherit it unchanged. A null value removes a variable.
ElevationElevationThe privilege level under which to run the command. Defaults to Elevation.Default.

OutputHandler

Processes output in raw, undelimited chunks as they arrive from the process.

Constructor

NameDescription
OutputHandler(Action<string>? onStandardOutput = null, Action<string>? onStandardError = null, Encoding? encoding = null)Creates a handler with delegates for the output and error streams. encoding defaults to UTF-8.

Properties

NameTypeDescription
EncodingEncodingThe encoding used to decode the process's output streams.

LineOutputHandler

Inherits from OutputHandler and buffers incoming chunks, invoking the delegates once per complete line. Incomplete trailing data is held until the rest of the line arrives.

Constructor

NameDescription
LineOutputHandler(Action<string>? onStandardOutput = null, Action<string>? onStandardError = null, Encoding? encoding = null)Creates a line-buffering handler with delegates for the output and error streams.

Elevation

Enum specifying the privilege level under which a command runs.

NameDescription
DefaultRun with the current process's privileges. Standard output and standard error are captured.
ElevatedOn Windows, launch through the runas verb, prompting for UAC consent; output is not captured. No effect on non-Windows platforms.

Contributing

Contributions are welcome! Feel free to open issues or submit pull requests.

License

This project is licensed under the MIT License. See the LICENSE.md file for details.

About

A library that provides an easy way to execute shell commands and handle the output via delegates with both synchronous and asynchronous support.

Topics

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

ktsu.RunCommand

A .NET library for executing external commands and handling their output through delegates, with synchronous and asynchronous APIs, cancellation, and control over the spawned process.

LicenseNuGet VersionNuGet VersionNuGet DownloadsGitHub commit activityGitHub contributorsGitHub Actions Workflow Status

Introduction

ktsu.RunCommand runs an external command and hands you its output as it arrives, instead of making you assemble Process, ProcessStartInfo, redirected streams and exit-code plumbing yourself. Output is delivered through delegates — either as raw chunks exactly as the process emits them, or buffered into complete lines — and every method returns the process exit code.

Arguments are passed as a vector rather than as one string, so a path containing spaces needs no manual quoting and cannot be mis-split. The process itself can be shaped through a working directory and an environment variable overlay, run elevated on Windows, and terminated along with its children through a cancellation token.

Features

  • Delegate-based output: Receive standard output and standard error through Action<string> delegates as the process produces them, rather than waiting for it to exit.
  • Raw or line-buffered: OutputHandler delivers undelimited chunks exactly as they arrive; LineOutputHandler buffers across chunks and raises one call per complete line.
  • Synchronous and asynchronous: Every operation is available as both Execute and ExecuteAsync, with the asynchronous implementation as the single source of truth.
  • Quote-free arguments: Pass the executable and each argument separately, so spaces in paths and arguments are handled by the platform rather than by string concatenation.
  • Working directory: Start the process in a specific directory without mutating the process-global current directory.
  • Environment variables: Apply an overlay over the inherited environment for a single call, adding, overriding, or removing individual variables.
  • Cancellation: A signalled CancellationToken terminates the process and always surfaces as an OperationCanceledException, never as a synthetic exit code.
  • Windows elevation: Launch through the runas verb for a UAC-elevated process.
  • Custom encoding: Decode the output streams with any Encoding; defaults to UTF-8.
  • Broad target support: .NET Standard 2.0 and 2.1 through .NET 10.

Installation

Package Manager Console

Install-Package ktsu.RunCommand

.NET CLI

dotnet add package ktsu.RunCommand

Package Reference

<PackageReferenceInclude="ktsu.RunCommand"Version="1.5.0" />

Usage Examples

Basic Example

Pass the executable and its arguments separately. All methods return the process exit code:

usingktsu.RunCommand;classProgram{staticvoidMain(){intexitCode=RunCommand.Execute("dotnet",["--version"]);if(exitCode==0){Console.WriteLine("Command executed successfully!");}else{Console.WriteLine($"Command failed with exit code: {exitCode}");}}}

Custom Output Handling

To handle the output of the command, provide delegates to the OutputHandler class:

usingktsu.RunCommand;classProgram{staticvoidMain(){intexitCode=RunCommand.Execute(fileName:"dotnet",arguments:["--version"],outputHandler:new(onStandardOutput:Console.Write,onStandardError:Console.Write));Console.WriteLine($"Process exited with code: {exitCode}");}}

NOTE:When using the default OutputHandler, the delegates receive undelimited chunks of output. This gives you exactly what the command produces, including whitespace and non-printable characters, to handle as you see fit.

Line-by-Line Output Handling

To handle the output one line at a time, use the LineOutputHandler class:

usingktsu.RunCommand;classProgram{staticvoidMain(){intexitCode=RunCommand.Execute(fileName:"dotnet",arguments:["--version"],outputHandler:newLineOutputHandler(onStandardOutput: line =>Console.WriteLine($"Output: {line}"),onStandardError: line =>Console.WriteLine($"Error: {line}")));Console.WriteLine($"Process exited with code: {exitCode}");}}

Asynchronous Execution

All of the above examples can be run asynchronously with ExecuteAsync:

usingktsu.RunCommand;classProgram{staticasyncTaskMain(){intexitCode=awaitRunCommand.ExecuteAsync("dotnet",["--version"]);Console.WriteLine($"Process exited with code: {exitCode}");}}

Cancellation

Passing a CancellationToken terminates the process when the token is signalled:

usingktsu.RunCommand;classProgram{staticasyncTaskMain(){usingCancellationTokenSourcecancellation=new(TimeSpan.FromSeconds(30));try{intexitCode=awaitRunCommand.ExecuteAsync(fileName:"dotnet",arguments:["build"],outputHandler:newLineOutputHandler(onStandardOutput:Console.WriteLine),cancellationToken:cancellation.Token);Console.WriteLine($"Process exited with code: {exitCode}");}catch(OperationCanceledException){Console.WriteLine("The command was cancelled.");}}}

A cancelled call always throws OperationCanceledException — it never returns the killed process's exit code — so cancellation cannot be mistaken for a genuine failure of the command.

On .NET Core 3.0 and later the entire process tree is terminated. On .NET Standard 2.0 and 2.1 only the process itself can be terminated, so any grandchildren it spawned are left running.

Process Options

CommandOptions shapes the process a command runs in. Pass it alongside an executable and its arguments:

usingktsu.RunCommand;usingktsu.Semantics.Paths;classProgram{staticasyncTaskMain(){intexitCode=awaitRunCommand.ExecuteAsync(fileName:"git",arguments:["status","--short"],outputHandler:newLineOutputHandler(onStandardOutput:Console.WriteLine),options:new(){WorkingDirectory=AbsoluteDirectoryPath.Create(@"C:\repos\my project"),EnvironmentVariables=newDictionary<string,string?>{["GIT_TERMINAL_PROMPT"]="0",["LC_ALL"]="C",},});Console.WriteLine($"Process exited with code: {exitCode}");}}

CommandOptions.Elevation carries the privilege level too, so a single options object replaces the separate Elevation argument.

Working Directory

Without a WorkingDirectory the process inherits the current directory of the calling process, which is what commands did before this option existed.

The type is AbsoluteDirectoryPath rather than a string on purpose. A relative directory would have to be resolved against the caller's current directory — the process-global state this option exists to avoid depending on, since it is shared by every thread and races with concurrent calls.

Environment Variables

EnvironmentVariables is an overlay on the inherited environment, not a replacement: a name you do not list keeps whatever the calling process had. A null value removes a variable, which is how you unset something the parent had set:

EnvironmentVariables=newDictionary<string,string?>{["GIT_DIR"]=null,}

Environment variables are the only control surface some tools expose, so this covers behaviour with no command-line equivalent — GIT_TERMINAL_PROMPT=0 to make an authenticating git fetch fail rather than block forever on a prompt no terminal will answer, GIT_ASKPASS/SSH_ASKPASS to supply credentials without putting them on a command line where any process listing can read them, and LC_ALL=C to force stable, machine-parseable output rather than whatever the host locale produces.

NOTE:EnvironmentVariables cannot be combined with Elevation.Elevated on Windows. Elevation requires UseShellExecute, which offers nowhere to pass an environment, so the call throws ArgumentException rather than silently dropping the variables.

Elevation (Windows)

To run a command with elevated privileges, set Elevation.Elevated. On Windows this launches the process with the runas verb, which triggers a UAC prompt:

usingktsu.RunCommand;classProgram{staticvoidMain(){intexitCode=RunCommand.Execute(fileName:"powershell",arguments:["-Command","Get-Service"],outputHandler:new(),options:new(){Elevation=Elevation.Elevated});Console.WriteLine($"Process exited with code: {exitCode}");}}

NOTE:Output redirection is incompatible with runas, so an OutputHandler passed alongside Elevation.Elevated will not be invoked. You still get the process exit code.

On non-Windows platforms Elevation.Elevated is a no-op — prefix your command with sudo yourself if you need elevation there.

Encoding

By default the library decodes the output streams as UTF-8. To use a different encoding, specify it in the OutputHandler or LineOutputHandler constructor:

usingSystem.Text;usingktsu.RunCommand;classProgram{staticvoidMain(){intexitCode=RunCommand.Execute(fileName:"dotnet",arguments:["--version"],outputHandler:new(onStandardOutput:Console.Write,onStandardError:Console.Write,encoding:Encoding.ASCII));}}

Deprecated: Single Command Strings

The overloads taking one command string are obsolete. They separate the executable from its arguments by splitting on the first space, which cannot represent an executable path that itself contains a space — on Windows that includes anything under C:\Program Files\:

// Obsolete, and broken: splits into "C:\Program" plus "Files\Git\bin\git.exe --version"awaitRunCommand.ExecuteAsync(@"C:\Program Files\Git\bin\git.exe --version");// CorrectawaitRunCommand.ExecuteAsync(@"C:\Program Files\Git\bin\git.exe",["--version"]);

Quoting does not rescue it, because the split happens before any quote handling. The string form is inherently ambiguous — no parse handles every combination of spaces and quotes without adopting a shell's full grammar — so rather than grow a half-grammar that moves the surprise elsewhere, these overloads are deprecated in favour of the argument-list ones, which have no such ambiguity because the executable is passed separately.

Migration is mechanical: split the string yourself at the boundaries you meant.

ObsoleteReplacement
Execute(command)Execute(fileName, arguments)
Execute(command, outputHandler)Execute(fileName, arguments, outputHandler)
Execute(command, elevation)Execute(fileName, arguments, outputHandler, options)
ExecuteAsync(command)ExecuteAsync(fileName, arguments)
ExecuteAsync(command, outputHandler)ExecuteAsync(fileName, arguments, outputHandler)
ExecuteAsync(command, cancellationToken)ExecuteAsync(fileName, arguments, outputHandler, cancellationToken)
ExecuteAsync(command, outputHandler, elevation, cancellationToken)ExecuteAsync(fileName, arguments, outputHandler, options, cancellationToken)

API Reference

RunCommand

Static class providing the command execution API. Every method returns the process exit code.

Methods

NameReturn TypeDescription
Execute(string fileName, IEnumerable<string> arguments)intExecutes a command synchronously.
Execute(string fileName, IEnumerable<string> arguments, OutputHandler outputHandler)intExecutes a command synchronously with custom output handling.
Execute(string fileName, IEnumerable<string> arguments, OutputHandler outputHandler, CommandOptions options)intExecutes a command synchronously with the given process options.
ExecuteAsync(string fileName, IEnumerable<string> arguments)Task<int>Executes a command asynchronously.
ExecuteAsync(string fileName, IEnumerable<string> arguments, OutputHandler outputHandler)Task<int>Executes a command asynchronously with custom output handling.
ExecuteAsync(string fileName, IEnumerable<string> arguments, OutputHandler outputHandler, CancellationToken cancellationToken)Task<int>As above, terminating the process and its children if the token is signalled.
ExecuteAsync(string fileName, IEnumerable<string> arguments, OutputHandler outputHandler, Elevation elevation, CancellationToken cancellationToken)Task<int>As above, at the given elevation level.
ExecuteAsync(string fileName, IEnumerable<string> arguments, OutputHandler outputHandler, CommandOptions options)Task<int>Executes a command asynchronously with the given process options.
ExecuteAsync(string fileName, IEnumerable<string> arguments, OutputHandler outputHandler, CommandOptions options, CancellationToken cancellationToken)Task<int>As above, terminating the process and its children if the token is signalled.

The overloads taking a single command string — four Execute and seven ExecuteAsync — are obsolete. See Deprecated: Single Command Strings for the migration table.

CommandOptions

Record describing how to shape the process a command runs in. Every member defaults to the behaviour commands had before the type existed, so an instance with nothing set is equivalent to not passing one at all.

Properties

NameTypeDescription
WorkingDirectoryAbsoluteDirectoryPath?The directory the process starts in, or null to inherit the caller's current directory.
EnvironmentVariablesIReadOnlyDictionary<string, string?>?Variables applied over the inherited environment, or null to inherit it unchanged. A null value removes a variable.
ElevationElevationThe privilege level under which to run the command. Defaults to Elevation.Default.

OutputHandler

Processes output in raw, undelimited chunks as they arrive from the process.

Constructor

NameDescription
OutputHandler(Action<string>? onStandardOutput = null, Action<string>? onStandardError = null, Encoding? encoding = null)Creates a handler with delegates for the output and error streams. encoding defaults to UTF-8.

Properties

NameTypeDescription
EncodingEncodingThe encoding used to decode the process's output streams.

LineOutputHandler

Inherits from OutputHandler and buffers incoming chunks, invoking the delegates once per complete line. Incomplete trailing data is held until the rest of the line arrives.

Constructor

NameDescription
LineOutputHandler(Action<string>? onStandardOutput = null, Action<string>? onStandardError = null, Encoding? encoding = null)Creates a line-buffering handler with delegates for the output and error streams.

Elevation

Enum specifying the privilege level under which a command runs.

NameDescription
DefaultRun with the current process's privileges. Standard output and standard error are captured.
ElevatedOn Windows, launch through the runas verb, prompting for UAC consent; output is not captured. No effect on non-Windows platforms.

Contributing

Contributions are welcome! Feel free to open issues or submit pull requests.

License

This project is licensed under the MIT License. See the LICENSE.md file for details.

About

A library that provides an easy way to execute shell commands and handle the output via delegates with both synchronous and asynchronous support.

Topics

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

ktsu.RunCommand

A .NET library for executing external commands and handling their output through delegates, with synchronous and asynchronous APIs, cancellation, and control over the spawned process.

LicenseNuGet VersionNuGet VersionNuGet DownloadsGitHub commit activityGitHub contributorsGitHub Actions Workflow Status

Introduction

ktsu.RunCommand runs an external command and hands you its output as it arrives, instead of making you assemble Process, ProcessStartInfo, redirected streams and exit-code plumbing yourself. Output is delivered through delegates — either as raw chunks exactly as the process emits them, or buffered into complete lines — and every method returns the process exit code.

Arguments are passed as a vector rather than as one string, so a path containing spaces needs no manual quoting and cannot be mis-split. The process itself can be shaped through a working directory and an environment variable overlay, run elevated on Windows, and terminated along with its children through a cancellation token.

Features

  • Delegate-based output: Receive standard output and standard error through Action<string> delegates as the process produces them, rather than waiting for it to exit.
  • Raw or line-buffered: OutputHandler delivers undelimited chunks exactly as they arrive; LineOutputHandler buffers across chunks and raises one call per complete line.
  • Synchronous and asynchronous: Every operation is available as both Execute and ExecuteAsync, with the asynchronous implementation as the single source of truth.
  • Quote-free arguments: Pass the executable and each argument separately, so spaces in paths and arguments are handled by the platform rather than by string concatenation.
  • Working directory: Start the process in a specific directory without mutating the process-global current directory.
  • Environment variables: Apply an overlay over the inherited environment for a single call, adding, overriding, or removing individual variables.
  • Cancellation: A signalled CancellationToken terminates the process and always surfaces as an OperationCanceledException, never as a synthetic exit code.
  • Windows elevation: Launch through the runas verb for a UAC-elevated process.
  • Custom encoding: Decode the output streams with any Encoding; defaults to UTF-8.
  • Broad target support: .NET Standard 2.0 and 2.1 through .NET 10.

Installation

Package Manager Console

Install-Package ktsu.RunCommand

.NET CLI

dotnet add package ktsu.RunCommand

Package Reference

<PackageReferenceInclude="ktsu.RunCommand"Version="1.5.0" />

Usage Examples

Basic Example

Pass the executable and its arguments separately. All methods return the process exit code:

usingktsu.RunCommand;classProgram{staticvoidMain(){intexitCode=RunCommand.Execute("dotnet",["--version"]);if(exitCode==0){Console.WriteLine("Command executed successfully!");}else{Console.WriteLine($"Command failed with exit code: {exitCode}");}}}

Custom Output Handling

To handle the output of the command, provide delegates to the OutputHandler class:

usingktsu.RunCommand;classProgram{staticvoidMain(){intexitCode=RunCommand.Execute(fileName:"dotnet",arguments:["--version"],outputHandler:new(onStandardOutput:Console.Write,onStandardError:Console.Write));Console.WriteLine($"Process exited with code: {exitCode}");}}

NOTE:When using the default OutputHandler, the delegates receive undelimited chunks of output. This gives you exactly what the command produces, including whitespace and non-printable characters, to handle as you see fit.

Line-by-Line Output Handling

To handle the output one line at a time, use the LineOutputHandler class:

usingktsu.RunCommand;classProgram{staticvoidMain(){intexitCode=RunCommand.Execute(fileName:"dotnet",arguments:["--version"],outputHandler:newLineOutputHandler(onStandardOutput: line =>Console.WriteLine($"Output: {line}"),onStandardError: line =>Console.WriteLine($"Error: {line}")));Console.WriteLine($"Process exited with code: {exitCode}");}}

Asynchronous Execution

All of the above examples can be run asynchronously with ExecuteAsync:

usingktsu.RunCommand;classProgram{staticasyncTaskMain(){intexitCode=awaitRunCommand.ExecuteAsync("dotnet",["--version"]);Console.WriteLine($"Process exited with code: {exitCode}");}}

Cancellation

Passing a CancellationToken terminates the process when the token is signalled:

usingktsu.RunCommand;classProgram{staticasyncTaskMain(){usingCancellationTokenSourcecancellation=new(TimeSpan.FromSeconds(30));try{intexitCode=awaitRunCommand.ExecuteAsync(fileName:"dotnet",arguments:["build"],outputHandler:newLineOutputHandler(onStandardOutput:Console.WriteLine),cancellationToken:cancellation.Token);Console.WriteLine($"Process exited with code: {exitCode}");}catch(OperationCanceledException){Console.WriteLine("The command was cancelled.");}}}

A cancelled call always throws OperationCanceledException — it never returns the killed process's exit code — so cancellation cannot be mistaken for a genuine failure of the command.

On .NET Core 3.0 and later the entire process tree is terminated. On .NET Standard 2.0 and 2.1 only the process itself can be terminated, so any grandchildren it spawned are left running.

Process Options

CommandOptions shapes the process a command runs in. Pass it alongside an executable and its arguments:

usingktsu.RunCommand;usingktsu.Semantics.Paths;classProgram{staticasyncTaskMain(){intexitCode=awaitRunCommand.ExecuteAsync(fileName:"git",arguments:["status","--short"],outputHandler:newLineOutputHandler(onStandardOutput:Console.WriteLine),options:new(){WorkingDirectory=AbsoluteDirectoryPath.Create(@"C:\repos\my project"),EnvironmentVariables=newDictionary<string,string?>{["GIT_TERMINAL_PROMPT"]="0",["LC_ALL"]="C",},});Console.WriteLine($"Process exited with code: {exitCode}");}}

CommandOptions.Elevation carries the privilege level too, so a single options object replaces the separate Elevation argument.

Working Directory

Without a WorkingDirectory the process inherits the current directory of the calling process, which is what commands did before this option existed.

The type is AbsoluteDirectoryPath rather than a string on purpose. A relative directory would have to be resolved against the caller's current directory — the process-global state this option exists to avoid depending on, since it is shared by every thread and races with concurrent calls.

Environment Variables

EnvironmentVariables is an overlay on the inherited environment, not a replacement: a name you do not list keeps whatever the calling process had. A null value removes a variable, which is how you unset something the parent had set:

EnvironmentVariables=newDictionary<string,string?>{["GIT_DIR"]=null,}

Environment variables are the only control surface some tools expose, so this covers behaviour with no command-line equivalent — GIT_TERMINAL_PROMPT=0 to make an authenticating git fetch fail rather than block forever on a prompt no terminal will answer, GIT_ASKPASS/SSH_ASKPASS to supply credentials without putting them on a command line where any process listing can read them, and LC_ALL=C to force stable, machine-parseable output rather than whatever the host locale produces.

NOTE:EnvironmentVariables cannot be combined with Elevation.Elevated on Windows. Elevation requires UseShellExecute, which offers nowhere to pass an environment, so the call throws ArgumentException rather than silently dropping the variables.

Elevation (Windows)

To run a command with elevated privileges, set Elevation.Elevated. On Windows this launches the process with the runas verb, which triggers a UAC prompt:

usingktsu.RunCommand;classProgram{staticvoidMain(){intexitCode=RunCommand.Execute(fileName:"powershell",arguments:["-Command","Get-Service"],outputHandler:new(),options:new(){Elevation=Elevation.Elevated});Console.WriteLine($"Process exited with code: {exitCode}");}}

NOTE:Output redirection is incompatible with runas, so an OutputHandler passed alongside Elevation.Elevated will not be invoked. You still get the process exit code.

On non-Windows platforms Elevation.Elevated is a no-op — prefix your command with sudo yourself if you need elevation there.

Encoding

By default the library decodes the output streams as UTF-8. To use a different encoding, specify it in the OutputHandler or LineOutputHandler constructor:

usingSystem.Text;usingktsu.RunCommand;classProgram{staticvoidMain(){intexitCode=RunCommand.Execute(fileName:"dotnet",arguments:["--version"],outputHandler:new(onStandardOutput:Console.Write,onStandardError:Console.Write,encoding:Encoding.ASCII));}}

Deprecated: Single Command Strings

The overloads taking one command string are obsolete. They separate the executable from its arguments by splitting on the first space, which cannot represent an executable path that itself contains a space — on Windows that includes anything under C:\Program Files\:

// Obsolete, and broken: splits into "C:\Program" plus "Files\Git\bin\git.exe --version"awaitRunCommand.ExecuteAsync(@"C:\Program Files\Git\bin\git.exe --version");// CorrectawaitRunCommand.ExecuteAsync(@"C:\Program Files\Git\bin\git.exe",["--version"]);

Quoting does not rescue it, because the split happens before any quote handling. The string form is inherently ambiguous — no parse handles every combination of spaces and quotes without adopting a shell's full grammar — so rather than grow a half-grammar that moves the surprise elsewhere, these overloads are deprecated in favour of the argument-list ones, which have no such ambiguity because the executable is passed separately.

Migration is mechanical: split the string yourself at the boundaries you meant.

ObsoleteReplacement
Execute(command)Execute(fileName, arguments)
Execute(command, outputHandler)Execute(fileName, arguments, outputHandler)
Execute(command, elevation)Execute(fileName, arguments, outputHandler, options)
ExecuteAsync(command)ExecuteAsync(fileName, arguments)
ExecuteAsync(command, outputHandler)ExecuteAsync(fileName, arguments, outputHandler)
ExecuteAsync(command, cancellationToken)ExecuteAsync(fileName, arguments, outputHandler, cancellationToken)
ExecuteAsync(command, outputHandler, elevation, cancellationToken)ExecuteAsync(fileName, arguments, outputHandler, options, cancellationToken)

API Reference

RunCommand

Static class providing the command execution API. Every method returns the process exit code.

Methods

NameReturn TypeDescription
Execute(string fileName, IEnumerable<string> arguments)intExecutes a command synchronously.
Execute(string fileName, IEnumerable<string> arguments, OutputHandler outputHandler)intExecutes a command synchronously with custom output handling.
Execute(string fileName, IEnumerable<string> arguments, OutputHandler outputHandler, CommandOptions options)intExecutes a command synchronously with the given process options.
ExecuteAsync(string fileName, IEnumerable<string> arguments)Task<int>Executes a command asynchronously.
ExecuteAsync(string fileName, IEnumerable<string> arguments, OutputHandler outputHandler)Task<int>Executes a command asynchronously with custom output handling.
ExecuteAsync(string fileName, IEnumerable<string> arguments, OutputHandler outputHandler, CancellationToken cancellationToken)Task<int>As above, terminating the process and its children if the token is signalled.
ExecuteAsync(string fileName, IEnumerable<string> arguments, OutputHandler outputHandler, Elevation elevation, CancellationToken cancellationToken)Task<int>As above, at the given elevation level.
ExecuteAsync(string fileName, IEnumerable<string> arguments, OutputHandler outputHandler, CommandOptions options)Task<int>Executes a command asynchronously with the given process options.
ExecuteAsync(string fileName, IEnumerable<string> arguments, OutputHandler outputHandler, CommandOptions options, CancellationToken cancellationToken)Task<int>As above, terminating the process and its children if the token is signalled.

The overloads taking a single command string — four Execute and seven ExecuteAsync — are obsolete. See Deprecated: Single Command Strings for the migration table.

CommandOptions

Record describing how to shape the process a command runs in. Every member defaults to the behaviour commands had before the type existed, so an instance with nothing set is equivalent to not passing one at all.

Properties

NameTypeDescription
WorkingDirectoryAbsoluteDirectoryPath?The directory the process starts in, or null to inherit the caller's current directory.
EnvironmentVariablesIReadOnlyDictionary<string, string?>?Variables applied over the inherited environment, or null to inherit it unchanged. A null value removes a variable.
ElevationElevationThe privilege level under which to run the command. Defaults to Elevation.Default.

OutputHandler

Processes output in raw, undelimited chunks as they arrive from the process.

Constructor

NameDescription
OutputHandler(Action<string>? onStandardOutput = null, Action<string>? onStandardError = null, Encoding? encoding = null)Creates a handler with delegates for the output and error streams. encoding defaults to UTF-8.

Properties

NameTypeDescription
EncodingEncodingThe encoding used to decode the process's output streams.

LineOutputHandler

Inherits from OutputHandler and buffers incoming chunks, invoking the delegates once per complete line. Incomplete trailing data is held until the rest of the line arrives.

Constructor

NameDescription
LineOutputHandler(Action<string>? onStandardOutput = null, Action<string>? onStandardError = null, Encoding? encoding = null)Creates a line-buffering handler with delegates for the output and error streams.

Elevation

Enum specifying the privilege level under which a command runs.

NameDescription
DefaultRun with the current process's privileges. Standard output and standard error are captured.
ElevatedOn Windows, launch through the runas verb, prompting for UAC consent; output is not captured. No effect on non-Windows platforms.

Contributing

Contributions are welcome! Feel free to open issues or submit pull requests.

License

This project is licensed under the MIT License. See the LICENSE.md file for details.

About

A library that provides an easy way to execute shell commands and handle the output via delegates with both synchronous and asynchronous support.

Topics

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages