MegaBites-AI/Windows-powershell
0372
1// Copyright (c) Microsoft Corporation.2// Licensed under the MIT License.3 4using System.Buffers;5using System.Collections.Generic;6using System.Management.Automation.Language;7 8namespace System.Management.Automation9{10 /// <summary>11 /// Shared helper class for common completion helper methods.12 /// </summary>13 internal static class CompletionHelpers14 {15 private static readonly SearchValues<char> s_defaultCharsToCheck = SearchValues.Create("$`");16 17 private const string SingleQuote = "'";18 private const string DoubleQuote = "\"";19 20 /// <summary>21 /// Get matching completions from word to complete.22 /// This makes it easier to handle different variations of completions with consideration of quotes.23 /// </summary>24 /// <param name="wordToComplete">The word to complete.</param>25 /// <param name="possibleCompletionValues">The possible completion values to iterate.</param>26 /// <param name="displayInfoMapper">The optional completion display info mapper delegate for tool tip and list item text.</param>27 /// <param name="resultType">The optional completion result type. Default is Text.</param>28 /// <param name="matchStrategy">The optional match strategy delegate.</param>29 /// <returns>List of matching completion results.</returns>30 internal static IEnumerable<CompletionResult> GetMatchingResults(31 string wordToComplete,32 IEnumerable<string> possibleCompletionValues,33 CompletionDisplayInfoMapper displayInfoMapper = null,34 CompletionResultType resultType = CompletionResultType.Text,35 MatchStrategy matchStrategy = null)36 {37 displayInfoMapper ??= DefaultDisplayInfoMapper;38 matchStrategy ??= DefaultMatch;39 40 string quote = HandleDoubleAndSingleQuote(ref wordToComplete);41 if (quote != SingleQuote)42 {43 wordToComplete = NormalizeToExpandableString(wordToComplete);44 }45 46 foreach (string value in possibleCompletionValues)47 {48 if (matchStrategy(value, wordToComplete))49 {50 string completionText = QuoteCompletionText(value, quote);51 52 (string toolTip, string listItemText) = displayInfoMapper(value);53 54 yield return new CompletionResult(completionText, listItemText, resultType, toolTip);55 }56 }57 }58 59 /// <summary>60 /// Provides the display information for a completion result.61 /// This delegate is used to map a string value to its corresponding display information.62 /// </summary>63 /// <param name="value">The input value to be mapped</param>64 /// <returns>Completion display info containing tool tip and list item text.</returns>65 internal delegate (string ToolTip, string ListItemText) CompletionDisplayInfoMapper(string value);66 67 /// <summary>68 /// Provides the default display information for a completion result.69 /// Defaults to using the input value for both the tool tip and list item text.70 /// </summary>71 /// <returns>Completion display info containing tool tip and list item text.</returns>72 internal static readonly CompletionDisplayInfoMapper DefaultDisplayInfoMapper = value73 => (value, value);74 75 /// <summary>76 /// Normalizes the input string to an expandable string format for PowerShell.77 /// </summary>78 /// <param name="value">The input string to be normalized.</param>79 /// <returns>The normalized string with special characters replaced by their PowerShell escape sequences.</returns>80 /// <remarks>81 /// This method replaces special characters in the input string with their PowerShell equivalent escape sequences:82 /// <list type="bullet">83 /// <item><description>Replaces "\r" (carriage return) with "`r".</description></item>84 /// <item><description>Replaces "\n" (newline) with "`n".</description></item>85 /// <item><description>Replaces "\t" (tab) with "`t".</description></item>86 /// <item><description>Replaces "\0" (null) with "`0".</description></item>87 /// <item><description>Replaces "\a" (bell) with "`a".</description></item>88 /// <item><description>Replaces "\b" (backspace) with "`b".</description></item>89 /// <item><description>Replaces "\u001b" (escape character) with "`e".</description></item>90 /// <item><description>Replaces "\f" (form feed) with "`f".</description></item>91 /// <item><description>Replaces "\v" (vertical tab) with "`v".</description></item>92 /// </list>93 /// </remarks>94 internal static string NormalizeToExpandableString(string value)95 => value96 .Replace("\r", "`r")97 .Replace("\n", "`n")98 .Replace("\t", "`t")99 .Replace("\0", "`0")100 .Replace("\a", "`a")101 .Replace("\b", "`b")102 .Replace("\u001b", "`e")103 .Replace("\f", "`f")104 .Replace("\v", "`v");105 106 /// <summary>107 /// Defines a strategy for determining if a value matches a word or pattern.108 /// </summary>109 /// <param name="value">The input string to check for a match.</param>110 /// <param name="wordToComplete">The word or pattern to match against.</param>111 /// <returns>112 /// <c>true</c> if the value matches the specified word or pattern; otherwise, <c>false</c>.113 /// </returns>114 internal delegate bool MatchStrategy(string value, string wordToComplete);115 116 /// <summary>117 /// Determines if the given value matches the specified word using a literal, case-insensitive prefix match.118 /// </summary>119 /// <returns>120 /// <c>true</c> if the value starts with the word (case-insensitively); otherwise, <c>false</c>.121 /// </returns>122 internal static readonly MatchStrategy LiteralMatchOrdinalIgnoreCase = (value, wordToComplete)123 => value.StartsWith(wordToComplete, StringComparison.OrdinalIgnoreCase);124 125 /// <summary>126 /// Determines if the given value matches the specified word using wildcard pattern matching.127 /// </summary>128 /// <returns>129 /// <c>true</c> if the value matches the word as a wildcard pattern; otherwise, <c>false</c>.130 /// </returns>131 /// <remarks>132 /// Wildcard pattern matching allows for flexible matching, where wilcards can represent133 /// multiple characters in the input. This strategy is case-insensitive.134 /// </remarks>135 internal static readonly MatchStrategy WildcardPatternMatchIgnoreCase = (value, wordToComplete)136 => WildcardPattern137 .Get(wordToComplete + "*", WildcardOptions.IgnoreCase)138 .IsMatch(value);139 140 /// <summary>141 /// Determines if the given value matches the specified word considering wildcard characters literally.142 /// </summary>143 /// <returns>144 /// <c>true</c> if the value matches either the literal normalized word or the wildcard pattern with escaping;145 /// otherwise, <c>false</c>.146 /// </returns>147 /// <remarks>148 /// This strategy first attempts a literal prefix match for performance and, if unsuccessful, escapes the word to complete to149 /// handle any problematic wildcard characters before performing a wildcard match.150 /// </remarks>151 internal static readonly MatchStrategy WildcardPatternEscapeMatch = (value, wordToComplete)152 => LiteralMatchOrdinalIgnoreCase(value, wordToComplete) ||153 WildcardPatternMatchIgnoreCase(value, WildcardPattern.Escape(wordToComplete));154 155 /// <summary>156 /// Determines if the given value matches the specified word taking into account wildcard characters.157 /// </summary>158 /// <returns>159 /// <c>true</c> if the value matches either the literal normalized word or the wildcard pattern; otherwise, <c>false</c>.160 /// </returns>161 /// <remarks>162 /// This strategy attempts a literal match first for performance and, if unsuccessful, evaluates the word against a wildcard pattern.163 /// </remarks>164 internal static readonly MatchStrategy DefaultMatch = (value, wordToComplete)165 => LiteralMatchOrdinalIgnoreCase(value, wordToComplete) ||166 WildcardPatternMatchIgnoreCase(value, wordToComplete);167 168 /// <summary>169 /// Removes wrapping quotes from a string and returns the quote used, if present.170 /// </summary>171 /// <param name="wordToComplete">172 /// The string to process, potentially surrounded by single or double quotes.173 /// This parameter is updated in-place to exclude the removed quotes.174 /// </param>175 /// <returns>176 /// The type of quote detected (single or double), or an empty string if no quote is found.177 /// </returns>178 /// <remarks>179 /// This method checks for single or double quotes at the start and end of the string.180 /// If wrapping quotes are detected and match, both are removed; otherwise, only the front quote is removed.181 /// The string is updated in-place, and only matching front-and-back quotes are stripped.182 /// If no quotes are detected or the input is empty, the original string remains unchanged.183 /// </remarks>184 internal static string HandleDoubleAndSingleQuote(ref string wordToComplete)185 {186 if (string.IsNullOrEmpty(wordToComplete))187 {188 return string.Empty;189 }190 191 char frontQuote = wordToComplete[0];192 bool hasFrontSingleQuote = frontQuote.IsSingleQuote();193 bool hasFrontDoubleQuote = frontQuote.IsDoubleQuote();194 195 if (!hasFrontSingleQuote && !hasFrontDoubleQuote)196 {197 return string.Empty;198 }199 200 string quoteInUse = hasFrontSingleQuote ? SingleQuote : DoubleQuote;201 202 int length = wordToComplete.Length;203 if (length == 1)204 {205 wordToComplete = string.Empty;206 return quoteInUse;207 }208 209 char backQuote = wordToComplete[length - 1];210 bool hasBackSingleQuote = backQuote.IsSingleQuote();211 bool hasBackDoubleQuote = backQuote.IsDoubleQuote();212 213 bool hasBothFrontAndBackQuotes =214 (hasFrontSingleQuote && hasBackSingleQuote) || (hasFrontDoubleQuote && hasBackDoubleQuote);215 216 if (hasBothFrontAndBackQuotes)217 {218 wordToComplete = wordToComplete.Substring(1, length - 2);219 return quoteInUse;220 }221 222 bool hasFrontQuoteAndNoBackQuote =223 (hasFrontSingleQuote || hasFrontDoubleQuote) && !hasBackSingleQuote && !hasBackDoubleQuote;224 225 if (hasFrontQuoteAndNoBackQuote)226 {227 wordToComplete = wordToComplete.Substring(1);228 return quoteInUse;229 }230 231 return string.Empty;232 }233 234 /// <summary>235 /// Determines whether the specified completion string requires quotes.236 /// Quoting is required if:237 /// <list type="bullet">238 /// <item><description>There are parsing errors in the input string.</description></item>239 /// <item><description>The parsed token count is not exactly two (the input token + EOF).</description></item>240 /// <item><description>The first token is a string or a PowerShell keyword containing special characters.</description></item>241 /// <item><description>The first token is a semi colon or comma token.</description></item>242 /// </list>243 /// </summary>244 /// <param name="completion">The input string to analyze for quoting requirements.</param>245 /// <returns><c>true</c> if the string requires quotes, <c>false</c> otherwise.</returns>246 internal static bool CompletionRequiresQuotes(string completion)247 {248 Parser.ParseInput(completion, out Token[] tokens, out ParseError[] errors);249 250 bool isExpectedTokenCount = tokens.Length == 2;251 252 bool requireQuote = errors.Length > 0 || !isExpectedTokenCount;253 254 Token firstToken = tokens[0];255 bool isStringToken = firstToken is StringToken;256 bool isKeywordToken = (firstToken.TokenFlags & TokenFlags.Keyword) != 0;257 bool isSemiToken = firstToken.Kind == TokenKind.Semi;258 bool isCommaToken = firstToken.Kind == TokenKind.Comma;259 260 if ((!requireQuote && isStringToken) || (isExpectedTokenCount && isKeywordToken))261 {262 requireQuote = ContainsCharsToCheck(firstToken.Text);263 }264 265 else if (isExpectedTokenCount && (isSemiToken || isCommaToken))266 {267 requireQuote = true;268 }269 270 return requireQuote;271 }272 273 /// <summary>274 /// Determines whether the given text contains an escaped newline string.275 /// </summary>276 /// <param name="text">The input string to check for escaped newlines.</param>277 /// <returns>278 /// <c>true</c> if the text contains the escaped Unix-style newline string ("`n") or279 /// the Windows-style newline string ("`r`n"); otherwise, <c>false</c>.280 /// </returns>281 private static bool ContainsEscapedNewlineString(string text)282 => text.Contains("`n", StringComparison.Ordinal);283 284 private static bool ContainsCharsToCheck(ReadOnlySpan<char> text)285 => text.ContainsAny(s_defaultCharsToCheck);286 287 /// <summary>288 /// Quotes a given completion text.289 /// </summary>290 /// <param name="completionText">291 /// The text to be quoted.292 /// </param>293 /// <param name="quote">294 /// The quote character to use for enclosing the text. Defaults to a single quote if not provided.295 /// </param>296 /// <returns>297 /// The quoted <paramref name="completionText"/>.298 /// </returns>299 internal static string QuoteCompletionText(string completionText, string quote)300 {301 // Escaped newlines e.g. `r`n need be surrounded with double quotes302 if (ContainsEscapedNewlineString(completionText))303 {304 return DoubleQuote + completionText + DoubleQuote;305 }306 307 if (!CompletionRequiresQuotes(completionText))308 {309 return quote + completionText + quote;310 }311 312 string quoteInUse = string.IsNullOrEmpty(quote) ? SingleQuote : quote;313 314 if (quoteInUse == SingleQuote)315 {316 completionText = CodeGeneration.EscapeSingleQuotedStringContent(completionText);317 }318 319 return quoteInUse + completionText + quoteInUse;320 }321 }322}323 