diff --git a/src/libraries/System.Console/src/System/ConsolePal.Unix.cs b/src/libraries/System.Console/src/System/ConsolePal.Unix.cs index 098601bfdbe92e..017c55e8c8a3ec 100644 --- a/src/libraries/System.Console/src/System/ConsolePal.Unix.cs +++ b/src/libraries/System.Console/src/System/ConsolePal.Unix.cs @@ -43,7 +43,8 @@ internal static class ConsolePal public static Stream OpenStandardInput() { - return new UnixConsoleStream(SafeFileHandleHelper.Open(() => Interop.Sys.Dup(Interop.Sys.FileDescriptors.STDIN_FILENO)), FileAccess.Read); + return new UnixConsoleStream(SafeFileHandleHelper.Open(() => Interop.Sys.Dup(Interop.Sys.FileDescriptors.STDIN_FILENO)), FileAccess.Read, + useReadLine: !Console.IsInputRedirected); } public static Stream OpenStandardOutput() @@ -68,7 +69,7 @@ public static Encoding OutputEncoding private static SyncTextReader? s_stdInReader; - private static SyncTextReader StdInReader + internal static SyncTextReader StdInReader { get { @@ -1410,15 +1411,19 @@ private sealed class UnixConsoleStream : ConsoleStream /// The file descriptor for the opened file. private readonly SafeFileHandle _handle; + private readonly bool _useReadLine; + /// Initialize the stream. /// The file handle wrapped by this stream. /// FileAccess.Read or FileAccess.Write. - internal UnixConsoleStream(SafeFileHandle handle, FileAccess access) + /// Use ReadLine API for reading. + internal UnixConsoleStream(SafeFileHandle handle, FileAccess access, bool useReadLine = false) : base(access) { Debug.Assert(handle != null, "Expected non-null console handle"); Debug.Assert(!handle.IsInvalid, "Expected valid console handle"); _handle = handle; + _useReadLine = useReadLine; } protected override void Dispose(bool disposing) @@ -1434,7 +1439,14 @@ public override int Read(byte[] buffer, int offset, int count) { ValidateRead(buffer, offset, count); - return ConsolePal.Read(_handle, buffer, offset, count); + if (_useReadLine) + { + return ConsolePal.StdInReader.ReadLine(buffer, offset, count); + } + else + { + return ConsolePal.Read(_handle, buffer, offset, count); + } } public override void Write(byte[] buffer, int offset, int count) diff --git a/src/libraries/System.Console/src/System/IO/StdInReader.cs b/src/libraries/System.Console/src/System/IO/StdInReader.cs index 99ebbd34c61496..6396176fbc905a 100644 --- a/src/libraries/System.Console/src/System/IO/StdInReader.cs +++ b/src/libraries/System.Console/src/System/IO/StdInReader.cs @@ -22,6 +22,7 @@ internal sealed class StdInReader : TextReader private readonly Stack _tmpKeys = new Stack(); // temporary working stack; should be empty outside of ReadLine private readonly Stack _availableKeys = new Stack(); // a queue of already processed key infos available for reading private readonly Encoding _encoding; + private Encoder? _bufferReadEncoder; private char[] _unprocessedBufferToBeRead; // Buffer that might have already been read from stdin but not yet processed. private const int BytesToBeRead = 1024; // No. of bytes to be read from the stream at a time. @@ -79,13 +80,63 @@ internal unsafe int ReadStdin(byte* buffer, int bufferSize) public override string? ReadLine() { - return ReadLine(consumeKeys: true); + bool isEnter = ReadLineCore(consumeKeys: true); + string? line = null; + if (isEnter || _readLineSB.Length > 0) + { + line = _readLineSB.ToString(); + _readLineSB.Clear(); + } + return line; } - private string? ReadLine(bool consumeKeys) + public int ReadLine(byte[] buffer, int offset, int count) + { + if (count == 0) + { + return 0; + } + + // Don't read a new line if there are remaining characters in the StringBuilder. + if (_readLineSB.Length == 0) + { + bool isEnter = ReadLineCore(consumeKeys: true); + if (isEnter) + { + _readLineSB.Append('\n'); + } + } + + // Encode line into buffer. + Encoder encoder = _bufferReadEncoder ??= _encoding.GetEncoder(); + int bytesUsedTotal = 0; + int charsUsedTotal = 0; + Span destination = buffer.AsSpan(offset, count); + foreach (ReadOnlyMemory chunk in _readLineSB.GetChunks()) + { + encoder.Convert(chunk.Span, destination, flush: false, out int charsUsed, out int bytesUsed, out bool completed); + destination = destination.Slice(bytesUsed); + bytesUsedTotal += bytesUsed; + charsUsedTotal += charsUsed; + + if (charsUsed == 0) + { + break; + } + } + _readLineSB.Remove(0, charsUsedTotal); + return bytesUsedTotal; + } + + // Reads a line in _readLineSB when consumeKeys is true, + // or _availableKeys when consumeKeys is false. + // Returns whether the line was terminated using the Enter key. + private bool ReadLineCore(bool consumeKeys) { Debug.Assert(_tmpKeys.Count == 0); - string? readLineStr = null; + + // Don't carry over chars from previous ReadLine call. + _readLineSB.Clear(); Interop.Sys.InitializeConsoleBeforeRead(); try @@ -110,23 +161,15 @@ internal unsafe int ReadStdin(byte* buffer, int bufferSize) // try to keep this very simple, at least for now. if (keyInfo.Key == ConsoleKey.Enter) { - readLineStr = _readLineSB.ToString(); - _readLineSB.Clear(); if (!previouslyProcessed) { Console.WriteLine(); } - break; + return true; } else if (IsEol(keyInfo.KeyChar)) { - string line = _readLineSB.ToString(); - _readLineSB.Clear(); - if (line.Length > 0) - { - readLineStr = line; - } - break; + return false; } else if (keyInfo.Key == ConsoleKey.Backspace) { @@ -166,7 +209,10 @@ internal unsafe int ReadStdin(byte* buffer, int bufferSize) } else if (keyInfo.Key == ConsoleKey.Tab) { - _readLineSB.Append(keyInfo.KeyChar); + if (consumeKeys) + { + _readLineSB.Append(keyInfo.KeyChar); + } if (!previouslyProcessed) { Console.Write(' '); @@ -182,7 +228,10 @@ internal unsafe int ReadStdin(byte* buffer, int bufferSize) } else if (keyInfo.KeyChar != '\0') { - _readLineSB.Append(keyInfo.KeyChar); + if (consumeKeys) + { + _readLineSB.Append(keyInfo.KeyChar); + } if (!previouslyProcessed) { Console.Write(keyInfo.KeyChar); @@ -200,8 +249,6 @@ internal unsafe int ReadStdin(byte* buffer, int bufferSize) _availableKeys.Push(_tmpKeys.Pop()); } } - - return readLineStr; } public override int Read() => ReadOrPeek(peek: false); @@ -213,7 +260,7 @@ private int ReadOrPeek(bool peek) // If there aren't any keys in our processed keys stack, read a line to populate it. if (_availableKeys.Count == 0) { - ReadLine(consumeKeys: false); + ReadLineCore(consumeKeys: false); } // Now if there are keys, use the first. diff --git a/src/libraries/System.Console/src/System/IO/SyncTextReader.Unix.cs b/src/libraries/System.Console/src/System/IO/SyncTextReader.Unix.cs index f98499db4387a7..9e68d74c1b0023 100644 --- a/src/libraries/System.Console/src/System/IO/SyncTextReader.Unix.cs +++ b/src/libraries/System.Console/src/System/IO/SyncTextReader.Unix.cs @@ -39,5 +39,8 @@ public bool KeyAvailable } } } + + public int ReadLine(byte[] buffer, int offset, int count) + => Inner.ReadLine(buffer, offset, count); } } diff --git a/src/libraries/System.Console/tests/ManualTests/ManualTests.cs b/src/libraries/System.Console/tests/ManualTests/ManualTests.cs index 9bbec557dbb732..5928b7a34132b8 100644 --- a/src/libraries/System.Console/tests/ManualTests/ManualTests.cs +++ b/src/libraries/System.Console/tests/ManualTests/ManualTests.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Threading.Tasks; +using System.IO; using Xunit; namespace System @@ -23,6 +24,26 @@ public static void ReadLine(bool consoleIn) AssertUserExpectedResults("the characters you typed properly echoed as you typed"); } + [ConditionalFact(nameof(ManualTestsEnabled))] + public static void ReadLineFromOpenStandardInput() + { + string expectedLine = "aab"; + + // Use Console.ReadLine + Console.WriteLine($"Please type 'a' 3 times, press 'Backspace' to erase 1, then type a single 'b' and press 'Enter'."); + string result = Console.ReadLine(); + Assert.Equal(expectedLine, result); + AssertUserExpectedResults("the characters you typed properly echoed as you typed"); + + // ReadLine from Console.OpenStandardInput + Console.WriteLine($"Please type 'a' 3 times, press 'Backspace' to erase 1, then type a single 'b' and press 'Enter'."); + using Stream inputStream = Console.OpenStandardInput(); + using StreamReader reader = new StreamReader(inputStream); + result = reader.ReadLine(); + Assert.Equal(expectedLine, result); + AssertUserExpectedResults("the characters you typed properly echoed as you typed"); + } + [ConditionalFact(nameof(ManualTestsEnabled))] public static void ReadLine_BackSpaceCanMoveAccrossWrappedLines() { @@ -36,6 +57,7 @@ public static void ReadLine_BackSpaceCanMoveAccrossWrappedLines() } [ConditionalFact(nameof(ManualTestsEnabled))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/40735", TestPlatforms.Windows)] public static void InPeek() { Console.WriteLine("Please type \"peek\" (without the quotes). You should see it as you type:"); @@ -91,19 +113,11 @@ static string RenderKeyChord(ConsoleKeyInfo key) public static IEnumerable GetKeyChords() { - yield return MkConsoleKeyInfo('\x01', ConsoleKey.A, ConsoleModifiers.Control); - yield return MkConsoleKeyInfo('\x01', ConsoleKey.A, ConsoleModifiers.Control | ConsoleModifiers.Alt); + yield return MkConsoleKeyInfo('\x02', ConsoleKey.B, ConsoleModifiers.Control); + yield return MkConsoleKeyInfo(OperatingSystem.IsWindows() ? '\x00' : '\x02', ConsoleKey.B, ConsoleModifiers.Control | ConsoleModifiers.Alt); yield return MkConsoleKeyInfo('\r', ConsoleKey.Enter, (ConsoleModifiers)0); - - if (OperatingSystem.IsWindows()) - { - // windows will report '\n' as 'Ctrl+Enter', which is typically not picked up by Unix terminals - yield return MkConsoleKeyInfo('\n', ConsoleKey.Enter, ConsoleModifiers.Control); - } - else - { - yield return MkConsoleKeyInfo('\n', ConsoleKey.J, ConsoleModifiers.Control); - } + // windows will report '\n' as 'Ctrl+Enter', which is typically not picked up by Unix terminals + yield return MkConsoleKeyInfo('\n', OperatingSystem.IsWindows() ? ConsoleKey.Enter : ConsoleKey.J, ConsoleModifiers.Control); static object[] MkConsoleKeyInfo (char keyChar, ConsoleKey consoleKey, ConsoleModifiers modifiers) { @@ -117,18 +131,6 @@ static object[] MkConsoleKeyInfo (char keyChar, ConsoleKey consoleKey, ConsoleMo } } - [ConditionalFact(nameof(ManualTestsEnabled))] - public static void OpenStandardInput() - { - Console.WriteLine("Please type \"console\" (without the quotes). You shouldn't see it as you type:"); - var stream = Console.OpenStandardInput(); - var textReader = new System.IO.StreamReader(stream); - var result = textReader.ReadLine(); - - Assert.Equal("console", result); - AssertUserExpectedResults("\"console\" correctly not echoed as you typed it"); - } - [ConditionalFact(nameof(ManualTestsEnabled))] public static void ConsoleOutWriteLine() { @@ -216,7 +218,7 @@ public static void CursorPositionAndArrowKeys() } } - AssertUserExpectedResults("the arrow keys move around the screen as expected with no other bad artificts"); + AssertUserExpectedResults("the arrow keys move around the screen as expected with no other bad artifacts"); } [ConditionalFact(nameof(ManualTestsEnabled))]