diff --git a/MedallionShell.Tests/IoCommandTest.cs b/MedallionShell.Tests/IoCommandTest.cs new file mode 100644 index 0000000..78a6996 --- /dev/null +++ b/MedallionShell.Tests/IoCommandTest.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Medallion.Shell.Tests; +using NUnit.Framework; + +namespace MedallionShell.Tests +{ + using static UnitTestHelpers; + + public class IOCommandTest + { + [Test] + public void TestStandardOutCannotBeAccessedAfterRedirectingIt() + { + var output = new List(); + var command = TestShell.Run(SampleCommand, "argecho", "a"); + var ioCommand = command.RedirectTo(output); + + var errorMessage = Assert.Throws(() => ioCommand.StandardOutput.GetHashCode()).Message; + errorMessage.ShouldEqual("StandardOutput is unavailable because it is already being piped to System.Collections.Generic.List`1[System.String]"); + + Assert.DoesNotThrow(() => command.StandardOutput.GetHashCode()); + + Assert.Throws(() => ioCommand.Result.StandardOutput.GetHashCode()) + .Message + .ShouldEqual(errorMessage); + Assert.Throws(() => command.Result.StandardOutput.GetHashCode()); + + CollectionAssert.AreEqual(new[] { "a" }, output); + ioCommand.Result.StandardError.ShouldEqual(command.Result.StandardError).ShouldEqual(string.Empty); + } + + [Test] + public void TestStandardErrorCannotBeAccessedAfterRedirectingIt() + { + var output = new List(); + var command = TestShell.Run(SampleCommand, "argecho", "a"); + var ioCommand = command.RedirectStandardErrorTo(output); + + var errorMessage = Assert.Throws(() => ioCommand.StandardError.GetHashCode()).Message; + errorMessage.ShouldEqual("StandardError is unavailable because it is already being piped to System.Collections.Generic.List`1[System.String]"); + + Assert.DoesNotThrow(() => command.StandardError.GetHashCode()); + + Assert.Throws(() => ioCommand.Result.StandardError.GetHashCode()) + .Message + .ShouldEqual(errorMessage); + Assert.Throws(() => command.Result.StandardError.GetHashCode()); + + Assert.IsEmpty(output); + ioCommand.Result.StandardOutput.ShouldEqual(command.Result.StandardOutput).ShouldEqual("a\r\n"); + } + + [Test] + public void TestStandardInputCannotBeAccessedAfterRedirectingIt() + { + var command = TestShell.Run(SampleCommand, "echo"); + var ioCommand = command.RedirectFrom(new[] { "a" }); + + var errorMessage = Assert.Throws(() => ioCommand.StandardInput.GetHashCode()).Message; + errorMessage.ShouldEqual("StandardInput is unavailable because it is already being piped from System.String[]"); + + Assert.DoesNotThrow(() => command.StandardInput.GetHashCode()); + + ioCommand.Result.StandardOutput.ShouldEqual(command.Result.StandardOutput).ShouldEqual("a\r\n"); + ioCommand.Result.StandardError.ShouldEqual(command.Result.StandardError).ShouldEqual(string.Empty); + } + } +} diff --git a/MedallionShell/Command.cs b/MedallionShell/Command.cs index 997ed4a..49f30be 100644 --- a/MedallionShell/Command.cs +++ b/MedallionShell/Command.cs @@ -136,7 +136,7 @@ public Command RedirectTo(Stream stream) { Throw.IfNull(stream, nameof(stream)); - return new IoCommand(this, this.StandardOutput.PipeToAsync(stream, leaveStreamOpen: true), ">", stream); + return new IOCommand(this, this.StandardOutput.PipeToAsync(stream, leaveStreamOpen: true), StandardIOStream.Out, stream); } /// @@ -148,7 +148,7 @@ public Command RedirectStandardErrorTo(Stream stream) { Throw.IfNull(stream, nameof(stream)); - return new IoCommand(this, this.StandardError.PipeToAsync(stream, leaveStreamOpen: true), "2>", stream); + return new IOCommand(this, this.StandardError.PipeToAsync(stream, leaveStreamOpen: true), StandardIOStream.Error, stream); } /// @@ -160,7 +160,7 @@ public Command RedirectFrom(Stream stream) { Throw.IfNull(stream, nameof(stream)); - return new IoCommand(this, this.StandardInput.PipeFromAsync(stream, leaveStreamOpen: true), "<", stream); + return new IOCommand(this, this.StandardInput.PipeFromAsync(stream, leaveStreamOpen: true), StandardIOStream.In, stream); } /// @@ -172,7 +172,7 @@ public Command RedirectTo(FileInfo file) { Throw.IfNull(file, nameof(file)); - return new IoCommand(this, this.StandardOutput.PipeToAsync(file), ">", file); + return new IOCommand(this, this.StandardOutput.PipeToAsync(file), StandardIOStream.Out, file); } /// @@ -184,7 +184,7 @@ public Command RedirectStandardErrorTo(FileInfo file) { Throw.IfNull(file, nameof(file)); - return new IoCommand(this, this.StandardError.PipeToAsync(file), "2>", file); + return new IOCommand(this, this.StandardError.PipeToAsync(file), StandardIOStream.Error, file); } /// @@ -196,7 +196,7 @@ public Command RedirectFrom(FileInfo file) { Throw.IfNull(file, nameof(file)); - return new IoCommand(this, this.StandardInput.PipeFromAsync(file), "<", file); + return new IOCommand(this, this.StandardInput.PipeFromAsync(file), StandardIOStream.In, file); } /// @@ -208,7 +208,7 @@ public Command RedirectTo(ICollection lines) { Throw.IfNull(lines, nameof(lines)); - return new IoCommand(this, this.StandardOutput.PipeToAsync(lines), ">", lines.GetType()); + return new IOCommand(this, this.StandardOutput.PipeToAsync(lines), StandardIOStream.Out, lines.GetType()); } /// @@ -220,7 +220,7 @@ public Command RedirectStandardErrorTo(ICollection lines) { Throw.IfNull(lines, nameof(lines)); - return new IoCommand(this, this.StandardError.PipeToAsync(lines), "2>", lines.GetType()); + return new IOCommand(this, this.StandardError.PipeToAsync(lines), StandardIOStream.Error, lines.GetType()); } /// @@ -232,7 +232,7 @@ public Command RedirectFrom(IEnumerable lines) { Throw.IfNull(lines, nameof(lines)); - return new IoCommand(this, this.StandardInput.PipeFromAsync(lines), "<", lines.GetType()); + return new IOCommand(this, this.StandardInput.PipeFromAsync(lines), StandardIOStream.In, lines.GetType()); } /// @@ -244,7 +244,7 @@ public Command RedirectTo(ICollection chars) { Throw.IfNull(chars, nameof(chars)); - return new IoCommand(this, this.StandardOutput.PipeToAsync(chars), ">", chars.GetType()); + return new IOCommand(this, this.StandardOutput.PipeToAsync(chars), StandardIOStream.Out, chars.GetType()); } /// @@ -256,7 +256,7 @@ public Command RedirectStandardErrorTo(ICollection chars) { Throw.IfNull(chars, nameof(chars)); - return new IoCommand(this, this.StandardError.PipeToAsync(chars), "2>", chars.GetType()); + return new IOCommand(this, this.StandardError.PipeToAsync(chars), StandardIOStream.Error, chars.GetType()); } /// @@ -268,7 +268,7 @@ public Command RedirectFrom(IEnumerable chars) { Throw.IfNull(chars, nameof(chars)); - return new IoCommand(this, this.StandardInput.PipeFromAsync(chars), "<", chars.GetType()); + return new IOCommand(this, this.StandardInput.PipeFromAsync(chars), StandardIOStream.In, chars.GetType()); } /// @@ -280,7 +280,7 @@ public Command RedirectTo(TextWriter writer) { Throw.IfNull(writer, nameof(writer)); - return new IoCommand(this, this.StandardOutput.PipeToAsync(writer, leaveWriterOpen: true), ">", writer); + return new IOCommand(this, this.StandardOutput.PipeToAsync(writer, leaveWriterOpen: true), StandardIOStream.Out, writer); } /// @@ -292,7 +292,7 @@ public Command RedirectStandardErrorTo(TextWriter writer) { Throw.IfNull(writer, nameof(writer)); - return new IoCommand(this, this.StandardError.PipeToAsync(writer, leaveWriterOpen: true), "2>", writer); + return new IOCommand(this, this.StandardError.PipeToAsync(writer, leaveWriterOpen: true), StandardIOStream.Error, writer); } /// @@ -304,7 +304,7 @@ public Command RedirectFrom(TextReader reader) { Throw.IfNull(reader, nameof(reader)); - return new IoCommand(this, this.StandardInput.PipeFromAsync(reader, leaveReaderOpen: true), "<", reader); + return new IOCommand(this, this.StandardInput.PipeFromAsync(reader, leaveReaderOpen: true), StandardIOStream.In, reader); } /// diff --git a/MedallionShell/CommandResult.cs b/MedallionShell/CommandResult.cs index 6313917..4773470 100644 --- a/MedallionShell/CommandResult.cs +++ b/MedallionShell/CommandResult.cs @@ -15,30 +15,35 @@ public sealed class CommandResult private readonly Lazy standardOutput, standardError; internal CommandResult(int exitCode, Command command) + : this(exitCode, () => command.StandardOutput.ReadToEnd(), () => command.StandardError.ReadToEnd()) + { + } + + internal CommandResult(int exitCode, Func standardOutput, Func standardError) { this.ExitCode = exitCode; - this.standardOutput = new Lazy(() => command.StandardOutput.ReadToEnd()); - this.standardError = new Lazy(() => command.StandardError.ReadToEnd()); + this.standardOutput = new Lazy(standardOutput); + this.standardError = new Lazy(standardError); } /// /// The exit code of the command's process /// - public int ExitCode { get; private set; } + public int ExitCode { get; } /// /// Returns true iff the exit code is 0 (indicating success) /// - public bool Success { get { return this.ExitCode == 0; } } + public bool Success => this.ExitCode == 0; /// /// If available, the full standard output text of the command /// - public string StandardOutput { get { return this.standardOutput.Value; } } + public string StandardOutput => this.standardOutput.Value; /// /// If available, the full standard error text of the command /// - public string StandardError { get { return this.standardError.Value; } } + public string StandardError => this.standardError.Value; } } diff --git a/MedallionShell/IoCommand.cs b/MedallionShell/IoCommand.cs index badfd61..600c328 100644 --- a/MedallionShell/IoCommand.cs +++ b/MedallionShell/IoCommand.cs @@ -6,26 +6,46 @@ namespace Medallion.Shell { - internal sealed class IoCommand : Command + internal sealed class IOCommand : Command { private readonly Command command; private readonly Task task; + private readonly StandardIOStream standardIOStream; // for toString - private readonly string @operator; private readonly object sourceOrSink; - public IoCommand(Command command, Task ioTask, string @operator, object sourceOrSink) + public IOCommand(Command command, Task ioTask, StandardIOStream standardIOStream, object sourceOrSink) { this.command = command; this.task = this.CreateTask(ioTask); - this.@operator = @operator; + this.standardIOStream = standardIOStream; this.sourceOrSink = sourceOrSink; } private async Task CreateTask(Task ioTask) { await ioTask.ConfigureAwait(false); - return await this.command.Task.ConfigureAwait(false); + var innerResult = await this.command.Task.ConfigureAwait(false); + + // We wrap the inner command's result so that we can apply our stream availability error + // checking (the Ignore() calls). However, we use the inner result's string values since + // accessing those consumes the stream and we want both this result and the inner result + // to have the value. + return new CommandResult( + innerResult.ExitCode, + standardOutput: () => + { + Ignore(this.StandardOutput); + return innerResult.StandardOutput; + }, + standardError: () => + { + Ignore(this.StandardError); + return innerResult.StandardError; + } + ); + + void Ignore(object ignored) { } } public override System.Diagnostics.Process Process @@ -41,20 +61,17 @@ public override IReadOnlyList Processes public override int ProcessId => this.command.ProcessId; public override IReadOnlyList ProcessIds => this.command.ProcessIds; - public override Streams.ProcessStreamWriter StandardInput - { - get { return this.command.StandardInput; } - } + public override Streams.ProcessStreamWriter StandardInput => this.standardIOStream != StandardIOStream.In + ? this.command.StandardInput + : throw new InvalidOperationException($"{nameof(this.StandardInput)} is unavailable because it is already being piped from {this.sourceOrSink}"); - public override Streams.ProcessStreamReader StandardOutput - { - get { return this.command.StandardOutput; } - } + public override Streams.ProcessStreamReader StandardOutput => this.standardIOStream != StandardIOStream.Out + ? this.command.StandardOutput + : throw new InvalidOperationException($"{nameof(this.StandardOutput)} is unavailable because it is already being piped to {this.sourceOrSink}"); - public override Streams.ProcessStreamReader StandardError - { - get { return this.command.StandardError; } - } + public override Streams.ProcessStreamReader StandardError => this.standardIOStream != StandardIOStream.Error + ? this.command.StandardError + : throw new InvalidOperationException($"{nameof(this.StandardError)} is unavailable because it is already being piped to {this.sourceOrSink}"); public override Task Task { @@ -66,11 +83,26 @@ public override void Kill() this.command.Kill(); } - public override string ToString() => $"{this.command} {this.@operator} {this.sourceOrSink}"; + public override string ToString() => $"{this.command} {ToString(this.standardIOStream)} {this.sourceOrSink}"; protected override void DisposeInternal() { this.command.As().Dispose(); } + + private static string ToString(StandardIOStream standardIOStream) => standardIOStream switch + { + StandardIOStream.In => "<", + StandardIOStream.Out => ">", + StandardIOStream.Error => "2>", + _ => throw new InvalidOperationException("should never get here") + }; + } + + internal enum StandardIOStream + { + In, + Out, + Error, } }