dotnet / command-line-api

Command line parsing, invocation, and rendering of terminal output.
https://github.com/dotnet/command-line-api/wiki
MIT License
3.34k stars 375 forks source link

Shell Completions #2425

Open slang25 opened 1 month ago

slang25 commented 1 month ago

I've just caught up on the last community standup and was interested to hear the direction for completions, and wanted to share how I'm solving this today.

First off, I love the idea of the automagic self-describing directives based completions today (dotnet-suggest), however as was mentioned this is slow (although very workable when AOTing). I abandoned this approach in the end mostly because:

So what I do for my internal tool (can't share the code sorry) is have a separate file that has the grammar of the command line in, and using a tool (complgen) to produce the per-shell scripts. I have the outputs of these then embedded my tool, so I can say jaz completions fish (the tool is named jaz) and it will output the fish shell snipped. This plays well with eco-system tools like homebrew (example).

Here is what my usage grammar looks like:

jaz [<OPTION>]... [<HELP>];
jaz <COMMAND> [<HELP>];

<COMMAND> ::= info               "About jaz"
            | clear              "Remove credentials from your profile" [<CREDENTIALS-PROFILE-OPTION>]
            | doctor             "Perform checkup to diagnose any issues"
            | logout             "Logout of SSO sessions"
            | whoami             "Get the caller ID of the current session" [<PROFILE-OPTION>] [<SHOW-ROLE-ARN-OPTION>]
            | update-metadata    "Update the environment metadata"
            | list-profiles      "List session profiles"
            | generate-profiles  "Generate SSO profiles"
            ;

<OPTION> ::= --sso-session  "The AWS SSO session to use" <SSO-SESSION>
           | --account-id   "The AWS account ID to use" <ACCOUNT-ID>
           | --role         "The AWS role to use" <ROLE>
           | --region       "The AWS region to use" <REGION>
           | <PROFILE-OPTION>
           | --version      "Show version information"
           ;

<SHOW-ROLE-ARN-OPTION> ::= --show-role-arn "Show the role ARN";

<PROFILE-OPTION> ::= --profile "The AWS profile to use" <PROFILE>;
<CREDENTIALS-PROFILE-OPTION> ::= --profile "The AWS profile to use" <CREDENTIALS-PROFILE>;

<PROFILE> ::= {{{ jaz list-profiles --include-all }}};
<CREDENTIALS-PROFILE> ::= {{{ jaz list-profiles }}};

<HELP> ::= (-h | --help)  "Show help and usage information";

You can see this supports calling back into itself for completions (see <CREDENTIALS-PROFILE>), this is awesome because I get nice completions across all shells, and it's fast because it's static, but I get the dynamic behaviours when I want them.

I'm not suggesting that this exact tool/grammar be selected as the approach, but as a general pattern I think this is really powerful yet simple for developers.

image
baronfel commented 1 month ago

I'm in general agreement with you here - I think the proper way forward for any completion system is a shell-specific, grammar-aware script that's generated from your CLI that embraces the static portions of your grammar in a shell-specific way, while allowing your app to supply dynamic completions when required. Shell-by-shell the balance of what can be put in the script vs needing the dynamic invocation would be tweaked. This is the pattern you see in so many other libraries, and it's that way for a reason!