diff --git a/docs/KNOWNISSUES.md b/docs/KNOWNISSUES.md
index 7356f041f..f06866c96 100644
--- a/docs/KNOWNISSUES.md
+++ b/docs/KNOWNISSUES.md
@@ -1,18 +1,5 @@
# Known Issues
-## SecureString
-
-The `SecureString` class is *not* the well-known C# `SecureString`, as the
-library is not available in .NET Core. However, PowerShell continues to use the
-type for two main reasons: FullCLR compatibility, and as a specially-treated
-type (that is, behavior is dependent on the type being `SecureString`). Instead,
-the `SecureString` is Mono's completely not secure whatsoever, but open-source
-and "compatible" stub that acts just like a plaintext `StringBuilder`. **It has
-no encryption.**
-
-Additionally `ReadLineSafe` is not implemented, meaning `Get-Credential` fails
-with `PlatformNotSupportedException`.
-
## Files excluded from the build
#### Microsoft.PowerShell.Commands.Management
diff --git a/src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleHostUserInterface.cs b/src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleHostUserInterface.cs
index 8dbf99f24..9984e0a60 100644
--- a/src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleHostUserInterface.cs
+++ b/src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleHostUserInterface.cs
@@ -251,6 +251,10 @@ namespace Microsoft.PowerShell
/// It also manages the cursor as keys are entered and "backspaced". However, it is possible that
/// while this method is running, the console buffer contents could change. Then, its cursor mgmt
/// will likely be messed up.
+ ///
+ /// Secondary implementation for Unix based on Console.ReadKey(), where
+ /// the advantage is portability through abstraction. Does not support
+ /// arrow key movement, but supports backspace.
///
///
///
@@ -285,9 +289,6 @@ namespace Microsoft.PowerShell
private object ReadLineSafe(bool isSecureString, char? printToken)
{
-#if LINUX
- throw new PlatformNotSupportedException("Cannot read secure strings!");
-#else
// Don't lock (instanceLock) in here -- the caller needs to do that...
PreRead();
@@ -296,13 +297,21 @@ namespace Microsoft.PowerShell
null;
SecureString secureResult = new SecureString();
StringBuilder result = new StringBuilder();
+#if LINUX
+ bool treatControlCAsInput = Console.TreatControlCAsInput;
+#else
ConsoleHandle handle = ConsoleControl.GetConioDeviceHandle();
ConsoleControl.ConsoleModes originalMode = ConsoleControl.GetMode(handle);
bool isModeChanged = true; // assume ConsoleMode is changed so that if ReadLineSetMode
// fails to return the value correctly, the original mode is
// restored.
+#endif
+
try
{
+#if LINUX
+ Console.TreatControlCAsInput = true;
+#else
// Ensure that we're in the proper line-input mode.
ConsoleControl.ConsoleModes desiredMode =
@@ -329,6 +338,7 @@ namespace Microsoft.PowerShell
isModeChanged = false;
}
rawui.ClearKeyCache();
+#endif
Coordinates originalCursorPos = rawui.CursorPosition;
@@ -339,22 +349,39 @@ namespace Microsoft.PowerShell
// end up having a immutable string holding the
// secret in memory.
//
+#if LINUX
+ ConsoleKeyInfo keyInfo = Console.ReadKey(true);
+#else
uint unused = 0;
string key = ConsoleControl.ReadConsole(handle, string.Empty, 1, false, out unused);
+#endif
+#if LINUX
+ // Handle Ctrl-C ending input
+ if (keyInfo.Key == ConsoleKey.C && keyInfo.Modifiers.HasFlag(ConsoleModifiers.Control))
+#else
if (string.IsNullOrEmpty(key) || (char)3 == key[0])
+#endif
{
PipelineStoppedException e = new PipelineStoppedException();
throw e;
}
+#if LINUX
+ if (keyInfo.Key == ConsoleKey.Enter)
+#else
if ((char)13 == key[0])
+#endif
{
//
// we are done if user presses ENTER key
//
break;
}
+#if LINUX
+ if (keyInfo.Key == ConsoleKey.Backspace)
+#else
if ((char)8 == key[0])
+#endif
{
//
// for backspace, remove last char appended
@@ -370,6 +397,13 @@ namespace Microsoft.PowerShell
WriteBackSpace(originalCursorPos);
}
}
+#if LINUX
+ else if (Char.IsControl(keyInfo.KeyChar))
+ {
+ // blacklist control characters
+ continue;
+ }
+#endif
else
{
//
@@ -377,11 +411,19 @@ namespace Microsoft.PowerShell
//
if (isSecureString)
{
+#if LINUX
+ secureResult.AppendChar(keyInfo.KeyChar);
+#else
secureResult.AppendChar(key[0]);
+#endif
}
else
{
+#if LINUX
+ result.Append(keyInfo.KeyChar);
+#else
result.Append(key);
+#endif
}
if (!string.IsNullOrEmpty(printTokenString))
{
@@ -391,12 +433,23 @@ namespace Microsoft.PowerShell
}
while (true);
}
+#if LINUX
+ catch (InvalidOperationException)
+ {
+ // ReadKey() failed so we stop
+ throw new PipelineStoppedException();
+ }
+#endif
finally
{
+#if LINUX
+ Console.TreatControlCAsInput = treatControlCAsInput;
+#else
if (isModeChanged)
{
ConsoleControl.SetMode(handle, originalMode);
}
+#endif
}
WriteLineToConsole();
PostRead(result.ToString());
@@ -408,10 +461,8 @@ namespace Microsoft.PowerShell
{
return result;
}
-#endif
}
-#if !LINUX
///
///
@@ -522,7 +573,7 @@ namespace Microsoft.PowerShell
}
-
+#if !LINUX
///
///
/// If is set on , unset it and return true;
@@ -1637,13 +1688,11 @@ namespace Microsoft.PowerShell
private string ReadLineFromConsole(bool endOnTab, string initialContent, bool calledFromPipeline, ref string restOfLine, ref ReadLineResult result)
{
-#if LINUX
- return ReadLineFromFile(initialContent);
-#else
- ConsoleHandle handle = ConsoleControl.GetConioDeviceHandle();
PreRead();
// Ensure that we're in the proper line-input mode.
+#if !LINUX
+ ConsoleHandle handle = ConsoleControl.GetConioDeviceHandle();
ConsoleControl.ConsoleModes m = ConsoleControl.GetMode(handle);
const ConsoleControl.ConsoleModes desiredMode =
@@ -1657,6 +1706,7 @@ namespace Microsoft.PowerShell
m |= desiredMode;
ConsoleControl.SetMode(handle, m);
}
+#endif
// If more characters are typed than you asked, then the next call to ReadConsole will return the
// additional characters beyond those you requested.
@@ -1673,18 +1723,44 @@ namespace Microsoft.PowerShell
// If input is terminated with a break key (Ctrl-C, Ctrl-Break, Close, etc.), then the buffer will be
// the empty string.
- uint keyState = 0;
+#if LINUX
+ // For Unix systems, we implement a basic readline loop around Console.ReadKey(), that
+ // supports backspace, arrow keys, Ctrl-C, and Ctrl-D. This readline is only used for
+ // interactive prompts (like Read-Host), otherwise it is assumed that PSReadLine is
+ // available. Therefore this explicitly does not support history or tab completion.
- rawui.ClearKeyCache();
+ bool treatControlCAsInput = Console.TreatControlCAsInput;
+ try
+ {
+
+ ConsoleKeyInfo keyInfo;
string s = "";
+ int index = 0;
+ int cursorLeft = Console.CursorLeft;
+ int cursorCurrent = cursorLeft;
+ bool insertMode = true;
+ Console.TreatControlCAsInput = true;
+#else
+ rawui.ClearKeyCache();
+ uint keyState = 0;
+ string s = "";
+#endif
do
{
+#if LINUX
+ keyInfo = Console.ReadKey(true);
+#else
s += ConsoleControl.ReadConsole(handle, initialContent, maxInputLineLength, endOnTab, out keyState);
-
Dbg.Assert(s != null, "s should never be null");
+#endif
+#if LINUX
+ // Handle Ctrl-C ending input
+ if (keyInfo.Key == ConsoleKey.C && keyInfo.Modifiers.HasFlag(ConsoleModifiers.Control))
+#else
if (s.Length == 0)
+#endif
{
result = ReadLineResult.endedOnBreak;
s = null;
@@ -1698,13 +1774,29 @@ namespace Microsoft.PowerShell
break;
}
+#if LINUX
+ if (keyInfo.Key == ConsoleKey.Enter)
+#else
if (s.EndsWith(Crlf, StringComparison.CurrentCulture))
+#endif
{
result = ReadLineResult.endedOnEnter;
+#if LINUX
+ // We're intercepting characters, so we need to echo the newline
+ Console.Out.WriteLine();
+#else
s = s.Remove(s.Length - Crlf.Length);
+#endif
break;
}
+#if LINUX
+ if (keyInfo.Key == ConsoleKey.Tab)
+ {
+ // This is unsupported
+ continue;
+ }
+#else
int i = s.IndexOf(Tab, StringComparison.CurrentCulture);
if (endOnTab && i != -1)
@@ -1754,6 +1846,120 @@ namespace Microsoft.PowerShell
break;
}
+#endif
+#if LINUX
+ if (keyInfo.Key == ConsoleKey.Backspace)
+ {
+ if (index > 0)
+ {
+ int length = s.Length;
+ s = s.Remove(index - 1, 1);
+ index--;
+ cursorCurrent = Console.CursorLeft;
+ Console.CursorLeft = cursorLeft;
+ Console.Out.Write(s.PadRight(length));
+ Console.CursorLeft = cursorCurrent - 1;
+ }
+ continue;
+ }
+
+ if (keyInfo.Key == ConsoleKey.Delete
+ || (keyInfo.Key == ConsoleKey.D && keyInfo.Modifiers.HasFlag(ConsoleModifiers.Control)))
+ {
+ if (index < s.Length)
+ {
+ int length = s.Length;
+ s = s.Remove(index, 1);
+ cursorCurrent = Console.CursorLeft;
+ Console.CursorLeft = cursorLeft;
+ Console.Out.Write(s.PadRight(length));
+ Console.CursorLeft = cursorCurrent;
+ }
+ continue;
+ }
+
+ if (keyInfo.Key == ConsoleKey.LeftArrow
+ || (keyInfo.Key == ConsoleKey.B && keyInfo.Modifiers.HasFlag(ConsoleModifiers.Control)))
+ {
+ if (Console.CursorLeft > cursorLeft)
+ {
+ Console.CursorLeft--;
+ index--;
+ }
+ continue;
+ }
+
+ if (keyInfo.Key == ConsoleKey.RightArrow
+ || (keyInfo.Key == ConsoleKey.F && keyInfo.Modifiers.HasFlag(ConsoleModifiers.Control)))
+ {
+ if (Console.CursorLeft < cursorLeft + s.Length)
+ {
+ Console.CursorLeft++;
+ index++;
+ }
+ continue;
+ }
+
+ if (keyInfo.Key == ConsoleKey.UpArrow
+ || keyInfo.Key == ConsoleKey.DownArrow
+ || keyInfo.Key == ConsoleKey.PageUp
+ || keyInfo.Key == ConsoleKey.PageDown)
+ {
+ // Arrow/Page Up/down is unimplemented, so fail gracefully
+ continue;
+ }
+
+ if (keyInfo.Key == ConsoleKey.Home
+ || (keyInfo.Key == ConsoleKey.A && keyInfo.Modifiers.HasFlag(ConsoleModifiers.Control)))
+ {
+ Console.CursorLeft = cursorLeft;
+ index = 0;
+ continue;
+ }
+
+ if (keyInfo.Key == ConsoleKey.End
+ || (keyInfo.Key == ConsoleKey.E && keyInfo.Modifiers.HasFlag(ConsoleModifiers.Control)))
+ {
+ Console.CursorLeft = cursorLeft + s.Length;
+ index = s.Length;
+ continue;
+ }
+
+ if (keyInfo.Key == ConsoleKey.Escape)
+ {
+ Console.CursorLeft = cursorLeft;
+ index = s.Length;
+ s = "";
+ continue;
+ }
+
+ if (keyInfo.Key == ConsoleKey.Insert)
+ {
+ // Toggle insert/overwrite mode
+ insertMode = !insertMode;
+ continue;
+ }
+
+ if (Char.IsControl(keyInfo.KeyChar))
+ {
+ // blacklist control characters
+ continue;
+ }
+
+ // Modify string
+ if (!insertMode) // then overwrite mode
+ {
+ s = s.Remove(index, 1);
+ }
+ s = s.Insert(index, keyInfo.KeyChar.ToString());
+ index++;
+
+ // Redisplay string
+ cursorCurrent = Console.CursorLeft;
+ Console.CursorLeft = cursorLeft;
+ Console.Out.Write(s);
+ Console.CursorLeft = cursorCurrent + 1;
+#endif
}
while (true);
@@ -1763,6 +1969,12 @@ namespace Microsoft.PowerShell
"s should only be null if input ended with a break");
return s;
+#if LINUX
+ }
+ finally
+ {
+ Console.TreatControlCAsInput = treatControlCAsInput;
+ }
#endif
}
diff --git a/test/powershell/Host/Read-Host.Tests.ps1 b/test/powershell/Host/Read-Host.Tests.ps1
new file mode 100644
index 000000000..847198154
--- /dev/null
+++ b/test/powershell/Host/Read-Host.Tests.ps1
@@ -0,0 +1,20 @@
+Describe "Read-Host" {
+ Context "[Console]::ReadKey() implementation on non-Windows" {
+ BeforeAll {
+ $powershell = Join-Path -Path $PsHome -ChildPath "powershell"
+ $assetsDir = Join-Path -Path $PSScriptRoot -ChildPath assets
+ if ($IsWindows) {
+ $ItArgs = @{ skip = $true }
+ } elseif (-not (Get-Command expect -ErrorAction Ignore)) {
+ $ItArgs = @{ pending = $true }
+ } else {
+ $ItArgs = @{ }
+ }
+ }
+
+ It @ItArgs "Should output correctly" {
+ & (Join-Path $assetsDir "Read-Host.Output.expect") $powershell | Out-Null
+ $LASTEXITCODE | Should Be 0
+ }
+ }
+}
diff --git a/test/powershell/Host/assets/Read-Host.Output.expect b/test/powershell/Host/assets/Read-Host.Output.expect
new file mode 100755
index 000000000..a43044ee4
--- /dev/null
+++ b/test/powershell/Host/assets/Read-Host.Output.expect
@@ -0,0 +1,27 @@
+#!/usr/bin/expect -f
+
+exp_internal 1
+
+# Expects path to PowerShell as first argument
+set powershell [lindex $argv 0]
+
+set timeout 60
+
+spawn $powershell -nologo -noprofile Read-Host prompt
+
+expect "prompt: $" {
+ send "input\r"
+}
+
+# This is the old, incorrect behavior
+expect "input\r\ninput\r" {
+ exit 2
+}
+
+# This is the expected behavior
+expect "input\r\n" {
+ exit 0
+}
+
+# Anything else is wrong
+exit 1