MegaBites-AI/Windows-powershell
0372
1// Copyright (c) Microsoft Corporation.2// Licensed under the MIT License.3 4using System.Buffers;5using System.Collections;6using System.Collections.Concurrent;7using System.Collections.Generic;8using System.Collections.ObjectModel;9using System.Data;10using System.Diagnostics.CodeAnalysis;11using System.Globalization;12using System.IO;13using System.Linq;14using System.Management.Automation.Internal;15using System.Management.Automation.Language;16using System.Management.Automation.Provider;17using System.Management.Automation.Runspaces;18using System.Reflection;19using System.Runtime.InteropServices;20using System.Text;21using System.Text.RegularExpressions;22using System.Threading;23 24using Microsoft.Management.Infrastructure;25using Microsoft.Management.Infrastructure.Options;26using Microsoft.PowerShell;27using Microsoft.PowerShell.Cim;28using Microsoft.PowerShell.Commands;29using Microsoft.PowerShell.Commands.Internal.Format;30 31namespace System.Management.Automation32{33 /// <summary>34 /// </summary>35 public static class CompletionCompleters36 {37 static CompletionCompleters()38 {39 AppDomain.CurrentDomain.AssemblyLoad += UpdateTypeCacheOnAssemblyLoad;40 }41 42 private static void UpdateTypeCacheOnAssemblyLoad(object sender, AssemblyLoadEventArgs args)43 {44 // Just null out the cache - we'll rebuild it the next time someone tries to complete a type.45 // We could rebuild it now, but we could be loading multiple assemblies (e.g. dependent assemblies)46 // and there is no sense in rebuilding anything until we're done loading all of the assemblies.47 Interlocked.Exchange(ref s_typeCache, null);48 }49 50 #region Command Names51 52 /// <summary>53 /// </summary>54 /// <param name="commandName"></param>55 /// <returns></returns>56 public static IEnumerable<CompletionResult> CompleteCommand(string commandName)57 {58 return CompleteCommand(commandName, null);59 }60 61 /// <summary>62 /// </summary>63 /// <param name="commandName"></param>64 /// <param name="moduleName"></param>65 /// <param name="commandTypes"></param>66 /// <returns></returns>67 public static IEnumerable<CompletionResult> CompleteCommand(string commandName, string moduleName, CommandTypes commandTypes = CommandTypes.All)68 {69 var runspace = Runspace.DefaultRunspace;70 if (runspace == null)71 {72 // No runspace, just return no results.73 return CommandCompletion.EmptyCompletionResult;74 }75 76 var helper = new PowerShellExecutionHelper(PowerShell.Create(RunspaceMode.CurrentRunspace));77 var executionContext = helper.CurrentPowerShell.Runspace.ExecutionContext;78 return CompleteCommand(new CompletionContext { WordToComplete = commandName, Helper = helper, ExecutionContext = executionContext }, moduleName, commandTypes);79 }80 81 internal static List<CompletionResult> CompleteCommand(CompletionContext context)82 {83 return CompleteCommand(context, null);84 }85 86 private static List<CompletionResult> CompleteCommand(CompletionContext context, string moduleName, CommandTypes types = CommandTypes.All)87 {88 var addAmpersandIfNecessary = IsAmpersandNeeded(context, false);89 90 string commandName = context.WordToComplete;91 string quote = CompletionHelpers.HandleDoubleAndSingleQuote(ref commandName);92 93 List<CompletionResult> commandResults = null;94 95 if (commandName.IndexOfAny(Utils.Separators.DirectoryOrDrive) == -1)96 {97 // The name to complete is neither module qualified nor is it a relative/rooted file path.98 99 Ast lastAst = null;100 if (context.RelatedAsts != null && context.RelatedAsts.Count > 0)101 {102 lastAst = context.RelatedAsts.Last();103 }104 105 commandResults = ExecuteGetCommandCommand(useModulePrefix: false);106 107 if (lastAst != null)108 {109 // We need to add the wildcard to the end so the regex is built correctly.110 commandName += "*";111 112 // Search the asts for function definitions that we might be calling113 var findFunctionsVisitor = new FindFunctionsVisitor();114 while (lastAst.Parent != null)115 {116 lastAst = lastAst.Parent;117 }118 119 lastAst.Visit(findFunctionsVisitor);120 121 WildcardPattern commandNamePattern = WildcardPattern.Get(commandName, WildcardOptions.IgnoreCase);122 foreach (var defn in findFunctionsVisitor.FunctionDefinitions)123 {124 if (commandNamePattern.IsMatch(defn.Name)125 && !commandResults.Any(cr => cr.CompletionText.Equals(defn.Name, StringComparison.OrdinalIgnoreCase)))126 {127 // Results found in the current script are prepended to show up at the top of the list.128 commandResults.Insert(0, GetCommandNameCompletionResult(defn.Name, defn, addAmpersandIfNecessary, quote));129 }130 }131 }132 }133 else134 {135 // If there is a single \, we might be looking for a module/snapin qualified command136 var indexOfFirstColon = commandName.IndexOf(':');137 var indexOfFirstBackslash = commandName.IndexOf('\\');138 if (indexOfFirstBackslash > 0 && (indexOfFirstBackslash < indexOfFirstColon || indexOfFirstColon == -1))139 {140 // First try the name before the backslash as a module name.141 // Use the exact module name provided by the user142 moduleName = commandName.Substring(0, indexOfFirstBackslash);143 commandName = commandName.Substring(indexOfFirstBackslash + 1);144 145 commandResults = ExecuteGetCommandCommand(useModulePrefix: true);146 }147 }148 149 return commandResults;150 151 List<CompletionResult> ExecuteGetCommandCommand(bool useModulePrefix)152 {153 var powershell = context.Helper154 .AddCommandWithPreferenceSetting("Get-Command", typeof(GetCommandCommand))155 .AddParameter("All")156 .AddParameter("Name", commandName + "*");157 158 if (moduleName != null)159 {160 powershell.AddParameter("Module", moduleName);161 }162 163 if (!types.Equals(CommandTypes.All))164 {165 powershell.AddParameter("CommandType", types);166 }167 168 // Exception is ignored, the user simply does not get any completion results if the pipeline fails169 Exception exceptionThrown;170 var commandInfos = context.Helper.ExecuteCurrentPowerShell(out exceptionThrown);171 172 if (commandInfos == null || commandInfos.Count == 0)173 {174 powershell.Commands.Clear();175 powershell176 .AddCommandWithPreferenceSetting("Get-Command", typeof(GetCommandCommand))177 .AddParameter("All")178 .AddParameter("Name", commandName)179 .AddParameter("UseAbbreviationExpansion");180 181 if (moduleName != null)182 {183 powershell.AddParameter("Module", moduleName);184 }185 186 if (!types.Equals(CommandTypes.All))187 {188 powershell.AddParameter("CommandType", types);189 }190 191 commandInfos = context.Helper.ExecuteCurrentPowerShell(out exceptionThrown);192 }193 194 List<CompletionResult> completionResults = null;195 196 if (commandInfos != null && commandInfos.Count > 1)197 {198 // OrderBy is using stable sorting199 var sortedCommandInfos = commandInfos.Order(new CommandNameComparer());200 completionResults = MakeCommandsUnique(sortedCommandInfos, useModulePrefix, addAmpersandIfNecessary, quote);201 }202 else203 {204 completionResults = MakeCommandsUnique(commandInfos, useModulePrefix, addAmpersandIfNecessary, quote);205 }206 207 return completionResults;208 }209 }210 211 private static readonly HashSet<string> s_keywordsToExcludeFromAddingAmpersand212 = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { nameof(TokenKind.InlineScript), nameof(TokenKind.Configuration) };213 214 internal static CompletionResult GetCommandNameCompletionResult(string name, object command, bool addAmpersandIfNecessary, string quote)215 {216 string syntax = name, listItem = name;217 218 var commandInfo = command as CommandInfo;219 if (commandInfo != null)220 {221 try222 {223 listItem = commandInfo.Name;224 // This may require parsing a script, which could fail in a number of different ways225 // (syntax errors, security exceptions, etc.) If so, the name is fine for the tooltip.226 syntax = commandInfo.Syntax;227 }228 catch (Exception)229 {230 }231 }232 233 syntax = string.IsNullOrEmpty(syntax) ? name : syntax;234 bool needAmpersand;235 236 if (CompletionHelpers.CompletionRequiresQuotes(name))237 {238 needAmpersand = quote == string.Empty && addAmpersandIfNecessary;239 string quoteInUse = quote == string.Empty ? "'" : quote;240 if (quoteInUse == "'")241 {242 name = name.Replace("'", "''");243 }244 else245 {246 name = name.Replace("`", "``");247 name = name.Replace("$", "`$");248 }249 250 name = quoteInUse + name + quoteInUse;251 }252 else253 {254 needAmpersand = quote == string.Empty && addAmpersandIfNecessary &&255 Tokenizer.IsKeyword(name) && !s_keywordsToExcludeFromAddingAmpersand.Contains(name);256 name = quote + name + quote;257 }258 259 // It's useless to call ForEach-Object (foreach) as the first command of a pipeline. For example:260 // PS C:\> fore<tab> ---> PS C:\> foreach (expected, use as the keyword)261 // PS C:\> fore<tab> ---> PS C:\> & foreach (unexpected, ForEach-Object is seldom used as the first command of a pipeline)262 if (needAmpersand && name != SpecialVariables.@foreach)263 {264 name = "& " + name;265 }266 267 return new CompletionResult(name, listItem, CompletionResultType.Command, syntax);268 }269 270 internal static List<CompletionResult> MakeCommandsUnique(IEnumerable<PSObject> commandInfoPsObjs, bool includeModulePrefix, bool addAmpersandIfNecessary, string quote)271 {272 List<CompletionResult> results = new List<CompletionResult>();273 if (commandInfoPsObjs == null || !commandInfoPsObjs.Any())274 {275 return results;276 }277 278 var commandTable = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);279 foreach (var psobj in commandInfoPsObjs)280 {281 object baseObj = PSObject.Base(psobj);282 string name = null;283 284 var commandInfo = baseObj as CommandInfo;285 if (commandInfo != null)286 {287 // Skip the private commands288 if (commandInfo.Visibility == SessionStateEntryVisibility.Private) { continue; }289 290 name = commandInfo.Name;291 if (includeModulePrefix && !string.IsNullOrEmpty(commandInfo.ModuleName))292 {293 // The command might be a prefixed commandInfo that we get by importing a module with the -Prefix parameter, for example:294 // FooModule.psm1: Get-Foo295 // import-module FooModule -Prefix PowerShell296 // --> command 'Get-PowerShellFoo' in the global session state (prefixed commandInfo)297 // command 'Get-Foo' in the module session state (un-prefixed commandInfo)298 // in that case, we should not add the module name qualification because it doesn't work299 if (string.IsNullOrEmpty(commandInfo.Prefix) || !ModuleCmdletBase.IsPrefixedCommand(commandInfo))300 {301 name = commandInfo.ModuleName + "\\" + commandInfo.Name;302 }303 }304 }305 else306 {307 name = baseObj as string;308 if (name == null) { continue; }309 }310 311 object value;312 if (!commandTable.TryGetValue(name, out value))313 {314 commandTable.Add(name, baseObj);315 }316 else317 {318 var list = value as List<object>;319 if (list != null)320 {321 list.Add(baseObj);322 }323 else324 {325 list = new List<object> { value, baseObj };326 commandTable[name] = list;327 }328 }329 }330 331 foreach (var keyValuePair in commandTable)332 {333 if (keyValuePair.Value is List<object> commandList)334 {335 var modulesWithCommand = new HashSet<string>(StringComparer.OrdinalIgnoreCase);336 var importedModules = new HashSet<string>(StringComparer.OrdinalIgnoreCase);337 var commandInfoList = new List<CommandInfo>(commandList.Count);338 for (int i = 0; i < commandList.Count; i++)339 {340 if (commandList[i] is not CommandInfo commandInfo)341 {342 continue;343 }344 345 commandInfoList.Add(commandInfo);346 if (commandInfo.CommandType == CommandTypes.Application)347 {348 continue;349 }350 351 modulesWithCommand.Add(commandInfo.ModuleName);352 if ((commandInfo.CommandType == CommandTypes.Cmdlet && commandInfo.CommandMetadata.CommandType is not null)353 || (commandInfo.CommandType is CommandTypes.Function or CommandTypes.Filter && commandInfo.Definition != string.Empty)354 || (commandInfo.CommandType == CommandTypes.Alias && commandInfo.Definition is not null))355 {356 // Checks if the command or source module has been imported.357 _ = importedModules.Add(commandInfo.ModuleName);358 }359 }360 361 if (commandInfoList.Count == 0)362 {363 continue;364 }365 366 int moduleCount = modulesWithCommand.Count;367 modulesWithCommand.Clear();368 int index;369 if (commandInfoList[0].CommandType == CommandTypes.Application370 || importedModules.Count == 1371 || moduleCount < 2)372 {373 // We can use the short name for this command because there's no ambiguity about which command it resolves to.374 // If the first element is an application then we know there's no conflicting commands/aliases (because of the command precedence).375 // If there's just 1 module imported then the short name refers to that module (and it will be the first element in the list)376 // If there's less than 2 unique modules exporting that command then we can use the short name because it can only refer to that module.377 index = 1;378 results.Add(GetCommandNameCompletionResult(keyValuePair.Key, commandInfoList[0], addAmpersandIfNecessary, quote));379 modulesWithCommand.Add(commandInfoList[0].ModuleName);380 }381 else382 {383 index = 0;384 }385 386 for (; index < commandInfoList.Count; index++)387 {388 CommandInfo commandInfo = commandInfoList[index];389 if (commandInfo.CommandType == CommandTypes.Application)390 {391 results.Add(GetCommandNameCompletionResult(commandInfo.Definition, commandInfo, addAmpersandIfNecessary, quote));392 }393 else if (!string.IsNullOrEmpty(commandInfo.ModuleName) && modulesWithCommand.Add(commandInfo.ModuleName))394 {395 var name = commandInfo.ModuleName + "\\" + commandInfo.Name;396 results.Add(GetCommandNameCompletionResult(name, commandInfo, addAmpersandIfNecessary, quote));397 }398 }399 }400 else401 {402 // The first command might be an un-prefixed commandInfo that we get by importing a module with the -Prefix parameter,403 // in that case, we should add the module name qualification because if the module is not in the module path, calling404 // 'Get-Foo' directly doesn't work405 string completionName = keyValuePair.Key;406 if (!includeModulePrefix)407 {408 var commandInfo = keyValuePair.Value as CommandInfo;409 if (commandInfo != null && !string.IsNullOrEmpty(commandInfo.Prefix))410 {411 Diagnostics.Assert(!string.IsNullOrEmpty(commandInfo.ModuleName), "the module name should exist if commandInfo.Prefix is not an empty string");412 if (!ModuleCmdletBase.IsPrefixedCommand(commandInfo))413 {414 completionName = commandInfo.ModuleName + "\\" + completionName;415 }416 }417 }418 419 results.Add(GetCommandNameCompletionResult(completionName, keyValuePair.Value, addAmpersandIfNecessary, quote));420 }421 }422 423 return results;424 }425 426 private sealed class FindFunctionsVisitor : AstVisitor427 {428 internal readonly List<FunctionDefinitionAst> FunctionDefinitions = new List<FunctionDefinitionAst>();429 430 public override AstVisitAction VisitFunctionDefinition(FunctionDefinitionAst functionDefinitionAst)431 {432 FunctionDefinitions.Add(functionDefinitionAst);433 return AstVisitAction.Continue;434 }435 }436 437 #endregion Command Names438 439 #region Module Names440 441 internal static List<CompletionResult> CompleteModuleName(CompletionContext context, bool loadedModulesOnly, bool skipEditionCheck = false)442 {443 var wordToComplete = context.WordToComplete ?? string.Empty;444 var result = new List<CompletionResult>();445 var quote = CompletionHelpers.HandleDoubleAndSingleQuote(ref wordToComplete);446 447 // Indicates if we should search for modules where the last part of the name matches the input text448 // eg: Host<Tab> finds Microsoft.PowerShell.Host449 // If the user has entered a manual wildcard, or a module name that contains a "." we assume they only want results that matches the input exactly.450 bool shortNameSearch = wordToComplete.Length > 0 && !WildcardPattern.ContainsWildcardCharacters(wordToComplete) && !wordToComplete.Contains('.');451 452 if (!wordToComplete.EndsWith('*'))453 {454 wordToComplete += "*";455 }456 457 string[] moduleNames;458 WildcardPattern shortNamePattern;459 if (shortNameSearch)460 {461 moduleNames = new string[] { wordToComplete, "*." + wordToComplete };462 shortNamePattern = new WildcardPattern(wordToComplete, WildcardOptions.IgnoreCase);463 }464 else465 {466 moduleNames = new string[] { wordToComplete };467 shortNamePattern = null;468 }469 470 var powershell = context.Helper.AddCommandWithPreferenceSetting("Get-Module", typeof(GetModuleCommand)).AddParameter("Name", moduleNames);471 if (!loadedModulesOnly)472 {473 powershell.AddParameter("ListAvailable", true);474 475 // -SkipEditionCheck should only be set or apply to -ListAvailable476 if (skipEditionCheck)477 {478 powershell.AddParameter("SkipEditionCheck", true);479 }480 }481 482 Collection<PSObject> psObjects = context.Helper.ExecuteCurrentPowerShell(out _);483 484 if (psObjects != null)485 {486 // When PowerShell is used interactively, completion is usually triggered by PSReadLine, with PSReadLine's SessionState487 // as the engine session state. In that case, results from the module search may contain a nested module of PSReadLine,488 // which should be filtered out below.489 // When the completion is triggered from global session state, such as when running 'TabExpansion2' from command line,490 // the module associated with engine session state will be null.491 //492 // Note that, it's intentional to not hard code the name 'PSReadLine' in the change, so that in case the tab completion493 // is triggered from within a different module, its nested modules can also be filtered out.494 HashSet<PSModuleInfo> nestedModulesToFilterOut = null;495 PSModuleInfo currentModule = context.ExecutionContext.EngineSessionState.Module;496 if (loadedModulesOnly && currentModule?.NestedModules.Count > 0)497 {498 nestedModulesToFilterOut = new(currentModule.NestedModules);499 }500 501 var completedModules = new HashSet<string>(StringComparer.OrdinalIgnoreCase);502 foreach (PSObject item in psObjects)503 {504 var moduleInfo = (PSModuleInfo)item.BaseObject;505 var completionText = moduleInfo.Name;506 if (!completedModules.Add(completionText))507 {508 continue;509 }510 511 if (shortNameSearch512 && completionText.Contains('.')513 && !shortNamePattern.IsMatch(completionText.Substring(completionText.LastIndexOf('.') + 1))514 && !shortNamePattern.IsMatch(completionText))515 {516 // This check is to make sure we don't return a module whose name only matches the user specified word in the middle.517 // For example, when user completes with 'gmo power', we should not return 'Microsoft.PowerShell.Utility'.518 continue;519 }520 521 if (nestedModulesToFilterOut is not null522 && nestedModulesToFilterOut.Contains(moduleInfo))523 {524 continue;525 }526 527 var toolTip = "Description: " + moduleInfo.Description + "\r\nModuleType: "528 + moduleInfo.ModuleType.ToString() + "\r\nPath: "529 + moduleInfo.Path;530 531 completionText = CompletionHelpers.QuoteCompletionText(completionText, quote);532 533 result.Add(new CompletionResult(completionText, listItemText: moduleInfo.Name, CompletionResultType.ParameterValue, toolTip));534 }535 }536 537 return result;538 }539 540 #endregion Module Names541 542 #region Command Parameters543 private static readonly string[] s_parameterNamesOfImportDSCResource = { "Name", "ModuleName", "ModuleVersion" };544 545 internal static List<CompletionResult> CompleteCommandParameter(CompletionContext context)546 {547 string partialName = null;548 bool withColon = false;549 CommandAst commandAst = null;550 List<CompletionResult> result = new List<CompletionResult>();551 552 // Find the parameter ast, it will be near or at the end553 CommandParameterAst parameterAst = null;554 DynamicKeywordStatementAst keywordAst = null;555 for (int i = context.RelatedAsts.Count - 1; i >= 0; i--)556 {557 keywordAst ??= context.RelatedAsts[i] as DynamicKeywordStatementAst;558 parameterAst = (context.RelatedAsts[i] as CommandParameterAst);559 if (parameterAst != null) break;560 }561 562 if (parameterAst != null)563 {564 keywordAst = parameterAst.Parent as DynamicKeywordStatementAst;565 }566 567 // If parent is DynamicKeywordStatementAst - 'Import-DscResource',568 // then customize the auto completion results569 if (keywordAst != null && string.Equals(keywordAst.Keyword.Keyword, "Import-DscResource", StringComparison.OrdinalIgnoreCase)570 && !string.IsNullOrWhiteSpace(context.WordToComplete) && context.WordToComplete.StartsWith('-'))571 {572 var lastAst = context.RelatedAsts.Last();573 var wordToMatch = string.Concat(context.WordToComplete.AsSpan(1), "*");574 var pattern = WildcardPattern.Get(wordToMatch, WildcardOptions.IgnoreCase);575 var parameterNames = keywordAst.CommandElements.Where(static ast => ast is CommandParameterAst).Select(static ast => (ast as CommandParameterAst).ParameterName);576 foreach (var parameterName in s_parameterNamesOfImportDSCResource)577 {578 if (pattern.IsMatch(parameterName) && !parameterNames.Contains(parameterName, StringComparer.OrdinalIgnoreCase))579 {580 string tooltip = "[String] " + parameterName;581 result.Add(new CompletionResult("-" + parameterName, parameterName, CompletionResultType.ParameterName, tooltip));582 }583 }584 585 if (result.Count > 0)586 {587 context.ReplacementLength = context.WordToComplete.Length;588 context.ReplacementIndex = lastAst.Extent.StartOffset;589 }590 591 return result;592 }593 594 bool bindPositionalParameters = true;595 if (parameterAst != null)596 {597 // Parent must be a command598 commandAst = (CommandAst)parameterAst.Parent;599 partialName = parameterAst.ParameterName;600 withColon = context.WordToComplete.EndsWith(':');601 }602 else603 {604 // No CommandParameterAst is found. It could be a StringConstantExpressionAst "-"605 if (context.RelatedAsts[context.RelatedAsts.Count - 1] is not StringConstantExpressionAst dashAst)606 return result;607 if (!dashAst.Value.Trim().Equals("-", StringComparison.OrdinalIgnoreCase))608 return result;609 610 // Parent must be a command611 commandAst = (CommandAst)dashAst.Parent;612 partialName = string.Empty;613 614 // If the user tries to tab complete a new parameter in front of a positional argument like: dir -<Tab> C:\615 // the user may want to add the parameter name so we don't want to bind positional arguments616 if (commandAst is not null)617 {618 foreach (var element in commandAst.CommandElements)619 {620 if (element.Extent.StartOffset > context.TokenAtCursor.Extent.StartOffset)621 {622 bindPositionalParameters = element is CommandParameterAst;623 break;624 }625 }626 }627 }628 629 PseudoBindingInfo pseudoBinding = new PseudoParameterBinder()630 .DoPseudoParameterBinding(commandAst, null, parameterAst, PseudoParameterBinder.BindingType.ParameterCompletion, bindPositionalParameters);631 // The command cannot be found or it's not a cmdlet, not a script cmdlet, not a function.632 // Try completing as if it the parameter is a command argument for native command completion.633 if (pseudoBinding == null)634 {635 return CompleteCommandArgument(context);636 }637 638 switch (pseudoBinding.InfoType)639 {640 case PseudoBindingInfoType.PseudoBindingFail:641 // The command is a cmdlet or script cmdlet. Binding failed642 result = GetParameterCompletionResults(partialName, uint.MaxValue, pseudoBinding.UnboundParameters, withColon);643 break;644 case PseudoBindingInfoType.PseudoBindingSucceed:645 // The command is a cmdlet or script cmdlet. Binding succeeded.646 result = GetParameterCompletionResults(partialName, pseudoBinding, parameterAst, withColon);647 break;648 }649 650 if (result.Count == 0)651 {652 result = pseudoBinding.CommandName.Equals("Set-Location", StringComparison.OrdinalIgnoreCase)653 ? new List<CompletionResult>(CompleteFilename(context, containerOnly: true, extension: null))654 : new List<CompletionResult>(CompleteFilename(context));655 }656 657 return result;658 }659 660 /// <summary>661 /// Get the parameter completion results when the pseudo binding was successful.662 /// </summary>663 /// <param name="parameterName"></param>664 /// <param name="bindingInfo"></param>665 /// <param name="parameterAst"></param>666 /// <param name="withColon"></param>667 /// <returns></returns>668 private static List<CompletionResult> GetParameterCompletionResults(string parameterName, PseudoBindingInfo bindingInfo, CommandParameterAst parameterAst, bool withColon)669 {670 Diagnostics.Assert(bindingInfo.InfoType.Equals(PseudoBindingInfoType.PseudoBindingSucceed), "The pseudo binding should succeed");671 List<CompletionResult> result = new List<CompletionResult>();672 Assembly commandAssembly = null;673 if (bindingInfo.CommandInfo is CmdletInfo cmdletInfo)674 {675 commandAssembly = cmdletInfo.CommandMetadata.CommandType.Assembly;676 }677 678 if (parameterName == string.Empty)679 {680 result = GetParameterCompletionResults(681 parameterName,682 bindingInfo.ValidParameterSetsFlags,683 bindingInfo.UnboundParameters,684 withColon,685 commandAssembly);686 return result;687 }688 689 if (bindingInfo.ParametersNotFound.Count > 0)690 {691 // The parameter name cannot be matched to any parameter692 if (bindingInfo.ParametersNotFound.Any(pAst => parameterAst.GetHashCode() == pAst.GetHashCode()))693 {694 return result;695 }696 }697 698 if (bindingInfo.AmbiguousParameters.Count > 0)699 {700 // The parameter name is ambiguous. It's ignored in the pseudo binding, and we should search in the UnboundParameters701 if (bindingInfo.AmbiguousParameters.Any(pAst => parameterAst.GetHashCode() == pAst.GetHashCode()))702 {703 result = GetParameterCompletionResults(704 parameterName,705 bindingInfo.ValidParameterSetsFlags,706 bindingInfo.UnboundParameters,707 withColon,708 commandAssembly);709 }710 711 return result;712 }713 714 if (bindingInfo.DuplicateParameters.Count > 0)715 {716 // The parameter name is resolved to a parameter that is already bound. We search it in the BoundParameters717 if (bindingInfo.DuplicateParameters.Any(pAst => parameterAst.GetHashCode() == pAst.Parameter.GetHashCode()))718 {719 result = GetParameterCompletionResults(720 parameterName,721 bindingInfo.ValidParameterSetsFlags,722 bindingInfo.BoundParameters.Values,723 withColon,724 commandAssembly);725 }726 727 return result;728 }729 730 // The parameter should be bound in the pseudo binding during the named binding731 string matchedParameterName = null;732 foreach (KeyValuePair<string, AstParameterArgumentPair> entry in bindingInfo.BoundArguments)733 {734 switch (entry.Value.ParameterArgumentType)735 {736 case AstParameterArgumentType.AstPair:737 {738 AstPair pair = (AstPair)entry.Value;739 if (pair.ParameterSpecified && pair.Parameter.GetHashCode() == parameterAst.GetHashCode())740 {741 matchedParameterName = entry.Key;742 }743 else if (pair.ArgumentIsCommandParameterAst && pair.Argument.GetHashCode() == parameterAst.GetHashCode())744 {745 // The parameter name cannot be resolved to a parameter746 return result;747 }748 }749 750 break;751 case AstParameterArgumentType.Fake:752 {753 FakePair pair = (FakePair)entry.Value;754 if (pair.ParameterSpecified && pair.Parameter.GetHashCode() == parameterAst.GetHashCode())755 {756 matchedParameterName = entry.Key;757 }758 }759 760 break;761 case AstParameterArgumentType.Switch:762 {763 SwitchPair pair = (SwitchPair)entry.Value;764 if (pair.ParameterSpecified && pair.Parameter.GetHashCode() == parameterAst.GetHashCode())765 {766 matchedParameterName = entry.Key;767 }768 }769 770 break;771 case AstParameterArgumentType.AstArray:772 case AstParameterArgumentType.PipeObject:773 break;774 }775 776 if (matchedParameterName != null)777 break;778 }779 780 if (matchedParameterName is null)781 {782 // The pseudo binder has skipped a parameter783 // This will happen when completing parameters for commands with dynamic parameters.784 result = GetParameterCompletionResults(785 parameterName,786 bindingInfo.ValidParameterSetsFlags,787 bindingInfo.UnboundParameters,788 withColon,789 commandAssembly);790 return result;791 }792 793 MergedCompiledCommandParameter param = bindingInfo.BoundParameters[matchedParameterName];794 795 WildcardPattern pattern = WildcardPattern.Get(parameterName + "*", WildcardOptions.IgnoreCase);796 string parameterType = "[" + ToStringCodeMethods.Type(param.Parameter.Type, dropNamespaces: true) + "] ";797 798 string helpMessage = string.Empty;799 if (param.Parameter.CompiledAttributes is not null)800 {801 foreach (Attribute attr in param.Parameter.CompiledAttributes)802 {803 if (attr is ParameterAttribute pattr && TryGetParameterHelpMessage(pattr, commandAssembly, out string attrHelpMessage))804 {805 helpMessage = $" - {attrHelpMessage}";806 break;807 }808 }809 }810 811 string colonSuffix = withColon ? ":" : string.Empty;812 if (pattern.IsMatch(matchedParameterName))813 {814 string completionText = $"-{matchedParameterName}{colonSuffix}";815 string tooltip = $"{parameterType}{matchedParameterName}{helpMessage}";816 result.Add(new CompletionResult(completionText, matchedParameterName, CompletionResultType.ParameterName, tooltip));817 }818 else819 {820 // Process alias when there is partial input821 foreach (var alias in param.Parameter.Aliases)822 {823 if (pattern.IsMatch(alias))824 {825 result.Add(new CompletionResult(826 $"-{alias}{colonSuffix}",827 alias,828 CompletionResultType.ParameterName,829 $"{parameterType}{alias}{helpMessage}"));830 }831 }832 }833 834 return result;835 }836 837#nullable enable838 /// <summary>839 /// Try and get the help message text for the parameter attribute.840 /// </summary>841 /// <param name="attr">The attribute to check for the help message.</param>842 /// <param name="assembly">The assembly to lookup resources messages, this should be the assembly the cmdlet is defined in.</param>843 /// <param name="message">The help message if it was found otherwise null.</param>844 /// <returns>True if the help message was set or false if not.></returns>845 private static bool TryGetParameterHelpMessage(846 ParameterAttribute attr,847 Assembly? assembly,848 [NotNullWhen(true)] out string? message)849 {850 message = null;851 852 if (attr.HelpMessage is not null)853 {854 message = attr.HelpMessage;855 return true;856 }857 858 if (assembly is null || attr.HelpMessageBaseName is null || attr.HelpMessageResourceId is null)859 {860 return false;861 }862 863 try864 {865 message = ResourceManagerCache.GetResourceString(assembly, attr.HelpMessageBaseName, attr.HelpMessageResourceId);866 return message is not null;867 }868 catch (Exception)869 {870 return false;871 }872 }873#nullable disable874 875 /// <summary>876 /// Get the parameter completion results by using the given valid parameter sets and available parameters.877 /// </summary>878 /// <param name="parameterName"></param>879 /// <param name="validParameterSetFlags"></param>880 /// <param name="parameters"></param>881 /// <param name="withColon"></param>882 /// <param name="commandAssembly">Optional assembly used to lookup parameter help messages.</param>883 /// <returns></returns>884 private static List<CompletionResult> GetParameterCompletionResults(885 string parameterName,886 uint validParameterSetFlags,887 IEnumerable<MergedCompiledCommandParameter> parameters,888 bool withColon,889 Assembly commandAssembly = null)890 {891 var result = new List<CompletionResult>();892 var commonParamResult = new List<CompletionResult>();893 var pattern = WildcardPattern.Get(parameterName + "*", WildcardOptions.IgnoreCase);894 var colonSuffix = withColon ? ":" : string.Empty;895 896 bool addCommonParameters = true;897 foreach (MergedCompiledCommandParameter param in parameters)898 {899 bool inParameterSet = (param.Parameter.ParameterSetFlags & validParameterSetFlags) != 0 || param.Parameter.IsInAllSets;900 if (!inParameterSet)901 continue;902 903 string name = param.Parameter.Name;904 string type = "[" + ToStringCodeMethods.Type(param.Parameter.Type, dropNamespaces: true) + "] ";905 string helpMessage = null;906 bool isCommonParameter = Cmdlet.CommonParameters.Contains(name, StringComparer.OrdinalIgnoreCase);907 List<CompletionResult> listInUse = isCommonParameter ? commonParamResult : result;908 909 if (pattern.IsMatch(name))910 {911 // Then using functions to back dynamic keywords, we don't necessarily912 // want all of the parameters to be shown to the user. Those that are marked913 // DontShow will not be displayed. Also, if any of the parameters have914 // don't show set, we won't show any of the common parameters either.915 bool showToUser = true;916 var compiledAttributes = param.Parameter.CompiledAttributes;917 if (compiledAttributes != null && compiledAttributes.Count > 0)918 {919 foreach (var attr in compiledAttributes)920 {921 if (attr is ParameterAttribute pattr)922 {923 if (pattr.DontShow)924 {925 showToUser = false;926 addCommonParameters = false;927 break;928 }929 930 if (helpMessage is null && TryGetParameterHelpMessage(pattr, commandAssembly, out string attrHelpMessage))931 {932 helpMessage = $" - {attrHelpMessage}";933 }934 }935 }936 }937 938 if (showToUser)939 {940 string completionText = $"-{name}{colonSuffix}";941 string tooltip = $"{type}{name}{helpMessage}";942 listInUse.Add(new CompletionResult(completionText, name, CompletionResultType.ParameterName,943 tooltip));944 }945 }946 else if (parameterName != string.Empty)947 {948 // Process alias when there is partial input949 foreach (var alias in param.Parameter.Aliases)950 {951 if (pattern.IsMatch(alias))952 {953 listInUse.Add(new CompletionResult(954 $"-{alias}{colonSuffix}",955 alias,956 CompletionResultType.ParameterName,957 type + alias));958 }959 }960 }961 }962 963 // Add the common parameters to the results if expected.964 if (addCommonParameters)965 {966 result.AddRange(commonParamResult);967 }968 969 return result;970 }971 972 /// <summary>973 /// Get completion results for operators that start with <paramref name="wordToComplete"/>974 /// </summary>975 /// <param name="wordToComplete">The starting text of the operator to complete.</param>976 /// <returns>A list of completion results.</returns>977 public static List<CompletionResult> CompleteOperator(string wordToComplete)978 {979 if (wordToComplete.StartsWith('-'))980 {981 wordToComplete = wordToComplete.Substring(1);982 }983 984 return (from op in Tokenizer._operatorText985 where op.StartsWith(wordToComplete, StringComparison.OrdinalIgnoreCase)986 orderby op987 select new CompletionResult("-" + op, op, CompletionResultType.ParameterName, GetOperatorDescription(op))).ToList();988 }989 990 private static string GetOperatorDescription(string op)991 {992 return ResourceManagerCache.GetResourceString(typeof(CompletionCompleters).Assembly,993 "System.Management.Automation.resources.TabCompletionStrings",994 op + "OperatorDescription");995 }996 997 #endregion Command Parameters998 999 #region Command Arguments1000 1001 internal static List<CompletionResult> CompleteCommandArgument(CompletionContext context)1002 {1003 CommandAst commandAst = null;1004 List<CompletionResult> result = new List<CompletionResult>();1005 1006 // Find the expression ast. It should be at the end if there is one1007 ExpressionAst expressionAst = null;1008 MemberExpressionAst secondToLastMemberAst = null;1009 Ast lastAst = context.RelatedAsts.Last();1010 1011 expressionAst = lastAst as ExpressionAst;1012 if (expressionAst != null)1013 {1014 if (expressionAst.Parent is CommandAst)1015 {1016 commandAst = (CommandAst)expressionAst.Parent;1017 1018 if (expressionAst is ErrorExpressionAst && expressionAst.Extent.Text.EndsWith(','))1019 {1020 context.WordToComplete = string.Empty;1021 // BUGBUG context.CursorPosition = expressionAst.Extent.StartScriptPosition;1022 }1023 else if (commandAst.CommandElements.Count == 1 || context.WordToComplete == string.Empty)1024 {1025 expressionAst = null;1026 }1027 else if (commandAst.CommandElements.Count > 2)1028 {1029 var length = commandAst.CommandElements.Count;1030 var index = 1;1031 1032 for (; index < length; index++)1033 {1034 if (commandAst.CommandElements[index] == expressionAst)1035 break;1036 }1037 1038 CommandElementAst secondToLastAst = null;1039 if (index > 1)1040 {1041 secondToLastAst = commandAst.CommandElements[index - 1];1042 secondToLastMemberAst = secondToLastAst as MemberExpressionAst;1043 }1044 1045 var partialPathAst = expressionAst as StringConstantExpressionAst;1046 if (partialPathAst != null && secondToLastAst != null &&1047 partialPathAst.StringConstantType == StringConstantType.BareWord &&1048 secondToLastAst.Extent.EndLineNumber == partialPathAst.Extent.StartLineNumber &&1049 secondToLastAst.Extent.EndColumnNumber == partialPathAst.Extent.StartColumnNumber &&1050 partialPathAst.Value.AsSpan().IndexOfAny('\\', '/') == 0)1051 {1052 var secondToLastStringConstantAst = secondToLastAst as StringConstantExpressionAst;1053 var secondToLastExpandableStringAst = secondToLastAst as ExpandableStringExpressionAst;1054 var secondToLastArrayAst = secondToLastAst as ArrayLiteralAst;1055 var secondToLastParamAst = secondToLastAst as CommandParameterAst;1056 1057 if (secondToLastStringConstantAst != null || secondToLastExpandableStringAst != null)1058 {1059 var fullPath = ConcatenateStringPathArguments(secondToLastAst, partialPathAst.Value, context);1060 expressionAst = secondToLastStringConstantAst != null1061 ? (ExpressionAst)secondToLastStringConstantAst1062 : (ExpressionAst)secondToLastExpandableStringAst;1063 1064 context.ReplacementIndex = ((InternalScriptPosition)secondToLastAst.Extent.StartScriptPosition).Offset;1065 context.ReplacementLength += ((InternalScriptPosition)secondToLastAst.Extent.EndScriptPosition).Offset - context.ReplacementIndex;1066 context.WordToComplete = fullPath;1067 // context.CursorPosition = secondToLastAst.Extent.StartScriptPosition;1068 }1069 else if (secondToLastArrayAst != null)1070 {1071 // Handle cases like: dir -Path .\cd, 'a b'\new<tab>1072 var lastArrayElement = secondToLastArrayAst.Elements.LastOrDefault();1073 var fullPath = ConcatenateStringPathArguments(lastArrayElement, partialPathAst.Value, context);1074 if (fullPath != null)1075 {1076 expressionAst = secondToLastArrayAst;1077 1078 context.ReplacementIndex = ((InternalScriptPosition)lastArrayElement.Extent.StartScriptPosition).Offset;1079 context.ReplacementLength += ((InternalScriptPosition)lastArrayElement.Extent.EndScriptPosition).Offset - context.ReplacementIndex;1080 context.WordToComplete = fullPath;1081 }1082 }1083 else if (secondToLastParamAst != null)1084 {1085 // Handle cases like: dir -Path: .\cd, 'a b'\new<tab> || dir -Path: 'a b'\new<tab>1086 var fullPath = ConcatenateStringPathArguments(secondToLastParamAst.Argument, partialPathAst.Value, context);1087 if (fullPath != null)1088 {1089 expressionAst = secondToLastParamAst.Argument;1090 1091 context.ReplacementIndex = ((InternalScriptPosition)secondToLastParamAst.Argument.Extent.StartScriptPosition).Offset;1092 context.ReplacementLength += ((InternalScriptPosition)secondToLastParamAst.Argument.Extent.EndScriptPosition).Offset - context.ReplacementIndex;1093 context.WordToComplete = fullPath;1094 }1095 else1096 {1097 var arrayArgAst = secondToLastParamAst.Argument as ArrayLiteralAst;1098 if (arrayArgAst != null)1099 {1100 var lastArrayElement = arrayArgAst.Elements.LastOrDefault();1101 fullPath = ConcatenateStringPathArguments(lastArrayElement, partialPathAst.Value, context);1102 if (fullPath != null)1103 {1104 expressionAst = arrayArgAst;1105 1106 context.ReplacementIndex = ((InternalScriptPosition)lastArrayElement.Extent.StartScriptPosition).Offset;1107 context.ReplacementLength += ((InternalScriptPosition)lastArrayElement.Extent.EndScriptPosition).Offset - context.ReplacementIndex;1108 context.WordToComplete = fullPath;1109 }1110 }1111 }1112 }1113 }1114 }1115 }1116 else if (expressionAst.Parent is ArrayLiteralAst && expressionAst.Parent.Parent is CommandAst)1117 {1118 commandAst = (CommandAst)expressionAst.Parent.Parent;1119 1120 if (commandAst.CommandElements.Count == 1 || context.WordToComplete == string.Empty)1121 {1122 // dir -Path a.txt, b.txt <tab>1123 expressionAst = null;1124 }1125 else1126 {1127 // dir -Path a.txt, b.txt c<tab>1128 expressionAst = (ExpressionAst)expressionAst.Parent;1129 }1130 }1131 else if (expressionAst.Parent is ArrayLiteralAst && expressionAst.Parent.Parent is CommandParameterAst)1132 {1133 // Handle scenarios such as1134 // dir -Path: a.txt, <tab> || dir -Path: a.txt, b.txt <tab>1135 commandAst = (CommandAst)expressionAst.Parent.Parent.Parent;1136 if (context.WordToComplete == string.Empty)1137 {1138 // dir -Path: a.txt, b.txt <tab>1139 expressionAst = null;1140 }1141 else1142 {1143 // dir -Path: a.txt, b<tab>1144 expressionAst = (ExpressionAst)expressionAst.Parent;1145 }1146 }1147 else if (expressionAst.Parent is CommandParameterAst && expressionAst.Parent.Parent is CommandAst)1148 {1149 commandAst = (CommandAst)expressionAst.Parent.Parent;1150 if (expressionAst is ErrorExpressionAst && expressionAst.Extent.Text.EndsWith(','))1151 {1152 // dir -Path: a.txt,<tab>1153 context.WordToComplete = string.Empty;1154 // context.CursorPosition = expressionAst.Extent.StartScriptPosition;1155 }1156 else if (context.WordToComplete == string.Empty)1157 {1158 // Handle scenario like this: Set-ExecutionPolicy -Scope:CurrentUser <tab>1159 expressionAst = null;1160 }1161 }1162 }1163 else1164 {1165 var paramAst = lastAst as CommandParameterAst;1166 if (paramAst != null)1167 {1168 commandAst = paramAst.Parent as CommandAst;1169 }1170 else1171 {1172 commandAst = lastAst as CommandAst;1173 }1174 }1175 1176 if (commandAst == null)1177 {1178 // We don't know if this could be expanded into anything interesting1179 return result;1180 }1181 1182 PseudoBindingInfo pseudoBinding = new PseudoParameterBinder()1183 .DoPseudoParameterBinding(commandAst, null, null, PseudoParameterBinder.BindingType.ArgumentCompletion);1184 1185 do1186 {1187 // The command cannot be found, or it's NOT a cmdlet, NOT a script cmdlet and NOT a function1188 if (pseudoBinding == null)1189 break;1190 1191 bool parsedArgumentsProvidesMatch = false;1192 1193 if (pseudoBinding.AllParsedArguments != null && pseudoBinding.AllParsedArguments.Count > 0)1194 {1195 ArgumentLocation argLocation;1196 bool treatAsExpression = false;1197 1198 if (expressionAst != null)1199 {1200 treatAsExpression = true;