Giter Club home page Giter Club logo

simple-exec's Introduction

SimpleExec

SimpleExec

NuGet version

Build status CodeQL analysis lint Spell check

SimpleExec is a .NET library that runs external commands. It wraps System.Diagnostics.Process to make things easier.

SimpleExec intentionally does not invoke the system shell.

Platform support: .NET 6.0 and later.

Quick start

using static SimpleExec.Command;
Run("foo", "arg1 arg2");

Run

Run("foo");
Run("foo", "arg1 arg2");
Run("foo", new[] { "arg1", "arg2" });

await RunAsync("foo");
await RunAsync("foo", "arg1 arg2");
await RunAsync("foo", new[] { "arg1", "arg2" });

By default, the command is echoed to standard output (stdout) for visibility.

Read

var (standardOutput1, standardError1) = await ReadAsync("foo");
var (standardOutput2, standardError2) = await ReadAsync("foo", "arg1 arg2");
var (standardOutput3, standardError3) = await ReadAsync("foo", new[] { "arg1", "arg2" });

Other optional arguments

string workingDirectory = "",
bool noEcho = false,
string? echoPrefix = null,
Action<IDictionary<string, string?>>? configureEnvironment = null,
bool createNoWindow = false,
Encoding? encoding = null,
Func<int, bool>? handleExitCode = null,
string? standardInput = null,
bool cancellationIgnoresProcessTree = false,
CancellationToken cancellationToken = default,

Exceptions

If the command has a non-zero exit code, an ExitCodeException is thrown with an int ExitCode property and a message in the form of:

$"The process exited with code {ExitCode}."

In the case of ReadAsync, an ExitCodeReadException is thrown, which inherits from ExitCodeException, and has string Out and Error properties, representing standard out (stdout) and standard error (stderr), and a message in the form of:

$@"The process exited with code {ExitCode}.

Standard Output:

{Out}

Standard Error:

{Error}"

Overriding default exit code handling

Most programs return a zero exit code when they succeed and a non-zero exit code fail. However, there are some programs which return a non-zero exit code when they succeed. For example, Robocopy returns an exit code less than 8 when it succeeds and 8 or greater when a failure occurs.

The throwing of exceptions for specific non-zero exit codes may be suppressed by passing a delegate to handleExitCode which returns true when it has handled the exit code and default exit code handling should be suppressed, and returns false otherwise.

For example, when running Robocopy, exception throwing should be suppressed for an exit code less than 8:

Run("ROBOCOPY", "from to", handleExitCode: code => code < 8);

Note that it may be useful to record the exit code. For example:

var exitCode = 0;
Run("ROBOCOPY", "from to", handleExitCode: code => (exitCode = code) < 8);

// see https://ss64.com/nt/robocopy-exit.html
var oneOrMoreFilesCopied = exitCode & 1;
var extraFilesOrDirectoriesDetected = exitCode & 2;
var misMatchedFilesOrDirectoriesDetected = exitCode & 4;

Run by Gregor Cresnar from the Noun Project.

simple-exec's People

Contributors

adamralph avatar dependabot[bot] avatar khalidabuhakmeh avatar paralexm avatar pauldotknopf avatar pgermishuys avatar rasmus-unity avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

simple-exec's Issues

Custom exit code handling

Use case(s)

Some commands return non-zero exit codes when they succeed. For example, Robocopy:

An Exit Code of 0-7 is success and any value >= 8 indicates that there was at least one failure during the copy operation.

When running such a command, the default exit code handling in SimpleExec may throw an exception when the command succeeds. Custom exit code handling is required for these scenarios.

Description

Run, RunAsync, Read, and ReadAsync have a new optional parameter: Func<int, bool> handleExit. The supplied delegate argument should return true to indicate that it handled the exit code, and that the default exit code handling in SimpleExec should be suppressed, and false otherwise.

For example:

Run("robocopy", handleExitCode: exitCode => exitCode < 8);

Breaking, because handleExitCode was added as an optional parameter before cancellationToken in RunAsync and ReadAsync.

Also deprecates NonZeroExitCodeException in favour of ExitCodeException, since the name of the former doesn't make sense when an argument is supplied to handleExitCode.

Alternatives

Catch and swallow the exception in consuming code when the exit code indicates success.

Additional context

Replaces #298. Thanks to @nixtar for raising that issue.

Depends on #311.

Issue capturing large output from "Read" functions.

My app is hanging when running a command that returns a lot of stdout.

I am able to reduce the stdout, little by little, and eventually see the command Read return successfully.

I think we are running into this issue. I'm going to look into it and see if it fixes it. If it does, I'll create a pull request.

Provide more succinct syntax for long argument lists

Use case(s)

When running commands with more than 2-3 arguments, it can be a bit verbose to break the arguments across multiple lines, particularly when using dotnet script compared with powershell.

Description

Make an option to remove newlines from the argument list such that consumers can use multiline literals as a more succinct way to break many arguments across multiple lines.

So instead of:

Run(
    "dotnet",
    "my-command sub-command " + 
    @"--releases-folder dist\release " +
    "--destination https://some-big-long-bucket-location " +
    @"--intermediates-folder build\something\something-else"
);

I could write:

Run(
    "dotnet",
    @"my-command sub-command  
    --releases-folder dist\release
    --destination https://some-big-long-bucket-location
    --intermediates-folder build\something\something-else",
    stripNewLines: true
);

Alternatives

The alternative is that I add .Replace("\r", "").Replace("\n", "") at the end of each argument string, or make an extension method to do it like .RemoveNewLines().

8.0.0 release

Process fails to start on node command with arguments

Attempting to run yarn install command Run("yarn", "install")
Results in

cmd.exe /c ""yarn" install"
module.js:550
    throw err;
    ^

Error: Cannot find module 'C:\Projects\Project1\src\node_modules\yarn\bin\yarn.js'

Actual command should be cmd.exe /c "yarn install".

Add support for the CreateNoWindow option

Use case(s)

SimpleExec is great when you need to call a console app from another console app. However, when you use it from a non-console app (e.g. WPF or Windows Forms), it opens a console window for the new process, which is annoying

Description

Adding a parameter to specify CreateNoWindow in the ProcessStartInfo object would fix this.

Alternatives

Another option would be to detect if the current process has a console window attached, and if not, set CreateNoWindow to true.

Additional context

N/A

Support passing environment variables to processes

Use case(s)

Some of our tooling (basically around yarn / npm) leverages environment variables instead of arguments and some scenarios it not possible to pass args via scripts nested in package.json. (How does software even work?)

Description

Since simple-exec is a wrapper around ProcessStartInfo which already supports EnviromentVariables and since UseShellExecute is false, it should allow the caller to optionally modify before executing:

public static void Run(string name, ..snip... , Action<IDictionary<string, string>> configEnvironmentVariables = null);

Example usage:

Run("foo", ...snip..., env => env["bar"] = "baz");

Alternatives

This is nasty yo

Environment.SetEnvironmentVariable("bar", "baz"); 
RunCmd("yarn", "somescript");
Environment.SetEnvironmentVariable("", ""); 

Additional context

None.

Add a timeout parameter to Run

Use case(s)

It's useful for when you run a long running process and don't want to wait more than X amount of time for it to end.

Description

Add a timeout parameter of type TimeSpan to the Run methods.

Alternatives

A cancellation token would be nice, but I am not sure how it would be implemented, because by default Process.WaitForExit only supports a timeout method.

Additional context

I am currently creating an online document converter and use LibreOffice. I don't want to wait more than 1 minute for a document to be converted to make sure the server is not overloaded.

P.S: I am happy to send a PR if you think this is a good idea

Prefix messages in stderr

A program printing to stderr should identify itself with a prefix. This feature introduces a default prefix of the entry assembly name. E.g. where the consuming project is named targets.csproj:

targets: Working directory: foo
targets: dotnet build Boo.sln

or build.csproj:

build: Working directory: foo
build: dotnet build Boo.sln

With an optional param to control the program name.

2.3.0 release

  • build tag
  • push packages
  • ensure README.md reflects new version
  • create next milestone
  • close milestone
  • tweet, mentioning contributors

4.1.0 release

  • build tag
  • push packages
  • ensure README.md reflects new version
  • create next milestone
  • close milestone
  • tweet, mentioning contributors

ExitCodeException loses error messages

Use case(s)

My task is failing during Command.ReadAsync with a NonZeroExitCodeException in production (Docker image running on AWS ECS). It's a nightmare to debug, because the exception doesn't contain the echo'ed text.

Description

ExitCodeException could include the output

Alternatives

We could create Command.TryReadAsync which doesn't through an exception but the return type would state the error code.

4.2.0 release

  • build tag
  • push packages
  • ensure README.md reflects new version
  • create next milestone
  • close milestone
  • tweet, mentioning contributors

Add API documentation

Sometimes this can be important. For example, the basic overload of Run() and Read() should have a remark stating that the command will be echoed to the console. This is not desirable if the command contains something like an API key. See SQLStreamStore/SQLStreamStore#193.

Add further optional params as pass-through to ProcessStartInfo

Use case(s)

ProcessStartInfo has several properties which may be useful to specify using optional params. Some of them are currently supported, some are not.

Property Supported
Arguments yes
CreateNoWindow yes
Domain no
ErrorDialog n/a
ErrorDialogParentHandle n/a
FileName yes
LoadUserProfile no
Password no
PasswordInClearText no
RedirectStandardError n/a
RedirectStandardInput n/a
RedirectStandardOutput n/a
StandardErrorEncoding n/a
StandardOutputEncoding yes
UseShellExecute n/a
UserName no
Verb n/a
WindowStyle n/a
WorkingDirectory yes

Description

In 7.0.0 we are adding support for preferred stdout encoding. Any further additions are breaking, since new optional params should appear before the CancellationToken in the async methods, as recommended.

If we are going to add further optional params as pass-through to ProcessStartInfo we should do it now, to minimise the need for further breaking changes later.

This enhancement adds the following optional params:

  • domain
  • loadUserProfile
  • password
  • passwordInClearText
  • userName

Alternatives

Use ProcessStartInfo directly, or use a different library.

Additional context

Thanks to @rasmus-unity for seeding this ion #278.

When process returns non-zero exit code, it's not possible to get output.

When executing a dotnet nuget publish, I get a 409 status if the package already exists. I'm trying to get the output, but this is only returned when the process returns a 0 status code. It could be a handy property on the non zero statuscode exception.

Something like:

        public static async Task<string> ReadAsync(string name, string args = null, string workingDirectory = null, bool noEcho = false)
        {
            using (var process = new Process())
            {
                process.StartInfo = ProcessStartInfo.Create(name, args, workingDirectory, true);
                await process.RunAsync(noEcho).ConfigureAwait(false);

                var output = await process.StandardOutput.ReadToEndAsync().ConfigureAwait(false);

                if (process.ExitCode != 0)
                {
                    process.Throw(output);
                }

                return output;
            }
}

If you want, I can create a PR that adds this.

Quazi-"tee" option for Read[Async]() and callbacks.

Hi, using simpleexec with bullseye, would be nice to have optionally Read/ReadAsync "tee" output into respective streams, as well as return data.

Also, what do you think about optional user-provided callback to "stream" data to the caller as it arrives? This would probably best be implemented on Run/RunAsync.
Use-case: one of the commands I run, prints name of the artifact it produces, would like to pick it up from there, as well as have it in the CI logs, which are read from stdout/stderr.

New API for version 9

Use case(s)

All uses of the package.

Description

A better API which makes a clear distinction between Run/RunAsync, which do NOT redirect the standard streams, and ReadAsync, which does. The latter gives access to standard output and standard error both in the return value from the method call, and when an exception is thrown due to a non-zero exit code.

(including other changes):

+#nullable enable
+override SimpleExec.ExitCodeException.Message.get -> string!
+override SimpleExec.ExitCodeReadException.Message.get -> string!
 SimpleExec.Command
 SimpleExec.ExitCodeException
 SimpleExec.ExitCodeException.ExitCode.get -> int
 SimpleExec.ExitCodeException.ExitCodeException(int exitCode) -> void
-SimpleExec.NonZeroExitCodeException
-SimpleExec.NonZeroExitCodeException.NonZeroExitCodeException() -> void
-static SimpleExec.Command.Read(string name, string args = null, string workingDirectory = null, bool noEcho = false, string windowsName = null, string windowsArgs = null, string logPrefix = null, System.Action<System.Collections.Generic.IDictionary<string, string>> configureEnvironment = null, bool createNoWindow = false, System.Text.Encoding encoding = null, System.Func<int, bool> handleExitCode = null) -> string
-static SimpleExec.Command.ReadAsync(string name, string args = null, string workingDirectory = null, bool noEcho = false, string windowsName = null, string windowsArgs = null, string logPrefix = null, System.Action<System.Collections.Generic.IDictionary<string, string>> configureEnvironment = null, bool createNoWindow = false, System.Text.Encoding encoding = null, System.Func<int, bool> handleExitCode = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<string>
-static SimpleExec.Command.Run(string name, string args = null, string workingDirectory = null, bool noEcho = false, string windowsName = null, string windowsArgs = null, string logPrefix = null, System.Action<System.Collections.Generic.IDictionary<string, string>> configureEnvironment = null, bool createNoWindow = false, System.Func<int, bool> handleExitCode = null) -> void
-static SimpleExec.Command.RunAsync(string name, string args = null, string workingDirectory = null, bool noEcho = false, string windowsName = null, string windowsArgs = null, string logPrefix = null, System.Action<System.Collections.Generic.IDictionary<string, string>> configureEnvironment = null, bool createNoWindow = false, System.Func<int, bool> handleExitCode = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task
+SimpleExec.ExitCodeReadException
+SimpleExec.ExitCodeReadException.ExitCodeReadException(int exitCode, string! standardOutput, string! standardError) -> void
+SimpleExec.ExitCodeReadException.StandardError.get -> string!
+SimpleExec.ExitCodeReadException.StandardOutput.get -> string!
+static SimpleExec.Command.ReadAsync(string! name, string! args = "", string! workingDirectory = "", string? windowsName = null, string? windowsArgs = null, System.Action<System.Collections.Generic.IDictionary<string!, string!>!>? configureEnvironment = null, System.Text.Encoding? encoding = null, System.Func<int, bool>? handleExitCode = null, string? standardInput = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<(string! StandardOutput, string! StandardError)>!
+static SimpleExec.Command.Run(string! name, string! args = "", string! workingDirectory = "", bool noEcho = false, string? windowsName = null, string? windowsArgs = null, string? echoPrefix = null, System.Action<System.Collections.Generic.IDictionary<string!, string!>!>? configureEnvironment = null, bool createNoWindow = false, System.Func<int, bool>? handleExitCode = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> void
+static SimpleExec.Command.RunAsync(string! name, string! args = "", string! workingDirectory = "", bool noEcho = false, string? windowsName = null, string? windowsArgs = null, string? echoPrefix = null, System.Action<System.Collections.Generic.IDictionary<string!, string!>!>? configureEnvironment = null, bool createNoWindow = false, System.Func<int, bool>? handleExitCode = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!

Alternatives

Continue with the current API.

Additional context

This also avoids scenarios such as #337 and #103.

Simpler API

void Run(string name, string args);
void Run(string name, string args, string workingDirectory);

When the exit code is non-zero, an exception will be thrown with a message containing the exit code and the contents of stderr.

Async API

Async versions of all the methods provided.

6.0.0 release

Attempt to run a command prompt command if on windows and process fails to start

We have this code in our build at https://github.com/SQLStreamStore/SQLStreamStore.HAL/blob/f3bfa641154b7c0a82c0d94e1ddec9ffd6c8c774/build/Program.cs#L131-L134

if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
    Run("cmd", "/c yarn", workingDirectory);
else
    Run("yarn", args, workingDirectory);

I'd rather just write

Run("yarn", args, workingDirectory);

But running that results in an exception on windows:

Bullseye/build-hal-docs: System.ComponentModel.Win32Exception (2): The system cannot find the file specified
   at System.Diagnostics.Process.StartWithCreateProcess(ProcessStartInfo startInfo)
   at System.Diagnostics.Process.Start()
   at SimpleExec.ProcessExtensions.EchoAndStart(Process process, Boolean noEcho) in C:\projects\simple-exec\SimpleExec\ProcessExtensions.cs:line 33
   at SimpleExec.Command.Run(String name, String args, String workingDirectory, Boolean noEcho) in C:\projects\simple-exec\SimpleExec\Command.cs:line 29
   at build.Program.<>c.<Main>b__6_0() in E:\dev\damianh\SQLStreamStore\build\Program.cs:line 22
   at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)
--- End of stack trace from previous location where exception was thrown ---
   at System.Threading.Tasks.Task.ExecuteWithThreadLocal(Task& currentTaskSlot)
--- End of stack trace from previous location where exception was thrown ---
   at Bullseye.Internal.ActionTarget.RunAsync(Boolean dryRun, Boolean parallel, Logger log, Func`2 messageOnly) in C:\projects\bullseye\Bullseye\Internal\ActionTarget.cs:line 25

Suggestion: on windows, catch this and then attempt Run("cmd", $"/c {name} {arg}", workingDirectory); ?

Don't redirect stderr

This change is marked as breaking, in case anyone is relying on the current behaviour, but it is expected that this won't actually break anything.

Use of stderr is not standardised and does not necessarily contain only information which is appropriate for an exception message. Sometimes, visibility of stderr during execution is desirable, to view progress, or to view test failures as they happen. With this change, stderr is no longer redirected, leaving the default, or configured behaviour, in place, which is usually to print to the console. The exception message when a command fails will no longer include the contents of stderr. It will include only the process exit code.

Original description Many commands contain long-winded information on stderr. Sometimes it is informative (exit code 0), sometimes it is an actual error.

Capturing and throwing stderr makes sense when using SimpleExec within a library/application, but not within the context of Bullseye and build scripts. It is best to let the stdout/stderr do it's thing, unabated, and just throw an exception if exit code is non-zero.

6.1.0 release

Ability to supply an array of valid/accepted exitcodes.

Use case(s)

Some processes use non zero exitcodes when running successfully.
For EG robocopy: https://ss64.com/nt/robocopy-exit.html and msiexec

Description

Add a int array parameter to Run, RunAsync, Read and ReadAsync that defaults to new int[] { 0 }.
Then change the current logic that checks if the exit code is not 0 to check that its not in the array.
To prevent breaking changes add new ExitCodeException and continue to throw NonZeroExitCodeException if the expected exit code only contains 0?
Or make parameter null by default and do a null check and throw NonZeroExitCodeException if null and exit code is non zero.

Alternatives

Consumer of this library catches NonZeroExitCodeException and implements their own exit code checks.

The filename, directory name, or volume label syntax is incorrect.

Hey @adamralph,

After I upgraded to 5.0.0, my build scripts (I have several like this one) started failing like this:

Bullseye/build: Starting...
cmd.exe /c "dotnet" build -c "Release" /bl:"C:\git\WeakEvent\artifacts\logs\build.binlog" "WeakEvent.sln"
The filename, directory name, or volume label syntax is incorrect.
Bullseye/build: Failed! The process exited with code 1. (36.6 ms)
Bullseye: Failed! (default) (70.7 ms)

If I copy/paste the command line from the output above, it works fine.

The same script works fine with previous versions of SimpleExec.

Could it be related to #100? Is there any other known breaking change?

5.0.0 release

Pass CancellationToken To RunAsync and ReadAsync

Use case(s)

Some processes are long-running tasks, and the ability to kill these processes might be a good thing. I have a process that starts playing an audio file and I'd like to stop the audio with a cancellationToken.

Description

            var audio = await SimpleExec.Command.ReadAsync(
                "afplay",
                string.Join(" ", arguments),
                noEcho: true,
                cancellationToken: cancellationToken
            );

The issue is that the cancellation request needs to call process.Kill or else the process continues to run in the background.

Alternatives

Handroll starting a process.

Additional context

I'll try and make an attempt myself at implementing the change, but my async foo is mediocre at best.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.