diff --git a/CliWrap.Tests/ConfigurationSpecs.cs b/CliWrap.Tests/ConfigurationSpecs.cs index 33a403d0..daba37db 100644 --- a/CliWrap.Tests/ConfigurationSpecs.cs +++ b/CliWrap.Tests/ConfigurationSpecs.cs @@ -38,8 +38,7 @@ public void I_can_configure_the_target_file() var modified = original.WithTargetFile("bar"); // Assert - original.Should().BeEquivalentTo(modified, o => o.Excluding(c => c.TargetFilePath)); - original.TargetFilePath.Should().NotBe(modified.TargetFilePath); + modified.Should().BeSameAs(original); modified.TargetFilePath.Should().Be("bar"); } @@ -53,8 +52,7 @@ public void I_can_configure_the_command_line_arguments() var modified = original.WithArguments("qqq ppp"); // Assert - original.Should().BeEquivalentTo(modified, o => o.Excluding(c => c.Arguments)); - original.Arguments.Should().NotBe(modified.Arguments); + modified.Should().BeSameAs(original); modified.Arguments.Should().Be("qqq ppp"); } @@ -68,8 +66,7 @@ public void I_can_configure_the_command_line_arguments_by_passing_an_array() var modified = original.WithArguments(["-a", "foo bar"]); // Assert - original.Should().BeEquivalentTo(modified, o => o.Excluding(c => c.Arguments)); - original.Arguments.Should().NotBe(modified.Arguments); + modified.Should().BeSameAs(original); modified.Arguments.Should().Be("-a \"foo bar\""); } @@ -90,8 +87,7 @@ public void I_can_configure_the_command_line_arguments_using_a_builder() ); // Assert - original.Should().BeEquivalentTo(modified, o => o.Excluding(c => c.Arguments)); - original.Arguments.Should().NotBe(modified.Arguments); + modified.Should().BeSameAs(original); modified .Arguments.Should() .Be("-a \"foo bar\" \"\\\"foo\\\\bar\\\"\" 3.14 foo bar -5 89.13"); @@ -107,8 +103,7 @@ public void I_can_configure_the_working_directory() var modified = original.WithWorkingDirectory("new"); // Assert - original.Should().BeEquivalentTo(modified, o => o.Excluding(c => c.WorkingDirPath)); - original.WorkingDirPath.Should().NotBe(modified.WorkingDirPath); + modified.Should().BeSameAs(original); modified.WorkingDirPath.Should().Be("new"); } @@ -124,8 +119,7 @@ public void I_can_configure_the_resource_policy() ); // Assert - original.Should().BeEquivalentTo(modified, o => o.Excluding(c => c.ResourcePolicy)); - original.ResourcePolicy.Should().NotBe(modified.ResourcePolicy); + modified.Should().BeSameAs(original); modified .ResourcePolicy.Should() .BeEquivalentTo(new ResourcePolicy(ProcessPriorityClass.High, 0x1, 1024, 2048)); @@ -146,8 +140,7 @@ public void I_can_configure_the_resource_policy_using_a_builder() ); // Assert - original.Should().BeEquivalentTo(modified, o => o.Excluding(c => c.ResourcePolicy)); - original.ResourcePolicy.Should().NotBe(modified.ResourcePolicy); + modified.Should().BeSameAs(original); modified .ResourcePolicy.Should() .BeEquivalentTo(new ResourcePolicy(ProcessPriorityClass.High, 0x1, 1024, 2048)); @@ -165,8 +158,7 @@ public void I_can_configure_the_user_credentials() ); // Assert - original.Should().BeEquivalentTo(modified, o => o.Excluding(c => c.Credentials)); - original.Credentials.Should().NotBe(modified.Credentials); + modified.Should().BeSameAs(original); modified .Credentials.Should() .BeEquivalentTo(new Credentials("domain", "username", "password", true)); @@ -184,8 +176,7 @@ public void I_can_configure_the_user_credentials_using_a_builder() ); // Assert - original.Should().BeEquivalentTo(modified, o => o.Excluding(c => c.Credentials)); - original.Credentials.Should().NotBe(modified.Credentials); + modified.Should().BeSameAs(original); modified .Credentials.Should() .BeEquivalentTo(new Credentials("domain", "username", "password", true)); @@ -203,8 +194,7 @@ public void I_can_configure_the_environment_variables() ); // Assert - original.Should().BeEquivalentTo(modified, o => o.Excluding(c => c.EnvironmentVariables)); - original.EnvironmentVariables.Should().NotBeEquivalentTo(modified.EnvironmentVariables); + modified.Should().BeSameAs(original); modified .EnvironmentVariables.Should() .BeEquivalentTo( @@ -226,8 +216,7 @@ public void I_can_configure_the_environment_variables_using_a_builder() ); // Assert - original.Should().BeEquivalentTo(modified, o => o.Excluding(c => c.EnvironmentVariables)); - original.EnvironmentVariables.Should().NotBeEquivalentTo(modified.EnvironmentVariables); + modified.Should().BeSameAs(original); modified .EnvironmentVariables.Should() .BeEquivalentTo( @@ -251,8 +240,7 @@ public void I_can_configure_the_result_validation_strategy() var modified = original.WithValidation(CommandResultValidation.None); // Assert - original.Should().BeEquivalentTo(modified, o => o.Excluding(c => c.Validation)); - original.Validation.Should().NotBe(modified.Validation); + modified.Should().BeSameAs(original); modified.Validation.Should().Be(CommandResultValidation.None); } @@ -263,11 +251,12 @@ public void I_can_configure_the_stdin_pipe() var original = Cli.Wrap("foo").WithStandardInputPipe(PipeSource.Null); // Act - var modified = original.WithStandardInputPipe(PipeSource.FromString("new")); + var modifiedPipeSource = PipeSource.FromString("new"); + var modified = original.WithStandardInputPipe(modifiedPipeSource); // Assert - original.Should().BeEquivalentTo(modified, o => o.Excluding(c => c.StandardInputPipe)); - original.StandardInputPipe.Should().NotBeSameAs(modified.StandardInputPipe); + modified.Should().BeSameAs(original); + modified.StandardInputPipe.Should().BeSameAs(modifiedPipeSource); } [Fact] @@ -277,11 +266,12 @@ public void I_can_configure_the_stdout_pipe() var original = Cli.Wrap("foo").WithStandardOutputPipe(PipeTarget.Null); // Act - var modified = original.WithStandardOutputPipe(PipeTarget.ToStream(Stream.Null)); + var modifiedPipeTarget = PipeTarget.ToStream(Stream.Null); + var modified = original.WithStandardOutputPipe(modifiedPipeTarget); // Assert - original.Should().BeEquivalentTo(modified, o => o.Excluding(c => c.StandardOutputPipe)); - original.StandardOutputPipe.Should().NotBeSameAs(modified.StandardOutputPipe); + modified.Should().BeSameAs(original); + modified.StandardOutputPipe.Should().BeSameAs(modifiedPipeTarget); } [Fact] @@ -291,10 +281,11 @@ public void I_can_configure_the_stderr_pipe() var original = Cli.Wrap("foo").WithStandardErrorPipe(PipeTarget.Null); // Act - var modified = original.WithStandardErrorPipe(PipeTarget.ToStream(Stream.Null)); + var modifiedPipeTarget = PipeTarget.ToStream(Stream.Null); + var modified = original.WithStandardErrorPipe(modifiedPipeTarget); // Assert - original.Should().BeEquivalentTo(modified, o => o.Excluding(c => c.StandardErrorPipe)); - original.StandardErrorPipe.Should().NotBeSameAs(modified.StandardErrorPipe); + modified.Should().BeSameAs(original); + modified.StandardErrorPipe.Should().BeSameAs(modifiedPipeTarget); } } diff --git a/CliWrap.Tests/ValidationSpecs.cs b/CliWrap.Tests/ValidationSpecs.cs index edc8a084..6be4b39b 100644 --- a/CliWrap.Tests/ValidationSpecs.cs +++ b/CliWrap.Tests/ValidationSpecs.cs @@ -21,7 +21,7 @@ public async Task I_can_try_to_execute_a_command_and_get_an_error_if_it_returns_ ); ex.ExitCode.Should().Be(1); - ex.Command.Should().BeEquivalentTo(cmd); + ex.Command.Should().BeEquivalentTo(cmd.Configuration); testOutput.WriteLine(ex.ToString()); } @@ -39,7 +39,7 @@ public async Task I_can_try_to_execute_a_command_with_buffering_and_get_a_detail ex.Message.Should().Contain("Exit code set to 1"); // expected stderr ex.ExitCode.Should().Be(1); - ex.Command.Should().BeEquivalentTo(cmd); + ex.Command.Should().BeEquivalentTo(cmd.Configuration); testOutput.WriteLine(ex.ToString()); } diff --git a/CliWrap/Command.Execution.cs b/CliWrap/Command.Execution.cs index 4346fe89..b8f82920 100644 --- a/CliWrap/Command.Execution.cs +++ b/CliWrap/Command.Execution.cs @@ -20,11 +20,13 @@ public partial class Command // BAT or CMD file, even if it's on the PATH. If the extension is specified, it will work in both cases. private string GetOptimallyQualifiedTargetFilePath() { + var configuration = _configuration; // Snapshot to avoid race conditions + // Currently, we only need this workaround for script files on Windows, so short-circuit // if we are on a different platform. if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - return TargetFilePath; + return configuration.TargetFilePath; } // Don't do anything for fully qualified paths or paths that already have an extension specified. @@ -33,11 +35,11 @@ private string GetOptimallyQualifiedTargetFilePath() // strings like 'c:foo.txt' (which is relative to the current directory on drive C), but it's good // enough for our purposes and the alternative is only available on .NET Standard 2.1+. if ( - Path.IsPathRooted(TargetFilePath) - || !string.IsNullOrWhiteSpace(Path.GetExtension(TargetFilePath)) + Path.IsPathRooted(configuration.TargetFilePath) + || !string.IsNullOrWhiteSpace(Path.GetExtension(configuration.TargetFilePath)) ) { - return TargetFilePath; + return configuration.TargetFilePath; } static IEnumerable GetProbeDirectoryPaths() @@ -68,19 +70,22 @@ static IEnumerable GetProbeDirectoryPaths() return ( from probeDirPath in GetProbeDirectoryPaths() where Directory.Exists(probeDirPath) - select Path.Combine(probeDirPath, TargetFilePath) into baseFilePath + select Path.Combine(probeDirPath, configuration.TargetFilePath) into baseFilePath from extension in new[] { "exe", "cmd", "bat" } select Path.ChangeExtension(baseFilePath, extension) - ).FirstOrDefault(File.Exists) ?? TargetFilePath; + ).FirstOrDefault(File.Exists) ?? configuration.TargetFilePath; } - private ProcessStartInfo CreateStartInfo() + /// + /// Creates and configures a new instance for executing the command. + /// + protected virtual ProcessStartInfo CreateStartInfo(ICommandConfiguration configuration) { var startInfo = new ProcessStartInfo { FileName = GetOptimallyQualifiedTargetFilePath(), - Arguments = Arguments, - WorkingDirectory = WorkingDirPath, + Arguments = configuration.Arguments, + WorkingDirectory = configuration.WorkingDirPath, RedirectStandardInput = true, RedirectStandardOutput = true, RedirectStandardError = true, @@ -98,17 +103,17 @@ private ProcessStartInfo CreateStartInfo() { // Disable CA1416 because we're handling an exception that is thrown by the property setters #pragma warning disable CA1416 - if (Credentials.Domain is not null) - startInfo.Domain = Credentials.Domain; + if (configuration.Credentials.Domain is not null) + startInfo.Domain = configuration.Credentials.Domain; - if (Credentials.UserName is not null) - startInfo.UserName = Credentials.UserName; + if (configuration.Credentials.UserName is not null) + startInfo.UserName = configuration.Credentials.UserName; - if (Credentials.Password is not null) - startInfo.Password = Credentials.Password.ToSecureString(); + if (configuration.Credentials.Password is not null) + startInfo.Password = configuration.Credentials.Password.ToSecureString(); - if (Credentials.LoadUserProfile) - startInfo.LoadUserProfile = Credentials.LoadUserProfile; + if (configuration.Credentials.LoadUserProfile) + startInfo.LoadUserProfile = configuration.Credentials.LoadUserProfile; #pragma warning restore CA1416 } catch (NotSupportedException ex) @@ -121,7 +126,7 @@ private ProcessStartInfo CreateStartInfo() } // Set environment variables - foreach (var (key, value) in EnvironmentVariables) + foreach (var (key, value) in configuration.EnvironmentVariables) { if (value is not null) { @@ -148,8 +153,8 @@ private async Task PipeStandardInputAsync( { try { - await StandardInputPipe - .CopyToAsync(process.StandardInput, cancellationToken) + await _configuration + .StandardInputPipe.CopyToAsync(process.StandardInput, cancellationToken) // Some streams do not support cancellation, so we add a fallback that // drops the task and returns early. // This is important with stdin because the process might finish before @@ -174,8 +179,8 @@ private async Task PipeStandardOutputAsync( { await using (process.StandardOutput.ToAsyncDisposable()) { - await StandardOutputPipe - .CopyFromAsync(process.StandardOutput, cancellationToken) + await _configuration + .StandardOutputPipe.CopyFromAsync(process.StandardOutput, cancellationToken) .ConfigureAwait(false); } } @@ -187,8 +192,8 @@ private async Task PipeStandardErrorAsync( { await using (process.StandardError.ToAsyncDisposable()) { - await StandardErrorPipe - .CopyFromAsync(process.StandardError, cancellationToken) + await _configuration + .StandardErrorPipe.CopyFromAsync(process.StandardError, cancellationToken) .ConfigureAwait(false); } } @@ -199,6 +204,8 @@ private async Task ExecuteAsync( CancellationToken gracefulCancellationToken = default ) { + var configuration = _configuration; // Snapshot to avoid race conditions + using var _ = process; // Additional cancellation to ensure we don't wait forever for the process to terminate @@ -273,16 +280,16 @@ private async Task ExecuteAsync( ); // Validate the exit code if required - if (process.ExitCode != 0 && Validation.IsZeroExitCodeValidationEnabled()) + if (process.ExitCode != 0 && configuration.Validation.IsZeroExitCodeValidationEnabled()) { throw new CommandExecutionException( - this, + configuration, process.ExitCode, $""" Command execution failed because the underlying process ({process.Name}#{process.Id}) returned a non-zero exit code ({process.ExitCode}). Command: - {TargetFilePath} {Arguments} + {configuration.TargetFilePath} {configuration.Arguments} You can suppress this validation by calling `{nameof(WithValidation)}({nameof( CommandResultValidation @@ -306,7 +313,8 @@ public CommandTask ExecuteAsync( CancellationToken gracefulCancellationToken ) { - var process = new ProcessEx(CreateStartInfo()); + var configuration = _configuration; // Snapshot to avoid race conditions + var process = new ProcessEx(CreateStartInfo(configuration)); // This method may fail, and we want to propagate the exceptions immediately instead // of wrapping them in a task, so it needs to be executed in a synchronous context. @@ -317,17 +325,17 @@ CancellationToken gracefulCancellationToken { // Disable CA1416 because we're handling an exception that is thrown by the property setters #pragma warning disable CA1416 - if (ResourcePolicy.Priority is not null) - p.PriorityClass = ResourcePolicy.Priority.Value; + if (configuration.ResourcePolicy.Priority is not null) + p.PriorityClass = configuration.ResourcePolicy.Priority.Value; - if (ResourcePolicy.Affinity is not null) - p.ProcessorAffinity = ResourcePolicy.Affinity.Value; + if (configuration.ResourcePolicy.Affinity is not null) + p.ProcessorAffinity = configuration.ResourcePolicy.Affinity.Value; - if (ResourcePolicy.MinWorkingSet is not null) - p.MinWorkingSet = ResourcePolicy.MinWorkingSet.Value; + if (configuration.ResourcePolicy.MinWorkingSet is not null) + p.MinWorkingSet = configuration.ResourcePolicy.MinWorkingSet.Value; - if (ResourcePolicy.MaxWorkingSet is not null) - p.MaxWorkingSet = ResourcePolicy.MaxWorkingSet.Value; + if (configuration.ResourcePolicy.MaxWorkingSet is not null) + p.MaxWorkingSet = configuration.ResourcePolicy.MaxWorkingSet.Value; #pragma warning restore CA1416 } catch (NotSupportedException ex) diff --git a/CliWrap/Command.cs b/CliWrap/Command.cs index 07524a20..58979127 100644 --- a/CliWrap/Command.cs +++ b/CliWrap/Command.cs @@ -1,8 +1,6 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Diagnostics.Contracts; -using System.IO; using CliWrap.Builders; namespace CliWrap; @@ -10,128 +8,141 @@ namespace CliWrap; /// /// Instructions for running a process. /// -public partial class Command( - string targetFilePath, - string arguments, - string workingDirPath, - ResourcePolicy resourcePolicy, - Credentials credentials, - IReadOnlyDictionary environmentVariables, - CommandResultValidation validation, - PipeSource standardInputPipe, - PipeTarget standardOutputPipe, - PipeTarget standardErrorPipe -) : ICommandConfiguration +public partial class Command : ICommandConfiguration { + private CommandConfiguration _configuration; + /// - /// Initializes an instance of . + /// Initializes an instance of using the specified configuration values. /// - public Command(string targetFilePath) + public Command( + string targetFilePath, + string arguments, + string workingDirPath, + ResourcePolicy resourcePolicy, + Credentials credentials, + IReadOnlyDictionary environmentVariables, + CommandResultValidation validation, + PipeSource standardInputPipe, + PipeTarget standardOutputPipe, + PipeTarget standardErrorPipe + ) : this( - targetFilePath, - string.Empty, - Directory.GetCurrentDirectory(), - ResourcePolicy.Default, - Credentials.Default, - new Dictionary(), - CommandResultValidation.ZeroExitCode, - PipeSource.Null, - PipeTarget.Null, - PipeTarget.Null + new CommandConfiguration( + targetFilePath, + arguments, + workingDirPath, + resourcePolicy, + credentials, + environmentVariables, + validation, + standardInputPipe, + standardOutputPipe, + standardErrorPipe + ) ) { } - /// - public string TargetFilePath { get; } = targetFilePath; + /// + /// Initializes an instance of using the default configuration. + /// + public Command(string targetFilePath) + : this(new CommandConfiguration(targetFilePath)) { } - /// - public string Arguments { get; } = arguments; + /// + /// Initializes an instance of using the configuration from + /// the specified instance. + /// + public Command(ICommandConfiguration configuration) + { + _configuration = new( + configuration.TargetFilePath, + configuration.Arguments, + configuration.WorkingDirPath, + configuration.ResourcePolicy, + configuration.Credentials, + configuration.EnvironmentVariables, + configuration.Validation, + configuration.StandardInputPipe, + configuration.StandardOutputPipe, + configuration.StandardErrorPipe + ); + } - /// - public string WorkingDirPath { get; } = workingDirPath; + /// + /// Gets the instance that contains the configuration values for this command. + /// + public ICommandConfiguration Configuration => _configuration; - /// - public ResourcePolicy ResourcePolicy { get; } = resourcePolicy; + // TODO: (breaking change) remove below delegating implementation of ICommandConfiguration - /// - public Credentials Credentials { get; } = credentials; + /// + public string TargetFilePath => _configuration.TargetFilePath; - /// - public IReadOnlyDictionary EnvironmentVariables { get; } = - environmentVariables; + /// + public string Arguments => _configuration.Arguments; - /// - public CommandResultValidation Validation { get; } = validation; + /// + public string WorkingDirPath => _configuration.WorkingDirPath; - /// - public PipeSource StandardInputPipe { get; } = standardInputPipe; + /// + public ResourcePolicy ResourcePolicy => _configuration.ResourcePolicy; - /// - public PipeTarget StandardOutputPipe { get; } = standardOutputPipe; + /// + public Credentials Credentials => _configuration.Credentials; - /// - public PipeTarget StandardErrorPipe { get; } = standardErrorPipe; + /// + public IReadOnlyDictionary EnvironmentVariables => + _configuration.EnvironmentVariables; + + /// + public CommandResultValidation Validation => _configuration.Validation; + + /// + public PipeSource StandardInputPipe => _configuration.StandardInputPipe; + + /// + public PipeTarget StandardOutputPipe => _configuration.StandardOutputPipe; + + /// + public PipeTarget StandardErrorPipe => _configuration.StandardErrorPipe; /// - /// Creates a copy of this command, setting the target file path to the specified value. + /// Sets the target file path to the specified value. /// - [Pure] - public Command WithTargetFile(string targetFilePath) => - new( - targetFilePath, - Arguments, - WorkingDirPath, - ResourcePolicy, - Credentials, - EnvironmentVariables, - Validation, - StandardInputPipe, - StandardOutputPipe, - StandardErrorPipe - ); + public Command WithTargetFile(string targetFilePath) + { + _configuration = _configuration with { TargetFilePath = targetFilePath }; + return this; + } /// - /// Creates a copy of this command, setting the arguments to the specified value. + /// Sets the arguments to the specified value. /// /// /// Avoid using this overload, as it requires the arguments to be escaped manually. /// Formatting errors may lead to unexpected bugs and security vulnerabilities. /// - [Pure] - public Command WithArguments(string arguments) => - new( - TargetFilePath, - arguments, - WorkingDirPath, - ResourcePolicy, - Credentials, - EnvironmentVariables, - Validation, - StandardInputPipe, - StandardOutputPipe, - StandardErrorPipe - ); + public Command WithArguments(string arguments) + { + _configuration = _configuration with { Arguments = arguments }; + return this; + } /// - /// Creates a copy of this command, setting the arguments to the value - /// obtained by formatting the specified enumeration. + /// Sets the arguments to the value obtained by formatting the specified enumeration. /// - [Pure] public Command WithArguments(IEnumerable arguments, bool escape) => WithArguments(args => args.Add(arguments, escape)); /// - /// Creates a copy of this command, setting the arguments to the value - /// obtained by formatting the specified enumeration. + /// Sets the arguments to the value obtained by formatting the specified enumeration. /// // TODO: (breaking change) remove in favor of optional parameter - [Pure] public Command WithArguments(IEnumerable arguments) => WithArguments(arguments, true); /// - /// Creates a copy of this command, setting the arguments to the value - /// configured by the specified delegate. + /// Sets the arguments to the value configured by the specified delegate. /// - [Pure] public Command WithArguments(Action configure) { var builder = new ArgumentsBuilder(); @@ -141,46 +152,26 @@ public Command WithArguments(Action configure) } /// - /// Creates a copy of this command, setting the working directory path to the specified value. + /// Sets the working directory path to the specified value. /// - [Pure] - public Command WithWorkingDirectory(string workingDirPath) => - new( - TargetFilePath, - Arguments, - workingDirPath, - ResourcePolicy, - Credentials, - EnvironmentVariables, - Validation, - StandardInputPipe, - StandardOutputPipe, - StandardErrorPipe - ); + public Command WithWorkingDirectory(string workingDirPath) + { + _configuration = _configuration with { WorkingDirPath = workingDirPath }; + return this; + } /// - /// Creates a copy of this command, setting the resource policy to the specified value. + /// Sets the resource policy to the specified value. /// - [Pure] - public Command WithResourcePolicy(ResourcePolicy resourcePolicy) => - new( - TargetFilePath, - Arguments, - WorkingDirPath, - resourcePolicy, - Credentials, - EnvironmentVariables, - Validation, - StandardInputPipe, - StandardOutputPipe, - StandardErrorPipe - ); + public Command WithResourcePolicy(ResourcePolicy resourcePolicy) + { + _configuration = _configuration with { ResourcePolicy = resourcePolicy }; + return this; + } /// - /// Creates a copy of this command, setting the resource policy to the value - /// configured by the specified delegate. + /// Sets the resource policy to the value configured by the specified delegate. /// - [Pure] public Command WithResourcePolicy(Action configure) { var builder = new ResourcePolicyBuilder(); @@ -190,28 +181,17 @@ public Command WithResourcePolicy(Action configure) } /// - /// Creates a copy of this command, setting the user credentials to the specified value. + /// Sets the user credentials to the specified value. /// - [Pure] - public Command WithCredentials(Credentials credentials) => - new( - TargetFilePath, - Arguments, - WorkingDirPath, - ResourcePolicy, - credentials, - EnvironmentVariables, - Validation, - StandardInputPipe, - StandardOutputPipe, - StandardErrorPipe - ); + public Command WithCredentials(Credentials credentials) + { + _configuration = _configuration with { Credentials = credentials }; + return this; + } /// - /// Creates a copy of this command, setting the user credentials to the value - /// configured by the specified delegate. + /// Sets the user credentials to the value configured by the specified delegate. /// - [Pure] public Command WithCredentials(Action configure) { var builder = new CredentialsBuilder(); @@ -221,30 +201,19 @@ public Command WithCredentials(Action configure) } /// - /// Creates a copy of this command, setting the environment variables to the specified value. + /// Sets the environment variables to the specified value. /// - [Pure] public Command WithEnvironmentVariables( IReadOnlyDictionary environmentVariables - ) => - new( - TargetFilePath, - Arguments, - WorkingDirPath, - ResourcePolicy, - Credentials, - environmentVariables, - Validation, - StandardInputPipe, - StandardOutputPipe, - StandardErrorPipe - ); + ) + { + _configuration = _configuration with { EnvironmentVariables = environmentVariables }; + return this; + } /// - /// Creates a copy of this command, setting the environment variables to the value - /// configured by the specified delegate. + /// Sets the environment variables to the value configured by the specified delegate. /// - [Pure] public Command WithEnvironmentVariables(Action configure) { var builder = new EnvironmentVariablesBuilder(); @@ -254,78 +223,46 @@ public Command WithEnvironmentVariables(Action conf } /// - /// Creates a copy of this command, setting the validation options to the specified value. + /// Sets the validation options to the specified value. /// - [Pure] - public Command WithValidation(CommandResultValidation validation) => - new( - TargetFilePath, - Arguments, - WorkingDirPath, - ResourcePolicy, - Credentials, - EnvironmentVariables, - validation, - StandardInputPipe, - StandardOutputPipe, - StandardErrorPipe - ); + public Command WithValidation(CommandResultValidation validation) + { + _configuration = _configuration with { Validation = validation }; + return this; + } /// - /// Creates a copy of this command, setting the standard input pipe to the specified source. + /// Sets the standard input pipe to the specified source. /// - [Pure] - public Command WithStandardInputPipe(PipeSource source) => - new( - TargetFilePath, - Arguments, - WorkingDirPath, - ResourcePolicy, - Credentials, - EnvironmentVariables, - Validation, - source, - StandardOutputPipe, - StandardErrorPipe - ); + public Command WithStandardInputPipe(PipeSource source) + { + _configuration = _configuration with { StandardInputPipe = source }; + return this; + } /// - /// Creates a copy of this command, setting the standard output pipe to the specified target. + /// Sets the standard output pipe to the specified target. /// - [Pure] - public Command WithStandardOutputPipe(PipeTarget target) => - new( - TargetFilePath, - Arguments, - WorkingDirPath, - ResourcePolicy, - Credentials, - EnvironmentVariables, - Validation, - StandardInputPipe, - target, - StandardErrorPipe - ); + public Command WithStandardOutputPipe(PipeTarget target) + { + _configuration = _configuration with { StandardOutputPipe = target }; + return this; + } /// - /// Creates a copy of this command, setting the standard error pipe to the specified target. + /// Sets the standard error pipe to the specified target. /// - [Pure] - public Command WithStandardErrorPipe(PipeTarget target) => - new( - TargetFilePath, - Arguments, - WorkingDirPath, - ResourcePolicy, - Credentials, - EnvironmentVariables, - Validation, - StandardInputPipe, - StandardOutputPipe, - target - ); + public Command WithStandardErrorPipe(PipeTarget target) + { + _configuration = _configuration with { StandardErrorPipe = target }; + return this; + } /// [ExcludeFromCodeCoverage] - public override string ToString() => $"{TargetFilePath} {Arguments}"; + public override string ToString() + { + var configuration = _configuration; // Snapshot to avoid race conditions + return $"{configuration.TargetFilePath} {configuration.Arguments}"; + } } diff --git a/CliWrap/CommandConfiguration.cs b/CliWrap/CommandConfiguration.cs new file mode 100644 index 00000000..237d270e --- /dev/null +++ b/CliWrap/CommandConfiguration.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using System.IO; + +namespace CliWrap; + +record struct CommandConfiguration( + string TargetFilePath, + string Arguments, + string WorkingDirPath, + ResourcePolicy ResourcePolicy, + Credentials Credentials, + IReadOnlyDictionary EnvironmentVariables, + CommandResultValidation Validation, + PipeSource StandardInputPipe, + PipeTarget StandardOutputPipe, + PipeTarget StandardErrorPipe +) : ICommandConfiguration +{ + public CommandConfiguration(string targetFilePath) + : this( + targetFilePath, + string.Empty, + Directory.GetCurrentDirectory(), + ResourcePolicy.Default, + Credentials.Default, + new Dictionary(), + CommandResultValidation.ZeroExitCode, + PipeSource.Null, + PipeTarget.Null, + PipeTarget.Null + ) { } +} diff --git a/CliWrap/Utils/ProcessEx.cs b/CliWrap/Utils/ProcessEx.cs index ff9b9ac5..db3bd8a1 100644 --- a/CliWrap/Utils/ProcessEx.cs +++ b/CliWrap/Utils/ProcessEx.cs @@ -26,11 +26,20 @@ internal class ProcessEx(ProcessStartInfo startInfo) : IDisposable // We are purposely using Stream instead of StreamWriter/StreamReader to push the concerns of // writing and reading to PipeSource/PipeTarget at the higher level. - public Stream StandardInput => _nativeProcess.StandardInput.BaseStream; - - public Stream StandardOutput => _nativeProcess.StandardOutput.BaseStream; - - public Stream StandardError => _nativeProcess.StandardError.BaseStream; + public Stream StandardInput => + _nativeProcess.StartInfo.RedirectStandardInput + ? _nativeProcess.StandardInput.BaseStream + : Stream.Null; + + public Stream StandardOutput => + _nativeProcess.StartInfo.RedirectStandardOutput + ? _nativeProcess.StandardOutput.BaseStream + : Stream.Null; + + public Stream StandardError => + _nativeProcess.StartInfo.RedirectStandardError + ? _nativeProcess.StandardError.BaseStream + : Stream.Null; // We have to keep track of StartTime ourselves because it becomes inaccessible after the process exits // https://github.com/Tyrrrz/CliWrap/issues/93