Skip to content

Argument on root unexpectedly "leaked" to commands #2785

@MattHyman

Description

@MattHyman

I am currently porting a CLI tool to system.commandLine and have run into a scenario where we only expect to take a argument on root but not on its commands. If we put the argument after the commands everything works but if we put it before the argument is silently ignored. Furthermore the argument appears in the help text for the commands where it is not relevant.

Here is an example of this scenario:

using System.CommandLine;
   using System.CommandLine.Help;
   
   // Minimal repro for two related issues with System.CommandLine 2.0.3:
   //
   // 1. Root command arguments appear in subcommand help text (/?).
   // 2. Root command argument preceding a subcommand is silently ignored with no error.
   
   var sampleArgument = new Argument<string>("sampleArgument")
   {
       Description = "An optional argument on the root command.",
       Arity = ArgumentArity.ZeroOrOne,
   };
   
   var feedbackCommand = new Command("/subcommand")
   {
       Description = "A subcommand with no arguments of its own.",
   };
   feedbackCommand.SetAction(_ => Console.WriteLine("[subcommand] Subcommand ran."));
   
   var rootCommand = new RootCommand("A sample app. Example: app hello");
   
   var builtInHelp = rootCommand.Options.OfType<HelpOption>().First();
   builtInHelp.Aliases.Clear();
   builtInHelp.Hidden = true;
   rootCommand.Options.Add(new HelpOption("/help", "/?"));
   
   rootCommand.Add(sampleArgument);
   rootCommand.Add(feedbackCommand);
   rootCommand.TreatUnmatchedTokensAsErrors = true;
   
   rootCommand.SetAction(parseResult =>
   {
       var value = parseResult.GetValue(sampleArgument);
       if (value is not null) Console.WriteLine($"[root] sampleArgument = {value}");
       else Console.WriteLine("[root] No argument specified.");
   });
   
   // Issue 1: /subcommand /?
   // Expected: help shows only /subcommand's own arguments/options.
   // Actual:   <sampleArgument> (root-only) appears in the usage line and
   //           Arguments section, implying it is an input to /subcommand.
   Console.WriteLine("=== Issue 1: app /subcommand /? ===");
   rootCommand.Parse(["/subcommand", "/?"]).Invoke();
   
   Console.WriteLine();
   
   // Issue 2: hello /subcommand
   // Expected: error because "hello" is not valid for /subcommand,
   //           or the root action runs with the argument.
   // Actual:   0 errors, "hello" is parsed into <sampleArgument> but never
   //           used — /subcommand runs and the root action is silently skipped.
   Console.WriteLine("=== Issue 2: app hello /subcommand ===");
   var result = rootCommand.Parse(["hello", "/subcommand"]);
   Console.WriteLine($"Errors: {result.Errors.Count}");
   Console.WriteLine($"Value of <sampleArgument> in parse result: \"{result.GetValue(sampleArgument) ?? "(null)"}\"");
   result.Invoke();

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions