KEMBAR78
Support registering a cover-all completer for native commands by daxian-dbw · Pull Request #25230 · PowerShell/PowerShell · GitHub
Skip to content

Conversation

@daxian-dbw
Copy link
Member

@daxian-dbw daxian-dbw commented Mar 25, 2025

PR Summary

This PR contains the following changes:

  1. Refactor Register-ArgumentCompleter to make it

    • Easier to read and run faster for native command registration
    • Not insert keys that are empty string or containing only white spaces.
    • Remove a registration when value to -ScriptBlock is null (currently it keeps the key value pair but only set the value to null)
  2. Add the parameter -NativeFallback to support registering a cover-all completer for native commands.

    • When a native command doesn't have a specific completer for it, the cover-all completer will be used for it.
    • This will make it easy for a module like Microsoft.PowerShell.UnixTabCompletion to hook up with PowerShell to provide tab completion for many native commands on Linux and macOS systems.

With this change, a new parameter set gets added (which includes the parameter -NativeFallback)

PS C:\> gcm Register-ArgumentCompleter -Syntax

Register-ArgumentCompleter -CommandName <string[]> -ScriptBlock <scriptblock> [-Native] [<CommonParameters>]

Register-ArgumentCompleter -ParameterName <string> -ScriptBlock <scriptblock> [-CommandName <string[]>] [<CommonParameters>]

Register-ArgumentCompleter -ScriptBlock <scriptblock> [-NativeFallback] [<CommonParameters>]

 

PR Context

The module Microsoft.PowerShell.UnixTabCompletion is designed to leverage the completion system of Bash and Zsh to provide tab completion for native utilities on Linux and macOS systems that already have completion support for Bash or Zsh. However, today, a native command completer has to be associated with a specific command, so the UnixTabCompletion module needs to discover all native utilities ahead of time and register a proxy completer for each of them, which not only takes time but also cannot pick up any utilities installed later. It will be helpful to support a cover-all completer so that if there is no specific completer registered for a native command, then we fall back to the cover-all completer for it.

PR Checklist

@daxian-dbw daxian-dbw added the CL-General Indicates that a PR should be marked as a general cmdlet change in the Change Log label Mar 25, 2025
@daxian-dbw
Copy link
Member Author

@MartinGC94 can you please review this PR when you have time? Thank you!

@MartinGC94
Copy link
Contributor

Only one of these catch-all completers can be registered and it sounds like you are planning on doing that in UnixTabCompletion so if anyone else tries to do the same it will break. How about we make a separate list that stores all the catch-all completers, which we would then loop through until one of the completers return a result?

@iSazonov
Copy link
Collaborator

Since we say about interactive session I believe the user knows what they want. If the user loads UnixTabCompletion they want the behavior. If they load a module with another completer they want another behavior. Otherwise, the loading order will be affected, which is even worse.
So I think it is better to have single last resort completer.

One thing we could consider is that if last resort completer has already been added, then when trying to add a new one, throw and require explicit deletion of the previous one before adding new.

@MartinGC94
Copy link
Contributor

It's not a user setting, it's an implementation detail. MS has made this unix completer module and plan on using this "catch-all" in their implementation. If someone else decides to make a similar module that covers some other commands then the user won't be able to use both modules at the same time. Throwing an error will let the user know that they can't use both at the same time but ideally one module shouldn't block another unless there's a good reason for it.

@iSazonov
Copy link
Collaborator

one module shouldn't block another unless there's a good reason for it.

The module loaded last always overlaps the previous ones. It's always been that way. (In some cases, we may use fully qualified names)
In our case, the situation is worse because if the module does not replace this completer, then side effects appear that will mislead the user.

@MartinGC94
Copy link
Contributor

In our case, the situation is worse because if the module does not replace this completer, then side effects appear that will mislead the user.

Is it though? If both modules include their own "catch-all" completer that is applied, then there are 3 possible scenarios when completing a command parameter:
1: None of the modules actually handles the command. Result: The user gets no completions as expected.
2: Only one of the two modules actually handles a command. Result: The user gets their expected completions.
3: Both modules handle the command. Result: The user gets completions, but the quality of one of the completers could be lower (eg. worse tooltip or something).

The only bad scenario is scenario 3, but even that isn't that bad because presumably, whoever made the worse completer still made a completer that is "good enough". Scenario 3 could also happen if a module includes a named completer so it never reaches the other modules "catch-all" completer.

@daxian-dbw daxian-dbw added WG-Interactive-Console the console experience WG-NeedsReview Needs a review by the labeled Working Group labels Mar 25, 2025
@daxian-dbw
Copy link
Member Author

daxian-dbw commented Mar 25, 2025

Only one of these catch-all completers can be registered and it sounds like you are planning on doing that in UnixTabCompletion so if anyone else tries to do the same it will break.

Interesting idea! However, I doubt about if there really will be another catch-all completer except for the UnixTabCompletion module. If a custom completer can tab-complete for fixed numbers of native commands, then it should register for those native commands individually and all point to the same script block, instead of registering a catch-all completer. The catch-all completer is literally supposed to catch all :)

I labelled this PR to be reviewed by the console interactive WG. At the meanwhile, @SeeminglyScience, what do you think?

@MartinGC94
Copy link
Contributor

I agree that people should try to register completers by their real command name, but for the same reasons you need it for UnixTabCompletion I can imagine someone needing it for their completer module which would similarly load completion info dynamically.

@iSazonov
Copy link
Collaborator

The scenario with the second module does not seem vital to me. If someone is going to create add-ons for new Unix commands, then they should not write a new module, but create add-ons for bash completion and UnixTabCompletion module will receive them automatically.

@microsoft-github-policy-service microsoft-github-policy-service bot added the Review - Needed The PR is being reviewed label Apr 8, 2025
Copy link
Collaborator

@SeeminglyScience SeeminglyScience left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry this is late, I apparently never clicked submit review :(

{
key = ParameterName;
}
table.Remove(key);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know that I'm suggesting a larger change here, but the pattern is typically Get-* | Unregister-*. Treating null as remove will potentially hide some design time errors like passing a misspelled variable name.

I definitely understand the need but I'd suggest tackling that separately

Copy link
Member Author

@daxian-dbw daxian-dbw May 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Totally agree there should be Get-ArgumentCompleter and Unregister-ArgumentCompleter cmdlets for better discoverability and manageability! I will open an issue to track.

For Register-ArgumentCompleter, today its behavior when specifying -ScriptBlock $null is to blindly set null to completerDictionary[key]. It essentially disables the previous setting for key without clearing that entry. So, change in this PR only just clear that key.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opened #25800 (Consider adding Get-ArgumentCompleter and Unregister-ArgumentCompleter for custom completer management)


// For a native command, if a cover-all completer is registered, then return it.
// For example, the 'Microsoft.PowerShell.UnixTabCompletion' module.
if (isNative && registeredCompleters.TryGetValue("*", out scriptBlock))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So two things to consider here

  1. Linux can have a binary named *
  2. If we document * as the catch all that might make it seem like we're generally supporting wildcard matching in the command name

What about a parameter called -Fallback that can't be used with Name? Then either stored in a different field, as null in registeredCompleters, or as some series of unlikely characters?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@SeeminglyScience, sorry for the long delay. I've updated the code to introduce a new switch parameter -NativeFallback. The key used for the fallback completer should be unlikely a file name or path. Can you please take another look? Thank you!

@SeeminglyScience
Copy link
Collaborator

The Interactive WG discussed the issue regarding multiple catch all completers today. For the first iteration of this we think it's best to keep to a single registered completer due to implementation concerns. If there is demand we will consider additional changes to support it.

@SeeminglyScience SeeminglyScience added WG-Reviewed A Working Group has reviewed this and made a recommendation and removed WG-NeedsReview Needs a review by the labeled Working Group labels Apr 30, 2025
private const string NativeFallbackSetName = "NativeFallbackSet";

// Use a key that is unlikely to be a file name or path to indicate the fallback completer for native commands.
internal const string FallbackCompleterKey = "___ps::<native_fallback_key>@@___";
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dictionary cannot take $null as the key, so I have to create a special key to indicate the fallback completer.

Copy link
Collaborator

@SeeminglyScience SeeminglyScience left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! ❤️

@daxian-dbw
Copy link
Member Author

/azp run

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@iSazonov
Copy link
Collaborator

Will this in 7.6?

@microsoft-github-policy-service microsoft-github-policy-service bot removed the Review - Needed The PR is being reviewed label Aug 29, 2025
@daxian-dbw daxian-dbw merged commit 53805fb into PowerShell:master Aug 29, 2025
63 of 71 checks passed
@daxian-dbw daxian-dbw deleted the native-completer branch August 29, 2025 22:50
@daxian-dbw
Copy link
Member Author

Will this in 7.6?

We haven't started the last 7.6 preview release yet. So yes, this one will be included :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CL-General Indicates that a PR should be marked as a general cmdlet change in the Change Log WG-Interactive-Console the console experience WG-Reviewed A Working Group has reviewed this and made a recommendation

Projects

Development

Successfully merging this pull request may close these issues.

4 participants