// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Data; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.Linq; using System.Management.Automation.Internal; using System.Management.Automation.Language; using System.Management.Automation.Runspaces; using System.Reflection; using System.Runtime.InteropServices; using System.Text; using System.Text.RegularExpressions; using System.Threading; using Microsoft.Management.Infrastructure; using Microsoft.Management.Infrastructure.Options; using Microsoft.PowerShell; using Microsoft.PowerShell.Cim; using Microsoft.PowerShell.Commands; using Microsoft.PowerShell.Commands.Internal.Format; namespace System.Management.Automation { /// /// public static class CompletionCompleters { static CompletionCompleters() { AppDomain.CurrentDomain.AssemblyLoad += UpdateTypeCacheOnAssemblyLoad; } private static void UpdateTypeCacheOnAssemblyLoad(object sender, AssemblyLoadEventArgs args) { // Just null out the cache - we'll rebuild it the next time someone tries to complete a type. // We could rebuild it now, but we could be loading multiple assemblies (e.g. dependent assemblies) // and there is no sense in rebuilding anything until we're done loading all of the assemblies. Interlocked.Exchange(ref s_typeCache, null); } #region Command Names /// /// /// /// public static IEnumerable CompleteCommand(string commandName) { return CompleteCommand(commandName, null); } /// /// /// /// /// /// [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")] public static IEnumerable CompleteCommand(string commandName, string moduleName, CommandTypes commandTypes = CommandTypes.All) { var runspace = Runspace.DefaultRunspace; if (runspace == null) { // No runspace, just return no results. return CommandCompletion.EmptyCompletionResult; } var helper = new PowerShellExecutionHelper(PowerShell.Create(RunspaceMode.CurrentRunspace)); var executionContext = helper.CurrentPowerShell.Runspace.ExecutionContext; return CompleteCommand(new CompletionContext { WordToComplete = commandName, Helper = helper, ExecutionContext = executionContext }, moduleName, commandTypes); } internal static List CompleteCommand(CompletionContext context) { return CompleteCommand(context, null); } private static List CompleteCommand(CompletionContext context, string moduleName, CommandTypes types = CommandTypes.All) { var addAmpersandIfNecessary = IsAmpersandNeeded(context, false); string commandName = context.WordToComplete; string quote = HandleDoubleAndSingleQuote(ref commandName); List commandResults = null; if (commandName.IndexOfAny(Utils.Separators.DirectoryOrDrive) == -1) { // The name to complete is neither module qualified nor is it a relative/rooted file path. Ast lastAst = null; if (context.RelatedAsts != null && context.RelatedAsts.Count > 0) { lastAst = context.RelatedAsts.Last(); } commandResults = ExecuteGetCommandCommand(useModulePrefix: false); if (lastAst != null) { // We need to add the wildcard to the end so the regex is built correctly. commandName += "*"; // Search the asts for function definitions that we might be calling var findFunctionsVisitor = new FindFunctionsVisitor(); while (lastAst.Parent != null) { lastAst = lastAst.Parent; } lastAst.Visit(findFunctionsVisitor); WildcardPattern commandNamePattern = WildcardPattern.Get(commandName, WildcardOptions.IgnoreCase); foreach (var defn in findFunctionsVisitor.FunctionDefinitions) { if (commandNamePattern.IsMatch(defn.Name) && !commandResults.Any(cr => cr.CompletionText.Equals(defn.Name, StringComparison.OrdinalIgnoreCase))) { // Results found in the current script are prepended to show up at the top of the list. commandResults.Insert(0, GetCommandNameCompletionResult(defn.Name, defn, addAmpersandIfNecessary, quote)); } } } } else { // If there is a single \, we might be looking for a module/snapin qualified command var indexOfFirstColon = commandName.IndexOf(':'); var indexOfFirstBackslash = commandName.IndexOf('\\'); if (indexOfFirstBackslash > 0 && (indexOfFirstBackslash < indexOfFirstColon || indexOfFirstColon == -1)) { // First try the name before the backslash as a module name. // Use the exact module name provided by the user moduleName = commandName.Substring(0, indexOfFirstBackslash); commandName = commandName.Substring(indexOfFirstBackslash + 1); commandResults = ExecuteGetCommandCommand(useModulePrefix: true); } } return commandResults; List ExecuteGetCommandCommand(bool useModulePrefix) { var powershell = context.Helper .AddCommandWithPreferenceSetting("Get-Command", typeof(GetCommandCommand)) .AddParameter("All") .AddParameter("Name", commandName + "*"); if (moduleName != null) { powershell.AddParameter("Module", moduleName); } if (!types.Equals(CommandTypes.All)) { powershell.AddParameter("CommandType", types); } // Exception is ignored, the user simply does not get any completion results if the pipeline fails Exception exceptionThrown; var commandInfos = context.Helper.ExecuteCurrentPowerShell(out exceptionThrown); if (commandInfos == null || commandInfos.Count == 0) { powershell.Commands.Clear(); powershell .AddCommandWithPreferenceSetting("Get-Command", typeof(GetCommandCommand)) .AddParameter("All") .AddParameter("Name", commandName) .AddParameter("UseAbbreviationExpansion"); if (moduleName != null) { powershell.AddParameter("Module", moduleName); } if (!types.Equals(CommandTypes.All)) { powershell.AddParameter("CommandType", types); } commandInfos = context.Helper.ExecuteCurrentPowerShell(out exceptionThrown); } List completionResults = null; if (commandInfos != null && commandInfos.Count > 1) { // OrderBy is using stable sorting var sortedCommandInfos = commandInfos.OrderBy(static a => a, new CommandNameComparer()); completionResults = MakeCommandsUnique(sortedCommandInfos, useModulePrefix, addAmpersandIfNecessary, quote); } else { completionResults = MakeCommandsUnique(commandInfos, useModulePrefix, addAmpersandIfNecessary, quote); } return completionResults; } } private static readonly HashSet s_keywordsToExcludeFromAddingAmpersand = new HashSet(StringComparer.OrdinalIgnoreCase) { nameof(TokenKind.InlineScript), nameof(TokenKind.Configuration) }; internal static CompletionResult GetCommandNameCompletionResult(string name, object command, bool addAmpersandIfNecessary, string quote) { string syntax = name, listItem = name; var commandInfo = command as CommandInfo; if (commandInfo != null) { try { listItem = commandInfo.Name; // This may require parsing a script, which could fail in a number of different ways // (syntax errors, security exceptions, etc.) If so, the name is fine for the tooltip. syntax = commandInfo.Syntax; } catch (Exception) { } } syntax = string.IsNullOrEmpty(syntax) ? name : syntax; bool needAmpersand; if (CompletionRequiresQuotes(name, false)) { needAmpersand = quote == string.Empty && addAmpersandIfNecessary; string quoteInUse = quote == string.Empty ? "'" : quote; if (quoteInUse == "'") { name = name.Replace("'", "''"); } else { name = name.Replace("`", "``"); name = name.Replace("$", "`$"); } name = quoteInUse + name + quoteInUse; } else { needAmpersand = quote == string.Empty && addAmpersandIfNecessary && Tokenizer.IsKeyword(name) && !s_keywordsToExcludeFromAddingAmpersand.Contains(name); name = quote + name + quote; } // It's useless to call ForEach-Object (foreach) as the first command of a pipeline. For example: // PS C:\> fore ---> PS C:\> foreach (expected, use as the keyword) // PS C:\> fore ---> PS C:\> & foreach (unexpected, ForEach-Object is seldom used as the first command of a pipeline) if (needAmpersand && name != SpecialVariables.@foreach) { name = "& " + name; } return new CompletionResult(name, listItem, CompletionResultType.Command, syntax); } internal static List MakeCommandsUnique(IEnumerable commandInfoPsObjs, bool includeModulePrefix, bool addAmpersandIfNecessary, string quote) { List results = new List(); if (commandInfoPsObjs == null || !commandInfoPsObjs.Any()) { return results; } var commandTable = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var psobj in commandInfoPsObjs) { object baseObj = PSObject.Base(psobj); string name = null; var commandInfo = baseObj as CommandInfo; if (commandInfo != null) { // Skip the private commands if (commandInfo.Visibility == SessionStateEntryVisibility.Private) { continue; } name = commandInfo.Name; if (includeModulePrefix && !string.IsNullOrEmpty(commandInfo.ModuleName)) { // The command might be a prefixed commandInfo that we get by importing a module with the -Prefix parameter, for example: // FooModule.psm1: Get-Foo // import-module FooModule -Prefix PowerShell // --> command 'Get-PowerShellFoo' in the global session state (prefixed commandInfo) // command 'Get-Foo' in the module session state (un-prefixed commandInfo) // in that case, we should not add the module name qualification because it doesn't work if (string.IsNullOrEmpty(commandInfo.Prefix) || !ModuleCmdletBase.IsPrefixedCommand(commandInfo)) { name = commandInfo.ModuleName + "\\" + commandInfo.Name; } } } else { name = baseObj as string; if (name == null) { continue; } } object value; if (!commandTable.TryGetValue(name, out value)) { commandTable.Add(name, baseObj); } else { var list = value as List; if (list != null) { list.Add(baseObj); } else { list = new List { value, baseObj }; commandTable[name] = list; } } } List endResults = null; foreach (var keyValuePair in commandTable) { var commandList = keyValuePair.Value as List; if (commandList != null) { if (endResults == null) { endResults = new List(); } // The first command might be an un-prefixed commandInfo that we get by importing a module with the -Prefix parameter, // in that case, we should add the module name qualification because if the module is not in the module path, calling // 'Get-Foo' directly doesn't work string completionName = keyValuePair.Key; if (!includeModulePrefix) { var commandInfo = commandList[0] as CommandInfo; if (commandInfo != null && !string.IsNullOrEmpty(commandInfo.Prefix)) { Diagnostics.Assert(!string.IsNullOrEmpty(commandInfo.ModuleName), "the module name should exist if commandInfo.Prefix is not an empty string"); if (!ModuleCmdletBase.IsPrefixedCommand(commandInfo)) { completionName = commandInfo.ModuleName + "\\" + completionName; } } } results.Add(GetCommandNameCompletionResult(completionName, commandList[0], addAmpersandIfNecessary, quote)); // For the other commands that are hidden, we need to disambiguate, // but put these at the end as it's less likely any of the hidden // commands are desired. If we can't add anything to disambiguate, // then we'll skip adding a completion result. for (int index = 1; index < commandList.Count; index++) { var commandInfo = commandList[index] as CommandInfo; Diagnostics.Assert(commandInfo != null, "Elements should always be CommandInfo"); if (commandInfo.CommandType == CommandTypes.Application) { endResults.Add(GetCommandNameCompletionResult(commandInfo.Definition, commandInfo, addAmpersandIfNecessary, quote)); } else if (!string.IsNullOrEmpty(commandInfo.ModuleName)) { var name = commandInfo.ModuleName + "\\" + commandInfo.Name; endResults.Add(GetCommandNameCompletionResult(name, commandInfo, addAmpersandIfNecessary, quote)); } } } else { // The first command might be an un-prefixed commandInfo that we get by importing a module with the -Prefix parameter, // in that case, we should add the module name qualification because if the module is not in the module path, calling // 'Get-Foo' directly doesn't work string completionName = keyValuePair.Key; if (!includeModulePrefix) { var commandInfo = keyValuePair.Value as CommandInfo; if (commandInfo != null && !string.IsNullOrEmpty(commandInfo.Prefix)) { Diagnostics.Assert(!string.IsNullOrEmpty(commandInfo.ModuleName), "the module name should exist if commandInfo.Prefix is not an empty string"); if (!ModuleCmdletBase.IsPrefixedCommand(commandInfo)) { completionName = commandInfo.ModuleName + "\\" + completionName; } } } results.Add(GetCommandNameCompletionResult(completionName, keyValuePair.Value, addAmpersandIfNecessary, quote)); } } if (endResults != null && endResults.Count > 0) { results.AddRange(endResults); } return results; } private sealed class FindFunctionsVisitor : AstVisitor { internal readonly List FunctionDefinitions = new List(); public override AstVisitAction VisitFunctionDefinition(FunctionDefinitionAst functionDefinitionAst) { FunctionDefinitions.Add(functionDefinitionAst); return AstVisitAction.Continue; } } #endregion Command Names #region Module Names internal static List CompleteModuleName(CompletionContext context, bool loadedModulesOnly, bool skipEditionCheck = false) { var moduleName = context.WordToComplete ?? string.Empty; var result = new List(); var quote = HandleDoubleAndSingleQuote(ref moduleName); if (!moduleName.EndsWith('*')) { moduleName += "*"; } var powershell = context.Helper.AddCommandWithPreferenceSetting("Get-Module", typeof(GetModuleCommand)).AddParameter("Name", moduleName); if (!loadedModulesOnly) { powershell.AddParameter("ListAvailable", true); // -SkipEditionCheck should only be set or apply to -ListAvailable if (skipEditionCheck) { powershell.AddParameter("SkipEditionCheck", true); } } Exception exceptionThrown; var psObjects = context.Helper.ExecuteCurrentPowerShell(out exceptionThrown); if (psObjects != null) { foreach (dynamic moduleInfo in psObjects) { var completionText = moduleInfo.Name.ToString(); var listItemText = completionText; var toolTip = "Description: " + moduleInfo.Description.ToString() + "\r\nModuleType: " + moduleInfo.ModuleType.ToString() + "\r\nPath: " + moduleInfo.Path.ToString(); if (CompletionRequiresQuotes(completionText, false)) { var quoteInUse = quote == string.Empty ? "'" : quote; if (quoteInUse == "'") completionText = completionText.Replace("'", "''"); completionText = quoteInUse + completionText + quoteInUse; } else { completionText = quote + completionText + quote; } result.Add(new CompletionResult(completionText, listItemText, CompletionResultType.ParameterValue, toolTip)); } } return result; } #endregion Module Names #region Command Parameters private static readonly string[] s_parameterNamesOfImportDSCResource = { "Name", "ModuleName", "ModuleVersion" }; internal static List CompleteCommandParameter(CompletionContext context) { string partialName = null; bool withColon = false; CommandAst commandAst = null; List result = new List(); // Find the parameter ast, it will be near or at the end CommandParameterAst parameterAst = null; DynamicKeywordStatementAst keywordAst = null; for (int i = context.RelatedAsts.Count - 1; i >= 0; i--) { if (keywordAst == null) keywordAst = context.RelatedAsts[i] as DynamicKeywordStatementAst; parameterAst = (context.RelatedAsts[i] as CommandParameterAst); if (parameterAst != null) break; } if (parameterAst != null) { keywordAst = parameterAst.Parent as DynamicKeywordStatementAst; } // If parent is DynamicKeywordStatementAst - 'Import-DscResource', // then customize the auto completion results if (keywordAst != null && string.Equals(keywordAst.Keyword.Keyword, "Import-DscResource", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrWhiteSpace(context.WordToComplete) && context.WordToComplete.StartsWith('-')) { var lastAst = context.RelatedAsts.Last(); var wordToMatch = string.Concat(context.WordToComplete.AsSpan(1), "*"); var pattern = WildcardPattern.Get(wordToMatch, WildcardOptions.IgnoreCase); var parameterNames = keywordAst.CommandElements.Where(static ast => ast is CommandParameterAst).Select(static ast => (ast as CommandParameterAst).ParameterName); foreach (var parameterName in s_parameterNamesOfImportDSCResource) { if (pattern.IsMatch(parameterName) && !parameterNames.Contains(parameterName, StringComparer.OrdinalIgnoreCase)) { string tooltip = "[String] " + parameterName; result.Add(new CompletionResult("-" + parameterName, parameterName, CompletionResultType.ParameterName, tooltip)); } } if (result.Count > 0) { context.ReplacementLength = context.WordToComplete.Length; context.ReplacementIndex = lastAst.Extent.StartOffset; } return result; } if (parameterAst != null) { // Parent must be a command commandAst = (CommandAst)parameterAst.Parent; partialName = parameterAst.ParameterName; withColon = context.WordToComplete.EndsWith(':'); } else { // No CommandParameterAst is found. It could be a StringConstantExpressionAst "-" if (!(context.RelatedAsts[context.RelatedAsts.Count - 1] is StringConstantExpressionAst dashAst)) return result; if (!dashAst.Value.Trim().Equals("-", StringComparison.OrdinalIgnoreCase)) return result; // Parent must be a command commandAst = (CommandAst)dashAst.Parent; partialName = string.Empty; } PseudoBindingInfo pseudoBinding = new PseudoParameterBinder() .DoPseudoParameterBinding(commandAst, null, parameterAst, PseudoParameterBinder.BindingType.ParameterCompletion); // The command cannot be found or it's not a cmdlet, not a script cmdlet, not a function. // Try completing as if it the parameter is a command argument for native command completion. if (pseudoBinding == null) { return CompleteCommandArgument(context); } switch (pseudoBinding.InfoType) { case PseudoBindingInfoType.PseudoBindingFail: // The command is a cmdlet or script cmdlet. Binding failed result = GetParameterCompletionResults(partialName, uint.MaxValue, pseudoBinding.UnboundParameters, withColon); break; case PseudoBindingInfoType.PseudoBindingSucceed: // The command is a cmdlet or script cmdlet. Binding succeeded. result = GetParameterCompletionResults(partialName, pseudoBinding, parameterAst, withColon); break; } if (result.Count == 0) { result = pseudoBinding.CommandName.Equals("Set-Location", StringComparison.OrdinalIgnoreCase) ? new List(CompleteFilename(context, containerOnly: true, extension: null)) : new List(CompleteFilename(context)); } return result; } /// /// Get the parameter completion results when the pseudo binding was successful. /// /// /// /// /// /// private static List GetParameterCompletionResults(string parameterName, PseudoBindingInfo bindingInfo, CommandParameterAst parameterAst, bool withColon) { Diagnostics.Assert(bindingInfo.InfoType.Equals(PseudoBindingInfoType.PseudoBindingSucceed), "The pseudo binding should succeed"); List result = new List(); if (parameterName == string.Empty) { result = GetParameterCompletionResults( parameterName, bindingInfo.ValidParameterSetsFlags, bindingInfo.UnboundParameters, withColon); return result; } if (bindingInfo.ParametersNotFound.Count > 0) { // The parameter name cannot be matched to any parameter if (bindingInfo.ParametersNotFound.Any(pAst => parameterAst.GetHashCode() == pAst.GetHashCode())) { return result; } } if (bindingInfo.AmbiguousParameters.Count > 0) { // The parameter name is ambiguous. It's ignored in the pseudo binding, and we should search in the UnboundParameters if (bindingInfo.AmbiguousParameters.Any(pAst => parameterAst.GetHashCode() == pAst.GetHashCode())) { result = GetParameterCompletionResults( parameterName, bindingInfo.ValidParameterSetsFlags, bindingInfo.UnboundParameters, withColon); } return result; } if (bindingInfo.DuplicateParameters.Count > 0) { // The parameter name is resolved to a parameter that is already bound. We search it in the BoundParameters if (bindingInfo.DuplicateParameters.Any(pAst => parameterAst.GetHashCode() == pAst.Parameter.GetHashCode())) { result = GetParameterCompletionResults( parameterName, bindingInfo.ValidParameterSetsFlags, bindingInfo.BoundParameters.Values, withColon); } return result; } // The parameter should be bound in the pseudo binding during the named binding string matchedParameterName = null; foreach (KeyValuePair entry in bindingInfo.BoundArguments) { switch (entry.Value.ParameterArgumentType) { case AstParameterArgumentType.AstPair: { AstPair pair = (AstPair)entry.Value; if (pair.ParameterSpecified && pair.Parameter.GetHashCode() == parameterAst.GetHashCode()) { matchedParameterName = entry.Key; } else if (pair.ArgumentIsCommandParameterAst && pair.Argument.GetHashCode() == parameterAst.GetHashCode()) { // The parameter name cannot be resolved to a parameter return result; } } break; case AstParameterArgumentType.Fake: { FakePair pair = (FakePair)entry.Value; if (pair.ParameterSpecified && pair.Parameter.GetHashCode() == parameterAst.GetHashCode()) { matchedParameterName = entry.Key; } } break; case AstParameterArgumentType.Switch: { SwitchPair pair = (SwitchPair)entry.Value; if (pair.ParameterSpecified && pair.Parameter.GetHashCode() == parameterAst.GetHashCode()) { matchedParameterName = entry.Key; } } break; case AstParameterArgumentType.AstArray: case AstParameterArgumentType.PipeObject: break; } if (matchedParameterName != null) break; } Diagnostics.Assert(matchedParameterName != null, "we should find matchedParameterName from the BoundArguments"); MergedCompiledCommandParameter param = bindingInfo.BoundParameters[matchedParameterName]; WildcardPattern pattern = WildcardPattern.Get(parameterName + "*", WildcardOptions.IgnoreCase); string parameterType = "[" + ToStringCodeMethods.Type(param.Parameter.Type, dropNamespaces: true) + "] "; string colonSuffix = withColon ? ":" : string.Empty; if (pattern.IsMatch(matchedParameterName)) { string completionText = "-" + matchedParameterName + colonSuffix; string tooltip = parameterType + matchedParameterName; result.Add(new CompletionResult(completionText, matchedParameterName, CompletionResultType.ParameterName, tooltip)); } // Process alias when there is partial input result.AddRange(from alias in param.Parameter.Aliases where pattern.IsMatch(alias) select new CompletionResult("-" + alias + colonSuffix, alias, CompletionResultType.ParameterName, parameterType + alias)); return result; } /// /// Get the parameter completion results by using the given valid parameter sets and available parameters. /// /// /// /// /// /// private static List GetParameterCompletionResults( string parameterName, uint validParameterSetFlags, IEnumerable parameters, bool withColon) { var result = new List(); var commonParamResult = new List(); var pattern = WildcardPattern.Get(parameterName + "*", WildcardOptions.IgnoreCase); var colonSuffix = withColon ? ":" : string.Empty; bool addCommonParameters = true; foreach (MergedCompiledCommandParameter param in parameters) { bool inParameterSet = (param.Parameter.ParameterSetFlags & validParameterSetFlags) != 0 || param.Parameter.IsInAllSets; if (!inParameterSet) continue; string name = param.Parameter.Name; string type = "[" + ToStringCodeMethods.Type(param.Parameter.Type, dropNamespaces: true) + "] "; bool isCommonParameter = Cmdlet.CommonParameters.Contains(name, StringComparer.OrdinalIgnoreCase); List listInUse = isCommonParameter ? commonParamResult : result; if (pattern.IsMatch(name)) { // Then using functions to back dynamic keywords, we don't necessarily // want all of the parameters to be shown to the user. Those that are marked // DontShow will not be displayed. Also, if any of the parameters have // don't show set, we won't show any of the common parameters either. bool showToUser = true; var compiledAttributes = param.Parameter.CompiledAttributes; if (compiledAttributes != null && compiledAttributes.Count > 0) { foreach (var attr in compiledAttributes) { var pattr = attr as ParameterAttribute; if (pattr != null && pattr.DontShow) { showToUser = false; addCommonParameters = false; break; } } } if (showToUser) { string completionText = "-" + name + colonSuffix; string tooltip = type + name; listInUse.Add(new CompletionResult(completionText, name, CompletionResultType.ParameterName, tooltip)); } } if (parameterName != string.Empty) { // Process alias when there is partial input listInUse.AddRange(from alias in param.Parameter.Aliases where pattern.IsMatch(alias) select new CompletionResult("-" + alias + colonSuffix, alias, CompletionResultType.ParameterName, type + alias)); } } // Add the common parameters to the results if expected. if (addCommonParameters) { result.AddRange(commonParamResult); } return result; } /// /// Get completion results for operators that start with /// /// The starting text of the operator to complete. /// A list of completion results. public static List CompleteOperator(string wordToComplete) { if (wordToComplete.StartsWith('-')) { wordToComplete = wordToComplete.Substring(1); } return (from op in Tokenizer._operatorText where op.StartsWith(wordToComplete, StringComparison.OrdinalIgnoreCase) orderby op select new CompletionResult("-" + op, op, CompletionResultType.ParameterName, GetOperatorDescription(op))).ToList(); } private static string GetOperatorDescription(string op) { return ResourceManagerCache.GetResourceString(typeof(CompletionCompleters).Assembly, "System.Management.Automation.resources.TabCompletionStrings", op + "OperatorDescription"); } #endregion Command Parameters #region Command Arguments internal static List CompleteCommandArgument(CompletionContext context) { CommandAst commandAst = null; List result = new List(); // Find the expression ast. It should be at the end if there is one ExpressionAst expressionAst = null; MemberExpressionAst secondToLastMemberAst = null; Ast lastAst = context.RelatedAsts.Last(); expressionAst = lastAst as ExpressionAst; if (expressionAst != null) { if (expressionAst.Parent is CommandAst) { commandAst = (CommandAst)expressionAst.Parent; if (expressionAst is ErrorExpressionAst && expressionAst.Extent.Text.EndsWith(',')) { context.WordToComplete = string.Empty; // BUGBUG context.CursorPosition = expressionAst.Extent.StartScriptPosition; } else if (commandAst.CommandElements.Count == 1 || context.WordToComplete == string.Empty) { expressionAst = null; } else if (commandAst.CommandElements.Count > 2) { var length = commandAst.CommandElements.Count; var index = 1; for (; index < length; index++) { if (commandAst.CommandElements[index] == expressionAst) break; } CommandElementAst secondToLastAst = null; if (index > 1) { secondToLastAst = commandAst.CommandElements[index - 1]; secondToLastMemberAst = secondToLastAst as MemberExpressionAst; } var partialPathAst = expressionAst as StringConstantExpressionAst; if (partialPathAst != null && secondToLastAst != null && partialPathAst.StringConstantType == StringConstantType.BareWord && secondToLastAst.Extent.EndLineNumber == partialPathAst.Extent.StartLineNumber && secondToLastAst.Extent.EndColumnNumber == partialPathAst.Extent.StartColumnNumber && partialPathAst.Value.IndexOfAny(Utils.Separators.Directory) == 0) { var secondToLastStringConstantAst = secondToLastAst as StringConstantExpressionAst; var secondToLastExpandableStringAst = secondToLastAst as ExpandableStringExpressionAst; var secondToLastArrayAst = secondToLastAst as ArrayLiteralAst; var secondToLastParamAst = secondToLastAst as CommandParameterAst; if (secondToLastStringConstantAst != null || secondToLastExpandableStringAst != null) { var fullPath = ConcatenateStringPathArguments(secondToLastAst, partialPathAst.Value, context); expressionAst = secondToLastStringConstantAst != null ? (ExpressionAst)secondToLastStringConstantAst : (ExpressionAst)secondToLastExpandableStringAst; context.ReplacementIndex = ((InternalScriptPosition)secondToLastAst.Extent.StartScriptPosition).Offset; context.ReplacementLength += ((InternalScriptPosition)secondToLastAst.Extent.EndScriptPosition).Offset - context.ReplacementIndex; context.WordToComplete = fullPath; // context.CursorPosition = secondToLastAst.Extent.StartScriptPosition; } else if (secondToLastArrayAst != null) { // Handle cases like: dir -Path .\cd, 'a b'\new var lastArrayElement = secondToLastArrayAst.Elements.LastOrDefault(); var fullPath = ConcatenateStringPathArguments(lastArrayElement, partialPathAst.Value, context); if (fullPath != null) { expressionAst = secondToLastArrayAst; context.ReplacementIndex = ((InternalScriptPosition)lastArrayElement.Extent.StartScriptPosition).Offset; context.ReplacementLength += ((InternalScriptPosition)lastArrayElement.Extent.EndScriptPosition).Offset - context.ReplacementIndex; context.WordToComplete = fullPath; } } else if (secondToLastParamAst != null) { // Handle cases like: dir -Path: .\cd, 'a b'\new || dir -Path: 'a b'\new var fullPath = ConcatenateStringPathArguments(secondToLastParamAst.Argument, partialPathAst.Value, context); if (fullPath != null) { expressionAst = secondToLastParamAst.Argument; context.ReplacementIndex = ((InternalScriptPosition)secondToLastParamAst.Argument.Extent.StartScriptPosition).Offset; context.ReplacementLength += ((InternalScriptPosition)secondToLastParamAst.Argument.Extent.EndScriptPosition).Offset - context.ReplacementIndex; context.WordToComplete = fullPath; } else { var arrayArgAst = secondToLastParamAst.Argument as ArrayLiteralAst; if (arrayArgAst != null) { var lastArrayElement = arrayArgAst.Elements.LastOrDefault(); fullPath = ConcatenateStringPathArguments(lastArrayElement, partialPathAst.Value, context); if (fullPath != null) { expressionAst = arrayArgAst; context.ReplacementIndex = ((InternalScriptPosition)lastArrayElement.Extent.StartScriptPosition).Offset; context.ReplacementLength += ((InternalScriptPosition)lastArrayElement.Extent.EndScriptPosition).Offset - context.ReplacementIndex; context.WordToComplete = fullPath; } } } } } } } else if (expressionAst.Parent is ArrayLiteralAst && expressionAst.Parent.Parent is CommandAst) { commandAst = (CommandAst)expressionAst.Parent.Parent; if (commandAst.CommandElements.Count == 1 || context.WordToComplete == string.Empty) { // dir -Path a.txt, b.txt expressionAst = null; } else { // dir -Path a.txt, b.txt c expressionAst = (ExpressionAst)expressionAst.Parent; } } else if (expressionAst.Parent is ArrayLiteralAst && expressionAst.Parent.Parent is CommandParameterAst) { // Handle scenarios such as // dir -Path: a.txt, || dir -Path: a.txt, b.txt commandAst = (CommandAst)expressionAst.Parent.Parent.Parent; if (context.WordToComplete == string.Empty) { // dir -Path: a.txt, b.txt expressionAst = null; } else { // dir -Path: a.txt, b expressionAst = (ExpressionAst)expressionAst.Parent; } } else if (expressionAst.Parent is CommandParameterAst && expressionAst.Parent.Parent is CommandAst) { commandAst = (CommandAst)expressionAst.Parent.Parent; if (expressionAst is ErrorExpressionAst && expressionAst.Extent.Text.EndsWith(',')) { // dir -Path: a.txt, context.WordToComplete = string.Empty; // context.CursorPosition = expressionAst.Extent.StartScriptPosition; } else if (context.WordToComplete == string.Empty) { // Handle scenario like this: Set-ExecutionPolicy -Scope:CurrentUser expressionAst = null; } } } else { var paramAst = lastAst as CommandParameterAst; if (paramAst != null) { commandAst = paramAst.Parent as CommandAst; } else { commandAst = lastAst as CommandAst; } } if (commandAst == null) { // We don't know if this could be expanded into anything interesting return result; } PseudoBindingInfo pseudoBinding = new PseudoParameterBinder() .DoPseudoParameterBinding(commandAst, null, null, PseudoParameterBinder.BindingType.ArgumentCompletion); do { // The command cannot be found, or it's NOT a cmdlet, NOT a script cmdlet and NOT a function if (pseudoBinding == null) break; bool parsedArgumentsProvidesMatch = false; if (pseudoBinding.AllParsedArguments != null && pseudoBinding.AllParsedArguments.Count > 0) { ArgumentLocation argLocation; bool treatAsExpression = false; if (expressionAst != null) { treatAsExpression = true; var dashExp = expressionAst as StringConstantExpressionAst; if (dashExp != null && dashExp.Value.Trim().Equals("-", StringComparison.OrdinalIgnoreCase)) { // "-" is represented as StringConstantExpressionAst. Most likely the user is typing a // after it, so in the pseudo binder, we ignore it to avoid treating it as an argument. // for example: // Get-Content -Path "- --> Get-Content -Path ".\-patt.txt" treatAsExpression = false; } } if (treatAsExpression) { argLocation = FindTargetArgumentLocation( pseudoBinding.AllParsedArguments, expressionAst); } else { argLocation = FindTargetArgumentLocation( pseudoBinding.AllParsedArguments, context.TokenAtCursor ?? context.TokenBeforeCursor); } if (argLocation != null) { context.PseudoBindingInfo = pseudoBinding; switch (pseudoBinding.InfoType) { case PseudoBindingInfoType.PseudoBindingSucceed: result = GetArgumentCompletionResultsWithSuccessfulPseudoBinding(context, argLocation, commandAst); break; case PseudoBindingInfoType.PseudoBindingFail: result = GetArgumentCompletionResultsWithFailedPseudoBinding(context, argLocation, commandAst); break; } parsedArgumentsProvidesMatch = true; } } if (!parsedArgumentsProvidesMatch) { int index = 0; CommandElementAst prevElem = null; if (expressionAst != null) { foreach (CommandElementAst eleAst in commandAst.CommandElements) { if (eleAst.GetHashCode() == expressionAst.GetHashCode()) break; prevElem = eleAst; index++; } } else { var token = context.TokenAtCursor ?? context.TokenBeforeCursor; foreach (CommandElementAst eleAst in commandAst.CommandElements) { if (eleAst.Extent.StartOffset > token.Extent.EndOffset) break; prevElem = eleAst; index++; } } // positional argument with position 0 if (index == 1) { CompletePositionalArgument( pseudoBinding.CommandName, commandAst, context, result, pseudoBinding.UnboundParameters, pseudoBinding.DefaultParameterSetFlag, uint.MaxValue, 0); } else { if (prevElem is CommandParameterAst && ((CommandParameterAst)prevElem).Argument == null) { var paramName = ((CommandParameterAst)prevElem).ParameterName; var pattern = WildcardPattern.Get(paramName + "*", WildcardOptions.IgnoreCase); foreach (MergedCompiledCommandParameter param in pseudoBinding.UnboundParameters) { if (pattern.IsMatch(param.Parameter.Name)) { ProcessParameter(pseudoBinding.CommandName, commandAst, context, result, param); break; } var isAliasMatch = false; foreach (string alias in param.Parameter.Aliases) { if (pattern.IsMatch(alias)) { isAliasMatch = true; ProcessParameter(pseudoBinding.CommandName, commandAst, context, result, param); break; } } if (isAliasMatch) break; } } } } } while (false); // Indicate if the current argument completion falls into those pre-defined cases and // has been processed already. bool hasBeenProcessed = false; if (result.Count > 0 && result[result.Count - 1].Equals(CompletionResult.Null)) { result.RemoveAt(result.Count - 1); hasBeenProcessed = true; if (result.Count > 0) return result; } // Handle some special cases such as: // & "get-comm --> & "Get-Command" // & "sa --> & ".\sa[v].txt" if (expressionAst == null && !hasBeenProcessed && commandAst.CommandElements.Count == 1 && commandAst.InvocationOperator != TokenKind.Unknown && context.WordToComplete != string.Empty) { // Use literal path after Ampersand var tryCmdletCompletion = false; var clearLiteralPathsKey = TurnOnLiteralPathOption(context); if (context.WordToComplete.Contains('-')) { tryCmdletCompletion = true; } try { var fileCompletionResults = new List(CompleteFilename(context)); if (tryCmdletCompletion) { // It's actually command name completion, other than argument completion var cmdletCompletionResults = CompleteCommand(context); if (cmdletCompletionResults != null && cmdletCompletionResults.Count > 0) { fileCompletionResults.AddRange(cmdletCompletionResults); } } return fileCompletionResults; } finally { if (clearLiteralPathsKey) context.Options.Remove("LiteralPaths"); } } if (expressionAst is StringConstantExpressionAst) { var pathAst = (StringConstantExpressionAst)expressionAst; // Handle static member completion: echo [int]:: var shareMatch = Regex.Match(pathAst.Value, @"^(\[[\w\d\.]+\]::[\w\d\*]*)$"); if (shareMatch.Success) { int fakeReplacementIndex, fakeReplacementLength; var input = shareMatch.Groups[1].Value; var completionParameters = CommandCompletion.MapStringInputToParsedInput(input, input.Length); var completionAnalysis = new CompletionAnalysis(completionParameters.Item1, completionParameters.Item2, completionParameters.Item3, context.Options); var ret = completionAnalysis.GetResults( context.Helper.CurrentPowerShell, out fakeReplacementIndex, out fakeReplacementLength); if (ret != null && ret.Count > 0) { string prefix = string.Concat(TokenKind.LParen.Text(), input.AsSpan(0, fakeReplacementIndex)); foreach (CompletionResult entry in ret) { string completionText = prefix + entry.CompletionText; if (entry.ResultType.Equals(CompletionResultType.Property)) completionText += TokenKind.RParen.Text(); result.Add(new CompletionResult(completionText, entry.ListItemText, entry.ResultType, entry.ToolTip)); } return result; } } // Handle member completion with wildcard: echo $a.* if (pathAst.Value.Contains('*') && secondToLastMemberAst != null && secondToLastMemberAst.Extent.EndLineNumber == pathAst.Extent.StartLineNumber && secondToLastMemberAst.Extent.EndColumnNumber == pathAst.Extent.StartColumnNumber) { var memberName = pathAst.Value.EndsWith('*') ? pathAst.Value : pathAst.Value + "*"; var targetExpr = secondToLastMemberAst.Expression; if (IsSplattedVariable(targetExpr)) { // It's splatted variable, and the member completion is not useful return result; } var memberAst = secondToLastMemberAst.Member as StringConstantExpressionAst; if (memberAst != null) { memberName = memberAst.Value + memberName; } CompleteMemberHelper(false, memberName, targetExpr, context, result); if (result.Count > 0) { context.ReplacementIndex = ((InternalScriptPosition)secondToLastMemberAst.Expression.Extent.EndScriptPosition).Offset + 1; if (memberAst != null) context.ReplacementLength += memberAst.Value.Length; return result; } } // Treat it as the file name completion // Handle this scenario: & 'c:\a b'\ string fileName = pathAst.Value; if (commandAst.InvocationOperator != TokenKind.Unknown && fileName.IndexOfAny(Utils.Separators.Directory) == 0 && commandAst.CommandElements.Count == 2 && commandAst.CommandElements[0] is StringConstantExpressionAst && commandAst.CommandElements[0].Extent.EndLineNumber == expressionAst.Extent.StartLineNumber && commandAst.CommandElements[0].Extent.EndColumnNumber == expressionAst.Extent.StartColumnNumber) { if (pseudoBinding != null) { // CommandElements[0] is resolved to a command return result; } else { var constantAst = (StringConstantExpressionAst)commandAst.CommandElements[0]; fileName = constantAst.Value + fileName; context.ReplacementIndex = ((InternalScriptPosition)constantAst.Extent.StartScriptPosition).Offset; context.ReplacementLength += ((InternalScriptPosition)constantAst.Extent.EndScriptPosition).Offset - context.ReplacementIndex; context.WordToComplete = fileName; // commandAst.InvocationOperator != TokenKind.Unknown, so we should use literal path var clearLiteralPathKey = TurnOnLiteralPathOption(context); try { return new List(CompleteFilename(context)); } finally { if (clearLiteralPathKey) context.Options.Remove("LiteralPaths"); } } } } // The default argument completion: file path completion, command name completion('WordToComplete' is not empty and contains a dash). // If the current argument completion has been process already, we don't go through the default argument completion anymore. if (!hasBeenProcessed) { var commandName = commandAst.GetCommandName(); var customCompleter = GetCustomArgumentCompleter( "NativeArgumentCompleters", new[] { commandName, Path.GetFileName(commandName), Path.GetFileNameWithoutExtension(commandName) }, context); if (customCompleter != null) { if (InvokeScriptArgumentCompleter( customCompleter, new object[] { context.WordToComplete, commandAst, context.CursorPosition.Offset }, result)) { return result; } } var clearLiteralPathKey = false; if (pseudoBinding == null) { // the command could be a native command such as notepad.exe, we use literal path in this case clearLiteralPathKey = TurnOnLiteralPathOption(context); } try { result = new List(CompleteFilename(context)); } finally { if (clearLiteralPathKey) context.Options.Remove("LiteralPaths"); } if (context.WordToComplete != string.Empty && context.WordToComplete.Contains('-')) { var commandResults = CompleteCommand(context); if (commandResults != null) result.AddRange(commandResults); } } return result; } internal static string ConcatenateStringPathArguments(CommandElementAst stringAst, string partialPath, CompletionContext completionContext) { var constantPathAst = stringAst as StringConstantExpressionAst; if (constantPathAst != null) { string quote = string.Empty; switch (constantPathAst.StringConstantType) { case StringConstantType.SingleQuoted: quote = "'"; break; case StringConstantType.DoubleQuoted: quote = "\""; break; default: break; } return quote + constantPathAst.Value + partialPath + quote; } else { var expandablePathAst = stringAst as ExpandableStringExpressionAst; string fullPath = null; if (expandablePathAst != null && IsPathSafelyExpandable(expandableStringAst: expandablePathAst, extraText: partialPath, executionContext: completionContext.ExecutionContext, expandedString: out fullPath)) { return fullPath; } } return null; } /// /// Get the argument completion results when the pseudo binding was not successful. /// private static List GetArgumentCompletionResultsWithFailedPseudoBinding( CompletionContext context, ArgumentLocation argLocation, CommandAst commandAst) { List result = new List(); PseudoBindingInfo bindingInfo = context.PseudoBindingInfo; if (argLocation.IsPositional) { CompletePositionalArgument( bindingInfo.CommandName, commandAst, context, result, bindingInfo.UnboundParameters, bindingInfo.DefaultParameterSetFlag, uint.MaxValue, argLocation.Position); } else { string paramName = argLocation.Argument.ParameterName; WildcardPattern pattern = WildcardPattern.Get(paramName + "*", WildcardOptions.IgnoreCase); foreach (MergedCompiledCommandParameter param in bindingInfo.UnboundParameters) { if (pattern.IsMatch(param.Parameter.Name)) { ProcessParameter(bindingInfo.CommandName, commandAst, context, result, param); break; } bool isAliasMatch = false; foreach (string alias in param.Parameter.Aliases) { if (pattern.IsMatch(alias)) { isAliasMatch = true; ProcessParameter(bindingInfo.CommandName, commandAst, context, result, param); break; } } if (isAliasMatch) break; } } return result; } /// /// Get the argument completion results when the pseudo binding was successful. /// private static List GetArgumentCompletionResultsWithSuccessfulPseudoBinding( CompletionContext context, ArgumentLocation argLocation, CommandAst commandAst) { PseudoBindingInfo bindingInfo = context.PseudoBindingInfo; Diagnostics.Assert(bindingInfo.InfoType.Equals(PseudoBindingInfoType.PseudoBindingSucceed), "Caller needs to make sure the pseudo binding was successful"); List result = new List(); if (argLocation.IsPositional && argLocation.Argument == null) { AstPair lastPositionalArg; AstParameterArgumentPair targetPositionalArg = FindTargetPositionalArgument( bindingInfo.AllParsedArguments, argLocation.Position, out lastPositionalArg); if (targetPositionalArg != null) argLocation.Argument = targetPositionalArg; else { if (lastPositionalArg != null) { bool lastPositionalGetBound = false; Collection parameterNames = new Collection(); foreach (KeyValuePair entry in bindingInfo.BoundArguments) { // positional argument if (!entry.Value.ParameterSpecified) { var arg = (AstPair)entry.Value; if (arg.Argument.GetHashCode() == lastPositionalArg.Argument.GetHashCode()) { lastPositionalGetBound = true; break; } } else if (entry.Value.ParameterArgumentType.Equals(AstParameterArgumentType.AstArray)) { // check if the positional argument would be bound to a "ValueFromRemainingArgument" parameter var arg = (AstArrayPair)entry.Value; if (arg.Argument.Any(exp => exp.GetHashCode() == lastPositionalArg.Argument.GetHashCode())) { parameterNames.Add(entry.Key); } } } if (parameterNames.Count > 0) { // parameter should be in BoundParameters foreach (string param in parameterNames) { MergedCompiledCommandParameter parameter = bindingInfo.BoundParameters[param]; ProcessParameter(bindingInfo.CommandName, commandAst, context, result, parameter, bindingInfo.BoundArguments); } return result; } else if (!lastPositionalGetBound) { // last positional argument was not bound, then positional argument 'tab' wants to // expand will not get bound either return result; } } CompletePositionalArgument( bindingInfo.CommandName, commandAst, context, result, bindingInfo.UnboundParameters, bindingInfo.DefaultParameterSetFlag, bindingInfo.ValidParameterSetsFlags, argLocation.Position, bindingInfo.BoundArguments); return result; } } if (argLocation.Argument != null) { Collection parameterNames = new Collection(); foreach (KeyValuePair entry in bindingInfo.BoundArguments) { if (entry.Value.ParameterArgumentType.Equals(AstParameterArgumentType.PipeObject)) continue; if (entry.Value.ParameterArgumentType.Equals(AstParameterArgumentType.AstArray) && !argLocation.Argument.ParameterSpecified) { var arrayArg = (AstArrayPair)entry.Value; var target = (AstPair)argLocation.Argument; if (arrayArg.Argument.Any(exp => exp.GetHashCode() == target.Argument.GetHashCode())) { parameterNames.Add(entry.Key); } } else if (entry.Value.GetHashCode() == argLocation.Argument.GetHashCode()) { parameterNames.Add(entry.Key); } } if (parameterNames.Count > 0) { // those parameters should be in BoundParameters foreach (string param in parameterNames) { MergedCompiledCommandParameter parameter = bindingInfo.BoundParameters[param]; ProcessParameter(bindingInfo.CommandName, commandAst, context, result, parameter, bindingInfo.BoundArguments); } } } return result; } /// /// Get the positional argument completion results based on the position it's in the command line. /// private static void CompletePositionalArgument( string commandName, CommandAst commandAst, CompletionContext context, List result, IEnumerable parameters, uint defaultParameterSetFlag, uint validParameterSetFlags, int position, Dictionary boundArguments = null) { bool isProcessedAsPositional = false; bool isDefaultParameterSetValid = defaultParameterSetFlag != 0 && (defaultParameterSetFlag & validParameterSetFlags) != 0; MergedCompiledCommandParameter positionalParam = null; foreach (MergedCompiledCommandParameter param in parameters) { bool isInParameterSet = (param.Parameter.ParameterSetFlags & validParameterSetFlags) != 0 || param.Parameter.IsInAllSets; if (!isInParameterSet) continue; var parameterSetDataCollection = param.Parameter.GetMatchingParameterSetData(validParameterSetFlags); foreach (ParameterSetSpecificMetadata parameterSetData in parameterSetDataCollection) { // in the first pass, we skip the remaining argument ones if (parameterSetData.ValueFromRemainingArguments) { continue; } // Check the position int positionInParameterSet = parameterSetData.Position; if (positionInParameterSet == int.MinValue || positionInParameterSet != position) { // The parameter is not positional, or its position is not what we want continue; } if (isDefaultParameterSetValid) { if (parameterSetData.ParameterSetFlag == defaultParameterSetFlag) { ProcessParameter(commandName, commandAst, context, result, param, boundArguments); isProcessedAsPositional = result.Count > 0; break; } else { if (positionalParam == null) positionalParam = param; } } else { isProcessedAsPositional = true; ProcessParameter(commandName, commandAst, context, result, param, boundArguments); break; } } if (isProcessedAsPositional) break; } if (!isProcessedAsPositional && positionalParam != null) { isProcessedAsPositional = true; ProcessParameter(commandName, commandAst, context, result, positionalParam, boundArguments); } if (!isProcessedAsPositional) { foreach (MergedCompiledCommandParameter param in parameters) { bool isInParameterSet = (param.Parameter.ParameterSetFlags & validParameterSetFlags) != 0 || param.Parameter.IsInAllSets; if (!isInParameterSet) continue; var parameterSetDataCollection = param.Parameter.GetMatchingParameterSetData(validParameterSetFlags); foreach (ParameterSetSpecificMetadata parameterSetData in parameterSetDataCollection) { // in the second pass, we check the remaining argument ones if (parameterSetData.ValueFromRemainingArguments) { ProcessParameter(commandName, commandAst, context, result, param, boundArguments); break; } } } } } /// /// Process a parameter to get the argument completion results. /// /// /// If the argument completion falls into these pre-defined cases: /// 1. The matching parameter is of type Enum /// 2. The matching parameter is of type SwitchParameter /// 3. The matching parameter is declared with ValidateSetAttribute /// 4. Falls into the native command argument completion /// a null instance of CompletionResult is added to the end of the /// "result" list, to indicate that this particular argument completion /// has been processed already. If the "result" list is still empty, we /// will not go through the default argument completion steps anymore. /// private static void ProcessParameter( string commandName, CommandAst commandAst, CompletionContext context, List result, MergedCompiledCommandParameter parameter, Dictionary boundArguments = null) { CompletionResult fullMatch = null; Type parameterType = GetEffectiveParameterType(parameter.Parameter.Type); if (parameterType.IsArray) { parameterType = parameterType.GetElementType(); } if (parameterType.IsEnum) { RemoveLastNullCompletionResult(result); string enumString = LanguagePrimitives.EnumSingleTypeConverter.EnumValues(parameterType); string separator = CultureInfo.CurrentUICulture.TextInfo.ListSeparator; string[] enumArray = enumString.Split(separator, StringSplitOptions.RemoveEmptyEntries); string wordToComplete = context.WordToComplete; string quote = HandleDoubleAndSingleQuote(ref wordToComplete); var pattern = WildcardPattern.Get(wordToComplete + "*", WildcardOptions.IgnoreCase); var enumList = new List(); foreach (string value in enumArray) { if (wordToComplete.Equals(value, StringComparison.OrdinalIgnoreCase)) { string completionText = quote == string.Empty ? value : quote + value + quote; fullMatch = new CompletionResult(completionText, value, CompletionResultType.ParameterValue, value); continue; } if (pattern.IsMatch(value)) { enumList.Add(value); } } if (fullMatch != null) { result.Add(fullMatch); } enumList.Sort(); result.AddRange(from entry in enumList let completionText = quote == string.Empty ? entry : quote + entry + quote select new CompletionResult(completionText, entry, CompletionResultType.ParameterValue, entry)); result.Add(CompletionResult.Null); return; } if (parameterType.Equals(typeof(SwitchParameter))) { RemoveLastNullCompletionResult(result); if (context.WordToComplete == string.Empty || context.WordToComplete.Equals("$", StringComparison.Ordinal)) { result.Add(new CompletionResult("$true", "$true", CompletionResultType.ParameterValue, "$true")); result.Add(new CompletionResult("$false", "$false", CompletionResultType.ParameterValue, "$false")); } result.Add(CompletionResult.Null); return; } foreach (ValidateArgumentsAttribute att in parameter.Parameter.ValidationAttributes) { if (att is ValidateSetAttribute setAtt) { RemoveLastNullCompletionResult(result); string wordToComplete = context.WordToComplete; string quote = HandleDoubleAndSingleQuote(ref wordToComplete); var pattern = WildcardPattern.Get(wordToComplete + "*", WildcardOptions.IgnoreCase); var setList = new List(); foreach (string value in setAtt.ValidValues) { if (value == string.Empty) { continue; } if (wordToComplete.Equals(value, StringComparison.OrdinalIgnoreCase)) { string completionText = quote == string.Empty ? value : quote + value + quote; fullMatch = new CompletionResult(completionText, value, CompletionResultType.ParameterValue, value); continue; } if (pattern.IsMatch(value)) { setList.Add(value); } } if (fullMatch != null) { result.Add(fullMatch); } setList.Sort(); foreach (string entry in setList) { string realEntry = entry; string completionText = entry; if (quote == string.Empty) { if (CompletionRequiresQuotes(entry, false)) { realEntry = CodeGeneration.EscapeSingleQuotedStringContent(entry); completionText = "'" + realEntry + "'"; } } else { if (quote.Equals("'", StringComparison.OrdinalIgnoreCase)) { realEntry = CodeGeneration.EscapeSingleQuotedStringContent(entry); } completionText = quote + realEntry + quote; } result.Add(new CompletionResult(completionText, entry, CompletionResultType.ParameterValue, entry)); } result.Add(CompletionResult.Null); return; } } NativeCommandArgumentCompletion(commandName, parameter.Parameter, result, commandAst, context, boundArguments); } private static IEnumerable NativeCommandArgumentCompletion_InferTypesOfArgument( Dictionary boundArguments, CommandAst commandAst, CompletionContext context, string parameterName) { if (boundArguments == null) { yield break; } AstParameterArgumentPair astParameterArgumentPair; if (!boundArguments.TryGetValue(parameterName, out astParameterArgumentPair)) { yield break; } Ast argumentAst = null; switch (astParameterArgumentPair.ParameterArgumentType) { case AstParameterArgumentType.AstPair: { AstPair astPair = (AstPair)astParameterArgumentPair; argumentAst = astPair.Argument; } break; case AstParameterArgumentType.PipeObject: { var pipelineAst = commandAst.Parent as PipelineAst; if (pipelineAst != null) { int i; for (i = 0; i < pipelineAst.PipelineElements.Count; i++) { if (pipelineAst.PipelineElements[i] == commandAst) break; } if (i != 0) { argumentAst = pipelineAst.PipelineElements[i - 1]; } } } break; default: break; } if (argumentAst == null) { yield break; } ExpressionAst argumentExpressionAst = argumentAst as ExpressionAst; if (argumentExpressionAst == null) { CommandExpressionAst argumentCommandExpressionAst = argumentAst as CommandExpressionAst; if (argumentCommandExpressionAst != null) { argumentExpressionAst = argumentCommandExpressionAst.Expression; } } object argumentValue; if (argumentExpressionAst != null && SafeExprEvaluator.TrySafeEval(argumentExpressionAst, context.ExecutionContext, out argumentValue)) { if (argumentValue != null) { IEnumerable enumerable = LanguagePrimitives.GetEnumerable(argumentValue) ?? new object[] { argumentValue }; foreach (var element in enumerable) { if (element == null) { continue; } PSObject pso = PSObject.AsPSObject(element); if ((pso.TypeNames.Count > 0) && (!(pso.TypeNames[0].Equals(pso.BaseObject.GetType().FullName, StringComparison.OrdinalIgnoreCase)))) { yield return new PSTypeName(pso.TypeNames[0]); } if (pso.BaseObject is not PSCustomObject) { yield return new PSTypeName(pso.BaseObject.GetType()); } } yield break; } } foreach (PSTypeName typeName in AstTypeInference.InferTypeOf(argumentAst, context.TypeInferenceContext, TypeInferenceRuntimePermissions.AllowSafeEval)) { yield return typeName; } } internal static IList NativeCommandArgumentCompletion_ExtractSecondaryArgument( Dictionary boundArguments, string parameterName) { List result = new List(); if (boundArguments == null) { return result; } AstParameterArgumentPair argumentValue; if (!boundArguments.TryGetValue(parameterName, out argumentValue)) { return result; } switch (argumentValue.ParameterArgumentType) { case AstParameterArgumentType.AstPair: { var value = (AstPair)argumentValue; if (value.Argument is StringConstantExpressionAst) { var argument = (StringConstantExpressionAst)value.Argument; result.Add(argument.Value); } else if (value.Argument is ArrayLiteralAst) { var argument = (ArrayLiteralAst)value.Argument; foreach (ExpressionAst entry in argument.Elements) { var entryAsString = entry as StringConstantExpressionAst; if (entryAsString != null) { result.Add(entryAsString.Value); } else { result.Clear(); break; } } } break; } case AstParameterArgumentType.AstArray: { var value = (AstArrayPair)argumentValue; var argument = value.Argument; foreach (ExpressionAst entry in argument) { var entryAsString = entry as StringConstantExpressionAst; if (entryAsString != null) { result.Add(entryAsString.Value); } else { result.Clear(); break; } } break; } default: break; } return result; } private static void NativeCommandArgumentCompletion( string commandName, CompiledCommandParameter parameter, List result, CommandAst commandAst, CompletionContext context, Dictionary boundArguments = null) { string parameterName = parameter.Name; // Fall back to the commandAst command name if a command name is not found. This can be caused by a script block or AST with the matching function definition being passed to CompleteInput // This allows for editors and other tools using CompleteInput with Script/AST definations to get values from RegisteredArgumentCompleters to better match the console experience. // See issue https://github.com/PowerShell/PowerShell/issues/10567 string actualCommandName = string.IsNullOrEmpty(commandName) ? commandAst.GetCommandName() : commandName; if (string.IsNullOrEmpty(actualCommandName)) { return; } string parameterFullName = $"{actualCommandName}:{parameterName}"; ScriptBlock customCompleter = GetCustomArgumentCompleter( "CustomArgumentCompleters", new[] { parameterFullName, parameterName }, context); if (customCompleter != null) { if (InvokeScriptArgumentCompleter( customCompleter, commandName, parameterName, context.WordToComplete, commandAst, context, result)) { return; } } var argumentCompleterAttribute = parameter.CompiledAttributes.OfType().FirstOrDefault(); if (argumentCompleterAttribute != null) { try { var completer = argumentCompleterAttribute.CreateArgumentCompleter(); if (completer != null) { var customResults = completer.CompleteArgument(commandName, parameterName, context.WordToComplete, commandAst, GetBoundArgumentsAsHashtable(context)); if (customResults != null) { result.AddRange(customResults); result.Add(CompletionResult.Null); return; } } else { if (InvokeScriptArgumentCompleter( argumentCompleterAttribute.ScriptBlock, commandName, parameterName, context.WordToComplete, commandAst, context, result)) { return; } } } catch (Exception) { } } var argumentCompletionsAttribute = parameter.CompiledAttributes.OfType().FirstOrDefault(); if (argumentCompletionsAttribute != null) { var customResults = argumentCompletionsAttribute.CompleteArgument(commandName, parameterName, context.WordToComplete, commandAst, GetBoundArgumentsAsHashtable(context)); if (customResults != null) { result.AddRange(customResults); result.Add(CompletionResult.Null); return; } } switch (commandName) { case "Get-Command": { if (parameterName.Equals("Module", StringComparison.OrdinalIgnoreCase)) { NativeCompletionGetCommand(context, /* moduleName: */ null, parameterName, result); break; } if (parameterName.Equals("Name", StringComparison.OrdinalIgnoreCase)) { var moduleNames = NativeCommandArgumentCompletion_ExtractSecondaryArgument(boundArguments, "Module"); if (moduleNames.Count > 0) { foreach (string module in moduleNames) { NativeCompletionGetCommand(context, module, parameterName, result); } } else { NativeCompletionGetCommand(context, /* moduleName: */ null, parameterName, result); } break; } if (parameterName.Equals("ParameterType", StringComparison.OrdinalIgnoreCase)) { NativeCompletionTypeName(context, result); break; } break; } case "Show-Command": { NativeCompletionGetHelpCommand(context, parameterName, /* isHelpRelated: */ false, result); break; } case "help": case "Get-Help": { NativeCompletionGetHelpCommand(context, parameterName, /* isHelpRelated: */ true, result); break; } case "Invoke-Expression": { if (parameterName.Equals("Command", StringComparison.OrdinalIgnoreCase)) { var commandResults = CompleteCommand(context); if (commandResults != null) result.AddRange(commandResults); } break; } case "Clear-EventLog": case "Get-EventLog": case "Limit-EventLog": case "Remove-EventLog": case "Write-EventLog": { NativeCompletionEventLogCommands(context, parameterName, result); break; } case "Get-Job": case "Receive-Job": case "Remove-Job": case "Stop-Job": case "Wait-Job": case "Suspend-Job": case "Resume-Job": { NativeCompletionJobCommands(context, parameterName, result); break; } case "Disable-ScheduledJob": case "Enable-ScheduledJob": case "Get-ScheduledJob": case "Unregister-ScheduledJob": { NativeCompletionScheduledJobCommands(context, parameterName, result); break; } case "Get-Module": { bool loadedModulesOnly = boundArguments == null || !boundArguments.ContainsKey("ListAvailable"); bool skipEditionCheck = !loadedModulesOnly && boundArguments.ContainsKey("SkipEditionCheck"); NativeCompletionModuleCommands(context, parameterName, result, loadedModulesOnly, skipEditionCheck: skipEditionCheck); break; } case "Remove-Module": { NativeCompletionModuleCommands(context, parameterName, result, loadedModulesOnly: true); break; } case "Import-Module": { bool skipEditionCheck = boundArguments != null && boundArguments.ContainsKey("SkipEditionCheck"); NativeCompletionModuleCommands(context, parameterName, result, isImportModule: true, skipEditionCheck: skipEditionCheck); break; } case "Debug-Process": case "Get-Process": case "Stop-Process": case "Wait-Process": case "Enter-PSHostProcess": { NativeCompletionProcessCommands(context, parameterName, result); break; } case "Get-PSDrive": case "Remove-PSDrive": { if (parameterName.Equals("PSProvider", StringComparison.OrdinalIgnoreCase)) { NativeCompletionProviderCommands(context, parameterName, result); } else if (parameterName.Equals("Name", StringComparison.OrdinalIgnoreCase)) { var psProviders = NativeCommandArgumentCompletion_ExtractSecondaryArgument(boundArguments, "PSProvider"); if (psProviders.Count > 0) { foreach (string psProvider in psProviders) { NativeCompletionDriveCommands(context, psProvider, parameterName, result); } } else { NativeCompletionDriveCommands(context, /* psProvider: */ null, parameterName, result); } } break; } case "New-PSDrive": { NativeCompletionProviderCommands(context, parameterName, result); break; } case "Get-PSProvider": { NativeCompletionProviderCommands(context, parameterName, result); break; } case "Get-Service": case "Start-Service": case "Restart-Service": case "Resume-Service": case "Set-Service": case "Stop-Service": case "Suspend-Service": { NativeCompletionServiceCommands(context, parameterName, result); break; } case "Clear-Variable": case "Get-Variable": case "Remove-Variable": case "Set-Variable": { NativeCompletionVariableCommands(context, parameterName, result); break; } case "Get-Alias": { NativeCompletionAliasCommands(context, parameterName, result); break; } case "Get-TraceSource": case "Set-TraceSource": case "Trace-Command": { NativeCompletionTraceSourceCommands(context, parameterName, result); break; } case "Push-Location": case "Set-Location": { NativeCompletionSetLocationCommand(context, parameterName, result); break; } case "Move-Item": case "Copy-Item": { NativeCompletionCopyMoveItemCommand(context, parameterName, result); break; } case "New-Item": { NativeCompletionNewItemCommand(context, parameterName, result); break; } case "ForEach-Object": { if (parameterName.Equals("MemberName", StringComparison.OrdinalIgnoreCase)) { NativeCompletionMemberName(context, result, commandAst); } break; } case "Group-Object": case "Measure-Object": case "Sort-Object": case "Where-Object": { if (parameterName.Equals("Property", StringComparison.OrdinalIgnoreCase)) { NativeCompletionMemberName(context, result, commandAst); } break; } case "Format-Custom": case "Format-List": case "Format-Table": case "Format-Wide": { if (parameterName.Equals("Property", StringComparison.OrdinalIgnoreCase)) { NativeCompletionMemberName(context, result, commandAst); } else if (parameterName.Equals("View", StringComparison.OrdinalIgnoreCase)) { NativeCompletionFormatViewName(context, boundArguments, result, commandAst, commandName); } break; } case "Select-Object": { if (parameterName.Equals("Property", StringComparison.OrdinalIgnoreCase) || parameterName.Equals("ExcludeProperty", StringComparison.OrdinalIgnoreCase) || parameterName.Equals("ExpandProperty", StringComparison.OrdinalIgnoreCase)) { NativeCompletionMemberName(context, result, commandAst); } break; } case "New-Object": { if (parameterName.Equals("TypeName", StringComparison.OrdinalIgnoreCase)) { NativeCompletionTypeName(context, result); } break; } case "Get-CimClass": case "Get-CimInstance": case "Get-CimAssociatedInstance": case "Invoke-CimMethod": case "New-CimInstance": case "Register-CimIndicationEvent": { NativeCompletionCimCommands(parameterName, boundArguments, result, commandAst, context); break; } default: { NativeCompletionPathArgument(context, parameterName, result); break; } } } private static Hashtable GetBoundArgumentsAsHashtable(CompletionContext context) { var result = new Hashtable(StringComparer.OrdinalIgnoreCase); if (context.PseudoBindingInfo != null) { var boundArguments = context.PseudoBindingInfo.BoundArguments; if (boundArguments != null) { foreach (var boundArgument in boundArguments) { var astPair = boundArgument.Value as AstPair; if (astPair != null) { var parameterAst = astPair.Argument as CommandParameterAst; var exprAst = parameterAst != null ? parameterAst.Argument : astPair.Argument as ExpressionAst; object value; if (exprAst != null && SafeExprEvaluator.TrySafeEval(exprAst, context.ExecutionContext, out value)) { result[boundArgument.Key] = value; } continue; } var switchPair = boundArgument.Value as SwitchPair; if (switchPair != null) { result[boundArgument.Key] = switchPair.Argument; continue; } // Ignored: // AstArrayPair - only used for ValueFromRemainingArguments, not that useful for tab completion // FakePair - missing argument, not that useful // PipeObjectPair - no actual argument, makes for a poor api } } } return result; } private static ScriptBlock GetCustomArgumentCompleter( string optionKey, IEnumerable keys, CompletionContext context) { ScriptBlock scriptBlock; var options = context.Options; if (options != null) { var customCompleters = options[optionKey] as Hashtable; if (customCompleters != null) { foreach (var key in keys) { if (customCompleters.ContainsKey(key)) { scriptBlock = customCompleters[key] as ScriptBlock; if (scriptBlock != null) return scriptBlock; } } } } var registeredCompleters = optionKey.Equals("NativeArgumentCompleters", StringComparison.OrdinalIgnoreCase) ? context.NativeArgumentCompleters : context.CustomArgumentCompleters; if (registeredCompleters != null) { foreach (var key in keys) { if (registeredCompleters.TryGetValue(key, out scriptBlock)) { return scriptBlock; } } } return null; } private static bool InvokeScriptArgumentCompleter( ScriptBlock scriptBlock, string commandName, string parameterName, string wordToComplete, CommandAst commandAst, CompletionContext context, List resultList) { bool result = InvokeScriptArgumentCompleter( scriptBlock, new object[] { commandName, parameterName, wordToComplete, commandAst, GetBoundArgumentsAsHashtable(context) }, resultList); if (result) { resultList.Add(CompletionResult.Null); } return result; } private static bool InvokeScriptArgumentCompleter( ScriptBlock scriptBlock, object[] argumentsToCompleter, List result) { Collection customResults = null; try { customResults = scriptBlock.Invoke(argumentsToCompleter); } catch (Exception) { } if (customResults == null || customResults.Count == 0) { return false; } foreach (var customResult in customResults) { var resultAsCompletion = customResult.BaseObject as CompletionResult; if (resultAsCompletion != null) { result.Add(resultAsCompletion); continue; } var resultAsString = customResult.ToString(); result.Add(new CompletionResult(resultAsString)); } return true; } // All the methods for native command argument completion will add a null instance of the type CompletionResult to the end of the // "result" list, to indicate that this particular argument completion has fallen into one of the native command argument completion methods, // and has been processed already. So if the "result" list is still empty afterward, we will not go through the default argument completion anymore. #region Native Command Argument Completion private static void RemoveLastNullCompletionResult(List result) { if (result.Count > 0 && result[result.Count - 1].Equals(CompletionResult.Null)) { result.RemoveAt(result.Count - 1); } } private static void NativeCompletionCimCommands( string parameter, Dictionary boundArguments, List result, CommandAst commandAst, CompletionContext context) { if (boundArguments != null) { AstParameterArgumentPair astParameterArgumentPair; if ((boundArguments.TryGetValue("ComputerName", out astParameterArgumentPair) || boundArguments.TryGetValue("CimSession", out astParameterArgumentPair)) && astParameterArgumentPair != null) { switch (astParameterArgumentPair.ParameterArgumentType) { case AstParameterArgumentType.PipeObject: case AstParameterArgumentType.Fake: break; default: return; // we won't tab-complete remote class names } } } if (parameter.Equals("Namespace", StringComparison.OrdinalIgnoreCase)) { NativeCompletionCimNamespace(result, context); result.Add(CompletionResult.Null); return; } string pseudoboundCimNamespace = NativeCommandArgumentCompletion_ExtractSecondaryArgument(boundArguments, "Namespace").FirstOrDefault(); if (parameter.Equals("ClassName", StringComparison.OrdinalIgnoreCase)) { NativeCompletionCimClassName(pseudoboundCimNamespace, result, context); result.Add(CompletionResult.Null); return; } bool gotInstance = false; IEnumerable cimClassTypeNames = null; string pseudoboundClassName = NativeCommandArgumentCompletion_ExtractSecondaryArgument(boundArguments, "ClassName").FirstOrDefault(); if (pseudoboundClassName != null) { gotInstance = false; var tmp = new List(); tmp.Add(new PSTypeName(typeof(CimInstance).FullName + "#" + (pseudoboundCimNamespace ?? "root/cimv2") + "/" + pseudoboundClassName)); cimClassTypeNames = tmp; } else if (boundArguments != null && boundArguments.ContainsKey("InputObject")) { gotInstance = true; cimClassTypeNames = NativeCommandArgumentCompletion_InferTypesOfArgument(boundArguments, commandAst, context, "InputObject"); } if (cimClassTypeNames != null) { foreach (PSTypeName typeName in cimClassTypeNames) { if (TypeInferenceContext.ParseCimCommandsTypeName(typeName, out pseudoboundCimNamespace, out pseudoboundClassName)) { if (parameter.Equals("ResultClassName", StringComparison.OrdinalIgnoreCase)) { NativeCompletionCimAssociationResultClassName(pseudoboundCimNamespace, pseudoboundClassName, result, context); } else if (parameter.Equals("MethodName", StringComparison.OrdinalIgnoreCase)) { NativeCompletionCimMethodName(pseudoboundCimNamespace, pseudoboundClassName, !gotInstance, result, context); } } } result.Add(CompletionResult.Null); } } private static readonly ConcurrentDictionary> s_cimNamespaceAndClassNameToAssociationResultClassNames = new ConcurrentDictionary>(StringComparer.OrdinalIgnoreCase); private static IEnumerable NativeCompletionCimAssociationResultClassName_GetResultClassNames( string cimNamespaceOfSource, string cimClassNameOfSource) { StringBuilder safeClassName = new StringBuilder(); foreach (char c in cimClassNameOfSource) { if (char.IsLetterOrDigit(c) || c == '_') { safeClassName.Append(c); } } List resultClassNames = new List(); using (var cimSession = CimSession.Create(null)) { CimClass cimClass = cimSession.GetClass(cimNamespaceOfSource ?? "root/cimv2", cimClassNameOfSource); while (cimClass != null) { string query = string.Format( CultureInfo.InvariantCulture, "associators of {{{0}}} WHERE SchemaOnly", cimClass.CimSystemProperties.ClassName); resultClassNames.AddRange( cimSession.QueryInstances(cimNamespaceOfSource ?? "root/cimv2", "WQL", query) .Select(static associationInstance => associationInstance.CimSystemProperties.ClassName)); cimClass = cimClass.CimSuperClass; } } resultClassNames.Sort(StringComparer.OrdinalIgnoreCase); return resultClassNames.Distinct(StringComparer.OrdinalIgnoreCase).ToList(); } private static void NativeCompletionCimAssociationResultClassName( string pseudoboundNamespace, string pseudoboundClassName, List result, CompletionContext context) { if (string.IsNullOrWhiteSpace(pseudoboundClassName)) { return; } IEnumerable resultClassNames = s_cimNamespaceAndClassNameToAssociationResultClassNames.GetOrAdd( (pseudoboundNamespace ?? "root/cimv2") + ":" + pseudoboundClassName, _ => NativeCompletionCimAssociationResultClassName_GetResultClassNames(pseudoboundNamespace, pseudoboundClassName)); WildcardPattern resultClassNamePattern = WildcardPattern.Get(context.WordToComplete + "*", WildcardOptions.IgnoreCase | WildcardOptions.CultureInvariant); result.AddRange(resultClassNames .Where(resultClassNamePattern.IsMatch) .Select(x => new CompletionResult(x, x, CompletionResultType.Type, string.Format(CultureInfo.InvariantCulture, "{0} -> {1}", pseudoboundClassName, x)))); } private static void NativeCompletionCimMethodName( string pseudoboundNamespace, string pseudoboundClassName, bool staticMethod, List result, CompletionContext context) { if (string.IsNullOrWhiteSpace(pseudoboundClassName)) { return; } CimClass cimClass; using (var cimSession = CimSession.Create(null)) { cimClass = cimSession.GetClass(pseudoboundNamespace ?? "root/cimv2", pseudoboundClassName); } WildcardPattern methodNamePattern = WildcardPattern.Get(context.WordToComplete + "*", WildcardOptions.CultureInvariant | WildcardOptions.IgnoreCase); List localResults = new List(); foreach (CimMethodDeclaration methodDeclaration in cimClass.CimClassMethods) { string methodName = methodDeclaration.Name; if (!methodNamePattern.IsMatch(methodName)) { continue; } bool currentMethodIsStatic = methodDeclaration.Qualifiers.Any(static q => q.Name.Equals("Static", StringComparison.OrdinalIgnoreCase)); if ((currentMethodIsStatic && !staticMethod) || (!currentMethodIsStatic && staticMethod)) { continue; } StringBuilder tooltipText = new StringBuilder(); tooltipText.Append(methodName); tooltipText.Append('('); bool gotFirstParameter = false; foreach (var methodParameter in methodDeclaration.Parameters) { bool outParameter = methodParameter.Qualifiers.Any(static q => q.Name.Equals("Out", StringComparison.OrdinalIgnoreCase)); if (!gotFirstParameter) { gotFirstParameter = true; } else { tooltipText.Append(", "); } if (outParameter) { tooltipText.Append("[out] "); } tooltipText.Append(CimInstanceAdapter.CimTypeToTypeNameDisplayString(methodParameter.CimType)); tooltipText.Append(' '); tooltipText.Append(methodParameter.Name); if (outParameter) { continue; } } tooltipText.Append(')'); localResults.Add(new CompletionResult(methodName, methodName, CompletionResultType.Method, tooltipText.ToString())); } result.AddRange(localResults.OrderBy(static x => x.ListItemText, StringComparer.OrdinalIgnoreCase)); } private static readonly ConcurrentDictionary> s_cimNamespaceToClassNames = new ConcurrentDictionary>(StringComparer.OrdinalIgnoreCase); private static IEnumerable NativeCompletionCimClassName_GetClassNames(string targetNamespace) { List result = new List(); using (CimSession cimSession = CimSession.Create(null)) { using (var operationOptions = new CimOperationOptions { ClassNamesOnly = true }) foreach (CimClass cimClass in cimSession.EnumerateClasses(targetNamespace, null, operationOptions)) using (cimClass) { string className = cimClass.CimSystemProperties.ClassName; result.Add(className); } } return result; } private static void NativeCompletionCimClassName( string pseudoBoundNamespace, List result, CompletionContext context) { string targetNamespace = pseudoBoundNamespace ?? "root/cimv2"; List regularClasses = new List(); List systemClasses = new List(); IEnumerable allClasses = s_cimNamespaceToClassNames.GetOrAdd( targetNamespace, NativeCompletionCimClassName_GetClassNames); WildcardPattern classNamePattern = WildcardPattern.Get(context.WordToComplete + "*", WildcardOptions.CultureInvariant | WildcardOptions.IgnoreCase); foreach (string className in allClasses) { if (context.Helper.CancelTabCompletion) { break; } if (!classNamePattern.IsMatch(className)) { continue; } if (className.Length > 0 && className[0] == '_') { systemClasses.Add(className); } else { regularClasses.Add(className); } } regularClasses.Sort(StringComparer.OrdinalIgnoreCase); systemClasses.Sort(StringComparer.OrdinalIgnoreCase); result.AddRange( regularClasses.Concat(systemClasses) .Select(className => new CompletionResult(className, className, CompletionResultType.Type, targetNamespace + ":" + className))); } private static void NativeCompletionCimNamespace( List result, CompletionContext context) { string containerNamespace = "root"; string prefixOfChildNamespace = string.Empty; if (!string.IsNullOrEmpty(context.WordToComplete)) { int lastSlashOrBackslash = context.WordToComplete.LastIndexOfAny(Utils.Separators.Directory); if (lastSlashOrBackslash != (-1)) { containerNamespace = context.WordToComplete.Substring(0, lastSlashOrBackslash); prefixOfChildNamespace = context.WordToComplete.Substring(lastSlashOrBackslash + 1); } } List namespaceResults = new List(); WildcardPattern childNamespacePattern = WildcardPattern.Get(prefixOfChildNamespace + "*", WildcardOptions.IgnoreCase | WildcardOptions.CultureInvariant); using (CimSession cimSession = CimSession.Create(null)) { foreach (CimInstance namespaceInstance in cimSession.EnumerateInstances(containerNamespace, "__Namespace")) using (namespaceInstance) { if (context.Helper.CancelTabCompletion) { break; } CimProperty namespaceNameProperty = namespaceInstance.CimInstanceProperties["Name"]; if (namespaceNameProperty == null) { continue; } if (!(namespaceNameProperty.Value is string childNamespace)) { continue; } if (!childNamespacePattern.IsMatch(childNamespace)) { continue; } namespaceResults.Add(new CompletionResult( containerNamespace + "/" + childNamespace, childNamespace, CompletionResultType.Namespace, containerNamespace + "/" + childNamespace)); } } result.AddRange(namespaceResults.OrderBy(static x => x.ListItemText, StringComparer.OrdinalIgnoreCase)); } private static void NativeCompletionGetCommand(CompletionContext context, string moduleName, string paramName, List result) { if (!string.IsNullOrEmpty(paramName) && paramName.Equals("Name", StringComparison.OrdinalIgnoreCase)) { RemoveLastNullCompletionResult(result); // Available commands var commandResults = CompleteCommand(context, moduleName); if (commandResults != null) result.AddRange(commandResults); // Consider files only if the -Module parameter is not present if (moduleName == null) { // ps1 files and directories. We only complete the files with .ps1 extension for Get-Command, because the -Syntax // may only works on files with .ps1 extension var ps1Extension = new HashSet(StringComparer.OrdinalIgnoreCase) { StringLiterals.PowerShellScriptFileExtension }; var moduleFilesResults = new List(CompleteFilename(context, /* containerOnly: */ false, ps1Extension)); if (moduleFilesResults.Count > 0) result.AddRange(moduleFilesResults); } result.Add(CompletionResult.Null); } else if (!string.IsNullOrEmpty(paramName) && paramName.Equals("Module", StringComparison.OrdinalIgnoreCase)) { RemoveLastNullCompletionResult(result); var modules = new HashSet(StringComparer.OrdinalIgnoreCase); var moduleResults = CompleteModuleName(context, loadedModulesOnly: true); if (moduleResults != null) { foreach (CompletionResult moduleResult in moduleResults) { if (!modules.Contains(moduleResult.ToolTip)) { modules.Add(moduleResult.ToolTip); result.Add(moduleResult); } } } moduleResults = CompleteModuleName(context, loadedModulesOnly: false); if (moduleResults != null) { foreach (CompletionResult moduleResult in moduleResults) { if (!modules.Contains(moduleResult.ToolTip)) { modules.Add(moduleResult.ToolTip); result.Add(moduleResult); } } } result.Add(CompletionResult.Null); } } private static void NativeCompletionGetHelpCommand(CompletionContext context, string paramName, bool isHelpRelated, List result) { if (!string.IsNullOrEmpty(paramName) && paramName.Equals("Name", StringComparison.OrdinalIgnoreCase)) { RemoveLastNullCompletionResult(result); // Available commands const CommandTypes commandTypes = CommandTypes.Cmdlet | CommandTypes.Function | CommandTypes.Alias | CommandTypes.ExternalScript | CommandTypes.Configuration; var commandResults = CompleteCommand(context, /* moduleName: */ null, commandTypes); if (commandResults != null) result.AddRange(commandResults); // ps1 files and directories var ps1Extension = new HashSet(StringComparer.OrdinalIgnoreCase) { StringLiterals.PowerShellScriptFileExtension }; var fileResults = new List(CompleteFilename(context, /* containerOnly: */ false, ps1Extension)); if (fileResults.Count > 0) result.AddRange(fileResults); if (isHelpRelated) { // Available topics var helpTopicResults = CompleteHelpTopics(context); if (helpTopicResults != null) result.AddRange(helpTopicResults); } result.Add(CompletionResult.Null); } } private static void NativeCompletionEventLogCommands(CompletionContext context, string paramName, List result) { if (!string.IsNullOrEmpty(paramName) && paramName.Equals("LogName", StringComparison.OrdinalIgnoreCase)) { RemoveLastNullCompletionResult(result); var logName = context.WordToComplete ?? string.Empty; var quote = HandleDoubleAndSingleQuote(ref logName); if (!logName.EndsWith('*')) { logName += "*"; } var pattern = WildcardPattern.Get(logName, WildcardOptions.IgnoreCase); var powerShellExecutionHelper = context.Helper; var powershell = powerShellExecutionHelper.AddCommandWithPreferenceSetting("Microsoft.PowerShell.Management\\Get-EventLog").AddParameter("LogName", "*"); Exception exceptionThrown; var psObjects = powerShellExecutionHelper.ExecuteCurrentPowerShell(out exceptionThrown); if (psObjects != null) { foreach (dynamic eventLog in psObjects) { var completionText = eventLog.Log.ToString(); var listItemText = completionText; if (CompletionRequiresQuotes(completionText, false)) { var quoteInUse = quote == string.Empty ? "'" : quote; if (quoteInUse == "'") completionText = completionText.Replace("'", "''"); completionText = quoteInUse + completionText + quoteInUse; } else { completionText = quote + completionText + quote; } if (pattern.IsMatch(listItemText)) { result.Add(new CompletionResult(completionText, listItemText, CompletionResultType.ParameterValue, listItemText)); } } } result.Add(CompletionResult.Null); } } private static void NativeCompletionJobCommands(CompletionContext context, string paramName, List result) { if (string.IsNullOrEmpty(paramName)) return; var wordToComplete = context.WordToComplete ?? string.Empty; var quote = HandleDoubleAndSingleQuote(ref wordToComplete); if (!wordToComplete.EndsWith('*')) { wordToComplete += "*"; } var pattern = WildcardPattern.Get(wordToComplete, WildcardOptions.IgnoreCase); var paramIsName = paramName.Equals("Name", StringComparison.OrdinalIgnoreCase); var (parameterName, value) = paramIsName ? ("Name", wordToComplete) : ("IncludeChildJob", (object)true); var powerShellExecutionHelper = context.Helper; powerShellExecutionHelper.AddCommandWithPreferenceSetting("Get-Job", typeof(GetJobCommand)).AddParameter(parameterName, value); Exception exceptionThrown; var psObjects = powerShellExecutionHelper.ExecuteCurrentPowerShell(out exceptionThrown); if (psObjects == null) return; if (paramName.Equals("Id", StringComparison.OrdinalIgnoreCase)) { RemoveLastNullCompletionResult(result); foreach (dynamic psJob in psObjects) { var completionText = psJob.Id.ToString(); if (pattern.IsMatch(completionText)) { var listItemText = completionText; completionText = quote + completionText + quote; result.Add(new CompletionResult(completionText, listItemText, CompletionResultType.ParameterValue, listItemText)); } } result.Add(CompletionResult.Null); } else if (paramName.Equals("InstanceId", StringComparison.OrdinalIgnoreCase)) { RemoveLastNullCompletionResult(result); foreach (dynamic psJob in psObjects) { var completionText = psJob.InstanceId.ToString(); if (pattern.IsMatch(completionText)) { var listItemText = completionText; completionText = quote + completionText + quote; result.Add(new CompletionResult(completionText, listItemText, CompletionResultType.ParameterValue, listItemText)); } } result.Add(CompletionResult.Null); } else if (paramIsName) { RemoveLastNullCompletionResult(result); foreach (dynamic psJob in psObjects) { var completionText = psJob.Name; var listItemText = completionText; if (CompletionRequiresQuotes(completionText, false)) { var quoteInUse = quote == string.Empty ? "'" : quote; if (quoteInUse == "'") completionText = completionText.Replace("'", "''"); completionText = quoteInUse + completionText + quoteInUse; } else { completionText = quote + completionText + quote; } result.Add(new CompletionResult(completionText, listItemText, CompletionResultType.ParameterValue, listItemText)); } result.Add(CompletionResult.Null); } } private static void NativeCompletionScheduledJobCommands(CompletionContext context, string paramName, List result) { if (string.IsNullOrEmpty(paramName)) return; var wordToComplete = context.WordToComplete ?? string.Empty; var quote = HandleDoubleAndSingleQuote(ref wordToComplete); if (!wordToComplete.EndsWith('*')) { wordToComplete += "*"; } var pattern = WildcardPattern.Get(wordToComplete, WildcardOptions.IgnoreCase); var powerShellExecutionHelper = context.Helper; if (paramName.Equals("Name", StringComparison.OrdinalIgnoreCase)) { powerShellExecutionHelper.AddCommandWithPreferenceSetting("PSScheduledJob\\Get-ScheduledJob").AddParameter("Name", wordToComplete); } else { powerShellExecutionHelper.AddCommandWithPreferenceSetting("PSScheduledJob\\Get-ScheduledJob"); } Exception exceptionThrown; var psObjects = powerShellExecutionHelper.ExecuteCurrentPowerShell(out exceptionThrown); if (psObjects == null) return; if (paramName.Equals("Id", StringComparison.OrdinalIgnoreCase)) { RemoveLastNullCompletionResult(result); foreach (dynamic psJob in psObjects) { var completionText = psJob.Id.ToString(); if (pattern.IsMatch(completionText)) { var listItemText = completionText; completionText = quote + completionText + quote; result.Add(new CompletionResult(completionText, listItemText, CompletionResultType.ParameterValue, listItemText)); } } result.Add(CompletionResult.Null); } else if (paramName.Equals("Name", StringComparison.OrdinalIgnoreCase)) { RemoveLastNullCompletionResult(result); foreach (dynamic psJob in psObjects) { var completionText = psJob.Name; var listItemText = completionText; if (CompletionRequiresQuotes(completionText, false)) { var quoteInUse = quote == string.Empty ? "'" : quote; if (quoteInUse == "'") completionText = completionText.Replace("'", "''"); completionText = quoteInUse + completionText + quoteInUse; } else { completionText = quote + completionText + quote; } result.Add(new CompletionResult(completionText, listItemText, CompletionResultType.ParameterValue, listItemText)); } result.Add(CompletionResult.Null); } } private static void NativeCompletionModuleCommands( CompletionContext context, string paramName, List result, bool loadedModulesOnly = false, bool isImportModule = false, bool skipEditionCheck = false) { if (string.IsNullOrEmpty(paramName)) { return; } if (paramName.Equals("Name", StringComparison.OrdinalIgnoreCase)) { RemoveLastNullCompletionResult(result); if (isImportModule) { var moduleExtensions = new HashSet(StringComparer.OrdinalIgnoreCase) { StringLiterals.PowerShellScriptFileExtension, StringLiterals.PowerShellModuleFileExtension, StringLiterals.PowerShellDataFileExtension, StringLiterals.PowerShellNgenAssemblyExtension, StringLiterals.PowerShellILAssemblyExtension, StringLiterals.PowerShellILExecutableExtension, StringLiterals.PowerShellCmdletizationFileExtension }; var moduleFilesResults = new List(CompleteFilename(context, containerOnly: false, moduleExtensions)); if (moduleFilesResults.Count > 0) result.AddRange(moduleFilesResults); var assemblyOrModuleName = context.WordToComplete; if (assemblyOrModuleName.IndexOfAny(Utils.Separators.DirectoryOrDrive) != -1) { // The partial input is a path, then we don't iterate modules under $ENV:PSModulePath return; } } var moduleResults = CompleteModuleName(context, loadedModulesOnly, skipEditionCheck); if (moduleResults != null && moduleResults.Count > 0) result.AddRange(moduleResults); result.Add(CompletionResult.Null); } else if (paramName.Equals("Assembly", StringComparison.OrdinalIgnoreCase)) { RemoveLastNullCompletionResult(result); var moduleExtensions = new HashSet(StringComparer.OrdinalIgnoreCase) { ".dll" }; var moduleFilesResults = new List(CompleteFilename(context, /* containerOnly: */ false, moduleExtensions)); if (moduleFilesResults.Count > 0) result.AddRange(moduleFilesResults); result.Add(CompletionResult.Null); } } private static void NativeCompletionProcessCommands(CompletionContext context, string paramName, List result) { if (string.IsNullOrEmpty(paramName)) return; var wordToComplete = context.WordToComplete ?? string.Empty; var quote = HandleDoubleAndSingleQuote(ref wordToComplete); if (!wordToComplete.EndsWith('*')) { wordToComplete += "*"; } var powerShellExecutionHelper = context.Helper; if (paramName.Equals("Id", StringComparison.OrdinalIgnoreCase)) { powerShellExecutionHelper.AddCommandWithPreferenceSetting("Microsoft.PowerShell.Management\\Get-Process"); } else { powerShellExecutionHelper.AddCommandWithPreferenceSetting("Microsoft.PowerShell.Management\\Get-Process").AddParameter("Name", wordToComplete); } Exception exceptionThrown; var psObjects = powerShellExecutionHelper.ExecuteCurrentPowerShell(out exceptionThrown); if (psObjects == null) return; if (paramName.Equals("Id", StringComparison.OrdinalIgnoreCase)) { RemoveLastNullCompletionResult(result); var pattern = WildcardPattern.Get(wordToComplete, WildcardOptions.IgnoreCase); foreach (dynamic process in psObjects) { var processId = process.Id.ToString(); if (pattern.IsMatch(processId)) { var processName = process.Name; var idAndName = $"{processId} - {processName}"; processId = quote + processId + quote; result.Add(new CompletionResult(processId, idAndName, CompletionResultType.ParameterValue, idAndName)); } } result.Add(CompletionResult.Null); } else if (paramName.Equals("Name", StringComparison.OrdinalIgnoreCase)) { RemoveLastNullCompletionResult(result); var uniqueSet = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (dynamic process in psObjects) { var completionText = process.Name; var listItemText = completionText; if (uniqueSet.Contains(completionText)) continue; uniqueSet.Add(completionText); if (CompletionRequiresQuotes(completionText, false)) { var quoteInUse = quote == string.Empty ? "'" : quote; if (quoteInUse == "'") completionText = completionText.Replace("'", "''"); completionText = quoteInUse + completionText + quoteInUse; } else { completionText = quote + completionText + quote; } // on macOS, system processes names will be empty if PowerShell isn't run as `sudo` if (string.IsNullOrEmpty(listItemText)) { continue; } result.Add(new CompletionResult(completionText, listItemText, CompletionResultType.ParameterValue, listItemText)); } result.Add(CompletionResult.Null); } } private static void NativeCompletionProviderCommands(CompletionContext context, string paramName, List result) { if (string.IsNullOrEmpty(paramName) || !paramName.Equals("PSProvider", StringComparison.OrdinalIgnoreCase)) { return; } RemoveLastNullCompletionResult(result); var providerName = context.WordToComplete ?? string.Empty; var quote = HandleDoubleAndSingleQuote(ref providerName); if (!providerName.EndsWith('*')) { providerName += "*"; } var powerShellExecutionHelper = context.Helper; powerShellExecutionHelper.AddCommandWithPreferenceSetting("Microsoft.PowerShell.Management\\Get-PSProvider").AddParameter("PSProvider", providerName); var psObjects = powerShellExecutionHelper.ExecuteCurrentPowerShell(out _); if (psObjects == null) return; foreach (dynamic providerInfo in psObjects) { var completionText = providerInfo.Name; var listItemText = completionText; if (CompletionRequiresQuotes(completionText, false)) { var quoteInUse = quote == string.Empty ? "'" : quote; if (quoteInUse == "'") completionText = completionText.Replace("'", "''"); completionText = quoteInUse + completionText + quoteInUse; } else { completionText = quote + completionText + quote; } result.Add(new CompletionResult(completionText, listItemText, CompletionResultType.ParameterValue, listItemText)); } result.Add(CompletionResult.Null); } private static void NativeCompletionDriveCommands(CompletionContext context, string psProvider, string paramName, List result) { if (string.IsNullOrEmpty(paramName) || !paramName.Equals("Name", StringComparison.OrdinalIgnoreCase)) return; RemoveLastNullCompletionResult(result); var wordToComplete = context.WordToComplete ?? string.Empty; var quote = HandleDoubleAndSingleQuote(ref wordToComplete); if (!wordToComplete.EndsWith('*')) { wordToComplete += "*"; } var powerShellExecutionHelper = context.Helper; var powershell = powerShellExecutionHelper .AddCommandWithPreferenceSetting("Microsoft.PowerShell.Management\\Get-PSDrive") .AddParameter("Name", wordToComplete); if (psProvider != null) powershell.AddParameter("PSProvider", psProvider); var psObjects = powerShellExecutionHelper.ExecuteCurrentPowerShell(out _); if (psObjects != null) { foreach (dynamic driveInfo in psObjects) { var completionText = driveInfo.Name; var listItemText = completionText; if (CompletionRequiresQuotes(completionText, false)) { var quoteInUse = quote == string.Empty ? "'" : quote; if (quoteInUse == "'") completionText = completionText.Replace("'", "''"); completionText = quoteInUse + completionText + quoteInUse; } else { completionText = quote + completionText + quote; } result.Add(new CompletionResult(completionText, listItemText, CompletionResultType.ParameterValue, listItemText)); } } result.Add(CompletionResult.Null); } private static void NativeCompletionServiceCommands(CompletionContext context, string paramName, List result) { if (string.IsNullOrEmpty(paramName)) return; var wordToComplete = context.WordToComplete ?? string.Empty; var quote = HandleDoubleAndSingleQuote(ref wordToComplete); if (!wordToComplete.EndsWith('*')) { wordToComplete += "*"; } Exception exceptionThrown; var powerShellExecutionHelper = context.Helper; if (paramName.Equals("DisplayName", StringComparison.OrdinalIgnoreCase)) { RemoveLastNullCompletionResult(result); powerShellExecutionHelper .AddCommandWithPreferenceSetting("Microsoft.PowerShell.Management\\Get-Service") .AddParameter("DisplayName", wordToComplete) .AddCommandWithPreferenceSetting("Microsoft.PowerShell.Utility\\Sort-Object") .AddParameter("Property", "DisplayName"); var psObjects = powerShellExecutionHelper.ExecuteCurrentPowerShell(out exceptionThrown); if (psObjects != null) { foreach (dynamic serviceInfo in psObjects) { var completionText = serviceInfo.DisplayName; var listItemText = completionText; if (CompletionRequiresQuotes(completionText, false)) { var quoteInUse = quote == string.Empty ? "'" : quote; if (quoteInUse == "'") completionText = completionText.Replace("'", "''"); completionText = quoteInUse + completionText + quoteInUse; } else { completionText = quote + completionText + quote; } result.Add(new CompletionResult(completionText, listItemText, CompletionResultType.ParameterValue, listItemText)); } } result.Add(CompletionResult.Null); } else if (paramName.Equals("Name", StringComparison.OrdinalIgnoreCase)) { RemoveLastNullCompletionResult(result); powerShellExecutionHelper.AddCommandWithPreferenceSetting("Microsoft.PowerShell.Management\\Get-Service").AddParameter("Name", wordToComplete); var psObjects = powerShellExecutionHelper.ExecuteCurrentPowerShell(out exceptionThrown); if (psObjects != null) { foreach (dynamic serviceInfo in psObjects) { var completionText = serviceInfo.Name; var listItemText = completionText; if (CompletionRequiresQuotes(completionText, false)) { var quoteInUse = quote == string.Empty ? "'" : quote; if (quoteInUse == "'") completionText = completionText.Replace("'", "''"); completionText = quoteInUse + completionText + quoteInUse; } else { completionText = quote + completionText + quote; } result.Add(new CompletionResult(completionText, listItemText, CompletionResultType.ParameterValue, listItemText)); } } result.Add(CompletionResult.Null); } } private static void NativeCompletionVariableCommands(CompletionContext context, string paramName, List result) { if (string.IsNullOrEmpty(paramName) || !paramName.Equals("Name", StringComparison.OrdinalIgnoreCase)) { return; } RemoveLastNullCompletionResult(result); var variableName = context.WordToComplete ?? string.Empty; var quote = HandleDoubleAndSingleQuote(ref variableName); if (!variableName.EndsWith('*')) { variableName += "*"; } var powerShellExecutionHelper = context.Helper; var powershell = powerShellExecutionHelper.AddCommandWithPreferenceSetting("Microsoft.PowerShell.Utility\\Get-Variable").AddParameter("Name", variableName); var psObjects = powerShellExecutionHelper.ExecuteCurrentPowerShell(out _); if (psObjects == null) return; foreach (dynamic variable in psObjects) { var effectiveQuote = quote; var completionText = variable.Name; var listItemText = completionText; // Handle special characters ? and * in variable names if (completionText.IndexOfAny(Utils.Separators.StarOrQuestion) != -1) { effectiveQuote = "'"; completionText = completionText.Replace("?", "`?"); completionText = completionText.Replace("*", "`*"); } if (!completionText.Equals("$", StringComparison.Ordinal) && CompletionRequiresQuotes(completionText, false)) { var quoteInUse = effectiveQuote == string.Empty ? "'" : effectiveQuote; if (quoteInUse == "'") completionText = completionText.Replace("'", "''"); completionText = quoteInUse + completionText + quoteInUse; } else { completionText = effectiveQuote + completionText + effectiveQuote; } result.Add(new CompletionResult(completionText, listItemText, CompletionResultType.ParameterValue, listItemText)); } result.Add(CompletionResult.Null); } private static void NativeCompletionAliasCommands(CompletionContext context, string paramName, List result) { if (string.IsNullOrEmpty(paramName) || (!paramName.Equals("Definition", StringComparison.OrdinalIgnoreCase) && !paramName.Equals("Name", StringComparison.OrdinalIgnoreCase))) { return; } RemoveLastNullCompletionResult(result); var powerShellExecutionHelper = context.Helper; if (paramName.Equals("Name", StringComparison.OrdinalIgnoreCase)) { var commandName = context.WordToComplete ?? string.Empty; var quote = HandleDoubleAndSingleQuote(ref commandName); if (!commandName.EndsWith('*')) { commandName += "*"; } Exception exceptionThrown; var powershell = powerShellExecutionHelper.AddCommandWithPreferenceSetting("Microsoft.PowerShell.Utility\\Get-Alias").AddParameter("Name", commandName); var psObjects = powerShellExecutionHelper.ExecuteCurrentPowerShell(out exceptionThrown); if (psObjects != null) { foreach (dynamic aliasInfo in psObjects) { var completionText = aliasInfo.Name; var listItemText = completionText; if (CompletionRequiresQuotes(completionText, false)) { var quoteInUse = quote == string.Empty ? "'" : quote; if (quoteInUse == "'") completionText = completionText.Replace("'", "''"); completionText = quoteInUse + completionText + quoteInUse; } else { completionText = quote + completionText + quote; } result.Add(new CompletionResult(completionText, listItemText, CompletionResultType.ParameterValue, listItemText)); } } } else { // Complete for the parameter Definition // Available commands const CommandTypes commandTypes = CommandTypes.Cmdlet | CommandTypes.Function | CommandTypes.ExternalScript | CommandTypes.Configuration; var commandResults = CompleteCommand(context, /* moduleName: */ null, commandTypes); if (commandResults != null && commandResults.Count > 0) result.AddRange(commandResults); // The parameter Definition takes a file var fileResults = new List(CompleteFilename(context)); if (fileResults.Count > 0) result.AddRange(fileResults); } result.Add(CompletionResult.Null); } private static void NativeCompletionTraceSourceCommands(CompletionContext context, string paramName, List result) { if (string.IsNullOrEmpty(paramName) || !paramName.Equals("Name", StringComparison.OrdinalIgnoreCase)) { return; } RemoveLastNullCompletionResult(result); var traceSourceName = context.WordToComplete ?? string.Empty; var quote = HandleDoubleAndSingleQuote(ref traceSourceName); if (!traceSourceName.EndsWith('*')) { traceSourceName += "*"; } var powerShellExecutionHelper = context.Helper; var powershell = powerShellExecutionHelper.AddCommandWithPreferenceSetting("Microsoft.PowerShell.Utility\\Get-TraceSource").AddParameter("Name", traceSourceName); Exception exceptionThrown; var psObjects = powerShellExecutionHelper.ExecuteCurrentPowerShell(out exceptionThrown); if (psObjects == null) return; foreach (dynamic trace in psObjects) { var completionText = trace.Name; var listItemText = completionText; if (CompletionRequiresQuotes(completionText, false)) { var quoteInUse = quote == string.Empty ? "'" : quote; if (quoteInUse == "'") completionText = completionText.Replace("'", "''"); completionText = quoteInUse + completionText + quoteInUse; } else { completionText = quote + completionText + quote; } result.Add(new CompletionResult(completionText, listItemText, CompletionResultType.ParameterValue, listItemText)); } result.Add(CompletionResult.Null); } private static void NativeCompletionSetLocationCommand(CompletionContext context, string paramName, List result) { if (string.IsNullOrEmpty(paramName) || (!paramName.Equals("Path", StringComparison.OrdinalIgnoreCase) && !paramName.Equals("LiteralPath", StringComparison.OrdinalIgnoreCase))) { return; } RemoveLastNullCompletionResult(result); context.WordToComplete ??= string.Empty; var clearLiteralPath = false; if (paramName.Equals("LiteralPath", StringComparison.OrdinalIgnoreCase)) { clearLiteralPath = TurnOnLiteralPathOption(context); } try { var fileNameResults = CompleteFilename(context, containerOnly: true, extension: null); if (fileNameResults != null) result.AddRange(fileNameResults); } finally { if (clearLiteralPath) context.Options.Remove("LiteralPaths"); } result.Add(CompletionResult.Null); } /// /// Provides completion results for NewItemCommand. /// /// Completion context. /// Name of the parameter whose value needs completion. /// List of completion suggestions. private static void NativeCompletionNewItemCommand(CompletionContext context, string paramName, List result) { if (string.IsNullOrEmpty(paramName)) { return; } var executionContext = context.ExecutionContext; var boundArgs = GetBoundArgumentsAsHashtable(context); var providedPath = boundArgs["Path"] as string ?? executionContext.SessionState.Path.CurrentLocation.Path; ProviderInfo provider; executionContext.LocationGlobber.GetProviderPath(providedPath, out provider); var isFileSystem = provider != null && provider.Name.Equals(FileSystemProvider.ProviderName, StringComparison.OrdinalIgnoreCase); // AutoComplete only if filesystem provider. if (isFileSystem) { if (paramName.Equals("ItemType", StringComparison.OrdinalIgnoreCase)) { if (!string.IsNullOrEmpty(context.WordToComplete)) { WildcardPattern patternEvaluator = WildcardPattern.Get(context.WordToComplete + "*", WildcardOptions.IgnoreCase); if (patternEvaluator.IsMatch("file")) { result.Add(new CompletionResult("File")); } else if (patternEvaluator.IsMatch("directory")) { result.Add(new CompletionResult("Directory")); } else if (patternEvaluator.IsMatch("symboliclink")) { result.Add(new CompletionResult("SymbolicLink")); } else if (patternEvaluator.IsMatch("junction")) { result.Add(new CompletionResult("Junction")); } else if (patternEvaluator.IsMatch("hardlink")) { result.Add(new CompletionResult("HardLink")); } } else { result.Add(new CompletionResult("File")); result.Add(new CompletionResult("Directory")); result.Add(new CompletionResult("SymbolicLink")); result.Add(new CompletionResult("Junction")); result.Add(new CompletionResult("HardLink")); } result.Add(CompletionResult.Null); } } } private static void NativeCompletionCopyMoveItemCommand(CompletionContext context, string paramName, List result) { if (string.IsNullOrEmpty(paramName)) { return; } if (paramName.Equals("LiteralPath", StringComparison.OrdinalIgnoreCase) || paramName.Equals("Path", StringComparison.OrdinalIgnoreCase)) { NativeCompletionPathArgument(context, paramName, result); } else if (paramName.Equals("Destination", StringComparison.OrdinalIgnoreCase)) { // The parameter Destination for Move-Item and Copy-Item takes literal path RemoveLastNullCompletionResult(result); context.WordToComplete ??= string.Empty; var clearLiteralPath = TurnOnLiteralPathOption(context); try { var fileNameResults = CompleteFilename(context); if (fileNameResults != null) result.AddRange(fileNameResults); } finally { if (clearLiteralPath) context.Options.Remove("LiteralPaths"); } result.Add(CompletionResult.Null); } } private static void NativeCompletionPathArgument(CompletionContext context, string paramName, List result) { if (string.IsNullOrEmpty(paramName) || (!paramName.Equals("LiteralPath", StringComparison.OrdinalIgnoreCase) && (!paramName.Equals("Path", StringComparison.OrdinalIgnoreCase)) && (!paramName.Equals("FilePath", StringComparison.OrdinalIgnoreCase)))) { return; } RemoveLastNullCompletionResult(result); context.WordToComplete ??= string.Empty; var clearLiteralPath = false; if (paramName.Equals("LiteralPath", StringComparison.OrdinalIgnoreCase)) { clearLiteralPath = TurnOnLiteralPathOption(context); } try { var fileNameResults = CompleteFilename(context); if (fileNameResults != null) result.AddRange(fileNameResults); } finally { if (clearLiteralPath) context.Options.Remove("LiteralPaths"); } result.Add(CompletionResult.Null); } private static IEnumerable GetInferenceTypes(CompletionContext context, CommandAst commandAst) { // Command is something like where-object/foreach-object/format-list/etc. where there is a parameter that is a property name // and we want member names based on the input object, which is either the parameter InputObject, or comes from the pipeline. if (commandAst.Parent is not PipelineAst pipelineAst) { return null; } int i; for (i = 0; i < pipelineAst.PipelineElements.Count; i++) { if (pipelineAst.PipelineElements[i] == commandAst) { break; } } IEnumerable prevType = null; if (i == 0) { // based on a type of the argument which is binded to 'InputObject' parameter. AstParameterArgumentPair pair; if (!context.PseudoBindingInfo.BoundArguments.TryGetValue("InputObject", out pair) || !pair.ArgumentSpecified) { return null; } var astPair = pair as AstPair; if (astPair == null || astPair.Argument == null) { return null; } prevType = AstTypeInference.InferTypeOf(astPair.Argument, context.TypeInferenceContext, TypeInferenceRuntimePermissions.AllowSafeEval); } else { // based on OutputTypeAttribute() of the first cmdlet in pipeline. prevType = AstTypeInference.InferTypeOf(pipelineAst.PipelineElements[i - 1], context.TypeInferenceContext, TypeInferenceRuntimePermissions.AllowSafeEval); } return prevType; } private static void NativeCompletionMemberName(CompletionContext context, List result, CommandAst commandAst) { IEnumerable prevType = GetInferenceTypes(context, commandAst); if (prevType is not null) { CompleteMemberByInferredType(context.TypeInferenceContext, prevType, result, context.WordToComplete + "*", filter: IsPropertyMember, isStatic: false); } result.Add(CompletionResult.Null); } private static void NativeCompletionFormatViewName( CompletionContext context, Dictionary boundArguments, List result, CommandAst commandAst, string commandName) { IEnumerable prevType = NativeCommandArgumentCompletion_InferTypesOfArgument(boundArguments, commandAst, context, "InputObject"); if (prevType is not null) { string[] inferTypeNames = prevType.Select(t => t.Name).ToArray(); CompleteFormatViewByInferredType(context, inferTypeNames, result, commandName); } result.Add(CompletionResult.Null); } private static void NativeCompletionTypeName(CompletionContext context, List result) { var wordToComplete = context.WordToComplete; var isQuoted = wordToComplete.Length > 0 && (wordToComplete[0].IsSingleQuote() || wordToComplete[0].IsDoubleQuote()); string prefix = string.Empty; string suffix = string.Empty; if (isQuoted) { prefix = suffix = wordToComplete.Substring(0, 1); var endQuoted = (wordToComplete.Length > 1) && wordToComplete[wordToComplete.Length - 1] == wordToComplete[0]; wordToComplete = wordToComplete.Substring(1, wordToComplete.Length - (endQuoted ? 2 : 1)); } if (wordToComplete.Contains('[')) { var cursor = (InternalScriptPosition)context.CursorPosition; cursor = cursor.CloneWithNewOffset(cursor.Offset - context.TokenAtCursor.Extent.StartOffset - (isQuoted ? 1 : 0)); var fullTypeName = Parser.ScanType(wordToComplete, ignoreErrors: true); var typeNameToComplete = CompletionAnalysis.FindTypeNameToComplete(fullTypeName, cursor); if (typeNameToComplete == null) return; var openBrackets = 0; var closeBrackets = 0; foreach (char c in wordToComplete) { if (c == '[') openBrackets += 1; else if (c == ']') closeBrackets += 1; } wordToComplete = typeNameToComplete.FullName; var typeNameText = fullTypeName.Extent.Text; if (!isQuoted) { // We need to add quotes - the square bracket messes up parsing the argument prefix = suffix = "'"; } if (closeBrackets < openBrackets) { suffix = suffix.Insert(0, new string(']', (openBrackets - closeBrackets))); } if (isQuoted && closeBrackets == openBrackets) { // Already quoted, and has matching []. We can give a better Intellisense experience // if we only replace the minimum. context.ReplacementIndex = typeNameToComplete.Extent.StartOffset + context.TokenAtCursor.Extent.StartOffset + 1; context.ReplacementLength = wordToComplete.Length; prefix = suffix = string.Empty; } else { prefix += typeNameText.Substring(0, typeNameToComplete.Extent.StartOffset); suffix = suffix.Insert(0, typeNameText.Substring(typeNameToComplete.Extent.EndOffset)); } } context.WordToComplete = wordToComplete; var typeResults = CompleteType(context, prefix, suffix); if (typeResults != null) { result.AddRange(typeResults); } result.Add(CompletionResult.Null); } #endregion Native Command Argument Completion /// /// Find the positional argument at the specific position from the parsed argument list. /// /// /// /// /// /// If the command line after the [tab] will not be truncated, the return value could be non-null: Get-Cmdlet [tab] abc /// If the command line after the [tab] is truncated, the return value will always be null /// private static AstPair FindTargetPositionalArgument(Collection parsedArguments, int position, out AstPair lastPositionalArgument) { int index = 0; lastPositionalArgument = null; foreach (AstParameterArgumentPair pair in parsedArguments) { if (!pair.ParameterSpecified && index == position) return (AstPair)pair; else if (!pair.ParameterSpecified) { index++; lastPositionalArgument = (AstPair)pair; } } // Cannot find an existing positional argument at 'position' return null; } /// /// Find the location where 'tab' is typed based on the line and column. /// private static ArgumentLocation FindTargetArgumentLocation(Collection parsedArguments, Token token) { int position = 0; AstParameterArgumentPair prevArg = null; foreach (AstParameterArgumentPair pair in parsedArguments) { switch (pair.ParameterArgumentType) { case AstParameterArgumentType.AstPair: { var arg = (AstPair)pair; if (arg.ParameterSpecified) { // Named argument if (arg.Parameter.Extent.StartOffset > token.Extent.StartOffset) { // case: Get-Cmdlet -Param abc return GenerateArgumentLocation(prevArg, position); } if (!arg.ParameterContainsArgument && arg.Argument.Extent.StartOffset > token.Extent.StartOffset) { // case: Get-Cmdlet -Param abc return new ArgumentLocation() { Argument = arg, IsPositional = false, Position = -1 }; } } else { // Positional argument if (arg.Argument.Extent.StartOffset > token.Extent.StartOffset) { // case: Get-Cmdlet abc return GenerateArgumentLocation(prevArg, position); } position++; } prevArg = arg; } break; case AstParameterArgumentType.Fake: case AstParameterArgumentType.Switch: { if (pair.Parameter.Extent.StartOffset > token.Extent.StartOffset) { return GenerateArgumentLocation(prevArg, position); } prevArg = pair; } break; case AstParameterArgumentType.AstArray: case AstParameterArgumentType.PipeObject: Diagnostics.Assert(false, "parsed arguments should not contain AstArray and PipeObject"); break; } } // The 'tab' should be typed after the last argument return GenerateArgumentLocation(prevArg, position); } /// /// /// The argument that is right before the 'tab' location. /// The number of positional arguments before the 'tab' location. /// private static ArgumentLocation GenerateArgumentLocation(AstParameterArgumentPair prev, int position) { // Tab is typed before the first argument if (prev == null) { return new ArgumentLocation() { Argument = null, IsPositional = true, Position = 0 }; } switch (prev.ParameterArgumentType) { case AstParameterArgumentType.AstPair: case AstParameterArgumentType.Switch: if (!prev.ParameterSpecified) return new ArgumentLocation() { Argument = null, IsPositional = true, Position = position }; return prev.Parameter.Extent.Text.EndsWith(':') ? new ArgumentLocation() { Argument = prev, IsPositional = false, Position = -1 } : new ArgumentLocation() { Argument = null, IsPositional = true, Position = position }; case AstParameterArgumentType.Fake: return new ArgumentLocation() { Argument = prev, IsPositional = false, Position = -1 }; default: Diagnostics.Assert(false, "parsed arguments should not contain AstArray and PipeObject"); return null; } } /// /// Find the location where 'tab' is typed based on the expressionAst. /// /// /// /// private static ArgumentLocation FindTargetArgumentLocation(Collection parsedArguments, ExpressionAst expAst) { Diagnostics.Assert(expAst != null, "Caller needs to make sure expAst is not null"); int position = 0; foreach (AstParameterArgumentPair pair in parsedArguments) { switch (pair.ParameterArgumentType) { case AstParameterArgumentType.AstPair: { AstPair arg = (AstPair)pair; if (arg.ArgumentIsCommandParameterAst) continue; if (arg.ParameterContainsArgument && arg.Argument == expAst) { return new ArgumentLocation() { IsPositional = false, Position = -1, Argument = arg }; } if (arg.Argument.GetHashCode() == expAst.GetHashCode()) { return arg.ParameterSpecified ? new ArgumentLocation() { IsPositional = false, Position = -1, Argument = arg } : new ArgumentLocation() { IsPositional = true, Position = position, Argument = arg }; } if (!arg.ParameterSpecified) position++; } break; case AstParameterArgumentType.Fake: case AstParameterArgumentType.Switch: // FakePair and SwitchPair contains no ExpressionAst break; case AstParameterArgumentType.AstArray: case AstParameterArgumentType.PipeObject: Diagnostics.Assert(false, "parsed arguments should not contain AstArray and PipeObject arguments"); break; } } // We should be able to find the ExpAst from the parsed argument list, if all parameters was specified correctly. // We may try to complete something incorrect // ls -Recurse -QQQ qwe<+tab> return null; } private sealed class ArgumentLocation { internal bool IsPositional { get; set; } internal int Position { get; set; } internal AstParameterArgumentPair Argument { get; set; } } #endregion Command Arguments #region Filenames /// /// /// /// [SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly")] public static IEnumerable CompleteFilename(string fileName) { var runspace = Runspace.DefaultRunspace; if (runspace == null) { // No runspace, just return no results. return CommandCompletion.EmptyCompletionResult; } var helper = new PowerShellExecutionHelper(PowerShell.Create(RunspaceMode.CurrentRunspace)); var executionContext = helper.CurrentPowerShell.Runspace.ExecutionContext; return CompleteFilename(new CompletionContext { WordToComplete = fileName, Helper = helper, ExecutionContext = executionContext }); } internal static IEnumerable CompleteFilename(CompletionContext context) { return CompleteFilename(context, containerOnly: false, extension: null); } [SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly")] internal static IEnumerable CompleteFilename(CompletionContext context, bool containerOnly, HashSet extension) { var wordToComplete = context.WordToComplete; var quote = HandleDoubleAndSingleQuote(ref wordToComplete); var results = new List(); // First, try to match \\server\share var shareMatch = Regex.Match(wordToComplete, "^\\\\\\\\([^\\\\]+)\\\\([^\\\\]*)$"); if (shareMatch.Success) { // Only match share names, no filenames. var server = shareMatch.Groups[1].Value; var sharePattern = WildcardPattern.Get(shareMatch.Groups[2].Value + "*", WildcardOptions.IgnoreCase); var ignoreHidden = context.GetOption("IgnoreHiddenShares", @default: false); var shares = GetFileShares(server, ignoreHidden); foreach (var share in shares) { if (sharePattern.IsMatch(share)) { string shareFullPath = "\\\\" + server + "\\" + share; if (quote != string.Empty) { shareFullPath = quote + shareFullPath + quote; } results.Add(new CompletionResult(shareFullPath, shareFullPath, CompletionResultType.ProviderContainer, shareFullPath)); } } } else { // We want to prefer relative paths in a completion result unless the user has already // specified a drive or portion of the path. var executionContext = context.ExecutionContext; var defaultRelative = string.IsNullOrWhiteSpace(wordToComplete) || (wordToComplete.IndexOfAny(Utils.Separators.Directory) != 0 && !Regex.Match(wordToComplete, @"^~[\\/]+.*").Success && !executionContext.LocationGlobber.IsAbsolutePath(wordToComplete, out _)); var relativePaths = context.GetOption("RelativePaths", @default: defaultRelative); var useLiteralPath = context.GetOption("LiteralPaths", @default: false); if (useLiteralPath && LocationGlobber.StringContainsGlobCharacters(wordToComplete)) { wordToComplete = WildcardPattern.Escape(wordToComplete, Utils.Separators.StarOrQuestion); } if (!defaultRelative && wordToComplete.Length >= 2 && wordToComplete[1] == ':' && char.IsLetter(wordToComplete[0]) && executionContext != null) { // We don't actually need the drive, but the drive must be "mounted" in PowerShell before completion // can succeed. This call will mount the drive if it wasn't already. executionContext.SessionState.Drive.GetAtScope(wordToComplete.Substring(0, 1), "global"); } var powerShellExecutionHelper = context.Helper; powerShellExecutionHelper .AddCommandWithPreferenceSetting("Microsoft.PowerShell.Management\\Resolve-Path") .AddParameter("Path", wordToComplete + "*"); Exception exceptionThrown; var psobjs = powerShellExecutionHelper.ExecuteCurrentPowerShell(out exceptionThrown); if (psobjs != null) { var isFileSystem = false; var wordContainsProviderId = ProviderSpecified(wordToComplete); if (psobjs.Count > 0) { dynamic firstObj = psobjs[0]; var provider = firstObj.Provider as ProviderInfo; isFileSystem = provider != null && provider.Name.Equals(FileSystemProvider.ProviderName, StringComparison.OrdinalIgnoreCase); } else { try { ProviderInfo provider; if (defaultRelative) { provider = executionContext.EngineSessionState.CurrentDrive.Provider; } else { executionContext.LocationGlobber.GetProviderPath(wordToComplete, out provider); } isFileSystem = provider != null && provider.Name.Equals(FileSystemProvider.ProviderName, StringComparison.OrdinalIgnoreCase); } catch (Exception) { } } if (isFileSystem) { bool hiddenFilesAreHandled = false; if (psobjs.Count > 0 && !LocationGlobber.StringContainsGlobCharacters(wordToComplete)) { string leaf = null; string pathWithoutProvider = wordContainsProviderId ? wordToComplete.Substring(wordToComplete.IndexOf(':') + 2) : wordToComplete; try { leaf = Path.GetFileName(pathWithoutProvider); } catch (Exception) { } var notHiddenEntries = new HashSet(StringComparer.OrdinalIgnoreCase); string providerPath = null; foreach (dynamic entry in psobjs) { providerPath = entry.ProviderPath; if (string.IsNullOrEmpty(providerPath)) { // This is unexpected. ProviderPath should never be null or an empty string leaf = null; break; } if (!notHiddenEntries.Contains(providerPath)) { notHiddenEntries.Add(providerPath); } } if (leaf != null) { leaf += "*"; var parentPath = Path.GetDirectoryName(providerPath); // ProviderPath should be absolute path for FileSystem entries if (!string.IsNullOrEmpty(parentPath)) { string[] entries = null; try { entries = Directory.GetFileSystemEntries(parentPath, leaf, _enumerationOptions); } catch (Exception) { } if (entries != null) { hiddenFilesAreHandled = true; if (entries.Length > notHiddenEntries.Count) { // Do the iteration only if there are hidden files foreach (var entry in entries) { if (notHiddenEntries.Contains(entry)) continue; var fileInfo = new FileInfo(entry); try { if ((fileInfo.Attributes & FileAttributes.Hidden) != 0) { PSObject wrapper = PSObject.AsPSObject(entry); psobjs.Add(wrapper); } } catch { // do nothing if can't get file attributes } } } } } } } if (!hiddenFilesAreHandled) { powerShellExecutionHelper .AddCommandWithPreferenceSetting("Microsoft.PowerShell.Management\\Get-ChildItem") .AddParameter("Path", wordToComplete + "*") .AddParameter("Hidden", true); var hiddenItems = powerShellExecutionHelper.ExecuteCurrentPowerShell(out exceptionThrown); if (hiddenItems != null && hiddenItems.Count > 0) { foreach (var hiddenItem in hiddenItems) { psobjs.Add(hiddenItem); } } } } // Sorting the results by the path var sortedPsobjs = psobjs.OrderBy(static a => a, new ItemPathComparer()); foreach (PSObject psobj in sortedPsobjs) { object baseObj = PSObject.Base(psobj); string path = null, providerPath = null; // Get the path, the PSObject could be: // 1. a PathInfo object -- results of Resolve-Path // 2. a FileSystemInfo Object -- results of Get-ChildItem // 3. a string -- the path results return by the direct .NET API invocation var baseObjAsPathInfo = baseObj as PathInfo; if (baseObjAsPathInfo != null) { path = baseObjAsPathInfo.Path; providerPath = baseObjAsPathInfo.ProviderPath; } else if (baseObj is FileSystemInfo) { // The target provider is the FileSystem dynamic dirResult = psobj; providerPath = dirResult.FullName; path = wordContainsProviderId ? dirResult.PSPath : providerPath; } else { var baseObjAsString = baseObj as string; if (baseObjAsString != null) { // The target provider is the FileSystem providerPath = baseObjAsString; path = wordContainsProviderId ? FileSystemProvider.ProviderName + "::" + baseObjAsString : providerPath; } } if (path == null) continue; if (isFileSystem && providerPath == null) continue; string completionText; if (relativePaths) { try { var sessionStateInternal = executionContext.EngineSessionState; completionText = sessionStateInternal.NormalizeRelativePath(path, sessionStateInternal.CurrentLocation.ProviderPath); string parentDirectory = ".." + StringLiterals.DefaultPathSeparator; if (!completionText.StartsWith(parentDirectory, StringComparison.Ordinal)) completionText = Path.Combine(".", completionText); } catch (Exception) { // The object at the specified path is not accessable, such as c:\hiberfil.sys (for hibernation) or c:\pagefile.sys (for paging) // We ignore those files continue; } } else { completionText = path; } if (ProviderSpecified(completionText) && !wordContainsProviderId) { // Remove the provider id from the path: cd \\scratch2\scratch\dongbw var index = completionText.IndexOf(':'); completionText = completionText.Substring(index + 2); } if (CompletionRequiresQuotes(completionText, !useLiteralPath)) { var quoteInUse = quote == string.Empty ? "'" : quote; if (quoteInUse == "'") { completionText = completionText.Replace("'", "''"); } else { // When double quote is in use, we have to escape the backtip and '$' even when using literal path // Get-Content -LiteralPath ".\a``g.txt" completionText = completionText.Replace("`", "``"); completionText = completionText.Replace("$", "`$"); } if (!useLiteralPath) { if (quoteInUse == "'") { completionText = completionText.Replace("[", "`["); completionText = completionText.Replace("]", "`]"); } else { completionText = completionText.Replace("[", "``["); completionText = completionText.Replace("]", "``]"); } } completionText = quoteInUse + completionText + quoteInUse; } else if (quote != string.Empty) { completionText = quote + completionText + quote; } if (isFileSystem) { // Use .NET APIs directly to reduce the time overhead var isContainer = Directory.Exists(providerPath); if (containerOnly && !isContainer) continue; if (!containerOnly && !isContainer && !CheckFileExtension(providerPath, extension)) continue; string tooltip = providerPath, listItemText = Path.GetFileName(providerPath); results.Add(new CompletionResult(completionText, listItemText, isContainer ? CompletionResultType.ProviderContainer : CompletionResultType.ProviderItem, tooltip)); } else { powerShellExecutionHelper .AddCommandWithPreferenceSetting("Microsoft.PowerShell.Management\\Get-Item") .AddParameter("LiteralPath", path); var items = powerShellExecutionHelper.ExecuteCurrentPowerShell(out exceptionThrown); if (items != null && items.Count == 1) { dynamic item = items[0]; var isContainer = LanguagePrimitives.ConvertTo(item.PSIsContainer); if (containerOnly && !isContainer) continue; powerShellExecutionHelper .AddCommandWithPreferenceSetting("Microsoft.PowerShell.Management\\Convert-Path") .AddParameter("LiteralPath", item.PSPath); var tooltips = powerShellExecutionHelper.ExecuteCurrentPowerShell(out exceptionThrown); string tooltip = null, listItemText = item.PSChildName; if (tooltips != null && tooltips.Count == 1) { tooltip = PSObject.Base(tooltips[0]) as string; } if (string.IsNullOrEmpty(listItemText)) { // For provider items that don't have PSChildName values, such as variable::error listItemText = item.Name; } results.Add(new CompletionResult(completionText, listItemText, isContainer ? CompletionResultType.ProviderContainer : CompletionResultType.ProviderItem, tooltip ?? path)); } else { // We can get here when get-item fails, perhaps due an acl or whatever. results.Add(new CompletionResult(completionText)); } } } } } return results; } [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] private struct SHARE_INFO_1 { public string netname; public int type; public string remark; } private const int MAX_PREFERRED_LENGTH = -1; private const int NERR_Success = 0; private const int ERROR_MORE_DATA = 234; private const int STYPE_DISKTREE = 0; private const int STYPE_MASK = 0x000000FF; private static readonly System.IO.EnumerationOptions _enumerationOptions = new System.IO.EnumerationOptions { MatchCasing = MatchCasing.CaseInsensitive, AttributesToSkip = 0 // Default is to skip Hidden and System files, so we clear this to retain existing behavior }; [DllImport("Netapi32.dll", CharSet = CharSet.Unicode)] private static extern int NetShareEnum(string serverName, int level, out IntPtr bufptr, int prefMaxLen, out uint entriesRead, out uint totalEntries, ref uint resumeHandle); internal static List GetFileShares(string machine, bool ignoreHidden) { #if UNIX return new List(); #else IntPtr shBuf; uint numEntries; uint totalEntries; uint resumeHandle = 0; int result = NetShareEnum(machine, 1, out shBuf, MAX_PREFERRED_LENGTH, out numEntries, out totalEntries, ref resumeHandle); var shares = new List(); if (result == NERR_Success || result == ERROR_MORE_DATA) { for (int i = 0; i < numEntries; ++i) { IntPtr curInfoPtr = (IntPtr)((long)shBuf + (Marshal.SizeOf() * i)); SHARE_INFO_1 shareInfo = Marshal.PtrToStructure(curInfoPtr); if ((shareInfo.type & STYPE_MASK) != STYPE_DISKTREE) continue; if (ignoreHidden && shareInfo.netname.EndsWith('$')) continue; shares.Add(shareInfo.netname); } } return shares; #endif } private static bool CheckFileExtension(string path, HashSet extension) { if (extension == null || extension.Count == 0) return true; var ext = System.IO.Path.GetExtension(path); return ext == null || extension.Contains(ext); } #endregion Filenames #region Variable /// /// /// /// public static IEnumerable CompleteVariable(string variableName) { var runspace = Runspace.DefaultRunspace; if (runspace == null) { // No runspace, just return no results. return CommandCompletion.EmptyCompletionResult; } var helper = new PowerShellExecutionHelper(PowerShell.Create(RunspaceMode.CurrentRunspace)); var executionContext = helper.CurrentPowerShell.Runspace.ExecutionContext; return CompleteVariable(new CompletionContext { WordToComplete = variableName, Helper = helper, ExecutionContext = executionContext }); } private static readonly string[] s_variableScopes = new string[] { "Global:", "Local:", "Script:", "Private:" }; private static readonly char[] s_charactersRequiringQuotes = new char[] { '-', '`', '&', '@', '\'', '"', '#', '{', '}', '(', ')', '$', ',', ';', '|', '<', '>', ' ', '.', '\\', '/', '\t', '^', }; internal static List CompleteVariable(CompletionContext context) { HashSet hashedResults = new HashSet(StringComparer.OrdinalIgnoreCase); List results = new List(); var wordToComplete = context.WordToComplete; var colon = wordToComplete.IndexOf(':'); var lastAst = context.RelatedAsts?.Last(); var variableAst = lastAst as VariableExpressionAst; var prefix = variableAst != null && variableAst.Splatted ? "@" : "$"; // Look for variables in the input (e.g. parameters, etc.) before checking session state - these // variables might not exist in session state yet. var wildcardPattern = WildcardPattern.Get(wordToComplete + "*", WildcardOptions.IgnoreCase); if (lastAst != null) { Ast parent = lastAst.Parent; var findVariablesVisitor = new FindVariablesVisitor { CompletionVariableAst = lastAst }; while (parent != null) { if (parent is IParameterMetadataProvider) { findVariablesVisitor.Top = parent; parent.Visit(findVariablesVisitor); } parent = parent.Parent; } foreach (Tuple varAst in findVariablesVisitor.VariableSources) { Ast astTarget = null; string userPath = null; VariableExpressionAst variableDefinitionAst = varAst.Item2 as VariableExpressionAst; if (variableDefinitionAst != null) { userPath = varAst.Item1; astTarget = varAst.Item2.Parent; } else { CommandAst commandParameterAst = varAst.Item2 as CommandAst; if (commandParameterAst != null) { userPath = varAst.Item1; astTarget = varAst.Item2; } } if (string.IsNullOrEmpty(userPath)) { Diagnostics.Assert(false, "Found a variable source but it was an unknown AST type."); } if (wildcardPattern.IsMatch(userPath)) { var completedName = (userPath.IndexOfAny(s_charactersRequiringQuotes) == -1) ? prefix + userPath : prefix + "{" + userPath + "}"; var tooltip = userPath; var ast = astTarget; while (ast != null) { var parameterAst = ast as ParameterAst; if (parameterAst != null) { var typeConstraint = parameterAst.Attributes.OfType().FirstOrDefault(); if (typeConstraint != null) { tooltip = StringUtil.Format("{0}${1}", typeConstraint.Extent.Text, userPath); } break; } var assignmentAst = ast.Parent as AssignmentStatementAst; if (assignmentAst != null) { if (assignmentAst.Left == ast) { tooltip = ast.Extent.Text; } break; } var commandAst = ast as CommandAst; if (commandAst != null) { PSTypeName discoveredType = AstTypeInference.InferTypeOf(ast, context.TypeInferenceContext, TypeInferenceRuntimePermissions.AllowSafeEval).FirstOrDefault(); if (discoveredType != null) { tooltip = StringUtil.Format("[{0}]${1}", discoveredType.Name, userPath); } break; } ast = ast.Parent; } AddUniqueVariable(hashedResults, results, completedName, userPath, tooltip); } } } string pattern; string provider; if (colon == -1) { pattern = "variable:" + wordToComplete + "*"; provider = string.Empty; } else { provider = wordToComplete.Substring(0, colon + 1); if (s_variableScopes.Contains(provider, StringComparer.OrdinalIgnoreCase)) { pattern = string.Concat("variable:", wordToComplete.AsSpan(colon + 1), "*"); } else { pattern = wordToComplete + "*"; } } var powerShellExecutionHelper = context.Helper; powerShellExecutionHelper .AddCommandWithPreferenceSetting("Microsoft.PowerShell.Management\\Get-Item").AddParameter("Path", pattern) .AddCommandWithPreferenceSetting("Microsoft.PowerShell.Utility\\Sort-Object").AddParameter("Property", "Name"); Exception exceptionThrown; var psobjs = powerShellExecutionHelper.ExecuteCurrentPowerShell(out exceptionThrown); if (psobjs != null) { foreach (dynamic psobj in psobjs) { var name = psobj.Name as string; if (!string.IsNullOrEmpty(name)) { var tooltip = name; var variable = PSObject.Base(psobj) as PSVariable; if (variable != null) { var value = variable.Value; if (value != null) { tooltip = StringUtil.Format("[{0}]${1}", ToStringCodeMethods.Type(value.GetType(), dropNamespaces: true), name); } } var completedName = (name.IndexOfAny(s_charactersRequiringQuotes) == -1) ? prefix + provider + name : prefix + "{" + provider + name + "}"; AddUniqueVariable(hashedResults, results, completedName, name, tooltip); } } } if (colon == -1 && "env".StartsWith(wordToComplete, StringComparison.OrdinalIgnoreCase)) { powerShellExecutionHelper .AddCommandWithPreferenceSetting("Microsoft.PowerShell.Management\\Get-Item").AddParameter("Path", "env:*") .AddCommandWithPreferenceSetting("Microsoft.PowerShell.Utility\\Sort-Object").AddParameter("Property", "Key"); psobjs = powerShellExecutionHelper.ExecuteCurrentPowerShell(out exceptionThrown); if (psobjs != null) { foreach (dynamic psobj in psobjs) { var name = psobj.Name as string; if (!string.IsNullOrEmpty(name)) { name = "env:" + name; var completedName = (name.IndexOfAny(s_charactersRequiringQuotes) == -1) ? prefix + name : prefix + "{" + name + "}"; AddUniqueVariable(hashedResults, results, completedName, name, "[string]" + name); } } } } // Return variables already in session state first, because we can sometimes give better information, // like the variables type. foreach (var specialVariable in s_specialVariablesCache.Value) { if (wildcardPattern.IsMatch(specialVariable)) { var completedName = (specialVariable.IndexOfAny(s_charactersRequiringQuotes) == -1) ? prefix + specialVariable : prefix + "{" + specialVariable + "}"; AddUniqueVariable(hashedResults, results, completedName, specialVariable, specialVariable); } } if (colon == -1) { // If no drive was specified, then look for matching drives/scopes pattern = wordToComplete + "*"; powerShellExecutionHelper .AddCommandWithPreferenceSetting("Microsoft.PowerShell.Management\\Get-PSDrive").AddParameter("Name", pattern) .AddCommandWithPreferenceSetting("Microsoft.PowerShell.Utility\\Sort-Object").AddParameter("Property", "Name"); psobjs = powerShellExecutionHelper.ExecuteCurrentPowerShell(out exceptionThrown); if (psobjs != null) { foreach (var psobj in psobjs) { var driveInfo = PSObject.Base(psobj) as PSDriveInfo; if (driveInfo != null) { var name = driveInfo.Name; if (name != null && !string.IsNullOrWhiteSpace(name) && name.Length > 1) { var completedName = (name.IndexOfAny(s_charactersRequiringQuotes) == -1) ? prefix + name + ":" : prefix + "{" + name + ":}"; var tooltip = string.IsNullOrEmpty(driveInfo.Description) ? name : driveInfo.Description; AddUniqueVariable(hashedResults, results, completedName, name, tooltip); } } } } var scopePattern = WildcardPattern.Get(pattern, WildcardOptions.IgnoreCase); foreach (var scope in s_variableScopes) { if (scopePattern.IsMatch(scope)) { var completedName = (scope.IndexOfAny(s_charactersRequiringQuotes) == -1) ? prefix + scope : prefix + "{" + scope + "}"; AddUniqueVariable(hashedResults, results, completedName, scope, scope); } } } return results; } private static void AddUniqueVariable(HashSet hashedResults, List results, string completionText, string listItemText, string tooltip) { if (!hashedResults.Contains(completionText)) { hashedResults.Add(completionText); results.Add(new CompletionResult(completionText, listItemText, CompletionResultType.Variable, tooltip)); } } private sealed class FindVariablesVisitor : AstVisitor { internal Ast Top; internal Ast CompletionVariableAst; internal readonly List> VariableSources = new List>(); public override AstVisitAction VisitVariableExpression(VariableExpressionAst variableExpressionAst) { if (variableExpressionAst != CompletionVariableAst) { VariableSources.Add(new Tuple(variableExpressionAst.VariablePath.UserPath, variableExpressionAst)); } return AstVisitAction.Continue; } public override AstVisitAction VisitCommand(CommandAst commandAst) { // MSFT: 784739 Stack overflow during tab completion of pipeline variable // $null | % -pv p { $p -> In this case $p is pipelinevariable // and is used in the same command. PipelineVariables are not available // in the command they are assigned in. Hence the following code ignores // if the variable being completed is in the command extent. if ((commandAst != CompletionVariableAst) && (!CompletionVariableAst.Extent.IsWithin(commandAst.Extent))) { string[] desiredParameters = new string[] { "PV", "PipelineVariable", "OV", "OutVariable" }; StaticBindingResult bindingResult = StaticParameterBinder.BindCommand(commandAst, false, desiredParameters); if (bindingResult != null) { ParameterBindingResult parameterBindingResult; foreach (string commandVariableParameter in desiredParameters) { if (bindingResult.BoundParameters.TryGetValue(commandVariableParameter, out parameterBindingResult)) { VariableSources.Add(new Tuple((string)parameterBindingResult.ConstantValue, commandAst)); } } } } return AstVisitAction.Continue; } public override AstVisitAction VisitFunctionDefinition(FunctionDefinitionAst functionDefinitionAst) { return functionDefinitionAst != Top ? AstVisitAction.SkipChildren : AstVisitAction.Continue; } public override AstVisitAction VisitScriptBlockExpression(ScriptBlockExpressionAst scriptBlockExpressionAst) { return scriptBlockExpressionAst != Top ? AstVisitAction.SkipChildren : AstVisitAction.Continue; } public override AstVisitAction VisitScriptBlock(ScriptBlockAst scriptBlockAst) { return scriptBlockAst != Top ? AstVisitAction.SkipChildren : AstVisitAction.Continue; } } private static readonly Lazy> s_specialVariablesCache = new Lazy>(BuildSpecialVariablesCache); private static SortedSet BuildSpecialVariablesCache() { var result = new SortedSet(); foreach (var member in typeof(SpecialVariables).GetFields(BindingFlags.NonPublic | BindingFlags.Static)) { if (member.FieldType.Equals(typeof(string))) { result.Add((string)member.GetValue(null)); } } return result; } #endregion Variables #region Comments internal static List CompleteComment(CompletionContext context, ref int replacementIndex, ref int replacementLength) { if (context.WordToComplete.StartsWith("<#", StringComparison.Ordinal)) { return CompleteCommentHelp(context, ref replacementIndex, ref replacementLength); } // Complete #requires statements if (context.WordToComplete.StartsWith("#requires ", StringComparison.OrdinalIgnoreCase)) { return CompleteRequires(context, ref replacementIndex, ref replacementLength); } var results = new List(); // Complete the history entries Match matchResult = Regex.Match(context.WordToComplete, @"^#([\w\-]*)$"); if (!matchResult.Success) { return results; } string wordToComplete = matchResult.Groups[1].Value; Collection psobjs; int entryId; if (Regex.IsMatch(wordToComplete, @"^[0-9]+$") && LanguagePrimitives.TryConvertTo(wordToComplete, out entryId)) { context.Helper.AddCommandWithPreferenceSetting("Get-History", typeof(GetHistoryCommand)).AddParameter("Id", entryId); psobjs = context.Helper.ExecuteCurrentPowerShell(out _); if (psobjs != null && psobjs.Count == 1) { var historyInfo = PSObject.Base(psobjs[0]) as HistoryInfo; if (historyInfo != null) { var commandLine = historyInfo.CommandLine; if (!string.IsNullOrEmpty(commandLine)) { // var tooltip = "Id: " + historyInfo.Id + "\n" + // "ExecutionStatus: " + historyInfo.ExecutionStatus + "\n" + // "StartExecutionTime: " + historyInfo.StartExecutionTime + "\n" + // "EndExecutionTime: " + historyInfo.EndExecutionTime + "\n"; // Use the commandLine as the Tooltip in case the commandLine is multiple lines of scripts results.Add(new CompletionResult(commandLine, commandLine, CompletionResultType.History, commandLine)); } } } return results; } wordToComplete = "*" + wordToComplete + "*"; context.Helper.AddCommandWithPreferenceSetting("Get-History", typeof(GetHistoryCommand)); psobjs = context.Helper.ExecuteCurrentPowerShell(out _); var pattern = WildcardPattern.Get(wordToComplete, WildcardOptions.IgnoreCase); if (psobjs != null) { for (int index = psobjs.Count - 1; index >= 0; index--) { var psobj = psobjs[index]; if (!(PSObject.Base(psobj) is HistoryInfo historyInfo)) continue; var commandLine = historyInfo.CommandLine; if (!string.IsNullOrEmpty(commandLine) && pattern.IsMatch(commandLine)) { // var tooltip = "Id: " + historyInfo.Id + "\n" + // "ExecutionStatus: " + historyInfo.ExecutionStatus + "\n" + // "StartExecutionTime: " + historyInfo.StartExecutionTime + "\n" + // "EndExecutionTime: " + historyInfo.EndExecutionTime + "\n"; // Use the commandLine as the Tooltip in case the commandLine is multiple lines of scripts results.Add(new CompletionResult(commandLine, commandLine, CompletionResultType.History, commandLine)); } } } return results; } private static List CompleteRequires(CompletionContext context, ref int replacementIndex, ref int replacementLength) { var results = new List(); int cursorIndex = context.CursorPosition.ColumnNumber - 1; string lineToCursor = context.CursorPosition.Line.Substring(0, cursorIndex); // RunAsAdministrator must be the last parameter in a Requires statement so no completion if the cursor is after the parameter. if (lineToCursor.Contains(" -RunAsAdministrator", StringComparison.OrdinalIgnoreCase)) { return results; } // Regex to find parameter like " -Parameter1" or " -" MatchCollection hashtableKeyMatches = Regex.Matches(lineToCursor, @"\s+-([A-Za-z]+|$)"); if (hashtableKeyMatches.Count == 0) { return results; } Group currentParameterMatch = hashtableKeyMatches[^1].Groups[1]; // Complete the parameter if the cursor is at a parameter if (currentParameterMatch.Index + currentParameterMatch.Length == cursorIndex) { string currentParameterPrefix = currentParameterMatch.Value; replacementIndex = context.CursorPosition.Offset - currentParameterPrefix.Length; replacementLength = currentParameterPrefix.Length; // Produce completions for all parameters that begin with the prefix we've found, // but which haven't already been specified in the line we need to complete foreach (KeyValuePair parameter in s_requiresParameters) { if (parameter.Key.StartsWith(currentParameterPrefix, StringComparison.OrdinalIgnoreCase) && !context.CursorPosition.Line.Contains($" -{parameter.Key}", StringComparison.OrdinalIgnoreCase)) { results.Add(new CompletionResult(parameter.Key, parameter.Key, CompletionResultType.ParameterName, parameter.Value)); } } return results; } // Regex to find parameter values (any text that appears after various delimiters) hashtableKeyMatches = Regex.Matches(lineToCursor, @"(\s+|,|;|{|\""|'|=)(\w+|$)"); string currentValue; if (hashtableKeyMatches.Count == 0) { currentValue = string.Empty; } else { currentValue = hashtableKeyMatches[^1].Groups[2].Value; } replacementIndex = context.CursorPosition.Offset - currentValue.Length; replacementLength = currentValue.Length; // Complete PSEdition parameter values if (currentParameterMatch.Value.Equals("PSEdition", StringComparison.OrdinalIgnoreCase)) { foreach (KeyValuePair psEditionEntry in s_requiresPSEditions) { if (psEditionEntry.Key.StartsWith(currentValue, StringComparison.OrdinalIgnoreCase)) { results.Add(new CompletionResult(psEditionEntry.Key, psEditionEntry.Key, CompletionResultType.ParameterValue, psEditionEntry.Value)); } } return results; } // Complete Modules module specification values if (currentParameterMatch.Value.Equals("Modules", StringComparison.OrdinalIgnoreCase)) { int hashtableStart = lineToCursor.LastIndexOf("@{"); int hashtableEnd = lineToCursor.LastIndexOf('}'); bool insideHashtable = hashtableStart != -1 && (hashtableEnd == -1 || hashtableEnd < hashtableStart); // If not inside a hashtable, try to complete a module simple name if (!insideHashtable) { context.WordToComplete = currentValue; return CompleteModuleName(context, true); } string hashtableString = lineToCursor.Substring(hashtableStart); // Regex to find hashtable keys with or without quotes hashtableKeyMatches = Regex.Matches(hashtableString, @"(@{|;)\s*(?:'|\""|\w*)\w*"); // Build the list of keys we might want to complete, based on what's already been provided var moduleSpecKeysToComplete = new HashSet(s_requiresModuleSpecKeys.Keys); bool sawModuleNameLast = false; foreach (Match existingHashtableKeyMatch in hashtableKeyMatches) { string existingHashtableKey = existingHashtableKeyMatch.Value.TrimStart(s_hashtableKeyPrefixes); if (string.IsNullOrEmpty(existingHashtableKey)) { continue; } // Remove the existing key we just saw moduleSpecKeysToComplete.Remove(existingHashtableKey); // We need to remember later if we saw "ModuleName" as the last hashtable key, for completions if (sawModuleNameLast = existingHashtableKey.Equals("ModuleName", StringComparison.OrdinalIgnoreCase)) { continue; } // "RequiredVersion" is mutually exclusive with "ModuleVersion" and "MaximumVersion" if (existingHashtableKey.Equals("ModuleVersion", StringComparison.OrdinalIgnoreCase) || existingHashtableKey.Equals("MaximumVersion", StringComparison.OrdinalIgnoreCase)) { moduleSpecKeysToComplete.Remove("RequiredVersion"); continue; } if (existingHashtableKey.Equals("RequiredVersion", StringComparison.OrdinalIgnoreCase)) { moduleSpecKeysToComplete.Remove("ModuleVersion"); moduleSpecKeysToComplete.Remove("MaximumVersion"); continue; } } Group lastHashtableKeyPrefixGroup = hashtableKeyMatches[^1].Groups[0]; // If we're not completing a key for the hashtable, try to complete module names, but nothing else bool completingHashtableKey = lastHashtableKeyPrefixGroup.Index + lastHashtableKeyPrefixGroup.Length == hashtableString.Length; if (!completingHashtableKey) { if (sawModuleNameLast) { context.WordToComplete = currentValue; return CompleteModuleName(context, true); } return results; } // Now try to complete hashtable keys foreach (string moduleSpecKey in moduleSpecKeysToComplete) { if (moduleSpecKey.StartsWith(currentValue, StringComparison.OrdinalIgnoreCase)) { results.Add(new CompletionResult(moduleSpecKey, moduleSpecKey, CompletionResultType.ParameterValue, s_requiresModuleSpecKeys[moduleSpecKey])); } } } return results; } private static readonly IReadOnlyDictionary s_requiresParameters = new SortedList(StringComparer.OrdinalIgnoreCase) { { "Modules", "Specifies PowerShell modules that the script requires." }, { "PSEdition", "Specifies a PowerShell edition that the script requires." }, { "RunAsAdministrator", "Specifies that PowerShell must be running as administrator on Windows." }, { "Version", "Specifies the minimum version of PowerShell that the script requires." }, }; private static readonly IReadOnlyDictionary s_requiresPSEditions = new SortedList(StringComparer.OrdinalIgnoreCase) { { "Core", "Specifies that the script requires PowerShell Core to run." }, { "Desktop", "Specifies that the script requires Windows PowerShell to run." }, }; private static readonly IReadOnlyDictionary s_requiresModuleSpecKeys = new SortedList(StringComparer.OrdinalIgnoreCase) { { "ModuleName", "Required. Specifies the module name." }, { "GUID", "Optional. Specifies the GUID of the module." }, { "ModuleVersion", "Specifies a minimum acceptable version of the module." }, { "RequiredVersion", "Specifies an exact, required version of the module." }, { "MaximumVersion", "Specifies the maximum acceptable version of the module." }, }; private static readonly char[] s_hashtableKeyPrefixes = new[] { '@', '{', ';', '"', '\'', ' ', }; private static List CompleteCommentHelp(CompletionContext context, ref int replacementIndex, ref int replacementLength) { // Finds comment keywords like ".DESCRIPTION" MatchCollection usedKeywords = Regex.Matches(context.TokenAtCursor.Text, @"(?<=^\s*\.)\w*", RegexOptions.Multiline); if (usedKeywords.Count == 0) { return null; } // Last keyword at or before the cursor Match lineKeyword = null; for (int i = usedKeywords.Count - 1; i >= 0; i--) { Match keyword = usedKeywords[i]; if (context.CursorPosition.Offset >= keyword.Index + context.TokenAtCursor.Extent.StartOffset) { lineKeyword = keyword; break; } } if (lineKeyword is null) { return null; } // Cursor is within or at the start/end of the keyword if (context.CursorPosition.Offset <= lineKeyword.Index + lineKeyword.Length + context.TokenAtCursor.Extent.StartOffset) { replacementIndex = context.TokenAtCursor.Extent.StartOffset + lineKeyword.Index; replacementLength = lineKeyword.Value.Length; var validKeywords = new HashSet(s_commentHelpKeywords.Keys, StringComparer.OrdinalIgnoreCase); foreach (Match keyword in usedKeywords) { if (keyword == lineKeyword || s_commentHelpAllowedDuplicateKeywords.Contains(keyword.Value)) { continue; } validKeywords.Remove(keyword.Value); } var result = new List(); foreach (string keyword in validKeywords) { if (keyword.StartsWith(lineKeyword.Value, StringComparison.OrdinalIgnoreCase)) { result.Add(new CompletionResult(keyword, keyword, CompletionResultType.Keyword, s_commentHelpKeywords[keyword])); } } return result.Count > 0 ? result : null; } // Finds the argument for the keyword (any characters following the keyword, ignoring leading/trailing whitespace). For example "C:\New folder" Match keywordArgument = Regex.Match(context.CursorPosition.Line, @"(?<=^\s*\.\w+\s+)\S.*(?<=\S)"); int lineStartIndex = lineKeyword.Index - context.CursorPosition.Line.IndexOf(lineKeyword.Value) + context.TokenAtCursor.Extent.StartOffset; int argumentIndex = keywordArgument.Success ? keywordArgument.Index : context.CursorPosition.ColumnNumber - 1; replacementIndex = lineStartIndex + argumentIndex; replacementLength = keywordArgument.Value.Length; if (lineKeyword.Value.Equals("PARAMETER", StringComparison.OrdinalIgnoreCase)) { return CompleteCommentParameterValue(context, keywordArgument.Value); } if (lineKeyword.Value.Equals("FORWARDHELPTARGETNAME", StringComparison.OrdinalIgnoreCase)) { var result = new List(CompleteCommand(keywordArgument.Value, "*", CommandTypes.All)); return result.Count > 0 ? result : null; } if (lineKeyword.Value.Equals("FORWARDHELPCATEGORY", StringComparison.OrdinalIgnoreCase)) { var result = new List(); foreach (string category in s_commentHelpForwardCategories) { if (category.StartsWith(keywordArgument.Value, StringComparison.OrdinalIgnoreCase)) { result.Add(new CompletionResult(category)); } } return result.Count > 0 ? result : null; } if (lineKeyword.Value.Equals("REMOTEHELPRUNSPACE", StringComparison.OrdinalIgnoreCase)) { var result = new List(); foreach (CompletionResult variable in CompleteVariable(keywordArgument.Value)) { // ListItemText is used because it excludes the "$" as expected by REMOTEHELPRUNSPACE. result.Add(new CompletionResult(variable.ListItemText, variable.ListItemText, variable.ResultType, variable.ToolTip)); } return result.Count > 0 ? result : null; } if (lineKeyword.Value.Equals("EXTERNALHELP", StringComparison.OrdinalIgnoreCase)) { context.WordToComplete = keywordArgument.Value; var result = new List(CompleteFilename(context, containerOnly: false, (new HashSet() { ".xml" }))); return result.Count > 0 ? result : null; } return null; } private static readonly IReadOnlyDictionary s_commentHelpKeywords = new SortedList(StringComparer.OrdinalIgnoreCase) { { "SYNOPSIS", "A brief description of the function or script. This keyword can be used only once in each topic." }, { "DESCRIPTION", "A detailed description of the function or script. This keyword can be used only once in each topic." }, { "PARAMETER", ".PARAMETER \nThe description of a parameter. Add a .PARAMETER keyword for each parameter in the function or script syntax." }, { "EXAMPLE", "A sample command that uses the function or script, optionally followed by sample output and a description. Repeat this keyword for each example." }, { "INPUTS", "The .NET types of objects that can be piped to the function or script. You can also include a description of the input objects." }, { "OUTPUTS", "The .NET type of the objects that the cmdlet returns. You can also include a description of the returned objects." }, { "NOTES", "Additional information about the function or script." }, { "LINK", "The name of a related topic. Repeat the .LINK keyword for each related topic. The .Link keyword content can also include a URI to an online version of the same help topic." }, { "COMPONENT", "The name of the technology or feature that the function or script uses, or to which it is related." }, { "ROLE", "The name of the user role for the help topic." }, { "FUNCTIONALITY", "The keywords that describe the intended use of the function." }, { "FORWARDHELPTARGETNAME", ".FORWARDHELPTARGETNAME \nRedirects to the help topic for the specified command." }, { "FORWARDHELPCATEGORY", ".FORWARDHELPCATEGORY \nSpecifies the help category of the item in .ForwardHelpTargetName" }, { "REMOTEHELPRUNSPACE", ".REMOTEHELPRUNSPACE \nSpecifies a session that contains the help topic. Enter a variable that contains a PSSession object." }, { "EXTERNALHELP", ".EXTERNALHELP \nThe .ExternalHelp keyword is required when a function or script is documented in XML files." } }; private static readonly HashSet s_commentHelpAllowedDuplicateKeywords = new(StringComparer.OrdinalIgnoreCase) { "PARAMETER", "EXAMPLE", "LINK" }; private static readonly string[] s_commentHelpForwardCategories = new string[] { "Alias", "Cmdlet", "HelpFile", "Function", "Provider", "General", "FAQ", "Glossary", "ScriptCommand", "ExternalScript", "Filter", "All" }; private static FunctionDefinitionAst GetCommentHelpFunctionTarget(CompletionContext context) { if (context.TokenAtCursor.Kind != TokenKind.Comment) { return null; } Ast lastAst = context.RelatedAsts[^1]; Ast firstAstAfterComment = lastAst.Find(ast => ast.Extent.StartOffset >= context.TokenAtCursor.Extent.EndOffset, searchNestedScriptBlocks: false); // Comment-based help can apply to a following function definition if it starts within 2 lines int commentEndLine = context.TokenAtCursor.Extent.EndLineNumber + 2; if (lastAst is NamedBlockAst) { // Helpblock before function inside advanced function if (firstAstAfterComment is not null && firstAstAfterComment.Extent.StartLineNumber <= commentEndLine && firstAstAfterComment is FunctionDefinitionAst outerHelpFunctionDefAst) { return outerHelpFunctionDefAst; } // Helpblock inside function if (lastAst.Parent.Parent is FunctionDefinitionAst innerHelpFunctionDefAst) { return innerHelpFunctionDefAst; } } if (lastAst is ScriptBlockAst) { // Helpblock before function if (firstAstAfterComment is not null && firstAstAfterComment.Extent.StartLineNumber <= commentEndLine && firstAstAfterComment is NamedBlockAst block && block.Statements.Count > 0 && block.Statements[0] is FunctionDefinitionAst statement) { return statement; } // Advanced function with help inside if (lastAst.Parent is FunctionDefinitionAst advFuncDefAst) { return advFuncDefAst; } } return null; } private static List CompleteCommentParameterValue(CompletionContext context, string wordToComplete) { FunctionDefinitionAst foundFunction = GetCommentHelpFunctionTarget(context); ReadOnlyCollection foundParameters = null; if (foundFunction is not null) { foundParameters = foundFunction.Parameters ?? foundFunction.Body.ParamBlock?.Parameters; } else if (context.RelatedAsts[^1] is ScriptBlockAst scriptAst) { // The helpblock is for a script file foundParameters = scriptAst.ParamBlock?.Parameters; } if (foundParameters is null || foundParameters.Count == 0) { return null; } var parametersToShow = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (ParameterAst parameter in foundParameters) { if (parameter.Name.VariablePath.UserPath.StartsWith(wordToComplete, StringComparison.OrdinalIgnoreCase)) { parametersToShow.Add(parameter.Name.VariablePath.UserPath); } } MatchCollection usedParameters = Regex.Matches(context.TokenAtCursor.Text, @"(?<=^\s*\.parameter\s+)\w.*(?<=\S)", RegexOptions.Multiline | RegexOptions.IgnoreCase); foreach (Match parameter in usedParameters) { if (wordToComplete.Equals(parameter.Value, StringComparison.OrdinalIgnoreCase)) { continue; } parametersToShow.Remove(parameter.Value); } var result = new List(); foreach (string parameter in parametersToShow) { result.Add(new CompletionResult(parameter)); } return result.Count > 0 ? result : null; } #endregion Comments #region Members // List of extension methods private static readonly List> s_extensionMethods = new List> { new Tuple("Where", "Where({ expression } [, mode [, numberToReturn]])"), new Tuple("ForEach", "ForEach(expression [, arguments...])") }; // List of DSC collection-value variables private static readonly HashSet s_dscCollectionVariables = new HashSet(StringComparer.OrdinalIgnoreCase) { "SelectedNodes", "AllNodes" }; internal static List CompleteMember(CompletionContext context, bool @static) { // If we get here, we know that either: // * the cursor appeared immediately after a member access token ('.' or '::'). // * the parent of the ast on the cursor was a member expression. // // In the first case, we have 2 possibilities: // * the last ast is an error ast because no member name was entered and we were in expression context // * the last ast is a string constant, with something like: echo $foo. var results = new List(); var lastAst = context.RelatedAsts.Last(); var lastAstAsMemberExpr = lastAst as MemberExpressionAst; Ast memberNameCandidateAst = null; ExpressionAst targetExpr = null; if (lastAstAsMemberExpr != null) { // If the cursor is not inside the member name in the member expression, assume // that the user had incomplete input, but the parser got lucky and succeeded parsing anyway. if (context.TokenAtCursor.Extent.StartOffset >= lastAstAsMemberExpr.Member.Extent.StartOffset) { memberNameCandidateAst = lastAstAsMemberExpr.Member; } targetExpr = lastAstAsMemberExpr.Expression; } else { memberNameCandidateAst = lastAst; } var memberNameAst = memberNameCandidateAst as StringConstantExpressionAst; var memberName = "*"; if (memberNameAst != null) { // Make sure to correctly handle: echo $foo. if (!memberNameAst.Value.Equals(".", StringComparison.OrdinalIgnoreCase) && !memberNameAst.Value.Equals("::", StringComparison.OrdinalIgnoreCase)) { memberName = memberNameAst.Value + "*"; } } else if (lastAst is not ErrorExpressionAst && targetExpr == null) { // I don't think we can complete anything interesting return results; } var commandAst = lastAst.Parent as CommandAst; if (commandAst != null) { int i; for (i = commandAst.CommandElements.Count - 1; i >= 0; --i) { if (commandAst.CommandElements[i] == lastAst) { break; } } var nextToLastAst = commandAst.CommandElements[i - 1]; var nextToLastExtent = nextToLastAst.Extent; var lastExtent = lastAst.Extent; if (nextToLastExtent.EndLineNumber == lastExtent.StartLineNumber && nextToLastExtent.EndColumnNumber == lastExtent.StartColumnNumber) { targetExpr = nextToLastAst as ExpressionAst; } } else if (lastAst.Parent is MemberExpressionAst) { // If 'targetExpr' has already been set, we should skip this step. This is for some member completion // cases in ISE. In ISE, we may add a new statement in the middle of existing statements as follows: // $xml = New-Object Xml // $xml. // $xml.Save("C:\data.xml") // In this example, we add $xml. between two existing statements, and the 'lastAst' in this case is // a MemberExpressionAst '$xml.$xml', whose parent is still a MemberExpressionAst '$xml.$xml.Save'. // But here we DO NOT want to re-assign 'targetExpr' to be '$xml.$xml'. 'targetExpr' in this case // should be '$xml'. if (targetExpr == null) { var memberExprAst = (MemberExpressionAst)lastAst.Parent; targetExpr = memberExprAst.Expression; } } else if (lastAst.Parent is BinaryExpressionAst && context.TokenAtCursor.Kind.Equals(TokenKind.Multiply)) { var memberExprAst = ((BinaryExpressionAst)lastAst.Parent).Left as MemberExpressionAst; if (memberExprAst != null) { targetExpr = memberExprAst.Expression; if (memberExprAst.Member is StringConstantExpressionAst) { memberName = ((StringConstantExpressionAst)memberExprAst.Member).Value + "*"; } } } if (targetExpr == null) { // Not sure what we have, but we're not looking for members. return results; } if (IsSplattedVariable(targetExpr)) { // It's splatted variable, member expansion is not useful return results; } CompleteMemberHelper(@static, memberName, targetExpr, context, results); if (results.Count == 0) { PSTypeName[] inferredTypes = null; if (@static) { var typeExpr = targetExpr as TypeExpressionAst; if (typeExpr != null) { inferredTypes = new[] { new PSTypeName(typeExpr.TypeName) }; } } else { inferredTypes = AstTypeInference.InferTypeOf(targetExpr, context.TypeInferenceContext, TypeInferenceRuntimePermissions.AllowSafeEval).ToArray(); } if (inferredTypes != null && inferredTypes.Length > 0) { // Use inferred types if we have any CompleteMemberByInferredType(context.TypeInferenceContext, inferredTypes, results, memberName, filter: null, isStatic: @static); } else { // Handle special DSC collection variables to complete the extension methods 'Where' and 'ForEach' // e.g. Configuration foo { node $AllNodes. --> $AllNodes.Where( var variableAst = targetExpr as VariableExpressionAst; var memberExprAst = targetExpr as MemberExpressionAst; bool shouldAddExtensionMethods = false; // We complete against extension methods 'Where' and 'ForEach' for the following DSC variables // $SelectedNodes, $AllNodes, $ConfigurationData.AllNodes if (variableAst != null) { // Handle $SelectedNodes and $AllNodes var variablePath = variableAst.VariablePath; if (variablePath.IsVariable && s_dscCollectionVariables.Contains(variablePath.UserPath) && IsInDscContext(variableAst)) { shouldAddExtensionMethods = true; } } else if (memberExprAst != null) { // Handle $ConfigurationData.AllNodes var member = memberExprAst.Member as StringConstantExpressionAst; if (IsConfigurationDataVariable(memberExprAst.Expression) && member != null && string.Equals("AllNodes", member.Value, StringComparison.OrdinalIgnoreCase) && IsInDscContext(memberExprAst)) { shouldAddExtensionMethods = true; } } if (shouldAddExtensionMethods) { CompleteExtensionMethods(memberName, results); } } if (results.Count == 0) { // Handle '$ConfigurationData' specially to complete 'AllNodes' for it if (IsConfigurationDataVariable(targetExpr) && IsInDscContext(targetExpr)) { var pattern = WildcardPattern.Get(memberName, WildcardOptions.IgnoreCase); if (pattern.IsMatch("AllNodes")) { results.Add(new CompletionResult("AllNodes", "AllNodes", CompletionResultType.Property, "AllNodes")); } } } } return results; } /// /// Complete members against extension methods 'Where' and 'ForEach' /// private static void CompleteExtensionMethods(string memberName, List results) { var pattern = WildcardPattern.Get(memberName, WildcardOptions.IgnoreCase); CompleteExtensionMethods(pattern, results); } /// /// Complete members against extension methods 'Where' and 'ForEach' based on the given pattern. /// private static void CompleteExtensionMethods(WildcardPattern pattern, List results) { results.AddRange(from member in s_extensionMethods where pattern.IsMatch(member.Item1) select new CompletionResult(member.Item1 + "(", member.Item1, CompletionResultType.Method, member.Item2)); } /// /// Verify if an expression Ast is representing the $ConfigurationData variable. /// private static bool IsConfigurationDataVariable(ExpressionAst targetExpr) { var variableExpr = targetExpr as VariableExpressionAst; if (variableExpr != null) { var varPath = variableExpr.VariablePath; if (varPath.IsVariable && varPath.UserPath.Equals("ConfigurationData", StringComparison.OrdinalIgnoreCase)) { return true; } } return false; } /// /// Verify if an expression Ast is within a configuration definition. /// private static bool IsInDscContext(ExpressionAst expression) { return Ast.GetAncestorAst(expression) != null; } private static void CompleteFormatViewByInferredType(CompletionContext context, string[] inferredTypeNames, List results, string commandName) { var typeInfoDB = context.TypeInferenceContext.ExecutionContext.FormatDBManager.GetTypeInfoDataBase(); if (typeInfoDB is null) { return; } Type controlBodyType = commandName switch { "Format-Table" => typeof(TableControlBody), "Format-List" => typeof(ListControlBody), "Format-Wide" => typeof(WideControlBody), "Format-Custom" => typeof(ComplexControlBody), _ => null }; Diagnostics.Assert(controlBodyType is not null, "This should never happen unless a new Format-* cmdlet is added"); var wordToComplete = context.WordToComplete; var quote = HandleDoubleAndSingleQuote(ref wordToComplete); WildcardPattern viewPattern = WildcardPattern.Get(wordToComplete + "*", WildcardOptions.IgnoreCase); var uniqueNames = new HashSet(); foreach (ViewDefinition viewDefinition in typeInfoDB.viewDefinitionsSection.viewDefinitionList) { if (viewDefinition?.appliesTo is not null && controlBodyType == viewDefinition.mainControl.GetType()) { foreach (TypeOrGroupReference applyTo in viewDefinition.appliesTo.referenceList) { foreach (string inferredTypeName in inferredTypeNames) { // We use 'StartsWith()' because 'applyTo.Name' can look like "System.Diagnostics.Process#IncludeUserName". if (applyTo.name.StartsWith(inferredTypeName, StringComparison.OrdinalIgnoreCase) && uniqueNames.Add(viewDefinition.name) && viewPattern.IsMatch(viewDefinition.name)) { string completionText = viewDefinition.name; // If the string is quoted or if it contains characters that need quoting, quote it in single quotes if (quote != string.Empty || viewDefinition.name.IndexOfAny(s_charactersRequiringQuotes) != -1) { completionText = "'" + completionText.Replace("'", "''") + "'"; } results.Add(new CompletionResult(completionText, viewDefinition.name, CompletionResultType.Text, viewDefinition.name)); } } } } } } internal static void CompleteMemberByInferredType(TypeInferenceContext context, IEnumerable inferredTypes, List results, string memberName, Func filter, bool isStatic) { bool extensionMethodsAdded = false; HashSet typeNameUsed = new HashSet(StringComparer.OrdinalIgnoreCase); WildcardPattern memberNamePattern = WildcardPattern.Get(memberName, WildcardOptions.IgnoreCase); foreach (var psTypeName in inferredTypes) { if (typeNameUsed.Contains(psTypeName.Name)) { continue; } typeNameUsed.Add(psTypeName.Name); var members = context.GetMembersByInferredType(psTypeName, isStatic, filter); foreach (var member in members) { AddInferredMember(member, memberNamePattern, results); } // Check if we need to complete against the extension methods 'Where' and 'ForEach' if (!extensionMethodsAdded && psTypeName.Type != null && IsStaticTypeEnumerable(psTypeName.Type)) { // Complete extension methods 'Where' and 'ForEach' for Enumerable types extensionMethodsAdded = true; CompleteExtensionMethods(memberNamePattern, results); } } if (results.Count > 0) { // Sort the results var powerShellExecutionHelper = context.Helper; powerShellExecutionHelper .AddCommandWithPreferenceSetting("Microsoft.PowerShell.Utility\\Sort-Object") .AddParameter("Property", new[] { "ResultType", "ListItemText" }) .AddParameter("Unique"); Exception unused; var sortedResults = powerShellExecutionHelper.ExecuteCurrentPowerShell(out unused, results); results.Clear(); results.AddRange(sortedResults.Select(static psobj => PSObject.Base(psobj) as CompletionResult)); } } private static void AddInferredMember(object member, WildcardPattern memberNamePattern, List results) { string memberName = null; bool isMethod = false; Func getToolTip = null; var propertyInfo = member as PropertyInfo; if (propertyInfo != null) { memberName = propertyInfo.Name; getToolTip = () => ToStringCodeMethods.Type(propertyInfo.PropertyType) + " " + memberName + " { " + (propertyInfo.GetGetMethod() != null ? "get; " : string.Empty) + (propertyInfo.GetSetMethod() != null ? "set; " : string.Empty) + "}"; } var fieldInfo = member as FieldInfo; if (fieldInfo != null) { memberName = fieldInfo.Name; getToolTip = () => ToStringCodeMethods.Type(fieldInfo.FieldType) + " " + memberName; } var methodCacheEntry = member as DotNetAdapter.MethodCacheEntry; if (methodCacheEntry != null) { memberName = methodCacheEntry[0].method.Name; isMethod = true; getToolTip = () => string.Join("\n", methodCacheEntry.methodInformationStructures.Select(static m => m.methodDefinition)); } var psMemberInfo = member as PSMemberInfo; if (psMemberInfo != null) { memberName = psMemberInfo.Name; isMethod = member is PSMethodInfo; getToolTip = psMemberInfo.ToString; } var cimProperty = member as CimPropertyDeclaration; if (cimProperty != null) { memberName = cimProperty.Name; isMethod = false; getToolTip = () => GetCimPropertyToString(cimProperty); } var memberAst = member as MemberAst; if (memberAst != null) { memberName = memberAst is CompilerGeneratedMemberFunctionAst ? "new" : memberAst.Name; isMethod = memberAst is FunctionMemberAst || memberAst is CompilerGeneratedMemberFunctionAst; getToolTip = memberAst.GetTooltip; } if (memberName == null || !memberNamePattern.IsMatch(memberName)) { return; } var completionResultType = isMethod ? CompletionResultType.Method : CompletionResultType.Property; var completionText = isMethod ? memberName + "(" : memberName; results.Add(new CompletionResult(completionText, memberName, completionResultType, getToolTip())); } private static string GetCimPropertyToString(CimPropertyDeclaration cimProperty) { string type; switch (cimProperty.CimType) { case Microsoft.Management.Infrastructure.CimType.DateTime: case Microsoft.Management.Infrastructure.CimType.Instance: case Microsoft.Management.Infrastructure.CimType.Reference: case Microsoft.Management.Infrastructure.CimType.DateTimeArray: case Microsoft.Management.Infrastructure.CimType.InstanceArray: case Microsoft.Management.Infrastructure.CimType.ReferenceArray: type = "CimInstance#" + cimProperty.CimType.ToString(); break; default: type = ToStringCodeMethods.Type(CimConverter.GetDotNetType(cimProperty.CimType)); break; } bool isReadOnly = ((cimProperty.Flags & CimFlags.ReadOnly) == CimFlags.ReadOnly); return type + " " + cimProperty.Name + " { get; " + (isReadOnly ? "}" : "set; }"); } private static bool IsWriteablePropertyMember(object member) { var propertyInfo = member as PropertyInfo; if (propertyInfo != null) { return propertyInfo.CanWrite; } var psPropertyInfo = member as PSPropertyInfo; if (psPropertyInfo != null) { return psPropertyInfo.IsSettable; } return false; } internal static bool IsPropertyMember(object member) { return member is PropertyInfo || member is FieldInfo || member is PSPropertyInfo || member is CimPropertyDeclaration || member is PropertyMemberAst; } private static bool IsMemberHidden(object member) { var psMemberInfo = member as PSMemberInfo; if (psMemberInfo != null) return psMemberInfo.IsHidden; var memberInfo = member as MemberInfo; if (memberInfo != null) return memberInfo.GetCustomAttributes(typeof(HiddenAttribute), false).Length > 0; var propertyMemberAst = member as PropertyMemberAst; if (propertyMemberAst != null) return propertyMemberAst.IsHidden; var functionMemberAst = member as FunctionMemberAst; if (functionMemberAst != null) return functionMemberAst.IsHidden; return false; } private static bool IsConstructor(object member) { var psMethod = member as PSMethod; if (psMethod != null) { var methodCacheEntry = psMethod.adapterData as DotNetAdapter.MethodCacheEntry; if (methodCacheEntry != null) { return methodCacheEntry.methodInformationStructures[0].method.IsConstructor; } } return false; } #endregion Members #region Types private abstract class TypeCompletionBase { internal abstract CompletionResult GetCompletionResult(string keyMatched, string prefix, string suffix); internal abstract CompletionResult GetCompletionResult(string keyMatched, string prefix, string suffix, string namespaceToRemove); internal static string RemoveBackTick(string typeName) { var backtick = typeName.LastIndexOf('`'); return backtick == -1 ? typeName : typeName.Substring(0, backtick); } } /// /// In OneCore PS, there is no way to retrieve all loaded assemblies. But we have the type catalog dictionary /// which contains the full type names of all available CoreCLR .NET types. We can extract the necessary info /// from the full type names to make type name auto-completion work. /// This type represents a non-generic type for type name completion. It only contains information that can be /// inferred from the full type name. /// private class TypeCompletionInStringFormat : TypeCompletionBase { /// /// Get the full type name of the type represented by this instance. /// internal string FullTypeName; /// /// Get the short type name of the type represented by this instance. /// internal string ShortTypeName { get { if (_shortTypeName == null) { int lastDotIndex = FullTypeName.LastIndexOf('.'); int lastPlusIndex = FullTypeName.LastIndexOf('+'); _shortTypeName = lastPlusIndex != -1 ? FullTypeName.Substring(lastPlusIndex + 1) : FullTypeName.Substring(lastDotIndex + 1); } return _shortTypeName; } } private string _shortTypeName; /// /// Get the namespace of the type represented by this instance. /// internal string Namespace { get { if (_namespace == null) { int lastDotIndex = FullTypeName.LastIndexOf('.'); _namespace = FullTypeName.Substring(0, lastDotIndex); } return _namespace; } } private string _namespace; /// /// Construct the CompletionResult based on the information of this instance. /// internal override CompletionResult GetCompletionResult(string keyMatched, string prefix, string suffix) { return GetCompletionResult(keyMatched, prefix, suffix, null); } /// /// Construct the CompletionResult based on the information of this instance. /// internal override CompletionResult GetCompletionResult(string keyMatched, string prefix, string suffix, string namespaceToRemove) { string completion = string.IsNullOrEmpty(namespaceToRemove) ? FullTypeName : FullTypeName.Substring(namespaceToRemove.Length + 1); string listItem = ShortTypeName; string tooltip = FullTypeName; return new CompletionResult(prefix + completion + suffix, listItem, CompletionResultType.Type, tooltip); } } /// /// In OneCore PS, there is no way to retrieve all loaded assemblies. But we have the type catalog dictionary /// which contains the full type names of all available CoreCLR .NET types. We can extract the necessary info /// from the full type names to make type name auto-completion work. /// This type represents a generic type for type name completion. It only contains information that can be /// inferred from the full type name. /// private sealed class GenericTypeCompletionInStringFormat : TypeCompletionInStringFormat { /// /// Get the number of generic type arguments required by the type represented by this instance. /// private int GenericArgumentCount { get { if (_genericArgumentCount == 0) { var backtick = FullTypeName.LastIndexOf('`'); var argCount = FullTypeName.Substring(backtick + 1); _genericArgumentCount = LanguagePrimitives.ConvertTo(argCount); } return _genericArgumentCount; } } private int _genericArgumentCount = 0; /// /// Construct the CompletionResult based on the information of this instance. /// internal override CompletionResult GetCompletionResult(string keyMatched, string prefix, string suffix) { return GetCompletionResult(keyMatched, prefix, suffix, null); } /// /// Construct the CompletionResult based on the information of this instance. /// internal override CompletionResult GetCompletionResult(string keyMatched, string prefix, string suffix, string namespaceToRemove) { string fullNameWithoutBacktip = RemoveBackTick(FullTypeName); string completion = string.IsNullOrEmpty(namespaceToRemove) ? fullNameWithoutBacktip : fullNameWithoutBacktip.Substring(namespaceToRemove.Length + 1); string typeName = RemoveBackTick(ShortTypeName); var listItem = typeName + "<>"; var tooltip = new StringBuilder(); tooltip.Append(fullNameWithoutBacktip); tooltip.Append('['); for (int i = 0; i < GenericArgumentCount; i++) { if (i != 0) tooltip.Append(", "); tooltip.Append(GenericArgumentCount == 1 ? "T" : string.Format(CultureInfo.InvariantCulture, "T{0}", i + 1)); } tooltip.Append(']'); return new CompletionResult(prefix + completion + suffix, listItem, CompletionResultType.Type, tooltip.ToString()); } } /// /// This type represents a non-generic type for type name completion. It contains the actual type instance. /// private class TypeCompletion : TypeCompletionBase { internal Type Type; protected string GetTooltipPrefix() { if (typeof(Delegate).IsAssignableFrom(Type)) return "Delegate "; if (Type.IsInterface) return "Interface "; if (Type.IsClass) return "Class "; if (Type.IsEnum) return "Enum "; if (typeof(ValueType).IsAssignableFrom(Type)) return "Struct "; return string.Empty; // what other interesting types are there? } internal override CompletionResult GetCompletionResult(string keyMatched, string prefix, string suffix) { return GetCompletionResult(keyMatched, prefix, suffix, null); } internal override CompletionResult GetCompletionResult(string keyMatched, string prefix, string suffix, string namespaceToRemove) { string completion = ToStringCodeMethods.Type(Type, false, keyMatched); // If the completion included a namespace and ToStringCodeMethods.Type found // an accelerator, then just use the type's FullName instead because the user // probably didn't want the accelerator. if (keyMatched.Contains('.') && !completion.Contains('.')) { completion = Type.FullName; } if (!string.IsNullOrEmpty(namespaceToRemove) && completion.Equals(Type.FullName, StringComparison.OrdinalIgnoreCase)) { // Remove the namespace only if the completion text contains namespace completion = completion.Substring(namespaceToRemove.Length + 1); } string listItem = Type.Name; string tooltip = GetTooltipPrefix() + Type.FullName; return new CompletionResult(prefix + completion + suffix, listItem, CompletionResultType.Type, tooltip); } } /// /// This type represents a generic type for type name completion. It contains the actual type instance. /// private sealed class GenericTypeCompletion : TypeCompletion { internal override CompletionResult GetCompletionResult(string keyMatched, string prefix, string suffix) { return GetCompletionResult(keyMatched, prefix, suffix, null); } internal override CompletionResult GetCompletionResult(string keyMatched, string prefix, string suffix, string namespaceToRemove) { string fullNameWithoutBacktip = RemoveBackTick(Type.FullName); string completion = string.IsNullOrEmpty(namespaceToRemove) ? fullNameWithoutBacktip : fullNameWithoutBacktip.Substring(namespaceToRemove.Length + 1); string typeName = RemoveBackTick(Type.Name); var listItem = typeName + "<>"; var tooltip = new StringBuilder(); tooltip.Append(GetTooltipPrefix()); tooltip.Append(fullNameWithoutBacktip); tooltip.Append('['); var genericParameters = Type.GetGenericArguments(); for (int i = 0; i < genericParameters.Length; i++) { if (i != 0) tooltip.Append(", "); tooltip.Append(genericParameters[i].Name); } tooltip.Append(']'); return new CompletionResult(prefix + completion + suffix, listItem, CompletionResultType.Type, tooltip.ToString()); } } /// /// This type represents a namespace for namespace completion. /// private sealed class NamespaceCompletion : TypeCompletionBase { internal string Namespace; internal override CompletionResult GetCompletionResult(string keyMatched, string prefix, string suffix) { var listItemText = Namespace; var dotIndex = listItemText.LastIndexOf('.'); if (dotIndex != -1) { listItemText = listItemText.Substring(dotIndex + 1); } return new CompletionResult(prefix + Namespace + suffix, listItemText, CompletionResultType.Namespace, "Namespace " + Namespace); } internal override CompletionResult GetCompletionResult(string keyMatched, string prefix, string suffix, string namespaceToRemove) { return GetCompletionResult(keyMatched, prefix, suffix); } } private sealed class TypeCompletionMapping { // The Key is the string we'll be searching on. It could complete to various things. internal string Key; internal List Completions = new List(); } private static TypeCompletionMapping[][] s_typeCache; private static TypeCompletionMapping[][] InitializeTypeCache() { #region Process_TypeAccelerators var entries = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var type in TypeAccelerators.Get) { TypeCompletionMapping entry; var typeCompletionInstance = new TypeCompletion { Type = type.Value }; if (entries.TryGetValue(type.Key, out entry)) { // Check if this accelerator type is already included in the mapping entry referenced by the same key. Type acceleratorType = type.Value; bool typeAlreadyIncluded = entry.Completions.Any( item => { var typeCompletion = item as TypeCompletion; return typeCompletion != null && typeCompletion.Type == acceleratorType; }); // If it's already included, skip it. // This may happen when an accelerator name is the same as the short name of the type it represents, // and aslo that type has more than one accelerator names. For example: // "float" -> System.Single // "single" -> System.Single if (typeAlreadyIncluded) { continue; } // If this accelerator type is not included in the mapping entry, add it in. // This may happen when an accelerator name happens to be the short name of a different type (rare case). entry.Completions.Add(typeCompletionInstance); } else { entries.Add(type.Key, new TypeCompletionMapping { Key = type.Key, Completions = { typeCompletionInstance } }); } // If the full type name has already been included, then we know for sure that the short type name has also been included. string fullTypeName = type.Value.FullName; if (entries.ContainsKey(fullTypeName)) { continue; } // Otherwise, add the mapping from full type name to the type entries.Add(fullTypeName, new TypeCompletionMapping { Key = fullTypeName, Completions = { typeCompletionInstance } }); // If the short type name is the same as the accelerator name, then skip it to avoid duplication. string shortTypeName = type.Value.Name; if (type.Key.Equals(shortTypeName, StringComparison.OrdinalIgnoreCase)) { continue; } // Otherwise, add a new mapping entry, or put the TypeCompletion instance in the existing mapping entry. // For example, this may happen if both System.TimeoutException and System.ServiceProcess.TimeoutException // are in the TypeAccelerator cache. if (!entries.TryGetValue(shortTypeName, out entry)) { entry = new TypeCompletionMapping { Key = shortTypeName }; entries.Add(shortTypeName, entry); } entry.Completions.Add(typeCompletionInstance); } #endregion Process_TypeAccelerators #region Process_CoreCLR_TypeCatalog // In CoreCLR, we have namespace-qualified type names of all available .NET Core types stored in TypeCatalog. // Populate the type completion cache using the namespace-qualified type names. foreach (string fullTypeName in ClrFacade.AvailableDotNetTypeNames) { var typeCompInString = new TypeCompletionInStringFormat { FullTypeName = fullTypeName }; HandleNamespace(entries, typeCompInString.Namespace); HandleType(entries, fullTypeName, typeCompInString.ShortTypeName, null); } #endregion Process_CoreCLR_TypeCatalog #region Process_LoadedAssemblies foreach (Assembly assembly in ClrFacade.GetAssemblies()) { // Ignore the assemblies that are already covered by the type catalog if (ClrFacade.AvailableDotNetAssemblyNames.Contains(assembly.FullName)) { continue; } try { foreach (Type type in assembly.GetTypes()) { // Ignore non-public types if (!TypeResolver.IsPublic(type)) { continue; } HandleNamespace(entries, type.Namespace); HandleType(entries, type.FullName, type.Name, type); } } catch (ReflectionTypeLoadException) { } } #endregion Process_LoadedAssemblies var grouping = entries.Values.GroupBy(static t => t.Key.Count(c => c == '.')).OrderBy(static g => g.Key).ToArray(); var localTypeCache = new TypeCompletionMapping[grouping.Last().Key + 1][]; foreach (var group in grouping) { localTypeCache[group.Key] = group.ToArray(); } Interlocked.Exchange(ref s_typeCache, localTypeCache); return localTypeCache; } /// /// Handle namespace when initializing the type cache. /// /// The TypeCompletionMapping dictionary. /// The namespace. private static void HandleNamespace(Dictionary entryCache, string @namespace) { if (string.IsNullOrEmpty(@namespace)) { return; } int dotIndex = 0; while (dotIndex != -1) { dotIndex = @namespace.IndexOf('.', dotIndex + 1); string subNamespace = dotIndex != -1 ? @namespace.Substring(0, dotIndex) : @namespace; TypeCompletionMapping entry; if (!entryCache.TryGetValue(subNamespace, out entry)) { entry = new TypeCompletionMapping { Key = subNamespace, Completions = { new NamespaceCompletion { Namespace = subNamespace } } }; entryCache.Add(subNamespace, entry); } else if (!entry.Completions.OfType().Any()) { entry.Completions.Add(new NamespaceCompletion { Namespace = subNamespace }); } } } /// /// Handle a type when initializing the type cache. /// /// The TypeCompletionMapping dictionary. /// The full type name. /// The short type name. /// The actual type object. It may be null if we are handling type information from the CoreCLR TypeCatalog. private static void HandleType(Dictionary entryCache, string fullTypeName, string shortTypeName, Type actualType) { if (string.IsNullOrEmpty(fullTypeName)) { return; } TypeCompletionBase typeCompletionBase = null; var backtick = fullTypeName.LastIndexOf('`'); var plusChar = fullTypeName.LastIndexOf('+'); bool isGenericTypeDefinition = backtick != -1; bool isNested = plusChar != -1; if (isGenericTypeDefinition) { // Nested generic types aren't useful for completion. if (isNested) { return; } typeCompletionBase = actualType != null ? (TypeCompletionBase)new GenericTypeCompletion { Type = actualType } : new GenericTypeCompletionInStringFormat { FullTypeName = fullTypeName }; // Remove the backtick, we only want 1 generic in our results for types like Func or Action. fullTypeName = fullTypeName.Substring(0, backtick); shortTypeName = shortTypeName.Substring(0, shortTypeName.LastIndexOf('`')); } else { typeCompletionBase = actualType != null ? (TypeCompletionBase)new TypeCompletion { Type = actualType } : new TypeCompletionInStringFormat { FullTypeName = fullTypeName }; } // If the full type name has already been included, then we know for sure that the short type // name and the accelerator type names (if there are any) have also been included. TypeCompletionMapping entry; if (!entryCache.TryGetValue(fullTypeName, out entry)) { entry = new TypeCompletionMapping { Key = fullTypeName, Completions = { typeCompletionBase } }; entryCache.Add(fullTypeName, entry); // Add a new mapping entry, or put the TypeCompletion instance in the existing mapping entry of the shortTypeName. // For example, this may happen to System.ServiceProcess.TimeoutException when System.TimeoutException is already in the cache. if (!entryCache.TryGetValue(shortTypeName, out entry)) { entry = new TypeCompletionMapping { Key = shortTypeName }; entryCache.Add(shortTypeName, entry); } entry.Completions.Add(typeCompletionBase); } } internal static List CompleteNamespace(CompletionContext context, string prefix = "", string suffix = "") { var localTypeCache = s_typeCache ?? InitializeTypeCache(); var results = new List(); var wordToComplete = context.WordToComplete; var dots = wordToComplete.Count(static c => c == '.'); if (dots >= localTypeCache.Length || localTypeCache[dots] == null) { return results; } var pattern = WildcardPattern.Get(wordToComplete + "*", WildcardOptions.IgnoreCase); foreach (var entry in localTypeCache[dots].Where(e => e.Completions.OfType().Any() && pattern.IsMatch(e.Key))) { foreach (var completion in entry.Completions) { results.Add(completion.GetCompletionResult(entry.Key, prefix, suffix)); } } results.Sort(static (c1, c2) => string.Compare(c1.ListItemText, c2.ListItemText, StringComparison.OrdinalIgnoreCase)); return results; } /// /// Complete a typename. /// /// /// public static IEnumerable CompleteType(string typeName) { // When completing types, we don't care about the runspace, types are visible across the appdomain var powershell = (Runspace.DefaultRunspace == null) ? PowerShell.Create() : PowerShell.Create(RunspaceMode.CurrentRunspace); var helper = new PowerShellExecutionHelper(powershell); var executionContext = helper.CurrentPowerShell.Runspace.ExecutionContext; return CompleteType(new CompletionContext { WordToComplete = typeName, Helper = helper, ExecutionContext = executionContext }); } internal static List CompleteType(CompletionContext context, string prefix = "", string suffix = "") { var localTypeCache = s_typeCache ?? InitializeTypeCache(); var results = new List(); var completionTextSet = new HashSet(StringComparer.OrdinalIgnoreCase); var wordToComplete = context.WordToComplete; var dots = wordToComplete.Count(static c => c == '.'); if (dots >= localTypeCache.Length || localTypeCache[dots] == null) { return results; } var pattern = WildcardPattern.Get(wordToComplete + "*", WildcardOptions.IgnoreCase); foreach (var entry in localTypeCache[dots].Where(e => pattern.IsMatch(e.Key))) { foreach (var completion in entry.Completions) { string namespaceToRemove = GetNamespaceToRemove(context, completion); var completionResult = completion.GetCompletionResult(entry.Key, prefix, suffix, namespaceToRemove); // We might get the same completion result twice. For example, the type cache has: // DscResource->System.Management.Automation.DscResourceAttribute (from accelerator) // DscResourceAttribute->System.Management.Automation.DscResourceAttribute (from short type name) // input '[DSCRes' can match both of them, but they actually resolves to the same completion text 'DscResource'. if (!completionTextSet.Contains(completionResult.CompletionText)) { results.Add(completionResult); completionTextSet.Add(completionResult.CompletionText); } } } // this is a temporary fix. Only the type defined in the same script get complete. Need to use using Module when that is available. if (context.RelatedAsts != null && context.RelatedAsts.Count > 0) { var scriptBlockAst = (ScriptBlockAst)context.RelatedAsts[0]; var typeAsts = scriptBlockAst.FindAll(static ast => ast is TypeDefinitionAst, false).Cast(); foreach (var typeAst in typeAsts.Where(ast => pattern.IsMatch(ast.Name))) { string toolTipPrefix = string.Empty; if (typeAst.IsInterface) toolTipPrefix = "Interface "; else if (typeAst.IsClass) toolTipPrefix = "Class "; else if (typeAst.IsEnum) toolTipPrefix = "Enum "; results.Add(new CompletionResult(prefix + typeAst.Name + suffix, typeAst.Name, CompletionResultType.Type, toolTipPrefix + typeAst.Name)); } } results.Sort(static (c1, c2) => string.Compare(c1.ListItemText, c2.ListItemText, StringComparison.OrdinalIgnoreCase)); return results; } private static string GetNamespaceToRemove(CompletionContext context, TypeCompletionBase completion) { if (completion is NamespaceCompletion || context.RelatedAsts == null || context.RelatedAsts.Count == 0) { return null; } var typeCompletion = completion as TypeCompletion; string typeNameSpace = typeCompletion != null ? typeCompletion.Type.Namespace : ((TypeCompletionInStringFormat)completion).Namespace; var scriptBlockAst = (ScriptBlockAst)context.RelatedAsts[0]; var matchingNsStates = scriptBlockAst.UsingStatements.Where(s => s.UsingStatementKind == UsingStatementKind.Namespace && typeNameSpace != null && typeNameSpace.StartsWith(s.Name.Value, StringComparison.OrdinalIgnoreCase)); string ns = string.Empty; foreach (var nsState in matchingNsStates) { if (nsState.Name.Extent.Text.Length > ns.Length) { ns = nsState.Name.Extent.Text; } } return ns; } #endregion Types #region Help Topics internal static List CompleteHelpTopics(CompletionContext context) { var results = new List(); string userHelpDir = HelpUtils.GetUserHomeHelpSearchPath(); string appHelpDir = Utils.GetApplicationBase(Utils.DefaultPowerShellShellID); string currentCulture = CultureInfo.CurrentCulture.Name; //search for help files for the current culture + en-US as fallback var searchPaths = new string[] { Path.Combine(userHelpDir, currentCulture), Path.Combine(appHelpDir, currentCulture), Path.Combine(userHelpDir, "en-US"), Path.Combine(appHelpDir, "en-US") }.Distinct(); string wordToComplete = context.WordToComplete + "*"; try { var wildcardPattern = WildcardPattern.Get(wordToComplete, WildcardOptions.IgnoreCase); foreach (var dir in searchPaths) { var currentDir = new DirectoryInfo(dir); if (currentDir.Exists) { foreach (var file in currentDir.EnumerateFiles("about_*.help.txt")) { if (wildcardPattern.IsMatch(file.Name)) { string topicName = file.Name.Substring(0, file.Name.LastIndexOf(".help.txt")); results.Add(new CompletionResult(topicName)); } } } } } catch (Exception) { } return results; } #endregion Help Topics #region Statement Parameters internal static List CompleteStatementFlags(TokenKind kind, string wordToComplete) { switch (kind) { case TokenKind.Switch: Diagnostics.Assert(!string.IsNullOrEmpty(wordToComplete) && wordToComplete[0].IsDash(), "the word to complete should start with '-'"); wordToComplete = wordToComplete.Substring(1); bool withColon = wordToComplete.EndsWith(':'); wordToComplete = withColon ? wordToComplete.Remove(wordToComplete.Length - 1) : wordToComplete; string enumString = LanguagePrimitives.EnumSingleTypeConverter.EnumValues(typeof(SwitchFlags)); string separator = CultureInfo.CurrentUICulture.TextInfo.ListSeparator; string[] enumArray = enumString.Split(separator, StringSplitOptions.RemoveEmptyEntries); var pattern = WildcardPattern.Get(wordToComplete + "*", WildcardOptions.IgnoreCase); var enumList = new List(); var result = new List(); CompletionResult fullMatch = null; foreach (string value in enumArray) { if (value.Equals(SwitchFlags.None.ToString(), StringComparison.OrdinalIgnoreCase)) { continue; } if (wordToComplete.Equals(value, StringComparison.OrdinalIgnoreCase)) { string completionText = withColon ? "-" + value + ":" : "-" + value; fullMatch = new CompletionResult(completionText, value, CompletionResultType.ParameterName, value); continue; } if (pattern.IsMatch(value)) { enumList.Add(value); } } if (fullMatch != null) { result.Add(fullMatch); } enumList.Sort(); result.AddRange(from entry in enumList let completionText = withColon ? "-" + entry + ":" : "-" + entry select new CompletionResult(completionText, entry, CompletionResultType.ParameterName, entry)); return result; default: break; } return null; } #endregion Statement Parameters #region Hashtable Keys /// /// Generate auto complete results for hashtable key within a Dynamickeyword. /// Results are generated based on properties of a DynamicKeyword matches given identifier. /// For example, following "D" matches "DestinationPath" /// /// Configuration /// { /// File /// { /// D^ /// } /// } /// /// /// /// /// internal static List CompleteHashtableKeyForDynamicKeyword( CompletionContext completionContext, DynamicKeywordStatementAst ast, HashtableAst hashtableAst) { Diagnostics.Assert(ast.Keyword != null, "DynamicKeywordStatementAst.Keyword can never be null"); List results = null; var dynamicKeywordProperties = ast.Keyword.Properties; var memberPattern = completionContext.WordToComplete + "*"; // // Capture all existing properties in hashtable // var propertiesName = new List(); int cursorOffset = completionContext.CursorPosition.Offset; foreach (var keyValueTuple in hashtableAst.KeyValuePairs) { var propName = keyValueTuple.Item1 as StringConstantExpressionAst; // Exclude the property name at cursor if (propName != null && propName.Extent.EndOffset != cursorOffset) { propertiesName.Add(propName.Value); } } if (dynamicKeywordProperties.Count > 0) { // Excludes existing properties in the hashtable statement var tempProperties = dynamicKeywordProperties.Where(p => !propertiesName.Contains(p.Key, StringComparer.OrdinalIgnoreCase)); if (tempProperties != null && tempProperties.Any()) { results = new List(); // Filter by name var wildcardPattern = WildcardPattern.Get(memberPattern, WildcardOptions.IgnoreCase); var matchedResults = tempProperties.Where(p => wildcardPattern.IsMatch(p.Key)); if (matchedResults == null || !matchedResults.Any()) { // Fallback to all non-exist properties in the hashtable statement matchedResults = tempProperties; } foreach (var p in matchedResults) { string psTypeName = LanguagePrimitives.ConvertTypeNameToPSTypeName(p.Value.TypeConstraint); if (psTypeName == "[]" || string.IsNullOrEmpty(psTypeName)) { psTypeName = "[" + p.Value.TypeConstraint + "]"; } if (string.Equals(psTypeName, "[MSFT_Credential]", StringComparison.OrdinalIgnoreCase)) { psTypeName = "[pscredential]"; } results.Add(new CompletionResult( p.Key + " = ", p.Key, CompletionResultType.Property, psTypeName)); } } } return results; } internal static List CompleteHashtableKey(CompletionContext completionContext, HashtableAst hashtableAst) { var typeAst = hashtableAst.Parent as ConvertExpressionAst; if (typeAst != null) { var result = new List(); CompleteMemberByInferredType( completionContext.TypeInferenceContext, AstTypeInference.InferTypeOf(typeAst, completionContext.TypeInferenceContext, TypeInferenceRuntimePermissions.AllowSafeEval), result, completionContext.WordToComplete + "*", IsWriteablePropertyMember, isStatic: false); return result; } // hashtable arguments sometimes have expected keys. Examples: // new-object System.Drawing.Point -prop @{ X=1; Y=1 } // dir | sort-object -prop @{Expression=... ; Ascending=... } // format-table -Property // Expression // FormatString // Label // Width // Alignment // format-list -Property // Expression // FormatString // Label // format-custom -Property // Expression // Depth // format-* -GroupBy // Expression // FormatString // Label // // Find out if we are in a command argument. Consider the following possibilities: // cmd @{} // cmd -foo @{} // cmd -foo:@{} // cmd @{},@{} // cmd -foo @{},@{} // cmd -foo:@{},@{} var ast = hashtableAst.Parent; // Handle completion for hashtable within DynamicKeyword statement var dynamicKeywordStatementAst = ast as DynamicKeywordStatementAst; if (dynamicKeywordStatementAst != null) { return CompleteHashtableKeyForDynamicKeyword(completionContext, dynamicKeywordStatementAst, hashtableAst); } if (ast is ArrayLiteralAst) { ast = ast.Parent; } if (ast is CommandParameterAst) { ast = ast.Parent; } var commandAst = ast as CommandAst; if (commandAst != null) { var binding = new PseudoParameterBinder().DoPseudoParameterBinding(commandAst, null, null, bindingType: PseudoParameterBinder.BindingType.ArgumentCompletion); if (binding == null) { return null; } string parameterName = null; foreach (var boundArg in binding.BoundArguments) { var astPair = boundArg.Value as AstPair; if (astPair != null) { if (astPair.Argument == hashtableAst) { parameterName = boundArg.Key; break; } continue; } var astArrayPair = boundArg.Value as AstArrayPair; if (astArrayPair != null) { if (astArrayPair.Argument.Contains(hashtableAst)) { parameterName = boundArg.Key; break; } continue; } } if (parameterName != null) { if (parameterName.Equals("GroupBy", StringComparison.OrdinalIgnoreCase)) { switch (binding.CommandName) { case "Format-Table": case "Format-List": case "Format-Wide": case "Format-Custom": return GetSpecialHashTableKeyMembers("Expression", "FormatString", "Label"); } return null; } if (parameterName.Equals("Property", StringComparison.OrdinalIgnoreCase)) { switch (binding.CommandName) { case "New-Object": var inferredType = AstTypeInference.InferTypeOf(commandAst, completionContext.TypeInferenceContext, TypeInferenceRuntimePermissions.AllowSafeEval); var result = new List(); CompleteMemberByInferredType( completionContext.TypeInferenceContext, inferredType, result, completionContext.WordToComplete + "*", IsWriteablePropertyMember, isStatic: false); return result; case "Select-Object": return GetSpecialHashTableKeyMembers("Name", "Expression"); case "Sort-Object": return GetSpecialHashTableKeyMembers("Expression", "Ascending", "Descending"); case "Group-Object": return GetSpecialHashTableKeyMembers("Expression"); case "Format-Table": return GetSpecialHashTableKeyMembers("Expression", "FormatString", "Label", "Width", "Alignment"); case "Format-List": return GetSpecialHashTableKeyMembers("Expression", "FormatString", "Label"); case "Format-Wide": return GetSpecialHashTableKeyMembers("Expression", "FormatString"); case "Format-Custom": return GetSpecialHashTableKeyMembers("Expression", "Depth"); } } } } return null; } private static List GetSpecialHashTableKeyMembers(params string[] keys) { // Resources were removed because they missed the deadline for loc. // return keys.Select(key => new CompletionResult(key, key, CompletionResultType.Property, // ResourceManagerCache.GetResourceString(typeof(CompletionCompleters).Assembly, // "TabCompletionStrings", key + "HashKeyDescription"))).ToList(); return keys.Select(static key => new CompletionResult(key, key, CompletionResultType.Property, key)).ToList(); } #endregion Hashtable Keys #region Helpers internal static bool IsPathSafelyExpandable(ExpandableStringExpressionAst expandableStringAst, string extraText, ExecutionContext executionContext, out string expandedString) { expandedString = null; // Expand the string if its type is DoubleQuoted or BareWord var constType = expandableStringAst.StringConstantType; if (constType == StringConstantType.DoubleQuotedHereString) { return false; } Diagnostics.Assert( constType == StringConstantType.BareWord || (constType == StringConstantType.DoubleQuoted && expandableStringAst.Extent.Text[0].IsDoubleQuote()), "the string to be expanded should be either BareWord or DoubleQuoted"); var varValues = new List(); foreach (ExpressionAst nestedAst in expandableStringAst.NestedExpressions) { if (!(nestedAst is VariableExpressionAst variableAst)) { return false; } string strValue = CombineVariableWithPartialPath(variableAst, null, executionContext); if (strValue != null) { varValues.Add(strValue); } else { return false; } } var formattedString = string.Format(CultureInfo.InvariantCulture, expandableStringAst.FormatExpression, varValues.ToArray()); string quote = (constType == StringConstantType.DoubleQuoted) ? "\"" : string.Empty; expandedString = quote + formattedString + extraText + quote; return true; } internal static string CombineVariableWithPartialPath(VariableExpressionAst variableAst, string extraText, ExecutionContext executionContext) { var varPath = variableAst.VariablePath; if (varPath.IsVariable || varPath.DriveName.Equals("env", StringComparison.OrdinalIgnoreCase)) { try { // We check the strict mode inside GetVariableValue object value = VariableOps.GetVariableValue(varPath, executionContext, variableAst); var strValue = (value == null) ? string.Empty : value as string; if (strValue == null) { object baseObj = PSObject.Base(value); if (baseObj is string || baseObj.GetType().IsPrimitive) { strValue = LanguagePrimitives.ConvertTo(value); } } if (strValue != null) { return strValue + extraText; } } catch (Exception) { } } return null; } internal static string HandleDoubleAndSingleQuote(ref string wordToComplete) { string quote = string.Empty; if (!string.IsNullOrEmpty(wordToComplete) && (wordToComplete[0].IsSingleQuote() || wordToComplete[0].IsDoubleQuote())) { char frontQuote = wordToComplete[0]; int length = wordToComplete.Length; if (length == 1) { wordToComplete = string.Empty; quote = frontQuote.IsSingleQuote() ? "'" : "\""; } else if (length > 1) { if ((wordToComplete[length - 1].IsDoubleQuote() && frontQuote.IsDoubleQuote()) || (wordToComplete[length - 1].IsSingleQuote() && frontQuote.IsSingleQuote())) { wordToComplete = wordToComplete.Substring(1, length - 2); quote = frontQuote.IsSingleQuote() ? "'" : "\""; } else if (!wordToComplete[length - 1].IsDoubleQuote() && !wordToComplete[length - 1].IsSingleQuote()) { wordToComplete = wordToComplete.Substring(1); quote = frontQuote.IsSingleQuote() ? "'" : "\""; } } } return quote; } internal static bool IsSplattedVariable(Ast targetExpr) { if (targetExpr is VariableExpressionAst && ((VariableExpressionAst)targetExpr).Splatted) { // It's splatted variable, member expansion is not useful return true; } return false; } internal static void CompleteMemberHelper( bool @static, string memberName, ExpressionAst targetExpr, CompletionContext context, List results) { object value; if (SafeExprEvaluator.TrySafeEval(targetExpr, context.ExecutionContext, out value) && value != null) { if (targetExpr is ArrayExpressionAst && value is not object[]) { // When the array contains only one element, the evaluation result would be that element. We wrap it into an array value = new[] { value }; } // Instead of Get-Member, we access the members directly and send as input to the pipe. var powerShellExecutionHelper = context.Helper; powerShellExecutionHelper .AddCommandWithPreferenceSetting("Microsoft.PowerShell.Core\\Where-Object") .AddParameter("Property", "Name") .AddParameter("Like") .AddParameter("Value", memberName) .AddCommandWithPreferenceSetting("Microsoft.PowerShell.Utility\\Sort-Object") .AddParameter("Property", new object[] { "MemberType", "Name" }); IEnumerable members; if (@static) { if (!(PSObject.Base(value) is Type type)) { return; } members = PSObject.DotNetStaticAdapter.BaseGetMembers(type); } else { members = PSObject.AsPSObject(value).Members; } var sortedMembers = powerShellExecutionHelper.ExecuteCurrentPowerShell(out _, members); foreach (var member in sortedMembers) { var memberInfo = (PSMemberInfo)PSObject.Base(member); if (memberInfo.IsHidden) { continue; } var completionText = memberInfo.Name; // Handle scenarios like this: $aa | add-member 'a b' 23; $aa.a if (completionText.IndexOfAny(s_charactersRequiringQuotes) != -1) { completionText = completionText.Replace("'", "''"); completionText = "'" + completionText + "'"; } var isMethod = memberInfo is PSMethodInfo; if (isMethod) { var isSpecial = (memberInfo is PSMethod) && ((PSMethod)memberInfo).IsSpecial; if (isSpecial) continue; completionText += '('; } string tooltip = memberInfo.ToString(); if (tooltip.Contains("),", StringComparison.Ordinal)) { var overloads = tooltip.Split("),", StringSplitOptions.RemoveEmptyEntries); var newTooltip = new StringBuilder(); foreach (var overload in overloads) { newTooltip.Append(overload.Trim() + ")\r\n"); } newTooltip.Remove(newTooltip.Length - 3, 3); tooltip = newTooltip.ToString(); } results.Add( new CompletionResult(completionText, memberInfo.Name, isMethod ? CompletionResultType.Method : CompletionResultType.Property, tooltip)); } var dictionary = PSObject.Base(value) as IDictionary; if (dictionary != null) { var pattern = WildcardPattern.Get(memberName, WildcardOptions.IgnoreCase); foreach (DictionaryEntry entry in dictionary) { if (!(entry.Key is string key)) continue; if (pattern.IsMatch(key)) { // Handle scenarios like this: $hashtable["abc#d"] = 100; $hashtable.ab if (key.IndexOfAny(s_charactersRequiringQuotes) != -1) { key = key.Replace("'", "''"); key = "'" + key + "'"; } results.Add(new CompletionResult(key, key, CompletionResultType.Property, key)); } } } if (!@static && IsValueEnumerable(PSObject.Base(value))) { // Complete extension methods 'Where' and 'ForEach' for Enumerable values CompleteExtensionMethods(memberName, results); } } } /// /// Check if a value is treated as Enumerable in powershell. /// private static bool IsValueEnumerable(object value) { object baseValue = PSObject.Base(value); if (baseValue == null || baseValue is string || baseValue is PSObject || baseValue is IDictionary || baseValue is System.Xml.XmlNode) { return false; } if (baseValue is IEnumerable || baseValue is IEnumerator || baseValue is DataTable) { return true; } return false; } /// /// Check if a strong type is treated as Enumerable in powershell. /// private static bool IsStaticTypeEnumerable(Type type) { if (type.Equals(typeof(string)) || typeof(IDictionary).IsAssignableFrom(type) || typeof(System.Xml.XmlNode).IsAssignableFrom(type)) { return false; } if (typeof(IEnumerable).IsAssignableFrom(type) || typeof(IEnumerator).IsAssignableFrom(type)) { return true; } return false; } private static bool CompletionRequiresQuotes(string completion, bool escape) { // If the tokenizer sees the completion as more than two tokens, or if there is some error, then // some form of quoting is necessary (if it's a variable, we'd need ${}, filenames would need [], etc.) Language.Token[] tokens; ParseError[] errors; Language.Parser.ParseInput(completion, out tokens, out errors); char[] charToCheck = escape ? new[] { '$', '[', ']', '`' } : new[] { '$', '`' }; // Expect no errors and 2 tokens (1 is for our completion, the other is eof) // Or if the completion is a keyword, we ignore the errors bool requireQuote = !(errors.Length == 0 && tokens.Length == 2); if ((!requireQuote && tokens[0] is StringToken) || (tokens.Length == 2 && (tokens[0].TokenFlags & TokenFlags.Keyword) != 0)) { requireQuote = false; var value = tokens[0].Text; if (value.IndexOfAny(charToCheck) != -1) requireQuote = true; } return requireQuote; } private static bool ProviderSpecified(string path) { var index = path.IndexOf(':'); return index != -1 && index + 1 < path.Length && path[index + 1] == ':'; } private static Type GetEffectiveParameterType(Type type) { var underlying = Nullable.GetUnderlyingType(type); return underlying ?? type; } /// /// Turn on the "LiteralPaths" option. /// /// /// /// Indicate whether the "LiteralPaths" option needs to be removed after operation /// private static bool TurnOnLiteralPathOption(CompletionContext completionContext) { bool clearLiteralPathsKey = false; if (completionContext.Options == null) { completionContext.Options = new Hashtable { { "LiteralPaths", true } }; clearLiteralPathsKey = true; } else if (!completionContext.Options.ContainsKey("LiteralPaths")) { // Dont escape '[',']','`' when the file name is treated as command name completionContext.Options.Add("LiteralPaths", true); clearLiteralPathsKey = true; } return clearLiteralPathsKey; } /// /// Return whether we need to add ampersand when it's necessary. /// /// /// /// internal static bool IsAmpersandNeeded(CompletionContext context, bool defaultChoice) { if (context.RelatedAsts != null && !string.IsNullOrEmpty(context.WordToComplete)) { var lastAst = context.RelatedAsts.Last(); var parent = lastAst.Parent as CommandAst; if (parent != null && parent.CommandElements.Count == 1 && ((!defaultChoice && parent.InvocationOperator == TokenKind.Unknown) || (defaultChoice && parent.InvocationOperator != TokenKind.Unknown))) { // - When the default choice is NOT to add ampersand, we only return true // when the invocation operator is NOT specified. // - When the default choice is to add ampersand, we only return false // when the invocation operator is specified. defaultChoice = !defaultChoice; } } return defaultChoice; } private sealed class ItemPathComparer : IComparer { public int Compare(PSObject x, PSObject y) { var xPathInfo = PSObject.Base(x) as PathInfo; var xFileInfo = PSObject.Base(x) as IO.FileSystemInfo; var xPathStr = PSObject.Base(x) as string; var yPathInfo = PSObject.Base(y) as PathInfo; var yFileInfo = PSObject.Base(y) as IO.FileSystemInfo; var yPathStr = PSObject.Base(y) as string; string xPath = null, yPath = null; if (xPathInfo != null) xPath = xPathInfo.ProviderPath; else if (xFileInfo != null) xPath = xFileInfo.FullName; else if (xPathStr != null) xPath = xPathStr; if (yPathInfo != null) yPath = yPathInfo.ProviderPath; else if (yFileInfo != null) yPath = yFileInfo.FullName; else if (yPathStr != null) yPath = yPathStr; if (string.IsNullOrEmpty(xPath) || string.IsNullOrEmpty(yPath)) Diagnostics.Assert(false, "Base object of item PSObject should be either PathInfo or FileSystemInfo"); return string.Compare(xPath, yPath, StringComparison.CurrentCultureIgnoreCase); } } private sealed class CommandNameComparer : IComparer { public int Compare(PSObject x, PSObject y) { string xName = null; string yName = null; object xObj = PSObject.Base(x); object yObj = PSObject.Base(y); var xCommandInfo = xObj as CommandInfo; xName = xCommandInfo != null ? xCommandInfo.Name : xObj as string; var yCommandInfo = yObj as CommandInfo; yName = yCommandInfo != null ? yCommandInfo.Name : yObj as string; if (xName == null || yName == null) Diagnostics.Assert(false, "Base object of Command PSObject should be either CommandInfo or string"); return string.Compare(xName, yName, StringComparison.OrdinalIgnoreCase); } } #endregion Helpers } /// /// This class is very similar to the restricted langauge checker, but it is meant to allow more things, yet still /// be considered "safe", at least in the sense that tab completion can rely on it to not do bad things. The primary /// use is for intellisense where you don't want to run arbitrary code, but you do want to know the values /// of various expressions so you can get the members. /// internal class SafeExprEvaluator : ICustomAstVisitor2 { internal static bool TrySafeEval(ExpressionAst ast, ExecutionContext executionContext, out object value) { if (!(bool)ast.Accept(new SafeExprEvaluator())) { value = null; return false; } try { // ConstrainedLanguage has already been applied as necessary when we construct CompletionContext Diagnostics.Assert(!(executionContext.HasRunspaceEverUsedConstrainedLanguageMode && executionContext.LanguageMode != PSLanguageMode.ConstrainedLanguage), "If the runspace has ever used constrained language mode, then the current language mode should already be set to constrained language"); // We're passing 'true' here for isTrustedInput, because SafeExprEvaluator ensures that the AST // has no dangerous side-effects such as arbitrary expression evaluation. It does require variable // access and a few other minor things, which staples of tab completion: // // $t = Get-Process // $t[0].MainModule. // value = Compiler.GetExpressionValue(ast, true, executionContext); return true; } catch { value = null; return false; } } public object VisitErrorStatement(ErrorStatementAst errorStatementAst) { return false; } public object VisitErrorExpression(ErrorExpressionAst errorExpressionAst) { return false; } public object VisitScriptBlock(ScriptBlockAst scriptBlockAst) { return false; } public object VisitParamBlock(ParamBlockAst paramBlockAst) { return false; } public object VisitNamedBlock(NamedBlockAst namedBlockAst) { return false; } public object VisitTypeConstraint(TypeConstraintAst typeConstraintAst) { return false; } public object VisitAttribute(AttributeAst attributeAst) { return false; } public object VisitNamedAttributeArgument(NamedAttributeArgumentAst namedAttributeArgumentAst) { return false; } public object VisitParameter(ParameterAst parameterAst) { return false; } public object VisitFunctionDefinition(FunctionDefinitionAst functionDefinitionAst) { return false; } public object VisitIfStatement(IfStatementAst ifStmtAst) { return false; } public object VisitTrap(TrapStatementAst trapStatementAst) { return false; } public object VisitSwitchStatement(SwitchStatementAst switchStatementAst) { return false; } public object VisitDataStatement(DataStatementAst dataStatementAst) { return false; } public object VisitForEachStatement(ForEachStatementAst forEachStatementAst) { return false; } public object VisitDoWhileStatement(DoWhileStatementAst doWhileStatementAst) { return false; } public object VisitForStatement(ForStatementAst forStatementAst) { return false; } public object VisitWhileStatement(WhileStatementAst whileStatementAst) { return false; } public object VisitCatchClause(CatchClauseAst catchClauseAst) { return false; } public object VisitTryStatement(TryStatementAst tryStatementAst) { return false; } public object VisitBreakStatement(BreakStatementAst breakStatementAst) { return false; } public object VisitContinueStatement(ContinueStatementAst continueStatementAst) { return false; } public object VisitReturnStatement(ReturnStatementAst returnStatementAst) { return false; } public object VisitExitStatement(ExitStatementAst exitStatementAst) { return false; } public object VisitThrowStatement(ThrowStatementAst throwStatementAst) { return false; } public object VisitDoUntilStatement(DoUntilStatementAst doUntilStatementAst) { return false; } public object VisitAssignmentStatement(AssignmentStatementAst assignmentStatementAst) { return false; } // REVIEW: we could relax this to allow specific commands public object VisitCommand(CommandAst commandAst) { return false; } public object VisitCommandExpression(CommandExpressionAst commandExpressionAst) { return false; } public object VisitCommandParameter(CommandParameterAst commandParameterAst) { return false; } public object VisitFileRedirection(FileRedirectionAst fileRedirectionAst) { return false; } public object VisitMergingRedirection(MergingRedirectionAst mergingRedirectionAst) { return false; } public object VisitExpandableStringExpression(ExpandableStringExpressionAst expandableStringExpressionAst) { return false; } public object VisitAttributedExpression(AttributedExpressionAst attributedExpressionAst) { return false; } public object VisitBlockStatement(BlockStatementAst blockStatementAst) { return false; } public object VisitInvokeMemberExpression(InvokeMemberExpressionAst invokeMemberExpressionAst) { return false; } public object VisitUsingExpression(UsingExpressionAst usingExpressionAst) { return false; } public object VisitTypeDefinition(TypeDefinitionAst typeDefinitionAst) { return false; } public object VisitPropertyMember(PropertyMemberAst propertyMemberAst) { return false; } public object VisitFunctionMember(FunctionMemberAst functionMemberAst) { return false; } public object VisitBaseCtorInvokeMemberExpression(BaseCtorInvokeMemberExpressionAst baseCtorInvokeMemberExpressionAst) { return false; } public object VisitUsingStatement(UsingStatementAst usingStatementAst) { return false; } public object VisitDynamicKeywordStatement(DynamicKeywordStatementAst dynamicKeywordStatementAst) { return false; } public object VisitPipelineChain(PipelineChainAst pipelineChainAst) { return false; } public object VisitConfigurationDefinition(ConfigurationDefinitionAst configurationDefinitionAst) { return configurationDefinitionAst.Body.Accept(this); } public object VisitStatementBlock(StatementBlockAst statementBlockAst) { if (statementBlockAst.Traps != null) return false; // REVIEW: we could relax this to allow multiple statements if (statementBlockAst.Statements.Count > 1) return false; var pipeline = statementBlockAst.Statements.FirstOrDefault(); return pipeline != null && (bool)pipeline.Accept(this); } public object VisitPipeline(PipelineAst pipelineAst) { var expr = pipelineAst.GetPureExpression(); return expr != null && (bool)expr.Accept(this); } public object VisitTernaryExpression(TernaryExpressionAst ternaryExpressionAst) { return (bool)ternaryExpressionAst.Condition.Accept(this) && (bool)ternaryExpressionAst.IfTrue.Accept(this) && (bool)ternaryExpressionAst.IfFalse.Accept(this); } public object VisitBinaryExpression(BinaryExpressionAst binaryExpressionAst) { return (bool)binaryExpressionAst.Left.Accept(this) && (bool)binaryExpressionAst.Right.Accept(this); } public object VisitUnaryExpression(UnaryExpressionAst unaryExpressionAst) { return (bool)unaryExpressionAst.Child.Accept(this); } public object VisitConvertExpression(ConvertExpressionAst convertExpressionAst) { return (bool)convertExpressionAst.Child.Accept(this); } public object VisitConstantExpression(ConstantExpressionAst constantExpressionAst) { return true; } public object VisitStringConstantExpression(StringConstantExpressionAst stringConstantExpressionAst) { return true; } public object VisitSubExpression(SubExpressionAst subExpressionAst) { return subExpressionAst.SubExpression.Accept(this); } public object VisitVariableExpression(VariableExpressionAst variableExpressionAst) { return true; } public object VisitTypeExpression(TypeExpressionAst typeExpressionAst) { return true; } public object VisitMemberExpression(MemberExpressionAst memberExpressionAst) { return (bool)memberExpressionAst.Expression.Accept(this) && (bool)memberExpressionAst.Member.Accept(this); } public object VisitIndexExpression(IndexExpressionAst indexExpressionAst) { return (bool)indexExpressionAst.Target.Accept(this) && (bool)indexExpressionAst.Index.Accept(this); } public object VisitArrayExpression(ArrayExpressionAst arrayExpressionAst) { return arrayExpressionAst.SubExpression.Accept(this); } public object VisitArrayLiteral(ArrayLiteralAst arrayLiteralAst) { return arrayLiteralAst.Elements.All(e => (bool)e.Accept(this)); } public object VisitHashtable(HashtableAst hashtableAst) { foreach (var keyValuePair in hashtableAst.KeyValuePairs) { if (!(bool)keyValuePair.Item1.Accept(this)) return false; if (!(bool)keyValuePair.Item2.Accept(this)) return false; } return true; } public object VisitScriptBlockExpression(ScriptBlockExpressionAst scriptBlockExpressionAst) { return true; } public object VisitParenExpression(ParenExpressionAst parenExpressionAst) { return parenExpressionAst.Pipeline.Accept(this); } } /// /// Completes with the property names of the InputObject. /// internal class PropertyNameCompleter : IArgumentCompleter { private readonly string _parameterNameOfInput; /// /// Initializes a new instance of the class. /// public PropertyNameCompleter() { _parameterNameOfInput = "InputObject"; } /// /// Initializes a new instance of the class. /// /// The name of the property of the input object for witch to complete with property names. public PropertyNameCompleter(string parameterNameOfInput) { _parameterNameOfInput = parameterNameOfInput; } IEnumerable IArgumentCompleter.CompleteArgument( string commandName, string parameterName, string wordToComplete, CommandAst commandAst, IDictionary fakeBoundParameters) { if (commandAst.Parent is not PipelineAst pipelineAst) { return null; } int i; for (i = 0; i < pipelineAst.PipelineElements.Count; i++) { if (pipelineAst.PipelineElements[i] == commandAst) { break; } } var typeInferenceContext = new TypeInferenceContext(); IEnumerable prevType; if (i == 0) { var parameterAst = (CommandParameterAst)commandAst.Find(ast => ast is CommandParameterAst cpa && cpa.ParameterName == "PropertyName", false); var pseudoBinding = new PseudoParameterBinder().DoPseudoParameterBinding(commandAst, null, parameterAst, PseudoParameterBinder.BindingType.ParameterCompletion); if (!pseudoBinding.BoundArguments.TryGetValue(_parameterNameOfInput, out var pair) || !pair.ArgumentSpecified) { return null; } if (pair is AstPair astPair && astPair.Argument != null) { prevType = AstTypeInference.InferTypeOf(astPair.Argument, typeInferenceContext, TypeInferenceRuntimePermissions.AllowSafeEval); } return null; } else { prevType = AstTypeInference.InferTypeOf(pipelineAst.PipelineElements[i - 1], typeInferenceContext, TypeInferenceRuntimePermissions.AllowSafeEval); } var result = new List(); CompletionCompleters.CompleteMemberByInferredType(typeInferenceContext, prevType, result, wordToComplete + "*", filter: CompletionCompleters.IsPropertyMember, isStatic: false); return result; } } }