-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Commands for generating shell completion scripts + statically-generated bash script #42416
base: main
Are you sure you want to change the base?
Conversation
I've implemented completion script generate for bash based partially on the excellent patterns that the Swift ArgumentParser library has used. Here's an example of the generated script (warning, 2100 lines of bash await you): It works remarkably well - one of the harder parts was figuring out when truly dynamic completions were required and injecting a call to the dotnet CLI's completion command to generate suggestions for those cases. But where completions can be known statically we inject them. There is no good handling for file-based completions yet - one hurdle I faced was the very-pluggable completions model currently doesn't give any sense of attribution of the different completion sources - if I knew a given |
4fa8920
to
6f266e8
Compare
64269f3
to
af2fa73
Compare
…names. Addresses the final addressable warning about top-level matching on -?. -? is a pattern in bash, so it needs to be escaped.
…or options without any completions.
…ro to prevent them from being generated. This is breaking from the parsing perspective, but all of these have single-digit usages on GitHub.com
Technically breaking from a parser perspective, but GitHub searches of usages for these all showed either zero with arguments or ~5.
…ure we include recursive options from parents
af2fa73
to
b93f1b8
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't look at shell-specific code or test code, but this looks great! I appreciated the videos in the description, but you might need to work on your typing speed (or crop the first half of the video) 😉
@@ -13,7 +13,8 @@ internal static class AddCommandParser | |||
|
|||
public static readonly CliArgument<string> ProjectArgument = new CliArgument<string>(CommonLocalizableStrings.ProjectArgumentName) | |||
{ | |||
Description = CommonLocalizableStrings.ProjectArgumentDescription | |||
Description = CommonLocalizableStrings.ProjectArgumentDescription, | |||
Hidden = true |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why no Arity for this? It seems like you added it to a lot of things.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You get some arities by default in System.CommandLine based on the type of the CliArgument/CliOption - this is one of those cases. You can see the logic used here. In this case, I just needed to hide the argument to paper over some completions pain points. Since we think very few users use this arg (this is one of the discussion points in your verb/noun PR, remember!) this should be fine while also not breaking existing users.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Regarding the ArgumentArity.Zero stuff you see around this PR - We have a lot of flags in the CLI. We don't currently have a unified way of modeling those (i.e. a CliOption subclass). It might be nice to do that (and/or S.CL to have that concept already)
src/Cli/dotnet/commands/dotnet-complete/CompleteCommandParser.cs
Outdated
Show resolved
Hide resolved
} | ||
} | ||
|
||
public class RegisterScriptCommand : CliCommand |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you explain the value of this command? Seems like it does strictly less than GenerateScript
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is some ground work that I should probably delete because it's vestigial at this time. The thinking is we should have an explicit command that users run to generate the completion script for their shell, and that makes total sense to do. Users that want completions can open their Powershell $PROFILE and add dotnet completions generate pwsh | Out-String | Invoke-Expression
to it and have up-to-date completions each time they load their shell. This can of course be optimized, etc. but this is the main idea of many shell-completion frameworks that exist today.
However, you still need to get users to do that step. This is why some other frameworks have an 'install' and 'uninstall' semantic. This looks something like dotnet completions register pwsh
, and the logic would be, roughly:
- determine the shell the user is requesting for
- ask the shell provider to see if the dotnet completions are registered
- this will be heuristic-based, likely, and require probing contents of files like $PROFILE for pwsh, .zshrc for Zsh, etc.
- if not, inject a section to call the
dotnet completions generate
command into the shell configuration files - (optionally) support uninstalling as well - do the same but find and remove the special section
The 'fuzziness' of this command is why I haven't implemented it yet. But once it exists, we could then have a discussion about if the SDK first-run experience should set up this completion registration automatically - we know today that so very very few of our users use completions, and having them set up would be a huge usability increase - as well as make it so that any fixes/enhancement to the completions we might make would be automatically applied!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the idea of a dotnet completions register pwsh command, but even more than that, I like the idea of making this part of the first-run welcome message but in a slightly different way. I'm envisioning two options:
- A message like "If you want to see what options are available, run 'dotnet completions register pwsh'."
- Optionally pause before executing their command with a question like "do you want completions? y/n" and let them choose.
I think something like that would dramatically increase usage, and I do agree with "huge usability increase" if people start using it...but I also don't like the idea of opting people into something without telling them. I actually do see an argument in favor of not having completions, namely that it can take up a lot of screen real estate, and it may not ultimately be helpful if we do a poor job naming our subcommand/options, and people feel the need to go online to read more about them anyway. As we're busy telling people how to opt in (or giving them the y/n option), we can make sure to mention something like "if you ever don't want this anymore, run 'dotnet completions unregister pwsh'." Though I'll admit that unregister might go the way of unregister in MSBuildLocator in becoming vestigial, I think it's positive to offer it as an option initially. It makes it less scary to opt into something if I know I can change my mind later.
As a small aside, I like install/uninstall more than register/unregister, but maybe that's just me.
src/System.CommandLine.StaticCompletions/HelpGenerationExtensions.cs
Outdated
Show resolved
Hide resolved
src/System.CommandLine.StaticCompletions/HelpGenerationExtensions.cs
Outdated
Show resolved
Hide resolved
src/System.CommandLine.StaticCompletions/System.CommandLine.StaticCompletions.csproj
Show resolved
Hide resolved
public static readonly CliOption<bool> VersionOption = new("--version") | ||
{ | ||
Arity = ArgumentArity.Zero, | ||
}; | ||
|
||
public static readonly CliOption<bool> InfoOption = new("--info"); | ||
public static readonly CliOption<bool> InfoOption = new("--info") | ||
{ | ||
Arity = ArgumentArity.Zero, | ||
}; | ||
|
||
public static readonly CliOption<bool> ListSdksOption = new("--list-sdks"); | ||
public static readonly CliOption<bool> ListSdksOption = new("--list-sdks") | ||
{ | ||
Arity = ArgumentArity.Zero, | ||
}; | ||
|
||
public static readonly CliOption<bool> ListRuntimesOption = new("--list-runtimes"); | ||
public static readonly CliOption<bool> ListRuntimesOption = new("--list-runtimes") | ||
{ | ||
Arity = ArgumentArity.Zero, | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Really these look like options but are more like commands - I'd like to model them that way in the future but previous PRs have broken and I haven't gone back to look at it. In any case, these aren't every actually invoked by us (they are handled by the runtime), they just exist here to correctly model our CLI for help/argument parsing purposes.
{ } | ||
|
||
private CompletionsCommand(Dictionary<string, IShellProvider> shellMap) | ||
: base("completions", "Commands for generating and registering completions for supported shells") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TODO: need localizations for the strings introduced in this command..
@baronfel I'm guessing this can only work when the options are statically registered and so it won't work for our new dotnet test experience, right? |
@Evangelink Depending on the shell we would support 'dynamic completions' - those that require calling into the System.CommandLine completion mechanisms to get values like NuGet packages or TFMs in a project file. Depending on how your system is set up this might be a viable pathway for you. We could chat about it and try to figure out what could work. |
<!-- None of these deps should be hard-coded, but something's wonky unless I opt into the whole massive Microsoft.NET.TestFramework mess. | ||
I don't want that. This should be able to stand alone. --> | ||
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.24528.1"/> | ||
<PackageReference Include="FluentAssertions" Version="6.12.0" /> | ||
<PackageReference Include="XUnit" Version="2.9.0" /> | ||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" /> | ||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0-preview-24610-01" /> | ||
<PackageReference Include="Microsoft.TestPlatform" Version="17.13.0-preview-24610-01" /> | ||
<PackageReference Include="Verify.Xunit" Version="25.0.2" /> | ||
<PackageReference Include="Verify.DiffPlex" Version="3.0.0" /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note for the SDK team: I'm trying on purpose to keep this test project as slim as possible, but that has two side-effects that I'm having trouble reasoning about:
- the package versions aren't coming through in the way I'd expect, requiring this duplication
- I suspect Helix is mad due to some missing convention
I'm really hoping to not have to take a dependency on the rather large SDK test framework project because that massively expands cycle times.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does central package versioning not get us the versions? Or rather, why not?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can try again and dig into the binlog, but the CPM setup was not working for this project and I didn't want to get blocked on the actual feature work (making tests, etc). Will take another pass at this.
The new tests seems to be failing due to some permissions issues - I notice that our version of Verify is quite old. I'm going to trigger package ingestion for that family of packages and then see if bumping those fixes the problem. |
…aterially impacted the end user experience
var initialFunctionName = command.FunctionName().MakeSafeFunctionName(); | ||
return | ||
$""" | ||
#! /bin/bash |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A nit: the proper syntax is without the space and this one is recommended to make scripts portable across the distros:
#! /bin/bash | |
#!/usr/bin/env bash |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks - I'll apply that locally because it'll also cause changes in the snapshots that I'll need to cover at the same time. Keep the correctness feedback coming - over time the generator can get incrementally better!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is a note about the spaced #! /
at https://tldp.org/LDP/abs/html/sha-bang.html#AEN214, which links to http://www.in-ulm.de/~mascheck/various/shebang/#details. In short, the space is not required.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have a little hesitation with respect to the security of #!/usr/bin/env bash
. I've normally used #!/usr/bin/bash
in the past, and that pinpoints the exact executable to use rather than grabbing the one specified in $PATH, which is far easier to tamper with.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just delete the #!
line altogether. See #42416 (comment)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/usr/bin/bash is not guaranteed on all platforms and it is recommended to use environment. This is how we are doing it in dotnet repos for several years. Compromised environment is not going to take us far.
No need to delete #! either, it helps testing the script in isolation. I always put it in interpreter as well as include scripts. Does not hurt. CRLF issue is also not limited to the interpreter line.
src/System.CommandLine.StaticCompletions/shells/BashShellProvider.cs
Outdated
Show resolved
Hide resolved
How does this handle CRLF vs. LF line endings in generated scripts?
so the C# files may have either CRLF or LF, depending on the system, and that choice is then baked into the binaries; but test/dotnet-completions.Tests/snapshots/bash/DotnetCliSnapshotTests.VerifyCompletions.verified.sh always has LF. I didn't find code that replaces CRLF with LF. Does Verifier ignore the CRLF vs. LF difference by default? If the |
The I suppose a text editor could recognize |
Is .NET SDK going to ship an Authenticode-signed PowerShell completion script? |
@KalleOlaviNiemitalo that's an interesting question. I'm leaning towards no due to ease for this PR. No other competing CLI framework I've seen attempts to support that, though I do see the use case. |
In addition, that wouldn't be something that source-build could easily support, for example. A user that requires signed scripts could always generate and sign the script themselves. |
@KalleOlaviNiemitalo said:
I anticipated this an instead chose to always emit newlines only in the script generation. Each script generator uses this configuration, as it's well-understood on all platforms and keeps the testing more similar so that we don't have to script for OS-specific newlines. |
@baronfel, I meant the multiline |
"""; | ||
|
||
public string GenerateCompletions(System.CommandLine.CliCommand command) => _dynamicCompletionScript; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will this interfere with the baked in completions provided here:
https://github.com/fish-shell/fish-shell/blob/master/share/completions/dotnet.fish
I'm keen to have descriptions on the completions, but I get that it's a bit of a stretch.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the pointer to this, I didn't know it existed. They are 5 years old however ;) I'm not actually sure how those are used by a fish user (are they just...ambient? or is there a user gesture required to enable them?), but in any case the completions in this PR would require a user to run the dotnet completions generate fish
command and use that output in their fish configuration, so nothing automatic would happen.
Separately, if someone implemented the IShellProvider for fish then users would have always-correct, up-to-date completions ready to go! So in short, this PR would make slower but more up to date completions available for fish users, but the groundwork is right there to make fast and up to date completions possible for fish users easily for all System.CommandLine users.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like if a user opts into our completions they will supersede the built-in definitions:
By default, Fish searches the following for completions, using the first available file that it finds:
- A directory for end-users to keep their own completions, usually ~/.config/fish/completions (controlled by the XDG_CONFIG_HOME environment variable);
- A directory for systems administrators to install completions for all users on the system, usually /etc/fish/completions;
- A user-specified directory for third-party vendor completions, usually ~/.local/share/fish/vendor_completions.d (controlled by the XDG_DATA_HOME environment variable);
- A directory for third-party software vendors to ship their own completions for their software, usually /usr/share/fish/vendor_completions.d;
- The completions shipped with fish, usually installed in /usr/share/fish/completions; and
- Completions automatically generated from the operating system’s manual, usually stored in ~/.local/share/fish/generated_completions.
Part of #42397
Implements:
Shell Support