From 14e8dacc1ef7b1aa2e6b55ff7982b40a1ec28178 Mon Sep 17 00:00:00 2001 From: George Fleming Date: Wed, 2 Mar 2016 11:47:36 -0800 Subject: [PATCH 1/2] Add support for Ctrl-C to interrupt commands, and add multi-line capability for script debugger input --- src/Microsoft.PowerShell.Linux.Host/main.cs | 89 ++++++- .../readline.cs | 251 ++++++++++-------- 2 files changed, 222 insertions(+), 118 deletions(-) diff --git a/src/Microsoft.PowerShell.Linux.Host/main.cs b/src/Microsoft.PowerShell.Linux.Host/main.cs index be52ea07159..8e6ec1621ac 100644 --- a/src/Microsoft.PowerShell.Linux.Host/main.cs +++ b/src/Microsoft.PowerShell.Linux.Host/main.cs @@ -177,6 +177,16 @@ internal class Listener /// private string partialLine = string.Empty; + /// + /// To keep track of whether CTRL-C is causing a command abort + /// + private bool abortCommand = false; + + /// + /// To keep track of whether we are in input mode + /// + private bool inputMode = false; + /// /// Gets or sets a value indicating whether the host application /// should exit. @@ -342,6 +352,13 @@ private void ExecuteHelper(string cmd, object input) // Ignore empty command lines. if (string.IsNullOrEmpty(cmd)) { + if (this.abortCommand) + { + incompleteLine = false; + partialLine = string.Empty; + this.abortCommand = false; + } + return; } @@ -514,6 +531,15 @@ private void Execute(string cmd) /// ConsoleCancelEventHandler. private void HandleControlC(object sender, ConsoleCancelEventArgs e) { + e.Cancel = true; + + if (this.inputMode) + { + this.abortCommand = true; + this.consoleReadLine.Abort(); + return; + } + try { lock (this.instanceLock) @@ -524,7 +550,6 @@ private void HandleControlC(object sender, ConsoleCancelEventArgs e) } } - e.Cancel = true; } catch (Exception exception) { @@ -559,7 +584,12 @@ internal void Run() } this.myHost.UI.Write(ConsoleColor.White, Console.BackgroundColor, prompt); + + // Since Console.TreatControlCAsInput is not implemented, we use inputMode to + // tell us how control-c should be handled + this.inputMode = true; string cmd = consoleReadLine.Read(this.myHost.Runspace, false); + this.inputMode = false; this.Execute(cmd); } } @@ -579,9 +609,28 @@ private void HandleDebuggerStopEvent(object sender, DebuggerStopEventArgs args) // loop to process Debugger commands. while (resumeAction == null) { - Console.Write("[DBG] PS >> "); + if (incompleteLine) + { + Console.Write(">> "); + } + else + { + Console.Write("[DBG] PS >> "); + } + this.inputMode = true; string command = consoleReadLine.Read(this.myHost.Runspace, true); - Console.WriteLine(); + this.inputMode = false; + + if (string.IsNullOrEmpty(command)) + { + if (this.abortCommand) + { + incompleteLine = false; + partialLine = string.Empty; + this.abortCommand = false; + } + continue; + } // Stream output from command processing to console. var output = new PSDataCollection(); @@ -600,10 +649,38 @@ private void HandleDebuggerStopEvent(object sender, DebuggerStopEventArgs args) // command or script. The returned DebuggerCommandResults object will indicate // whether the command was evaluated by the debugger and if the debugger should // be released with a specific resume action. + PSCommand psCommand = new PSCommand(); - psCommand.AddScript(command).AddCommand("Out-String").AddParameter("Stream", true); - DebuggerCommandResults results = debugger.ProcessCommand(psCommand, output); - if (results.ResumeAction != null) + + if (incompleteLine) + { + psCommand.AddScript(partialLine + command).AddCommand("Out-String").AddParameter("Stream", true); + } + else + { + psCommand.AddScript(command).AddCommand("Out-String").AddParameter("Stream", true); + } + incompleteLine = false; + + DebuggerCommandResults results = null; + try + { + results = debugger.ProcessCommand(psCommand, output); + } + catch (IncompleteParseException) + { + incompleteLine = true; + command = command.Trim(); + partialLine += command; + partialLine += System.Environment.NewLine; + } + + if (!incompleteLine) + { + partialLine = string.Empty; + } + + if (!incompleteLine && results.ResumeAction != null) { resumeAction = results.ResumeAction; } diff --git a/src/Microsoft.PowerShell.Linux.Host/readline.cs b/src/Microsoft.PowerShell.Linux.Host/readline.cs index 7a2df6a234d..6d1d12b0706 100644 --- a/src/Microsoft.PowerShell.Linux.Host/readline.cs +++ b/src/Microsoft.PowerShell.Linux.Host/readline.cs @@ -6,6 +6,7 @@ namespace Microsoft.PowerShell.Linux.Host using System.Management.Automation; using System.Management.Automation.Runspaces; using System.Text; + using System.Threading; /// /// This class is used to read the command line and color the text as @@ -90,6 +91,11 @@ internal class ConsoleReadLine /// private string preTabBuffer; + /// + /// To keep track whether we should abort the Console.ReadKey loop + /// + private bool abortReadKey = false; + /// /// Initializes a new instance of the ConsoleReadLine class. /// @@ -129,134 +135,142 @@ public string Read(Runspace runspace, bool nested) this.powershell.Runspace = runspace; this.Initialize(); - while (true) + while (abortReadKey != true) { - ConsoleKeyInfo key = Console.ReadKey(true); - - // Basic Emacs-style readline implementation - if (key.Modifiers.HasFlag(ConsoleModifiers.Control)) + if (Console.KeyAvailable) { - switch (key.Key) + ConsoleKeyInfo key = Console.ReadKey(true); + + // Basic Emacs-style readline implementation + if (key.Modifiers.HasFlag(ConsoleModifiers.Control)) { - case ConsoleKey.A: - this.OnHome(); - break; - case ConsoleKey.E: - this.OnEnd(); - break; - // TODO: save buffer, yank with Ctrl-Y - /* - case ConsoleKey.K: - this.OnEscape(); - break; - */ - case ConsoleKey.D: - this.OnDelete(); - break; - case ConsoleKey.B: - this.OnLeft(key.Modifiers); - break; - case ConsoleKey.F: - this.OnRight(key.Modifiers); - break; - case ConsoleKey.P: - // TODO: incremental search - case ConsoleKey.R: - this.OnUpArrow(nested); - break; - case ConsoleKey.N: - // TODO: incremental search - case ConsoleKey.S: - this.OnDownArrow(nested); - break; - case ConsoleKey.J: - this.OnEnter(); - break; - // TODO: save and restore buffer - /* - case ConsoleKey.L: - this.BufferFromString("clear"); - previousKeyPress = key; - return this.OnEnter(); - */ + switch (key.Key) + { + case ConsoleKey.A: + this.OnHome(); + break; + case ConsoleKey.E: + this.OnEnd(); + break; + // TODO: save buffer, yank with Ctrl-Y + /* + case ConsoleKey.K: + this.OnEscape(); + break; + */ + case ConsoleKey.D: + this.OnDelete(); + break; + case ConsoleKey.B: + this.OnLeft(key.Modifiers); + break; + case ConsoleKey.F: + this.OnRight(key.Modifiers); + break; + case ConsoleKey.P: + // TODO: incremental search + case ConsoleKey.R: + this.OnUpArrow(nested); + break; + case ConsoleKey.N: + // TODO: incremental search + case ConsoleKey.S: + this.OnDownArrow(nested); + break; + case ConsoleKey.J: + this.OnEnter(); + break; + // TODO: save and restore buffer + /* + case ConsoleKey.L: + this.BufferFromString("clear"); + previousKeyPress = key; + return this.OnEnter(); + */ + } } - } - else if (key.Modifiers.HasFlag(ConsoleModifiers.Alt)) - { - switch (key.Key) + else if (key.Modifiers.HasFlag(ConsoleModifiers.Alt)) { - // TODO: OnDelete(key) - // TODO: OnBackspace(key) - case ConsoleKey.B: - this.OnLeft(key.Modifiers); - break; - case ConsoleKey.F: - this.OnRight(key.Modifiers); - break; + switch (key.Key) + { + // TODO: OnDelete(key) + // TODO: OnBackspace(key) + case ConsoleKey.B: + this.OnLeft(key.Modifiers); + break; + case ConsoleKey.F: + this.OnRight(key.Modifiers); + break; + } } - } - // Unmodified keys - else - { - switch (key.Key) + // Unmodified keys + else { - case ConsoleKey.Backspace: - this.OnBackspace(); - break; - case ConsoleKey.Delete: - this.OnDelete(); - break; - case ConsoleKey.Enter: - previousKeyPress = key; - return this.OnEnter(); - case ConsoleKey.RightArrow: - this.OnRight(key.Modifiers); - break; - case ConsoleKey.LeftArrow: - this.OnLeft(key.Modifiers); - break; - case ConsoleKey.Escape: - this.OnEscape(); - break; - case ConsoleKey.Home: - this.OnHome(); - break; - case ConsoleKey.End: - this.OnEnd(); - break; - case ConsoleKey.Tab: - this.OnTab(); - break; - case ConsoleKey.UpArrow: - this.OnUpArrow(nested); - break; - case ConsoleKey.DownArrow: - this.OnDownArrow(nested); - break; + switch (key.Key) + { + case ConsoleKey.Backspace: + this.OnBackspace(); + break; + case ConsoleKey.Delete: + this.OnDelete(); + break; + case ConsoleKey.Enter: + previousKeyPress = key; + return this.OnEnter(); + case ConsoleKey.RightArrow: + this.OnRight(key.Modifiers); + break; + case ConsoleKey.LeftArrow: + this.OnLeft(key.Modifiers); + break; + case ConsoleKey.Escape: + this.OnEscape(); + break; + case ConsoleKey.Home: + this.OnHome(); + break; + case ConsoleKey.End: + this.OnEnd(); + break; + case ConsoleKey.Tab: + this.OnTab(); + break; + case ConsoleKey.UpArrow: + this.OnUpArrow(nested); + break; + case ConsoleKey.DownArrow: + this.OnDownArrow(nested); + break; - // TODO: case ConsoleKey.LeftWindows: not available in linux - // TODO: case ConsoleKey.RightWindows: not available in linux + // TODO: case ConsoleKey.LeftWindows: not available in linux + // TODO: case ConsoleKey.RightWindows: not available in linux - default: + default: - if (key.KeyChar == '\x0D') - { - goto case ConsoleKey.Enter; // Ctrl-M - } + if (key.KeyChar == '\x0D') + { + goto case ConsoleKey.Enter; // Ctrl-M + } - if (key.KeyChar == '\x08') - { - goto case ConsoleKey.Backspace; // Ctrl-H - } + if (key.KeyChar == '\x08') + { + goto case ConsoleKey.Backspace; // Ctrl-H + } - this.Insert(key.KeyChar); + this.Insert(key.KeyChar); - this.Render(); - break; + this.Render(); + break; + } } + previousKeyPress = key; + } + else + { + Thread.Sleep(250); } - previousKeyPress = key; } + return string.Empty; } /// @@ -270,6 +284,7 @@ private void Initialize() this.current = 0; this.rendered = 0; this.cursor = new Cursor(); + this.abortReadKey = false; } /// @@ -733,6 +748,18 @@ private void Render() this.cursor.Place(this.current); } + /// + /// Abort current command + /// + public void Abort() + { + ConsoleColor saveFGColor = Console.ForegroundColor; + Console.ForegroundColor = ConsoleColor.Red; + Console.Out.WriteLine("^C"); + Console.ForegroundColor = saveFGColor; + this.abortReadKey = true; + } + /// /// A helper class for maintaining the cursor while editing the command line. /// From f9a98684f2119519e58c8628ae3e939fa85d57ff Mon Sep 17 00:00:00 2001 From: George Fleming Date: Thu, 3 Mar 2016 15:24:01 -0800 Subject: [PATCH 2/2] Updated code per Andy comments --- src/Microsoft.PowerShell.Linux.Host/main.cs | 55 ++-- .../readline.cs | 265 ++++++++++-------- 2 files changed, 168 insertions(+), 152 deletions(-) diff --git a/src/Microsoft.PowerShell.Linux.Host/main.cs b/src/Microsoft.PowerShell.Linux.Host/main.cs index 8e6ec1621ac..9714a08d5ff 100644 --- a/src/Microsoft.PowerShell.Linux.Host/main.cs +++ b/src/Microsoft.PowerShell.Linux.Host/main.cs @@ -185,7 +185,7 @@ internal class Listener /// /// To keep track of whether we are in input mode /// - private bool inputMode = false; + private bool awaitingUserInput = false; /// /// Gets or sets a value indicating whether the host application @@ -377,14 +377,8 @@ private void ExecuteHelper(string cmd, object input) { this.currentPowerShell.Runspace = this.myRunSpace; - if (incompleteLine) - { - this.currentPowerShell.AddScript(partialLine + cmd); - } - else - { - this.currentPowerShell.AddScript(cmd); - } + string fullCommand = incompleteLine ? (partialLine + cmd) : cmd; + this.currentPowerShell.AddScript(fullCommand); incompleteLine = false; // Add the default outputter to the end of the pipe and then call the @@ -411,11 +405,7 @@ private void ExecuteHelper(string cmd, object input) catch (IncompleteParseException) { incompleteLine = true; - cmd = cmd.Trim(); - partialLine += cmd; - - partialLine += System.Environment.NewLine; - + partialLine = $"{partialLine}{cmd}{System.Environment.NewLine}"; } finally @@ -533,7 +523,7 @@ private void HandleControlC(object sender, ConsoleCancelEventArgs e) { e.Cancel = true; - if (this.inputMode) + if (this.awaitingUserInput) { this.abortCommand = true; this.consoleReadLine.Abort(); @@ -585,11 +575,11 @@ internal void Run() this.myHost.UI.Write(ConsoleColor.White, Console.BackgroundColor, prompt); - // Since Console.TreatControlCAsInput is not implemented, we use inputMode to + // Since Console.TreatControlCAsInput is not implemented, we use awaitingUserInput to // tell us how control-c should be handled - this.inputMode = true; + this.awaitingUserInput = true; string cmd = consoleReadLine.Read(this.myHost.Runspace, false); - this.inputMode = false; + this.awaitingUserInput = false; this.Execute(cmd); } } @@ -609,17 +599,12 @@ private void HandleDebuggerStopEvent(object sender, DebuggerStopEventArgs args) // loop to process Debugger commands. while (resumeAction == null) { - if (incompleteLine) - { - Console.Write(">> "); - } - else - { - Console.Write("[DBG] PS >> "); - } - this.inputMode = true; + string prompt = incompleteLine ? ">> " : "[DBG] PS >> "; + Console.Write(prompt); + + this.awaitingUserInput = true; string command = consoleReadLine.Read(this.myHost.Runspace, true); - this.inputMode = false; + this.awaitingUserInput = false; if (string.IsNullOrEmpty(command)) { @@ -652,14 +637,8 @@ private void HandleDebuggerStopEvent(object sender, DebuggerStopEventArgs args) PSCommand psCommand = new PSCommand(); - if (incompleteLine) - { - psCommand.AddScript(partialLine + command).AddCommand("Out-String").AddParameter("Stream", true); - } - else - { - psCommand.AddScript(command).AddCommand("Out-String").AddParameter("Stream", true); - } + string fullCommand = incompleteLine ? (partialLine + command) : command; + psCommand.AddScript(fullCommand).AddCommand("Out-String").AddParameter("Stream", true); incompleteLine = false; DebuggerCommandResults results = null; @@ -670,9 +649,7 @@ private void HandleDebuggerStopEvent(object sender, DebuggerStopEventArgs args) catch (IncompleteParseException) { incompleteLine = true; - command = command.Trim(); - partialLine += command; - partialLine += System.Environment.NewLine; + partialLine = $"{partialLine}{command}{System.Environment.NewLine}"; } if (!incompleteLine) diff --git a/src/Microsoft.PowerShell.Linux.Host/readline.cs b/src/Microsoft.PowerShell.Linux.Host/readline.cs index 6d1d12b0706..6b2198cb723 100644 --- a/src/Microsoft.PowerShell.Linux.Host/readline.cs +++ b/src/Microsoft.PowerShell.Linux.Host/readline.cs @@ -96,6 +96,12 @@ internal class ConsoleReadLine /// private bool abortReadKey = false; + /// + /// Amount of time to sleep before checking for user keystroke, + /// in milliseconds + /// + private const int sleepTime = 80; + /// /// Initializes a new instance of the ConsoleReadLine class. /// @@ -134,8 +140,9 @@ public string Read(Runspace runspace, bool nested) { this.powershell.Runspace = runspace; this.Initialize(); + bool finishedTyping = false; - while (abortReadKey != true) + while (!abortReadKey) { if (Console.KeyAvailable) { @@ -144,135 +151,167 @@ public string Read(Runspace runspace, bool nested) // Basic Emacs-style readline implementation if (key.Modifiers.HasFlag(ConsoleModifiers.Control)) { - switch (key.Key) - { - case ConsoleKey.A: - this.OnHome(); - break; - case ConsoleKey.E: - this.OnEnd(); - break; - // TODO: save buffer, yank with Ctrl-Y - /* - case ConsoleKey.K: - this.OnEscape(); - break; - */ - case ConsoleKey.D: - this.OnDelete(); - break; - case ConsoleKey.B: - this.OnLeft(key.Modifiers); - break; - case ConsoleKey.F: - this.OnRight(key.Modifiers); - break; - case ConsoleKey.P: - // TODO: incremental search - case ConsoleKey.R: - this.OnUpArrow(nested); - break; - case ConsoleKey.N: - // TODO: incremental search - case ConsoleKey.S: - this.OnDownArrow(nested); - break; - case ConsoleKey.J: - this.OnEnter(); - break; - // TODO: save and restore buffer - /* - case ConsoleKey.L: - this.BufferFromString("clear"); - previousKeyPress = key; - return this.OnEnter(); - */ - } + finishedTyping = ProcessControlKey(key, nested); } else if (key.Modifiers.HasFlag(ConsoleModifiers.Alt)) { - switch (key.Key) - { - // TODO: OnDelete(key) - // TODO: OnBackspace(key) - case ConsoleKey.B: - this.OnLeft(key.Modifiers); - break; - case ConsoleKey.F: - this.OnRight(key.Modifiers); - break; - } + finishedTyping = ProcessAltKey(key); } // Unmodified keys else { - switch (key.Key) - { - case ConsoleKey.Backspace: - this.OnBackspace(); - break; - case ConsoleKey.Delete: - this.OnDelete(); - break; - case ConsoleKey.Enter: - previousKeyPress = key; - return this.OnEnter(); - case ConsoleKey.RightArrow: - this.OnRight(key.Modifiers); - break; - case ConsoleKey.LeftArrow: - this.OnLeft(key.Modifiers); - break; - case ConsoleKey.Escape: - this.OnEscape(); - break; - case ConsoleKey.Home: - this.OnHome(); - break; - case ConsoleKey.End: - this.OnEnd(); - break; - case ConsoleKey.Tab: - this.OnTab(); - break; - case ConsoleKey.UpArrow: - this.OnUpArrow(nested); - break; - case ConsoleKey.DownArrow: - this.OnDownArrow(nested); - break; - - // TODO: case ConsoleKey.LeftWindows: not available in linux - // TODO: case ConsoleKey.RightWindows: not available in linux - - default: - - if (key.KeyChar == '\x0D') - { - goto case ConsoleKey.Enter; // Ctrl-M - } - - if (key.KeyChar == '\x08') - { - goto case ConsoleKey.Backspace; // Ctrl-H - } - - this.Insert(key.KeyChar); - - this.Render(); - break; - } + finishedTyping = ProcessNormalKey(key, nested); } previousKeyPress = key; } else { - Thread.Sleep(250); + Thread.Sleep(sleepTime); + } + + if (finishedTyping) + { + return this.OnEnter(); } } return string.Empty; } + /// + /// Process Control-Key combo + /// + private bool ProcessControlKey(ConsoleKeyInfo key, bool nested) + { + switch (key.Key) + { + case ConsoleKey.A: + this.OnHome(); + break; + case ConsoleKey.E: + this.OnEnd(); + break; + // TODO: save buffer, yank with Ctrl-Y + /* + case ConsoleKey.K: + this.OnEscape(); + break; + */ + case ConsoleKey.D: + this.OnDelete(); + break; + case ConsoleKey.B: + this.OnLeft(key.Modifiers); + break; + case ConsoleKey.F: + this.OnRight(key.Modifiers); + break; + case ConsoleKey.P: + // TODO: incremental search + case ConsoleKey.R: + this.OnUpArrow(nested); + break; + case ConsoleKey.N: + // TODO: incremental search + case ConsoleKey.S: + this.OnDownArrow(nested); + break; + case ConsoleKey.J: + this.OnEnter(); + break; + // TODO: save and restore buffer + /* + case ConsoleKey.L: + this.BufferFromString("clear"); + previousKeyPress = key; + return this.OnEnter(); + */ + } + return false; + } + + /// + /// Process Alt-Key combo + /// + private bool ProcessAltKey(ConsoleKeyInfo key) + { + switch (key.Key) + { + // TODO: OnDelete(key) + // TODO: OnBackspace(key) + case ConsoleKey.B: + this.OnLeft(key.Modifiers); + break; + case ConsoleKey.F: + this.OnRight(key.Modifiers); + break; + } + return false; + } + + /// + /// Process normal key + /// + private bool ProcessNormalKey(ConsoleKeyInfo key, bool nested) + { + switch (key.Key) + { + case ConsoleKey.Backspace: + this.OnBackspace(); + break; + case ConsoleKey.Delete: + this.OnDelete(); + break; + case ConsoleKey.Enter: + previousKeyPress = key; + return true; + case ConsoleKey.RightArrow: + this.OnRight(key.Modifiers); + break; + case ConsoleKey.LeftArrow: + this.OnLeft(key.Modifiers); + break; + case ConsoleKey.Escape: + this.OnEscape(); + break; + case ConsoleKey.Home: + this.OnHome(); + break; + case ConsoleKey.End: + this.OnEnd(); + break; + case ConsoleKey.Tab: + this.OnTab(); + break; + case ConsoleKey.UpArrow: + this.OnUpArrow(nested); + break; + case ConsoleKey.DownArrow: + this.OnDownArrow(nested); + break; + + // TODO: case ConsoleKey.LeftWindows: not available in linux + // TODO: case ConsoleKey.RightWindows: not available in linux + + default: + + if (key.KeyChar == '\x0D') + { + goto case ConsoleKey.Enter; // Ctrl-M + } + + if (key.KeyChar == '\x08') + { + goto case ConsoleKey.Backspace; // Ctrl-H + } + + this.Insert(key.KeyChar); + + this.Render(); + break; + } + return false; + } + /// /// Initializes the buffer. ///