mirror of
https://github.com/PowerShell/PowerShell
synced 2026-06-08 12:12:50 +00:00
Implement a completion for View parameter of format cmdlets (#14513)
This commit is contained in:
@@ -24,6 +24,7 @@ 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
|
||||
{
|
||||
@@ -2282,6 +2283,14 @@ namespace System.Management.Automation
|
||||
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":
|
||||
@@ -2291,6 +2300,10 @@ namespace System.Management.Automation
|
||||
{
|
||||
NativeCompletionMemberName(context, result, commandAst);
|
||||
}
|
||||
else if (parameterName.Equals("View", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
NativeCompletionFormatViewName(context, boundArguments, result, commandAst, commandName);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
@@ -3785,44 +3798,78 @@ namespace System.Management.Automation
|
||||
result.Add(CompletionResult.Null);
|
||||
}
|
||||
|
||||
private static void NativeCompletionMemberName(CompletionContext context, List<CompletionResult> result, CommandAst commandAst)
|
||||
private static IEnumerable<PSTypeName> 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 PipelineAst pipelineAst))
|
||||
return;
|
||||
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<PSTypeName> 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;
|
||||
return null;
|
||||
}
|
||||
|
||||
var astPair = pair as AstPair;
|
||||
if (astPair == null || astPair.Argument == null)
|
||||
{
|
||||
return;
|
||||
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);
|
||||
}
|
||||
|
||||
CompleteMemberByInferredType(context.TypeInferenceContext, prevType, result, context.WordToComplete + "*", filter: IsPropertyMember, isStatic: false);
|
||||
return prevType;
|
||||
}
|
||||
|
||||
private static void NativeCompletionMemberName(CompletionContext context, List<CompletionResult> result, CommandAst commandAst)
|
||||
{
|
||||
IEnumerable<PSTypeName> 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<string, AstParameterArgumentPair> boundArguments,
|
||||
List<CompletionResult> result,
|
||||
CommandAst commandAst,
|
||||
string commandName)
|
||||
{
|
||||
IEnumerable<PSTypeName> 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);
|
||||
}
|
||||
|
||||
@@ -5697,6 +5744,59 @@ namespace System.Management.Automation
|
||||
return Ast.GetAncestorAst<ConfigurationDefinitionAst>(expression) != null;
|
||||
}
|
||||
|
||||
private static void CompleteFormatViewByInferredType(CompletionContext context, string[] inferredTypeNames, List<CompletionResult> 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<string>();
|
||||
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<PSTypeName> inferredTypes, List<CompletionResult> results, string memberName, Func<object, bool> filter, bool isStatic)
|
||||
{
|
||||
bool extensionMethodsAdded = false;
|
||||
|
||||
@@ -301,6 +301,111 @@ Describe "TabCompletion" -Tags CI {
|
||||
$actual | Should -BeExactly $expected
|
||||
}
|
||||
|
||||
Context "Format cmdlet's View paramter completion" {
|
||||
BeforeAll {
|
||||
$viewDefinition = @'
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Configuration>
|
||||
<ViewDefinitions>
|
||||
<View>
|
||||
<Name>R A M</Name>
|
||||
<ViewSelectedBy>
|
||||
<TypeName>System.Diagnostics.Process</TypeName>
|
||||
</ViewSelectedBy>
|
||||
<TableControl>
|
||||
<TableHeaders>
|
||||
<TableColumnHeader>
|
||||
<Label>ProcName</Label>
|
||||
<Width>40</Width>
|
||||
<Alignment>Center</Alignment>
|
||||
</TableColumnHeader>
|
||||
<TableColumnHeader>
|
||||
<Label>PagedMem</Label>
|
||||
<Width>40</Width>
|
||||
<Alignment>Center</Alignment>
|
||||
</TableColumnHeader>
|
||||
<TableColumnHeader>
|
||||
<Label>PeakWS</Label>
|
||||
<Width>40</Width>
|
||||
<Alignment>Center</Alignment>
|
||||
</TableColumnHeader>
|
||||
</TableHeaders>
|
||||
<TableRowEntries>
|
||||
<TableRowEntry>
|
||||
<TableColumnItems>
|
||||
<TableColumnItem>
|
||||
<Alignment>Center</Alignment>
|
||||
<PropertyName>Name</PropertyName>
|
||||
</TableColumnItem>
|
||||
<TableColumnItem>
|
||||
<Alignment>Center</Alignment>
|
||||
<PropertyName>PagedMemorySize</PropertyName>
|
||||
</TableColumnItem>
|
||||
<TableColumnItem>
|
||||
<Alignment>Center</Alignment>
|
||||
<PropertyName>PeakWorkingSet</PropertyName>
|
||||
</TableColumnItem>
|
||||
</TableColumnItems>
|
||||
</TableRowEntry>
|
||||
</TableRowEntries>
|
||||
</TableControl>
|
||||
</View>
|
||||
</ViewDefinitions>
|
||||
</Configuration>
|
||||
'@
|
||||
|
||||
$tempViewFile = Join-Path -Path $TestDrive -ChildPath 'processViewDefinition.ps1xml'
|
||||
Set-Content -LiteralPath $tempViewFile -Value $viewDefinition -Force
|
||||
|
||||
$ps = [PowerShell]::Create()
|
||||
$null = $ps.AddScript("Update-FormatData -AppendPath $tempViewFile")
|
||||
$ps.Invoke()
|
||||
$ps.HadErrors | Should -BeFalse
|
||||
$ps.Commands.Clear()
|
||||
|
||||
Remove-Item -LiteralPath $tempViewFile -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
It 'Should complete Get-ChildItem | <cmd> -View' -TestCases (
|
||||
@{ cmd = 'Format-Table'; expected = "children childrenWithHardlink$(if ($EnabledExperimentalFeatures.Contains('PSUnixFileStat')) { ' childrenWithUnixStat' })" },
|
||||
@{ cmd = 'Format-List'; expected = 'children' },
|
||||
@{ cmd = 'Format-Wide'; expected = 'children' },
|
||||
@{ cmd = 'Format-Custom'; expected = '' }
|
||||
) {
|
||||
param($cmd, $expected)
|
||||
|
||||
# The completion is based on OutputTypeAttribute() of the cmdlet.
|
||||
$res = TabExpansion2 -inputScript "Get-ChildItem | $cmd -View " -cursorColumn "Get-ChildItem | $cmd -View ".Length
|
||||
$completionText = $res.CompletionMatches.CompletionText | Sort-Object
|
||||
$completionText -join ' ' | Should -BeExactly $expected
|
||||
}
|
||||
|
||||
It 'Should complete $processList = Get-Process; $processList | <cmd>' -TestCases (
|
||||
@{ cmd = 'Format-Table -View '; expected = "'R A M'", "Priority", "process", "ProcessModule", "ProcessWithUserName", "StartTime" },
|
||||
@{ cmd = 'Format-List -View '; expected = '' },
|
||||
@{ cmd = 'Format-Wide -View '; expected = 'process' },
|
||||
@{ cmd = 'Format-Custom -View '; expected = '' },
|
||||
@{ cmd = 'Format-Table -View S'; expected = "StartTime" },
|
||||
@{ cmd = "Format-Table -View 'S"; expected = "'StartTime'" },
|
||||
@{ cmd = "Format-Table -View R"; expected = "'R A M'" }
|
||||
) {
|
||||
param($cmd, $expected)
|
||||
|
||||
$null = $ps.AddScript({
|
||||
param ($cmd)
|
||||
$processList = Get-Process
|
||||
$res = TabExpansion2 -inputScript "`$processList | $cmd" -cursorColumn "`$processList | $cmd".Length
|
||||
$completionText = $res.CompletionMatches.CompletionText | Sort-Object
|
||||
$completionText
|
||||
}).AddArgument($cmd)
|
||||
|
||||
$result = $ps.Invoke()
|
||||
$ps.Commands.Clear()
|
||||
$expected = ($expected | Sort-Object) -join ' '
|
||||
$result -join ' ' | Should -BeExactly $expected
|
||||
}
|
||||
}
|
||||
|
||||
Context NativeCommand {
|
||||
BeforeAll {
|
||||
$nativeCommand = (Get-Command -CommandType Application -TotalCount 1).Name
|
||||
@@ -1384,7 +1489,7 @@ dir -Recurse `
|
||||
## Save original culture and temporarily set it to da-DK because there's no localized help for da-DK.
|
||||
$OriginalCulture = [cultureinfo]::CurrentCulture
|
||||
[cultureinfo]::CurrentCulture="da-DK"
|
||||
|
||||
|
||||
$res = TabExpansion2 -inputScript 'get-help about_spla' -cursorColumn 'get-help about_spla'.Length
|
||||
$res.CompletionMatches | Should -HaveCount 1
|
||||
$res.CompletionMatches[0].CompletionText | Should -BeExactly 'about_Splatting'
|
||||
|
||||
Reference in New Issue
Block a user