using System; using System.IO; using System.Linq; using System.Management.Automation; using System.Reflection; using Microsoft.PowerShell; using Xunit; namespace Test { public partial class ReadLine { private void SetHistory(params string[] historyItems) { PSConsoleReadLine.ClearHistory(); foreach (var item in historyItems) { PSConsoleReadLine.AddToHistory(item); } } [SkippableFact] public void History() { TestSetup(KeyMode.Cmd); // No history SetHistory(); Test("", Keys(_.UpArrow, _.DownArrow)); SetHistory("dir c*", "ps p*"); Test("dir c*", Keys(_.UpArrow, _.UpArrow)); Test("dir c*", Keys(_.UpArrow, _.UpArrow, _.DownArrow)); } [SkippableFact] public void ParallelHistorySaving() { TestSetup(KeyMode.Cmd); string historySavingFile = Path.GetTempFileName(); var options = new SetPSReadLineOption { HistorySaveStyle = HistorySaveStyle.SaveIncrementally, MaximumHistoryCount = 3, }; typeof(SetPSReadLineOption) .GetField("_historySavePath", BindingFlags.Instance | BindingFlags.NonPublic) .SetValue(options, historySavingFile); PSConsoleReadLine.SetOptions(options); // Set the initial history items. string[] initialHistoryItems = new[] { "gcm help", "dir ~" }; SetHistory(initialHistoryItems); // The initial history items should be saved to file. string[] text = File.ReadAllLines(historySavingFile); Assert.Equal(initialHistoryItems.Length, text.Length); for (int i = 0; i < text.Length; i++) { Assert.Equal(initialHistoryItems[i], text[i]); } // Add another line to the file to mimic the new history saving from a different session. using (var file = File.AppendText(historySavingFile)) { file.WriteLine("cd Downloads"); } PSConsoleReadLine.AddToHistory("cd Documents"); string[] expectedSavedLines = new[] { "gcm help", "dir ~", "cd Downloads", "cd Documents" }; text = File.ReadAllLines(historySavingFile); Assert.Equal(expectedSavedLines.Length, text.Length); for (int i = 0; i < text.Length; i++) { Assert.Equal(expectedSavedLines[i], text[i]); } string[] expectedHistoryItems = new[] { "dir ~", "cd Documents", "cd Downloads" }; var historyItems = PSConsoleReadLine.GetHistoryItems(); Assert.Equal(expectedHistoryItems.Length, historyItems.Length); for (int i = 0; i < historyItems.Length; i++) { Assert.Equal(expectedHistoryItems[i], historyItems[i].CommandLine); } } [SkippableFact] public void SensitiveHistoryDefaultBehavior_One() { TestSetup(KeyMode.Cmd); // No history SetHistory(); Test("", Keys(_.UpArrow, _.DownArrow)); var options = PSConsoleReadLine.GetOptions(); var oldHistoryFilePath = options.HistorySavePath; var oldHistorySaveStyle = options.HistorySaveStyle; // AddToHistoryHandler should be set to the default handler. Assert.Same(PSConsoleReadLineOptions.DefaultAddToHistoryHandler, options.AddToHistoryHandler); var newHistoryFilePath = Path.GetTempFileName(); var newHistorySaveStyle = HistorySaveStyle.SaveIncrementally; string[] expectedHistoryItems = new[] { "gcm c*", "ConvertTo-SecureString -AsPlainText -String abc -Force", "dir p*", "Publish-Module -NuGetApiKey abc", "ps c*", "mycommand -password abc", "echo foo", "cmd1 /token abc", "echo bar", "cmd2 --apikey abc", "echo zoo", "pki secret", "gcm p*" }; string[] expectedSavedItems = new[] { "gcm c*", "dir p*", "ps c*", "echo foo", "echo bar", "echo zoo", "gcm p*" }; try { options.HistorySavePath = newHistoryFilePath; options.HistorySaveStyle = newHistorySaveStyle; SetHistory(expectedHistoryItems); // Sensitive input history should be kept in the internal history queue. var historyItems = PSConsoleReadLine.GetHistoryItems(); Assert.Equal(expectedHistoryItems.Length, historyItems.Length); for (int i = 0; i < expectedHistoryItems.Length; i++) { Assert.Equal(expectedHistoryItems[i], historyItems[i].CommandLine); } // Sensitive input history should NOT be saved to the history file. string[] text = File.ReadAllLines(newHistoryFilePath); Assert.Equal(expectedSavedItems.Length, text.Length); for (int i = 0; i < text.Length; i++) { Assert.Equal(expectedSavedItems[i], text[i]); } } finally { options.HistorySavePath = oldHistoryFilePath; options.HistorySaveStyle = oldHistorySaveStyle; File.Delete(newHistoryFilePath); } } [SkippableFact] public void SensitiveHistoryDefaultBehavior_Two() { TestSetup(KeyMode.Cmd); // Clear history SetHistory(); var options = PSConsoleReadLine.GetOptions(); var oldHistoryFilePath = options.HistorySavePath; var oldHistorySaveStyle = options.HistorySaveStyle; // AddToHistoryHandler should be set to the default handler. Assert.Same(PSConsoleReadLineOptions.DefaultAddToHistoryHandler, options.AddToHistoryHandler); var newHistoryFilePath = Path.GetTempFileName(); var newHistorySaveStyle = HistorySaveStyle.SaveIncrementally; string[] expectedHistoryItems = new[] { "$token = 'abcd'", // Assign expr-value to sensitive variable. Not saved to file. "Set-Secret abc $mySecret", // 'Set-Secret' will not be save to file. "ConvertTo-SecureString stringValue -AsPlainText", // '-AsPlainText' is an alert. Not saved to file. "Get-Secret PSGalleryApiKey -AsPlainText", // For eligible secret-mgmt command, the whole command is skipped, so '-AsPlainText' here is OK. "$token = Get-Secret -Name github-token -Vault MySecret", "[MyType]::CallRestAPI($token, $url, $args)", "$template -f $token", "Invoke-RestCall $url -UseDefaultToken", "Publish-Module -NuGetApiKey $apikey", "Publish-Module -NuGetApiKey (Get-Secret PSGalleryApiKey -AsPlainText)", "Publish-Module -NuGetApiKey (Get-NewSecret -Name apikey)", // 'Get-NewSecret' is not in our allow-list. Not saved to file. "Send-HeartBeat -UseDefaultToken", "Send-HeartBeat -password $pass -SavePassword", "Invoke-WebRequest -Token xxx", // Expr-value as argument to '-Token'. Not saved to file. "Invoke-WebRequest -Token (2+2)", // Expr-value as argument to '-Token'. Not saved to file. "Get-SecretInfo -Name mytoken; Get-SecretVault; Register-SecretVault; Remove-Secret apikey", "Get-SecretInfo -Name mytoken; Get-SecretVault; Register-SecretVault; Remove-Secret apikey; Set-Secret", // 'Set-Secret' Not saved to file. "Set-SecretInfo -Name apikey; Set-SecretVaultDefault; Test-SecretVault; Unlock-SecretVault -password $pwd; Unregister-SecretVault -SecretVault vaultInfo", "Get-ResultFromTwo -Secret1 (Get-Secret -Name blah -AsPlainText) -Secret2 $secret2", "Get-ResultFromTwo -Secret1 (Get-Secret -Name blah -AsPlainText) -Secret2 sdv87ysdfayf798hfasd8f7ha", // '-Secret2' has expr-value argument. Not saved to file. "$environment -brand $brand -userBitWardenEmail $bwuser -userBitWardenPassword $bwpass" // '-userBitWardenPassword' matches sensitive pattern and it has parsing error. Not save to file. }; string[] expectedSavedItems = new[] { "Get-Secret PSGalleryApiKey -AsPlainText", "$token = Get-Secret -Name github-token -Vault MySecret", "[MyType]::CallRestAPI($token, $url, $args)", "$template -f $token", "Invoke-RestCall $url -UseDefaultToken", "Publish-Module -NuGetApiKey $apikey", "Publish-Module -NuGetApiKey (Get-Secret PSGalleryApiKey -AsPlainText)", "Send-HeartBeat -UseDefaultToken", "Send-HeartBeat -password $pass -SavePassword", "Get-SecretInfo -Name mytoken; Get-SecretVault; Register-SecretVault; Remove-Secret apikey", "Set-SecretInfo -Name apikey; Set-SecretVaultDefault; Test-SecretVault; Unlock-SecretVault -password $pwd; Unregister-SecretVault -SecretVault vaultInfo", "Get-ResultFromTwo -Secret1 (Get-Secret -Name blah -AsPlainText) -Secret2 $secret2", }; try { options.HistorySavePath = newHistoryFilePath; options.HistorySaveStyle = newHistorySaveStyle; SetHistory(expectedHistoryItems); // Sensitive input history should be kept in the internal history queue. var historyItems = PSConsoleReadLine.GetHistoryItems(); Assert.Equal(expectedHistoryItems.Length, historyItems.Length); for (int i = 0; i < expectedHistoryItems.Length; i++) { Assert.Equal(expectedHistoryItems[i], historyItems[i].CommandLine); } // Sensitive input history should NOT be saved to the history file. string[] text = File.ReadAllLines(newHistoryFilePath); Assert.Equal(expectedSavedItems.Length, text.Length); for (int i = 0; i < text.Length; i++) { Assert.Equal(expectedSavedItems[i], text[i]); } } finally { options.HistorySavePath = oldHistoryFilePath; options.HistorySaveStyle = oldHistorySaveStyle; File.Delete(newHistoryFilePath); } } [SkippableFact] public void SensitiveHistoryOptionalBehavior() { TestSetup(KeyMode.Cmd); // No history SetHistory(); Test("", Keys(_.UpArrow, _.DownArrow)); var options = PSConsoleReadLine.GetOptions(); var oldHistoryFilePath = options.HistorySavePath; var oldHistorySaveStyle = options.HistorySaveStyle; // AddToHistoryHandler should be set to the default handler. Assert.Same(PSConsoleReadLineOptions.DefaultAddToHistoryHandler, options.AddToHistoryHandler); var newHistoryFilePath = Path.GetTempFileName(); var newHistorySaveStyle = HistorySaveStyle.SaveIncrementally; Func newAddToHistoryHandler_ReturnBool = s => s.Contains("gal"); Func newAddToHistoryHandler_ReturnEnum = s => s.Contains("gal") ? AddToHistoryOption.MemoryOnly : s.Contains("gmo") ? AddToHistoryOption.SkipAdding : AddToHistoryOption.MemoryAndFile; Func newAddToHistoryHandler_ReturnOther = s => "string value"; string[] commandInputs = new[] { "gmo p*", "gcm c*", "gal dir", "ConvertTo-SecureString -AsPlainText -String abc -Force" }; string[] expectedQueuedItems = new[] { "gcm c*", "gal dir", "ConvertTo-SecureString -AsPlainText -String abc -Force" }; string[] expectedSavedItems = new[] { "gcm c*", "ConvertTo-SecureString -AsPlainText -String abc -Force" }; try { options.HistorySavePath = newHistoryFilePath; options.HistorySaveStyle = newHistorySaveStyle; // // Set null to the handler means we don't do the check. // options.AddToHistoryHandler = null; SetHistory(commandInputs); // All commands should be kept in the internal history queue. var historyItems = PSConsoleReadLine.GetHistoryItems(); Assert.Equal(commandInputs.Length, historyItems.Length); for (int i = 0; i < commandInputs.Length; i++) { Assert.Equal(commandInputs[i], historyItems[i].CommandLine); } // All commands are saved to the history file when 'ScrubSensitiveHistory' is set to 'false'. string[] text = File.ReadAllLines(newHistoryFilePath); Assert.Equal(commandInputs.Length, text.Length); for (int i = 0; i < text.Length; i++) { Assert.Equal(commandInputs[i], text[i]); } // // Use a handler that return boolean value. // true: Add to memory and file // false: Skip adding to history // options.AddToHistoryHandler = newAddToHistoryHandler_ReturnBool; // Clear the history file. File.WriteAllText(newHistoryFilePath, string.Empty); SetHistory(commandInputs); historyItems = PSConsoleReadLine.GetHistoryItems(); Assert.Single(historyItems); Assert.Equal("gal dir", historyItems[0].CommandLine); text = File.ReadAllLines(newHistoryFilePath); Assert.Single(text); Assert.Equal("gal dir", text[0]); // // Use a handler that return the expected enum type. // options.AddToHistoryHandler = newAddToHistoryHandler_ReturnEnum; File.WriteAllText(newHistoryFilePath, string.Empty); SetHistory(commandInputs); historyItems = PSConsoleReadLine.GetHistoryItems(); Assert.Equal(expectedQueuedItems.Length, historyItems.Length); for (int i = 0; i < expectedQueuedItems.Length; i++) { Assert.Equal(expectedQueuedItems[i], historyItems[i].CommandLine); } text = File.ReadAllLines(newHistoryFilePath); Assert.Equal(expectedSavedItems.Length, text.Length); for (int i = 0; i < text.Length; i++) { Assert.Equal(expectedSavedItems[i], text[i]); } // // Use a handler that return unexpected value. // - same behavior as setting the handler to null. // options.AddToHistoryHandler = newAddToHistoryHandler_ReturnOther; File.WriteAllText(newHistoryFilePath, string.Empty); SetHistory(commandInputs); historyItems = PSConsoleReadLine.GetHistoryItems(); Assert.Equal(commandInputs.Length, historyItems.Length); for (int i = 0; i < commandInputs.Length; i++) { Assert.Equal(commandInputs[i], historyItems[i].CommandLine); } // All commands are saved to the history file when 'ScrubSensitiveHistory' is set to 'false'. text = File.ReadAllLines(newHistoryFilePath); Assert.Equal(commandInputs.Length, text.Length); for (int i = 0; i < text.Length; i++) { Assert.Equal(commandInputs[i], text[i]); } } finally { options.HistorySavePath = oldHistoryFilePath; options.HistorySaveStyle = oldHistorySaveStyle; options.AddToHistoryHandler = PSConsoleReadLineOptions.DefaultAddToHistoryHandler; File.Delete(newHistoryFilePath); } } [SkippableFact] public void SensitiveHistoryOptionalBehaviorWithScriptBlock() { TestSetup(KeyMode.Cmd); // No history SetHistory(); Test("", Keys(_.UpArrow, _.DownArrow)); var options = PSConsoleReadLine.GetOptions(); var oldHistoryFilePath = options.HistorySavePath; var oldHistorySaveStyle = options.HistorySaveStyle; // AddToHistoryHandler should be set to the default handler. Assert.Same(PSConsoleReadLineOptions.DefaultAddToHistoryHandler, options.AddToHistoryHandler); var newHistoryFilePath = Path.GetTempFileName(); var newHistorySaveStyle = HistorySaveStyle.SaveIncrementally; Func newAddToHistoryHandler_ReturnBool = LanguagePrimitives.ConvertTo>( ScriptBlock.Create(@" param([string]$line) $line.Contains('gal')")); Func newAddToHistoryHandler_ReturnEnum = LanguagePrimitives.ConvertTo>( ScriptBlock.Create(@" param([string]$line) if ($line.Contains('gal')) { [psobject]::AsPSObject([Microsoft.PowerShell.AddToHistoryOption]::MemoryOnly) } elseif ($line.Contains('gmo')) { 'SkipAdding' } else { [Microsoft.PowerShell.AddToHistoryOption]::MemoryAndFile }")); Func newAddToHistoryHandler_ReturnOther = LanguagePrimitives.ConvertTo>( ScriptBlock.Create(@" param([string]$line) 'string value'")); string[] commandInputs = new[] { "gmo p*", "gcm c*", "gal dir", "ConvertTo-SecureString -AsPlainText -String abc -Force" }; string[] expectedQueuedItems = new[] { "gcm c*", "gal dir", "ConvertTo-SecureString -AsPlainText -String abc -Force" }; string[] expectedSavedItems = new[] { "gcm c*", "ConvertTo-SecureString -AsPlainText -String abc -Force" }; try { options.HistorySavePath = newHistoryFilePath; options.HistorySaveStyle = newHistorySaveStyle; // // Set null to the handler means we don't do the check. // options.AddToHistoryHandler = null; SetHistory(commandInputs); // All commands should be kept in the internal history queue. var historyItems = PSConsoleReadLine.GetHistoryItems(); Assert.Equal(commandInputs.Length, historyItems.Length); for (int i = 0; i < commandInputs.Length; i++) { Assert.Equal(commandInputs[i], historyItems[i].CommandLine); } // All commands are saved to the history file when 'ScrubSensitiveHistory' is set to 'false'. string[] text = File.ReadAllLines(newHistoryFilePath); Assert.Equal(commandInputs.Length, text.Length); for (int i = 0; i < text.Length; i++) { Assert.Equal(commandInputs[i], text[i]); } // // Use a handler that return boolean value. // true: Add to memory and file // false: Skip adding to history // options.AddToHistoryHandler = newAddToHistoryHandler_ReturnBool; // Clear the history file. File.WriteAllText(newHistoryFilePath, string.Empty); SetHistory(commandInputs); historyItems = PSConsoleReadLine.GetHistoryItems(); Assert.Single(historyItems); Assert.Equal("gal dir", historyItems[0].CommandLine); text = File.ReadAllLines(newHistoryFilePath); Assert.Single(text); Assert.Equal("gal dir", text[0]); // // Use a handler that return the expected enum type. // options.AddToHistoryHandler = newAddToHistoryHandler_ReturnEnum; File.WriteAllText(newHistoryFilePath, string.Empty); SetHistory(commandInputs); historyItems = PSConsoleReadLine.GetHistoryItems(); Assert.Equal(expectedQueuedItems.Length, historyItems.Length); for (int i = 0; i < expectedQueuedItems.Length; i++) { Assert.Equal(expectedQueuedItems[i], historyItems[i].CommandLine); } text = File.ReadAllLines(newHistoryFilePath); Assert.Equal(expectedSavedItems.Length, text.Length); for (int i = 0; i < text.Length; i++) { Assert.Equal(expectedSavedItems[i], text[i]); } // // Use a handler that return unexpected value. // - same behavior as setting the handler to null. // options.AddToHistoryHandler = newAddToHistoryHandler_ReturnOther; File.WriteAllText(newHistoryFilePath, string.Empty); SetHistory(commandInputs); historyItems = PSConsoleReadLine.GetHistoryItems(); Assert.Equal(commandInputs.Length, historyItems.Length); for (int i = 0; i < commandInputs.Length; i++) { Assert.Equal(commandInputs[i], historyItems[i].CommandLine); } // All commands are saved to the history file when 'ScrubSensitiveHistory' is set to 'false'. text = File.ReadAllLines(newHistoryFilePath); Assert.Equal(commandInputs.Length, text.Length); for (int i = 0; i < text.Length; i++) { Assert.Equal(commandInputs[i], text[i]); } } finally { options.HistorySavePath = oldHistoryFilePath; options.HistorySaveStyle = oldHistorySaveStyle; options.AddToHistoryHandler = PSConsoleReadLineOptions.DefaultAddToHistoryHandler; File.Delete(newHistoryFilePath); } } [SkippableFact] public void HistoryRecallCurrentLine() { TestSetup(KeyMode.Cmd); // Recall history backward and forward. SetHistory("echo foo", "echo bar"); Test("ec", Keys( "ec", _.UpArrow, CheckThat(() => AssertLineIs("echo bar")), _.UpArrow, CheckThat(() => AssertLineIs("echo foo")), _.DownArrow, CheckThat(() => AssertLineIs("echo bar")), _.DownArrow)); // Verify that the saved current line gets reset when the line gets edited. // Recall history, then edit the line, and recall again. SetHistory("echo foo", "echo bar"); Test("get", Keys( "ec", _.UpArrow, _.DownArrow, CheckThat(() => AssertLineIs("ec")), _.Escape, "get", _.UpArrow, _.DownArrow)); // Recall history, then edit the line, and recall again. SetHistory("echo foo", "echo bar"); Test("ge", Keys( "ec", _.UpArrow, _.DownArrow, CheckThat(() => AssertLineIs("ec")), _.Backspace, _.Backspace, "ge", CheckThat(() => AssertLineIs("ge")), _.UpArrow, _.DownArrow)); // Recall history, then edit the line, and recall again. SetHistory("echo foo", "echo bar"); Test("", Keys( "ec", _.UpArrow, _.DownArrow, CheckThat(() => AssertLineIs("ec")), "h", CheckThat(() => AssertLineIs("ech")), _.UpArrow, CheckThat(() => AssertLineIs("echo bar")), _.DownArrow, CheckThat(() => AssertLineIs("ech")), _.Escape)); } [SkippableFact] public void HistorySearchCurrentLine() { TestSetup(KeyMode.Cmd, new KeyHandler("UpArrow", PSConsoleReadLine.HistorySearchBackward), new KeyHandler("DownArrow", PSConsoleReadLine.HistorySearchForward)); // Search history backward and forward. SetHistory("echo foo", "echo bar"); Test("ec", Keys( "ec", _.UpArrow, CheckThat(() => AssertLineIs("echo bar")), _.UpArrow, CheckThat(() => AssertLineIs("echo foo")), _.DownArrow, CheckThat(() => AssertLineIs("echo bar")), _.DownArrow)); // Verify that the saved current line gets reset when the line gets edited. // Search history, then edit the line, and search again. SetHistory("echo foo", "echo bar"); Test("echo ", Keys( "ec", _.UpArrow, _.DownArrow, CheckThat(() => AssertLineIs("ec")), _.Escape, "echo ", _.UpArrow, CheckThat(() => AssertLineIs("echo bar")), _.DownArrow)); // Search history, then edit the line, and search again. SetHistory("echo foo", "echo bar"); Test("echo", Keys( "ec", _.UpArrow, _.DownArrow, "ho", CheckThat(() => AssertLineIs("echo")), _.UpArrow, _.DownArrow)); // Search history, then edit the line, and search again. SetHistory("echo foo", "echo bar"); Test("e", Keys( "ec", _.UpArrow, _.DownArrow, _.Backspace, CheckThat(() => AssertLineIs("e")), _.UpArrow, _.DownArrow)); // Search history, then edit the line, and search again. SetHistory("echo foo", "echo bar"); Test("", Keys( "ec", _.UpArrow, _.DownArrow, "ho f", _.UpArrow, CheckThat(() => AssertLineIs("echo foo")), _.DownArrow, CheckThat(() => AssertLineIs("echo f")), _.Escape)); } [SkippableFact] public void HistorySavedCurrentLine() { TestSetup(KeyMode.Cmd, new KeyHandler("F3", PSConsoleReadLine.BeginningOfHistory), new KeyHandler("Shift+F3", PSConsoleReadLine.EndOfHistory)); // Mix different history commands to verify that the saved current line and // the history index stay the same while in a series of history commands. SetHistory("echo foo", "echo bar"); Test("ec", Keys( "ec", _.UpArrow, CheckThat(() => AssertLineIs("echo bar")), _.F3, CheckThat(() => AssertLineIs("echo foo")), _.DownArrow, CheckThat(() => AssertLineIs("echo bar")), _.DownArrow)); SetHistory("echo foo", "get zoo", "echo bar"); Test("ec", Keys( "ec", _.UpArrow, CheckThat(() => AssertLineIs("echo bar")), _.F3, CheckThat(() => AssertLineIs("echo foo")), _.Shift_F3)); SetHistory("echo foo", "get zoo", "echo bar"); Test("e", Keys( "e", _.UpArrow, CheckThat(() => AssertLineIs("echo bar")), _.UpArrow, CheckThat(() => AssertLineIs("get zoo")), _.Shift_F3)); SetHistory("echo foo", "get zoo", "echo bar"); Test("ech", Keys( "ech", _.F8, CheckThat(() => AssertLineIs("echo bar")), _.F3, CheckThat(() => AssertLineIs("echo foo")), _.DownArrow, CheckThat(() => AssertLineIs("get zoo")), _.DownArrow, CheckThat(() => AssertLineIs("echo bar")), _.DownArrow)); SetHistory("echo foo", "get zoo", "echo bar"); Test("ech", Keys( "ech", _.F8, CheckThat(() => AssertLineIs("echo bar")), _.F8, CheckThat(() => AssertLineIs("echo foo")), _.Shift_F3)); SetHistory("echo foo", "get bar", "echo f"); Test("ec", Keys( "ec", _.UpArrow, CheckThat(() => AssertLineIs("echo f")), _.F8, CheckThat(() => AssertLineIs("echo foo")), _.Shift_F8, CheckThat(() => AssertLineIs("echo f")), _.DownArrow)); SetHistory("echo foo", "get bar", "echo f"); Test("ec", Keys( "ec", _.UpArrow, _.F8, _.DownArrow, CheckThat(() => AssertLineIs("get bar")), _.DownArrow, CheckThat(() => AssertLineIs("echo f")), _.Shift_F3)); SetHistory("echo kv", "get bar", "echo f"); Test("e", Keys( "e", _.UpArrow, CheckThat(() => AssertLineIs("echo f")), _.Ctrl_r, "v", _.Escape, CheckThat(() => AssertLineIs("echo kv")), _.Ctrl_s, "f", _.Escape, CheckThat(() => AssertLineIs("echo f")), _.UpArrow, CheckThat(() => AssertLineIs("get bar")), _.DownArrow, _.DownArrow)); } [SkippableFact] public void SearchHistory() { TestSetup(KeyMode.Cmd, new KeyHandler("UpArrow", PSConsoleReadLine.HistorySearchBackward), new KeyHandler("DownArrow", PSConsoleReadLine.HistorySearchForward)); // No history SetHistory(); Test("", Keys(_.UpArrow, _.DownArrow)); // Clear history in case the above added some history (but it shouldn't) SetHistory(); Test(" ", Keys(' ', _.UpArrow, _.DownArrow)); PSConsoleReadLine.SetOptions(new SetPSReadLineOption {HistorySearchCursorMovesToEnd = false}); var emphasisColors = Tuple.Create(PSConsoleReadLineOptions.DefaultEmphasisColor, _console.BackgroundColor); SetHistory("dosomething", "ps p*", "dir", "echo zzz"); Test("dosomething", Keys( "d", _.UpArrow, CheckThat(() => { AssertScreenIs(1, emphasisColors, 'd', TokenClassification.Command, "ir"); AssertCursorLeftIs(1); }), _.UpArrow, CheckThat(() => { AssertScreenIs(1, emphasisColors, 'd', TokenClassification.Command, "osomething"); AssertCursorLeftIs(1); }))); PSConsoleReadLine.SetOptions(new SetPSReadLineOption {HistorySearchCursorMovesToEnd = true}); SetHistory("dosomething", "ps p*", "dir", "echo zzz"); Test("dosomething", Keys( "d", _.UpArrow, CheckThat(() => { AssertScreenIs(1, emphasisColors, 'd', TokenClassification.Command, "ir"); AssertCursorLeftIs(3); }), _.UpArrow, CheckThat(() => { AssertScreenIs(1, emphasisColors, 'd', TokenClassification.Command, "osomething"); AssertCursorLeftIs(11); }), _.DownArrow, CheckThat(() => { AssertScreenIs(1, emphasisColors, 'd', TokenClassification.Command, "ir"); AssertCursorLeftIs(3); }), _.UpArrow, CheckThat(() => { AssertScreenIs(1, emphasisColors, 'd', TokenClassification.Command, "osomething"); AssertCursorLeftIs(11); }))); } [SkippableFact] public void HistorySearchCursorMovesToEnd() { TestSetup(KeyMode.Cmd, new KeyHandler("UpArrow", PSConsoleReadLine.HistorySearchBackward), new KeyHandler("DownArrow", PSConsoleReadLine.HistorySearchForward)); PSConsoleReadLine.SetOptions(new SetPSReadLineOption {HistorySearchCursorMovesToEnd = true}); var emphasisColors = Tuple.Create(PSConsoleReadLineOptions.DefaultEmphasisColor, _console.BackgroundColor); SetHistory("dosomething", "ps p*", "dir", "echo zzz"); Test("dosomething", Keys( "d", _.UpArrow, CheckThat(() => { AssertScreenIs(1, emphasisColors, 'd', TokenClassification.Command, "ir"); AssertCursorLeftIs(3); }), _.UpArrow, CheckThat(() => { AssertScreenIs(1, emphasisColors, 'd', TokenClassification.Command, "osomething"); AssertCursorLeftIs(11); }), _.DownArrow, CheckThat(() => { AssertScreenIs(1, emphasisColors, 'd', TokenClassification.Command, "ir"); AssertCursorLeftIs(3); }), _.UpArrow, CheckThat(() => { AssertScreenIs(1, emphasisColors, 'd', TokenClassification.Command, "osomething"); AssertCursorLeftIs(11); }))); } [SkippableFact] public void BeginningOfHistory() { Skip.IfNot(KeyboardHasLessThan); TestSetup(KeyMode.Emacs); SetHistory("echo first", "echo second", "echo third"); Test("echo first", Keys(_.Alt_Less)); SetHistory("echo first", "echo second", "echo third"); Test("echo second", Keys(_.Alt_Less, _.DownArrow)); } [SkippableFact] public void EndOfHistory() { Skip.IfNot(KeyboardHasGreaterThan); TestSetup(KeyMode.Emacs); SetHistory("echo first", "echo second", "echo third"); Test("", Keys(_.UpArrow, _.Alt_Greater)); // Make sure end of history restores the "current" line if // there was anything entered before going through history Test("abc", Keys("abc", _.UpArrow, _.Alt_Greater)); // Make sure we don't recall the previous "current" line // after we accepted it. Test("", Keys(_.Alt_Greater)); } [SkippableFact] public void InteractiveHistorySearch() { TestSetup(KeyMode.Emacs); SetHistory("echo aaa"); Test("echo aaa", Keys(_.Ctrl_r, 'a')); var emphasisColors = Tuple.Create(PSConsoleReadLineOptions.DefaultEmphasisColor, _console.BackgroundColor); var statusColors = Tuple.Create(_console.ForegroundColor, _console.BackgroundColor); // Test entering multiple characters and the line is updated with new matches SetHistory("zz1", "echo abc", "zz2", "echo abb", "zz3", "echo aaa", "zz4"); Test("echo abc", Keys(_.Ctrl_r, 'a', CheckThat(() => AssertScreenIs(2, TokenClassification.Command, "echo", TokenClassification.None, " ", emphasisColors, 'a', TokenClassification.None, "aa", NextLine, statusColors, "bck-i-search: a_")), 'b', CheckThat(() => AssertScreenIs(2, TokenClassification.Command, "echo", TokenClassification.None, " ", emphasisColors, "ab", TokenClassification.None, 'b', NextLine, statusColors, "bck-i-search: ab_")), 'c', CheckThat(() => AssertScreenIs(2, TokenClassification.Command, "echo", TokenClassification.None, " ", emphasisColors, "abc", NextLine, statusColors, "bck-i-search: abc_")))); // Test repeated Ctrl+r goes back through multiple matches SetHistory("zz1", "echo abc", "zz2", "echo abb", "zz3", "echo aaa", "zz4"); Test("echo abc", Keys(_.Ctrl_r, 'a', CheckThat(() => AssertScreenIs(2, TokenClassification.Command, "echo", TokenClassification.None, " ", emphasisColors, 'a', TokenClassification.None, "aa", NextLine, statusColors, "bck-i-search: a_")), _.Ctrl_r, CheckThat(() => AssertScreenIs(2, TokenClassification.Command, "echo", TokenClassification.None, " ", emphasisColors, 'a', TokenClassification.None, "bb", NextLine, statusColors, "bck-i-search: a_")), _.Ctrl_r, CheckThat(() => AssertScreenIs(2, TokenClassification.Command, "echo", TokenClassification.None, " ", emphasisColors, 'a', TokenClassification.None, "bc", NextLine, statusColors, "bck-i-search: a_")))); // Test that the current match doesn't change when typing // additional characters, only emphasis should change. SetHistory("zz1", "echo abzz", "echo abc", "zz2"); Test("echo abc", Keys(_.Ctrl_r, 'a', CheckThat(() => AssertScreenIs(2, TokenClassification.Command, "echo", TokenClassification.None, " ", emphasisColors, 'a', TokenClassification.None, "bc", NextLine, statusColors, "bck-i-search: a_")), 'b', CheckThat(() => AssertScreenIs(2, TokenClassification.Command, "echo", TokenClassification.None, " ", emphasisColors, "ab", TokenClassification.None, 'c', NextLine, statusColors, "bck-i-search: ab_")))); // Test that abort restores line state before Ctrl+r SetHistory("zz1", "echo abzz", "echo abc", "zz2"); Test("echo zed", Keys("echo zed", _.Ctrl_r, 'a', CheckThat(() => AssertScreenIs(2, TokenClassification.Command, "echo", TokenClassification.None, " ", emphasisColors, 'a', TokenClassification.None, "bc", NextLine, statusColors, "bck-i-search: a_")), _.Ctrl_g, CheckThat(() => AssertScreenIs(2, TokenClassification.Command, "echo", TokenClassification.None, " ", TokenClassification.None, "zed", NextLine)))); // Test that a random function terminates the search and has an // effect on the line found in history SetHistory("zz1", "echo abzz", "echo abc", "zz2"); Test("echo zed", Keys(_.Ctrl_r, 'a', CheckThat(() => AssertScreenIs(2, TokenClassification.Command, "echo", TokenClassification.None, " ", emphasisColors, 'a', TokenClassification.None, "bc", NextLine, statusColors, "bck-i-search: a_")), _.Alt_d, "zed")); // Test that Escape terminates the search leaving the // cursor at the point in the match. SetHistory("zz1", "echo abzz", "echo abc", "zz2"); Test("echo yabc", Keys(_.Ctrl_r, 'a', CheckThat(() => AssertScreenIs(2, TokenClassification.Command, "echo", TokenClassification.None, " ", emphasisColors, 'a', TokenClassification.None, "bc", NextLine, statusColors, "bck-i-search: a_")), _.Escape, "y")); // Test entering multiple characters, then backspace, make sure we restore // the correct line SetHistory("zz1", "echo abc", "zz2", "echo abb", "zz3", "echo aaa", "zz4"); Test("echo aaa", Keys(_.Ctrl_r, _.Backspace, // Try backspace on empty search string "ab", CheckThat(() => AssertScreenIs(2, TokenClassification.Command, "echo", TokenClassification.None, " ", emphasisColors, "ab", TokenClassification.None, 'b', NextLine, statusColors, "bck-i-search: ab_")), _.Backspace, CheckThat(() => AssertScreenIs(2, TokenClassification.Command, "echo", TokenClassification.None, " ", emphasisColors, 'a', TokenClassification.None, "aa", NextLine, statusColors, "bck-i-search: a_")))); SetHistory("zz1", "echo abzz", "echo abc", "zz2"); Test("", Keys(_.Ctrl_r, 'a', CheckThat(() => AssertScreenIs(2, TokenClassification.Command, "echo", TokenClassification.None, " ", emphasisColors, 'a', TokenClassification.None, "bc", NextLine, statusColors, "bck-i-search: a_")), _.Ctrl_r, CheckThat(() => AssertScreenIs(2, TokenClassification.Command, "echo", TokenClassification.None, " ", emphasisColors, 'a', TokenClassification.None, "bzz", NextLine, statusColors, "bck-i-search: a_")), _.Ctrl_r, CheckThat(() => AssertScreenIs(2, TokenClassification.Command, "echo", TokenClassification.None, " ", TokenClassification.None, "abzz", NextLine, statusColors, "failed-bck-i-search: a_")), _.Ctrl_s, CheckThat(() => AssertScreenIs(2, TokenClassification.Command, "echo", TokenClassification.None, " ", emphasisColors, 'a', TokenClassification.None, "bzz", NextLine, statusColors, "fwd-i-search: a_")), _.Ctrl_g)); // Test that searching works after a failed search SetHistory("echo aa1", "echo bb1", "echo bb2", "echo aa2"); Test("echo aa1", Keys(_.Ctrl_r, "zz", _.Backspace, _.Backspace, "a1", CheckThat(() => AssertScreenIs(2, TokenClassification.Command, "echo", TokenClassification.None, " a", emphasisColors, "a1", NextLine, statusColors, "bck-i-search: a1_")) )); // Test that searching works after backspace after a successful search SetHistory("echo aa1", "echo bb1", "echo bb2", "echo aa2"); Test("echo aa2", Keys( _.Ctrl_r, "aa", CheckThat(() => AssertScreenIs(2, TokenClassification.Command, "echo", TokenClassification.None, " ", emphasisColors, "aa", TokenClassification.None, "2", NextLine, statusColors, "bck-i-search: aa_")), _.Ctrl_r, CheckThat(() => AssertScreenIs(2, TokenClassification.Command, "echo", TokenClassification.None, " ", emphasisColors, "aa", TokenClassification.None, "1", NextLine, statusColors, "bck-i-search: aa_")), _.Backspace, CheckThat(() => AssertScreenIs(2, TokenClassification.Command, "echo", TokenClassification.None, " ", emphasisColors, "a", TokenClassification.None, "a2", NextLine, statusColors, "bck-i-search: a_")), 'a', _.Ctrl_r, CheckThat(() => AssertScreenIs(2, TokenClassification.Command, "echo", TokenClassification.None, " ", emphasisColors, "aa", TokenClassification.None, "1", NextLine, statusColors, "bck-i-search: aa_")), _.Backspace)); // TODO: long search line // TODO: start with Ctrl+s // TODO: "fast" typing in search where buffered keys after search is accepted } [SkippableFact] public void AddToHistoryHandler() { TestSetup(KeyMode.Cmd); PSConsoleReadLine.SetOptions(new SetPSReadLineOption {AddToHistoryHandler = s => s.StartsWith("z")}); SetHistory("zzzz", "azzz"); Test("zzzz", Keys(_.UpArrow)); } [SkippableFact] public void HistoryNoDuplicates() { TestSetup(KeyMode.Cmd); PSConsoleReadLine.SetOptions(new SetPSReadLineOption {HistoryNoDuplicates = false}); SetHistory("zzzz", "aaaa", "bbbb", "bbbb", "cccc"); Assert.Equal(5, PSConsoleReadLine.GetHistoryItems().Length); Test("aaaa", Keys(Enumerable.Repeat(_.UpArrow, 4))); // Changing the option should affect existing history. PSConsoleReadLine.SetOptions(new SetPSReadLineOption {HistoryNoDuplicates = true}); Test("zzzz", Keys(Enumerable.Repeat(_.UpArrow, 4))); SetHistory("aaaa", "bbbb", "bbbb", "cccc"); Assert.Equal(3, PSConsoleReadLine.GetHistoryItems().Length); Test("aaaa", Keys(Enumerable.Repeat(_.UpArrow, 3))); SetHistory("aaaa", "bbbb", "bbbb", "cccc"); Test("cccc", Keys( Enumerable.Repeat(_.UpArrow, 3), Enumerable.Repeat(_.DownArrow, 2))); // No history SetHistory(); Test("", Keys(_.UpArrow, _.DownArrow)); } [SkippableFact] public void HistorySearchNoDuplicates() { TestSetup(KeyMode.Cmd, new KeyHandler("UpArrow", PSConsoleReadLine.HistorySearchBackward), new KeyHandler("DownArrow", PSConsoleReadLine.HistorySearchForward)); PSConsoleReadLine.SetOptions(new SetPSReadLineOption {HistoryNoDuplicates = true}); SetHistory("0000", "echo aaaa", "1111", "echo bbbb", "2222", "echo bbbb", "3333", "echo cccc", "4444"); Test("echo aaaa", Keys("echo", Enumerable.Repeat(_.UpArrow, 3))); SetHistory("0000", "echo aaaa", "1111", "echo bbbb", "2222", "echo bbbb", "3333", "echo cccc", "4444"); Test("echo cccc", Keys( "echo", Enumerable.Repeat(_.UpArrow, 3), Enumerable.Repeat(_.DownArrow, 2))); } [SkippableFact] public void InteractiveHistorySearchNoDuplicates() { TestSetup(KeyMode.Emacs); PSConsoleReadLine.SetOptions(new SetPSReadLineOption {HistoryNoDuplicates = true}); SetHistory("0000", "echo aaaa", "1111", "echo bbbb", "2222", "echo bbbb", "3333", "echo cccc", "4444"); Test("echo aaaa", Keys( _.Ctrl_r, "echo", _.Ctrl_r, _.Ctrl_r)); SetHistory("0000", "echo aaaa", "1111", "echo bbbb", "2222", "echo bbbb", "3333", "echo cccc", "4444"); Test("echo cccc", Keys( _.Ctrl_r, "echo", _.Ctrl_r, _.Ctrl_r, _.Ctrl_s, _.Ctrl_s)); SetHistory("0000", "echo aaaa", "1111", "echo bbbb", "2222", "echo bbbb", "3333", "echo cccc", "4444"); Test("echo aaaa", Keys( _.Ctrl_r, "echo", _.Ctrl_r, _.Ctrl_r, _.Ctrl_h, _.Ctrl_r, _.Ctrl_r)); } [SkippableFact] public void HistoryCount() { TestSetup(KeyMode.Cmd); SetHistory("zzzz", "aaaa", "bbbb", "cccc"); // There should be 4 items in history, the following should remove the // oldest history item. PSConsoleReadLine.SetOptions(new SetPSReadLineOption {MaximumHistoryCount = 3}); Test("aaaa", Keys(Enumerable.Repeat(_.UpArrow, 4))); Test("zzzz", Keys("zzzz")); Test("aaaa", Keys("aaaa")); Test("bbbb", Keys("bbbb")); Test("cccc", Keys("cccc")); Test("aaaa", Keys(Enumerable.Repeat(_.UpArrow, 4))); } } }