-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Prefer CLI commands to loose files #49485
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
Prefer CLI commands to loose files #49485
Conversation
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.
Pull Request Overview
This PR updates the argument parsing logic in dotnet's CLI to favor built-in commands over user C# files that share the same name as a CLI command.
- Simplifies the initial parsing of arguments by always calling Parser.Instance.Parse(args).
- Adds a conditional re-parsing that prepends the "run" command when an unmatched file path is detected.
Comments suppressed due to low confidence (1)
src/Cli/dotnet/Program.cs:139
- [nitpick] Add a comment clarifying why tokens with a type other than Argument are included even if they match the unmatched command; this will aid future maintainers in understanding the token filtering logic.
foreach (var token in parseResult.Tokens)
parseResult = Parser.Instance.Parse(args); | ||
// If we get didn't match any built-in commands, and a C# file path is the first argument, | ||
// parse as `dotnet run file.cs ..rest_of_args` instead. | ||
if (parseResult.CommandResult.Command is RootCommand |
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.
If we don't match any 'actual' commands, the CommandResult will be dotnet
itself because of how we've modeled it in Parser.cs. We can inspect the intended state of the ParseResult and figure out what the user intended from there.
// If we get didn't match any built-in commands, and a C# file path is the first argument, | ||
// parse as `dotnet run file.cs ..rest_of_args` instead. | ||
if (parseResult.CommandResult.Command is RootCommand | ||
&& parseResult.GetValue(Parser.DotnetSubCommand) is { } unmatchedCommandOrFile |
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 DotnetSubCommand
argument (misnomer alert!) is also used later in the Program.cs when we try to run globally-installed tools with the naming convention dotnet-<arg>
. It's intended for analysis like this!
/backport to release/10.0.1xx-preview6 |
Started backporting to release/10.0.1xx-preview6: https://github.com/dotnet/sdk/actions/runs/15749026895 |
&& VirtualProjectBuildingCommand.IsValidEntryPointPath(unmatchedCommandOrFile)) | ||
{ | ||
List<string> otherTokens = new(parseResult.Tokens.Count - 1); | ||
foreach (var token in parseResult.Tokens) |
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 do we need a foreach? Are there situations when the unmatchedCommandOrFile
won't be the first token? And if there are such situations, does it matter that we reorder the tokens below (we put unmatchedCommandOrFile
before all other tokens)?
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.
surprisingly yes! Here are some examples (assume a file called 'dnx' in the CWD where these commands are executed):
dotnet dnx
Tokens
has 1 entry (argument: dnx), could just skip the first one
dotnet dnx --help
Tokens
has two entries (argument: dnx, option:--help) , could just skip the first one. Note that in this case--help
is recognized as an Option token because thedotnet
RootCommand exposes a--help
option.
dotnet dnx --arbitrary
Tokens
has two entries (argument: dnx, argument:--arbitrary), could just skip the first one. Note that in this case--arbitrary
is classified as anArgument
token because there's no Option exposed on thedotnet
RootCommand that matches that token
So far, all of this has been able to be handled by a .Skip(1)
. The only real problem comes in the following kind of case:
dotnet --help dnx
Tokens
has two entries (option:--help, argument:dnx), and we can't just skip the first one. In this case, because an token was recognized as an Option (because it matched anOption
definition on thedotnet
RootCommand), this syntax is valid. This is because in S.CL, options configured on a command are valid for placement anywhere after the command they are attached to is recognized - they become 'in scope' in a way. This means that to handle this case we need to do the foreach onTokens
.
As to your question about ordering - we are maintaining the original ordering as much as possible, and I think the only real impact here is that any dotnet
-based Option tokens will appear immediately after dotnet run <arg>
, e.g.:
dotnet --help foo --arbitrary
=> dotnet run foo --help --arbitrary
This seems like a reasonable remapping to me.
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 reordering, I noticed the following in the current SDK:
dotnet --help run
shows the help text for run subcommand.dotnet --help myapp.dll
fails with an error message, it does not launchmyapp.dll
.
Could not execute because the specified command or file was not found.
Possible reasons for this include:
* You misspelled a built-in dotnet command.
* You intended to execute a .NET program, but dotnet-myapp.dll does not exist.
* You intended to run a global tool, but a dotnet-prefixed executable with this name could not be found on the PATH.
Having seen all that I think the solution in this PR is most consistent with the idea that this is just a "conditional alias" for dotnet run
.
Co-authored-by: Jan Jones <jan.jones.cz@gmail.com>
@baronfel does this still prefer loose files if they're specified with an qualified path, e.g. |
@DamianEdwards if we ever made a CLI command that started with a With this PR, we'd see if a CLI command matching |
Can we add 1-2 tests which would fail with the original implementation, but pass with this implementation? |
@RikkiGibson I'm going to merge this so we have the fix, but @jjonescz volunteered to add some later. |
When testing #49461 (comment) I discovered that #48387 introduced a subtle failure mode - if you have a file that we think is a C# runnable file in the current directory that matches the name of a CLI command, we'll prefer your file over the CLI command.
This is backwards to me - we should be preferring our commands. This PR does that!
Some proof is in order: Here I have a directory with a file called
dnx
in it. I explicitly run it, and we see a build failure because I'm missing NuGet feeds. Point is, we're running the single-file app.Then, I call
dotnet dnx
. Before, this would also try to run the app. Now it correctly runs the actualdnx
command.