csharp/adamralph/simple-exec/SimpleExec/Command.cs

Command.cs
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace SimpleExec
{
    /// 
    /// Contains methods for running commands and reading standard output (stdout).
    /// 
    public static clast Command
    {
        private static readonly Action defaultAction = _ => { };
        private static readonly string defaultEchoPrefix = astembly.GetEntryastembly()?.GetName().Name ?? "SimpleExec";

        /// 
        /// Runs a command without redirecting standard output (stdout) and standard error (stderr) and without writing to standard input (stdin).
        /// By default, the command line is echoed to standard error (stderr).
        /// 
        /// The name of the command. This can be a path to an executable file.
        /// The arguments to past to the command.
        /// The working directory in which to run the command.
        /// Whether or not to echo the resulting command line and working directory (if specified) to standard error (stderr).
        /// The name of the command to use on Windows only.
        /// The arguments to past to the command on Windows only.
        /// The prefix to use when echoing the command line and working directory (if specified) to standard error (stderr).
        /// An action which configures environment variables for the command.
        /// Whether to run the command in a new window.
        /// 
        /// A delegate which accepts an  representing exit code of the command and
        /// returns  when it has handled the exit code and default exit code handling should be suppressed, and
        /// returns  otherwise.
        /// 
        /// A  to observe while waiting for the command to exit.
        /// The command exited with non-zero exit code.
        /// 
        /// By default, the resulting command line and the working directory (if specified) are echoed to standard error (stderr).
        /// To suppress this behavior, provide the  parameter with a value of true.
        /// 
        public static void Run(
            string name,
            string? args = null,
            string? workingDirectory = null,
            bool noEcho = false,
            string? windowsName = null,
            string? windowsArgs = null,
            string? echoPrefix = null,
            Action? configureEnvironment = null,
            bool createNoWindow = false,
            Func? handleExitCode = null,
            CancellationToken cancellationToken = default)
        {
            Validate(name);

            using var process = new Process();

            process.StartInfo = ProcessStartInfo.Create(
                RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? windowsName ?? name : name,
                (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? windowsArgs ?? args : args) ?? "",
                workingDirectory ?? "",
                false,
                configureEnvironment ?? defaultAction,
                createNoWindow);

            process.Run(noEcho, echoPrefix ?? defaultEchoPrefix, cancellationToken);

            if (!(handleExitCode?.Invoke(process.ExitCode) ?? false) && process.ExitCode != 0)
            {
                throw new ExitCodeException(process.ExitCode);
            }
        }

        /// 
        /// Runs a command asynchronously without redirecting standard output (stdout) and standard error (stderr) and without writing to standard input (stdin).
        /// By default, the command line is echoed to standard error (stderr).
        /// 
        /// The name of the command. This can be a path to an executable file.
        /// The arguments to past to the command.
        /// The working directory in which to run the command.
        /// Whether or not to echo the resulting command line and working directory (if specified) to standard error (stderr).
        /// The name of the command to use on Windows only.
        /// The arguments to past to the command on Windows only.
        /// The prefix to use when echoing the command line and working directory (if specified) to standard error (stderr).
        /// An action which configures environment variables for the command.
        /// Whether to run the command in a new window.
        /// 
        /// A delegate which accepts an  representing exit code of the command and
        /// returns  when it has handled the exit code and default exit code handling should be suppressed, and
        /// returns  otherwise.
        /// 
        /// A  to observe while waiting for the command to exit.
        /// A  that represents the asynchronous running of the command.
        /// The command exited with non-zero exit code.
        /// 
        /// By default, the resulting command line and the working directory (if specified) are echoed to standard error (stderr).
        /// To suppress this behavior, provide the  parameter with a value of true.
        /// 
        public static async Task RunAsync(
            string name,
            string? args = null,
            string? workingDirectory = null,
            bool noEcho = false,
            string? windowsName = null,
            string? windowsArgs = null,
            string? echoPrefix = null,
            Action? configureEnvironment = null,
            bool createNoWindow = false,
            Func? handleExitCode = null,
            CancellationToken cancellationToken = default)
        {
            Validate(name);

            using var process = new Process();

            process.StartInfo = ProcessStartInfo.Create(
                RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? windowsName ?? name : name,
                (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? windowsArgs ?? args : args) ?? "",
                workingDirectory ?? "",
                false,
                configureEnvironment ?? defaultAction,
                createNoWindow);

            await process.RunAsync(noEcho, echoPrefix ?? defaultEchoPrefix, cancellationToken).ConfigureAwait(false);

            if (!(handleExitCode?.Invoke(process.ExitCode) ?? false) && process.ExitCode != 0)
            {
                throw new ExitCodeException(process.ExitCode);
            }
        }

        /// 
        /// Runs a command and reads standard output (stdout) and standard error (stderr) and optionally writes to standard input (stdin).
        /// 
        /// The name of the command. This can be a path to an executable file.
        /// The arguments to past to the command.
        /// The working directory in which to run the command.
        /// The name of the command to use on Windows only.
        /// The arguments to past to the command on Windows only.
        /// An action which configures environment variables for the command.
        /// The preferred  for standard output (stdout) and standard error (stderr).
        /// 
        /// A delegate which accepts an  representing exit code of the command and
        /// returns  when it has handled the exit code and default exit code handling should be suppressed, and
        /// returns  otherwise.
        /// 
        /// The contents of standard input (stdin).
        /// A  to observe while waiting for the command to exit.
        /// 
        /// A  representing the asynchronous running of the command and reading of standard output (stdout) and standard error (stderr).
        /// The task result is a  representing the contents of standard output (stdout) and standard error (stderr).
        /// 
        /// 
        /// The command exited with non-zero exit code. The exception contains the contents of standard output (stdout) and standard error (stderr).
        /// 
        public static async Task ReadAsync(
            string name,
            string? args = null,
            string? workingDirectory = null,
            string? windowsName = null,
            string? windowsArgs = null,
            Action? configureEnvironment = null,
            Encoding? encoding = null,
            Func? handleExitCode = null,
            string? standardInput = null,
            CancellationToken cancellationToken = default)
        {
            Validate(name);

            using var process = new Process();

            process.StartInfo = ProcessStartInfo.Create(
                RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? windowsName ?? name : name,
                (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? windowsArgs ?? args : args) ?? "",
                workingDirectory ?? "",
                true,
                configureEnvironment ?? defaultAction,
                true,
                encoding);

            var runProcess = process.RunAsync(true, defaultEchoPrefix, cancellationToken);

            Task readOutput;
            Task readError;

            try
            {
                await process.StandardInput.WriteAsync(standardInput).ConfigureAwait(false);
                process.StandardInput.Close();

                readOutput = process.StandardOutput.ReadToEndAsync();
                readError = process.StandardError.ReadToEndAsync();
            }
            catch (Exception)
            {
                await runProcess.ConfigureAwait(false);
                throw;
            }

            await Task.WhenAll(runProcess, readOutput, readError).ConfigureAwait(false);

#pragma warning disable CA1849 // Call async methods when in an async method
            var output = readOutput.Result;
            var error = readError.Result;
#pragma warning restore CA1849 // Call async methods when in an async method

            return (handleExitCode?.Invoke(process.ExitCode) ?? false) || process.ExitCode == 0
                ? new Result(output, error)
                : throw new ExitCodeReadException(process.ExitCode, output, error);
        }

        private static void Validate(string name)
        {
            if (string.IsNullOrWhiteSpace(name))
            {
                throw new ArgumentException("The command name is missing.", nameof(name));
            }
        }
    }
}