/********************************************************************++ Copyright (c) Microsoft Corporation. All rights reserved. --********************************************************************/ using System; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; using System.Management.Automation; using System.Management.Automation.Language; using System.Management.Automation.Runspaces; using Microsoft.PowerShell.Internal; namespace Microsoft.PowerShell { public partial class PSConsoleReadLine { /// /// Insert the key /// [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")] public static void SelfInsert(ConsoleKeyInfo? key = null, object arg = null) { if (!key.HasValue) { return; } if (arg is int) { var count = (int)arg; if (count <= 0) return; if (count > 1) { var toInsert = new string(key.Value.KeyChar, count); if (_singleton._visualSelectionCommandCount > 0) { int start, length; _singleton.GetRegion(out start, out length); Replace(start, length, toInsert); } else { Insert(toInsert); } return; } } if (_singleton._visualSelectionCommandCount > 0) { int start, length; _singleton.GetRegion(out start, out length); Replace(start, length, new string(key.Value.KeyChar, 1)); } else { Insert(key.Value.KeyChar); } } /// /// Reverts all of the input to the current input. /// [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")] public static void RevertLine(ConsoleKeyInfo? key = null, object arg = null) { if (_singleton._statusIsErrorMessage) { // After an edit, clear the error message _singleton.ClearStatusMessage(render: false); } while (_singleton._undoEditIndex > 0) { _singleton._edits[_singleton._undoEditIndex - 1].Undo(); _singleton._undoEditIndex--; } _singleton.Render(); } /// /// Cancel the current input, leaving the input on the screen, /// but returns back to the host so the prompt is evaluated again. /// [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")] public static void CancelLine(ConsoleKeyInfo? key = null, object arg = null) { _singleton.ClearStatusMessage(false); _singleton._current = _singleton._buffer.Length; // We want to display ^C to show the line was canceled. Instead of appending ^C // (or (char)3), we append 2 spaces so we don't affect tokenization too much, e.g. // changing a keyword to a command. _singleton._buffer.Append(" "); _singleton.ReallyRender(); // Now that we've rendered with this extra spaces, go back and replace the spaces // with ^C colored in red (so it stands out.) var coordinates = _singleton.ConvertOffsetToCoordinates(_singleton._current); var console = _singleton._console; var consoleBuffer = _singleton._consoleBuffer; int i = (coordinates.Y - _singleton._initialY) * console.BufferWidth + coordinates.X; consoleBuffer[i].UnicodeChar = '^'; consoleBuffer[i].ForegroundColor = ConsoleColor.Red; consoleBuffer[i].BackgroundColor = console.BackgroundColor; consoleBuffer[i+1].UnicodeChar = 'C'; consoleBuffer[i+1].ForegroundColor = ConsoleColor.Red; consoleBuffer[i+1].BackgroundColor = console.BackgroundColor; console.WriteBufferLines(consoleBuffer, ref _singleton._initialY); var y = coordinates.Y + 1; _singleton.PlaceCursor(0, ref y); _singleton._buffer.Clear(); // Clear so we don't actually run the input _singleton._current = 0; // If Render is called, _current must be correct. _singleton._currentHistoryIndex = _singleton._history.Count; _singleton._inputAccepted = true; } /// /// Like ForwardKillLine - deletes text from the point to the end of the line, /// but does not put the deleted text in the kill ring. /// [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")] public static void ForwardDeleteLine(ConsoleKeyInfo? key = null, object arg = null) { var current = _singleton._current; var buffer = _singleton._buffer; if (buffer.Length > 0 && current < buffer.Length) { int length = buffer.Length - current; var str = buffer.ToString(current, length); _singleton.SaveEditItem(EditItemDelete.Create(str, current)); buffer.Remove(current, length); _singleton.Render(); } } /// /// Like BackwardKillLine - deletes text from the point to the start of the line, /// but does not put the deleted text in the kill ring. /// [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")] public static void BackwardDeleteLine(ConsoleKeyInfo? key = null, object arg = null) { if (_singleton._current > 0) { _singleton._clipboard = _singleton._buffer.ToString(0, _singleton._current); _singleton.SaveEditItem(EditItemDelete.Create(_singleton._clipboard, 0)); _singleton._buffer.Remove(0, _singleton._current); _singleton._current = 0; _singleton.Render(); } } /// /// Delete the character before the cursor. /// [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")] public static void BackwardDeleteChar(ConsoleKeyInfo? key = null, object arg = null) { if (_singleton._visualSelectionCommandCount > 0) { int start, length; _singleton.GetRegion(out start, out length); Delete(start, length); return; } if (_singleton._buffer.Length > 0 && _singleton._current > 0) { int qty = (arg is int) ? (int) arg : 1; qty = Math.Min(qty, _singleton._current); int startDeleteIndex = _singleton._current - qty; _singleton.SaveEditItem( EditItemDelete.Create( _singleton._buffer.ToString(startDeleteIndex, qty), startDeleteIndex, BackwardDeleteChar, arg) ); _singleton.SaveToClipboard(startDeleteIndex, qty); _singleton._buffer.Remove(startDeleteIndex, qty); _singleton._current = startDeleteIndex; _singleton.Render(); } else { Ding(); } } private void DeleteCharImpl(int qty, bool orExit) { qty = Math.Min(qty, _singleton._buffer.Length + 1 + ViEndOfLineFactor - _singleton._current); if (_visualSelectionCommandCount > 0) { int start, length; GetRegion(out start, out length); Delete(start, length); return; } if (_buffer.Length > 0) { if (_current < _buffer.Length) { SaveEditItem(EditItemDelete.Create(_buffer.ToString(_current, qty), _current, DeleteChar, qty)); SaveToClipboard(_current, qty); _buffer.Remove(_current, qty); if (_current >= _buffer.Length) { _current = Math.Max(0, _buffer.Length - 1); } Render(); } } else if (orExit) { throw new ExitException(); } } /// /// Delete the character under the cursor. /// [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")] public static void DeleteChar(ConsoleKeyInfo? key = null, object arg = null) { int qty = (arg is int) ? (int)arg : 1; _singleton.DeleteCharImpl(qty, orExit: false); } /// /// Delete the character under the cursor, or if the line is empty, exit the process /// [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")] public static void DeleteCharOrExit(ConsoleKeyInfo? key = null, object arg = null) { _singleton.DeleteCharImpl(1, orExit: true); } private bool AcceptLineImpl(bool validate) { ParseInput(); if (_parseErrors.Any(e => e.IncompleteInput)) { Insert('\n'); return false; } // If text was pasted, for performance reasons we skip rendering for some time, // but if input is accepted, we won't have another chance to render. // // Also - if there was an emphasis, we want to clear that before accepting // and that requires rendering. bool renderNeeded = _emphasisStart >= 0 || _queuedKeys.Count > 0; _emphasisStart = -1; _emphasisLength = 0; var insertionPoint = _current; // Make sure cursor is at the end before writing the line _current = _buffer.Length; if (renderNeeded) { ReallyRender(); } // Only run validation if we haven't before. If we have and status line shows an error, // treat that as a -Force and accept the input so it is added to history, and PowerShell // can report an error as it normally does. if (validate && !_statusIsErrorMessage) { var errorMessage = Validate(_ast); if (!string.IsNullOrWhiteSpace(errorMessage)) { // If there are more keys, assume the user pasted with a right click and // we should insert a newline even though validation failed. if (_queuedKeys.Count > 0) { // Validation may have moved the cursor. Because there are queued // keys, we need to move the cursor back to the correct place, and // ignore where validation put the cursor because the queued keys // will be inserted in the wrong place. SetCursorPosition(insertionPoint); Insert('\n'); } _statusLinePrompt = ""; _statusBuffer.Append(errorMessage); _statusIsErrorMessage = true; Render(); return false; } } if (_statusIsErrorMessage) { ClearStatusMessage(render: true); } var coordinates = ConvertOffsetToCoordinates(_current); var y = coordinates.Y + 1; PlaceCursor(0, ref y); _inputAccepted = true; return true; } class CommandValidationVisitor : AstVisitor { private readonly Ast _rootAst; internal string detectedError; internal CommandValidationVisitor(Ast rootAst) { _rootAst = rootAst; } public override AstVisitAction VisitCommand(CommandAst commandAst) { var commandName = commandAst.GetCommandName(); if (commandName != null) { if (_singleton._engineIntrinsics != null) { var commandInfo = _singleton._engineIntrinsics.InvokeCommand.GetCommand(commandName, CommandTypes.All); if (commandInfo == null && !_singleton.UnresolvedCommandCouldSucceed(commandName, _rootAst)) { _singleton._current = commandAst.CommandElements[0].Extent.EndOffset; detectedError = string.Format(CultureInfo.CurrentCulture, PSReadLineResources.CommandNotFoundError, commandName); return AstVisitAction.StopVisit; } } if (commandAst.CommandElements.Any(e => e is ScriptBlockExpressionAst)) { if (_singleton._options.CommandsToValidateScriptBlockArguments == null || !_singleton._options.CommandsToValidateScriptBlockArguments.Contains(commandName)) { return AstVisitAction.SkipChildren; } } } if (_singleton._options.CommandValidationHandler != null) { try { _singleton._options.CommandValidationHandler(commandAst); } catch (Exception e) { detectedError = e.Message; } } return !string.IsNullOrWhiteSpace(detectedError) ? AstVisitAction.StopVisit : AstVisitAction.Continue; } } private string Validate(Ast rootAst) { if (_parseErrors != null && _parseErrors.Length > 0) { // Move the cursor to the point of error _current = _parseErrors[0].Extent.EndOffset; return _parseErrors[0].Message; } var validationVisitor = new CommandValidationVisitor(rootAst); rootAst.Visit(validationVisitor); if (!string.IsNullOrWhiteSpace(validationVisitor.detectedError)) { return validationVisitor.detectedError; } return null; } private bool UnresolvedCommandCouldSucceed(string commandName, Ast rootAst) { // This is a little hacky, but we check for a few things where part of the current // command defines/imports new commands that PowerShell might not yet know about. // There is little reason to go to great lengths at being correct here, validation // is just a small usability tweak to avoid cluttering up history - PowerShell // will report errors for stuff we actually let through. // Do we define a function matching the command name? var fnDefns = rootAst.FindAll(ast => ast is FunctionDefinitionAst, true).OfType(); if (fnDefns.Any(fnDefnAst => fnDefnAst.Name.Equals(commandName, StringComparison.OrdinalIgnoreCase))) { return true; } var cmdAsts = rootAst.FindAll(ast => ast is CommandAst, true).OfType(); foreach (var cmdAst in cmdAsts) { // If we dot source something, we can't in general know what is being // dot sourced so just assume the unresolved command will work. // If we use the invocation operator, allow that because an expression // is being invoked and it's reasonable to just allow it. if (cmdAst.InvocationOperator != TokenKind.Unknown) { return true; } // Are we importing a module or being tricky with Invoke-Expression? Let those through. var candidateCommand = cmdAst.GetCommandName(); if (candidateCommand.Equals("Import-Module", StringComparison.OrdinalIgnoreCase) || candidateCommand.Equals("ipmo", StringComparison.OrdinalIgnoreCase) || candidateCommand.Equals("Invoke-Expression", StringComparison.OrdinalIgnoreCase) || candidateCommand.Equals("iex", StringComparison.OrdinalIgnoreCase)) { return true; } } if (commandName.Length == 1) { switch (commandName[0]) { // The following are debugger commands that should be accepted if we're debugging // because the console host will interpret these commands directly. case 's': case 'v': case 'o': case 'c': case 'q': case 'k': case 'l': case 'S': case 'V': case 'O': case 'C': case 'Q': case 'K': case 'L': case '?': case 'h': case 'H': // Ideally we would check $PSDebugContext, but it is set at function // scope, and because we're in a module, we can't find that variable // (arguably a PowerShell issue.) // NestedPromptLevel is good enough though - it's rare to be in a nested. var nestedPromptLevel = _engineIntrinsics.SessionState.PSVariable.GetValue("NestedPromptLevel"); if (nestedPromptLevel is int) { return ((int)nestedPromptLevel) > 0; } break; } } return false; } static bool StaticParameterBindingSupported(CommandInfo commandInfo) { var aliasInfo = commandInfo as AliasInfo; if (aliasInfo != null) { commandInfo = aliasInfo.ResolvedCommand; } return (commandInfo is ExternalScriptInfo) || (commandInfo is CmdletInfo) || (commandInfo is FunctionInfo); } /// /// Attempt to execute the current input. If the current input is incomplete (for /// example there is a missing closing parenthesis, bracket, or quote, then the /// continuation prompt is displayed on the next line and PSReadline waits for /// keys to edit the current input. /// [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")] public static void AcceptLine(ConsoleKeyInfo? key = null, object arg = null) { _singleton.AcceptLineImpl(false); } /// /// Attempt to execute the current input. If the current input is incomplete (for /// example there is a missing closing parenthesis, bracket, or quote, then the /// continuation prompt is displayed on the next line and PSReadline waits for /// keys to edit the current input. /// [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")] public static void ValidateAndAcceptLine(ConsoleKeyInfo? key = null, object arg = null) { _singleton.AcceptLineImpl(true); } /// /// Attempt to execute the current input. If it can be executed (like AcceptLine), /// then recall the next item from history the next time Readline is called. /// [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")] public static void AcceptAndGetNext(ConsoleKeyInfo? key = null, object arg = null) { if (_singleton.AcceptLineImpl(false)) { if (_singleton._currentHistoryIndex < (_singleton._history.Count - 1)) { _singleton._getNextHistoryIndex = _singleton._currentHistoryIndex + 1; } else { Ding(); } } } /// /// The continuation prompt is displayed on the next line and PSReadline waits for /// keys to edit the current input. This is useful to enter multi-line input as /// a single command even when a single line is complete input by itself. /// [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")] public static void AddLine(ConsoleKeyInfo? key = null, object arg = null) { Insert('\n'); } /// /// A new empty line is created above the current line regardless of where the cursor /// is on the current line. The cursor moves to the beginning of the new line. /// [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")] public static void InsertLineAbove(ConsoleKeyInfo? key = null, object arg = null) { // Move the current position to the beginning of the current line and only the current line. if (_singleton.LineIsMultiLine()) { int i = Math.Max(0, _singleton._current - 1); for (; i > 0; i--) { if (_singleton._buffer[i] == '\n') { i += 1; break; } } _singleton._current = i; } else { _singleton._current = 0; } Insert('\n'); PreviousLine(); } /// /// A new empty line is created below the current line regardless of where the cursor /// is on the current line. The cursor moves to the beginning of the new line. /// [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")] public static void InsertLineBelow(ConsoleKeyInfo? key = null, object arg = null) { // Move the current position to the end of the current line and only the current line. if (_singleton.LineIsMultiLine()) { int i = _singleton._current; for (; i < _singleton._buffer.Length; i++) { if (_singleton._buffer[i] == '\n') { break; } } _singleton._current = i; } else { _singleton._current = _singleton._buffer.Length; } Insert('\n'); } } }