Skip to content

fix #2591 #2644

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

Merged
merged 6 commits into from
Aug 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ System.CommandLine.Completions
System.CommandLine.Help
public class HelpAction : System.CommandLine.Invocation.SynchronousCommandLineAction
.ctor()
public System.Boolean ClearsParseErrors { get; }
public System.Int32 MaxWidth { get; set; }
public System.Int32 Invoke(System.CommandLine.ParseResult parseResult)
public class HelpOption : System.CommandLine.Option
Expand All @@ -190,6 +191,7 @@ System.CommandLine.Invocation
public abstract class AsynchronousCommandLineAction : CommandLineAction
public System.Threading.Tasks.Task<System.Int32> InvokeAsync(System.CommandLine.ParseResult parseResult, System.Threading.CancellationToken cancellationToken = null)
public abstract class CommandLineAction
public System.Boolean ClearsParseErrors { get; }
public System.Boolean Terminating { get; }
protected System.Void set_Terminating(System.Boolean value)
public class ParseErrorAction : SynchronousCommandLineAction
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
using System.Linq;
using Xunit;

namespace System.CommandLine.ApiCompatibility.Tests
namespace System.CommandLine.Tests
{
public class LocalizationTests
{
Expand Down
62 changes: 61 additions & 1 deletion src/System.CommandLine.Tests/ParseErrorReportingTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ namespace System.CommandLine.Tests;
public class ParseErrorReportingTests
{
[Fact] // https://github.com/dotnet/command-line-api/issues/817
public void Parse_error_reporting_reports_error_when_help_is_used_and_required_subcommand_is_missing()
public void Help_is_shown_when_required_subcommand_is_missing()
{
var root = new RootCommand
{
Expand Down Expand Up @@ -112,4 +112,64 @@ public void When_no_help_option_is_present_then_help_is_not_shown_for_parse_erro

output.ToString().Should().NotShowHelp();
}

[Fact]
public void Custom_action_can_ignore_parse_errors_on_child_commands()
{
Command subcommand = new Command("subcommand");
subcommand.Action = new SynchronousTestAction(
_ => { },
true,
true);
var rootCommand = new RootCommand
{
Subcommands = { subcommand }
};

var result = rootCommand.Parse("subcommand --nonexistent option and other things");

result.Errors.Should().BeEmpty();
}

[Fact]
public void Custom_action_cannot_ignore_parse_errors_on_parent_commands()
{
Command subcommand = new Command("subcommand");
subcommand.Action = new SynchronousTestAction(
_ => { },
true,
true);
var rootCommand = new RootCommand
{
Subcommands = { subcommand }
};

var result = rootCommand.Parse("--what nope subcommand");

result.Errors.Should().NotBeEmpty();
}

[Fact]
public void Pre_actions_cannot_clear_parse_errors()
{
var rootCommand = new RootCommand
{
Directives =
{
new Directive("pre")
{
Action = new SynchronousTestAction(
_ => { },
false,
true)
}
},
};

rootCommand.SetAction(_ => { });

var result = rootCommand.Parse("[pre] --not valid");

result.Errors.Should().NotBeEmpty();
}
}
2 changes: 1 addition & 1 deletion src/System.CommandLine.Tests/ParsingValidationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1269,7 +1269,7 @@ public void Multiple_validators_on_the_same_argument_do_not_report_duplicate_err
}

[Fact] // https://github.com/dotnet/command-line-api/issues/1609
internal void When_there_is_an_arity_error_then_further_errors_are_not_reported()
public void When_there_is_an_arity_error_then_further_errors_are_not_reported()
{
var option = new Option<string>("-o");
option.Validators.Add(result =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,18 @@ public class SynchronousTestAction : SynchronousCommandLineAction
{
private readonly Action<ParseResult> _invoke;

public SynchronousTestAction(Action<ParseResult> invoke, bool terminating = true)
public SynchronousTestAction(
Action<ParseResult> invoke,
bool terminating = true,
bool clearsParseErrors = false)
{
ClearsParseErrors = clearsParseErrors;
_invoke = invoke;
Terminating = terminating;
}

public override bool ClearsParseErrors { get; }

public override int Invoke(ParseResult parseResult)
{
_invoke(parseResult);
Expand All @@ -28,12 +34,18 @@ public class AsynchronousTestAction : AsynchronousCommandLineAction
{
private readonly Action<ParseResult> _invoke;

public AsynchronousTestAction(Action<ParseResult> invoke, bool terminating = true)
public AsynchronousTestAction(
Action<ParseResult> invoke,
bool terminating = true,
bool clearsParseErrors = false)
{
ClearsParseErrors = clearsParseErrors;
_invoke = invoke;
Terminating = terminating;
}

public override bool ClearsParseErrors { get; }

public override Task<int> InvokeAsync(ParseResult parseResult, CancellationToken cancellationToken = default)
{
_invoke(parseResult);
Expand Down
18 changes: 18 additions & 0 deletions src/System.CommandLine.Tests/VersionOptionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,24 @@ public void When_the_version_option_is_specified_then_there_are_no_parse_errors_
parseResult.Errors.Should().BeEmpty();
}

[Fact] // https://github.com/dotnet/command-line-api/issues/2591
public void When_the_version_option_is_specified_then_there_are_no_parse_errors_due_to_missing_required_option()
{
Option<int> option = new("-x")
{
Required = true
};
RootCommand root = new()
{
option
};
root.SetAction(_ => 0);

var parseResult = root.Parse("--version");

parseResult.Errors.Should().BeEmpty();
}

[Fact]
public async Task Version_option_appears_in_help()
{
Expand Down
2 changes: 2 additions & 0 deletions src/System.CommandLine/Completions/CompletionAction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,6 @@ public override int Invoke(ParseResult parseResult)

return 0;
}

public override bool ClearsParseErrors => true;
}
2 changes: 2 additions & 0 deletions src/System.CommandLine/Help/HelpAction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,5 +65,7 @@ public override int Invoke(ParseResult parseResult)

return 0;
}

public override bool ClearsParseErrors => true;
}
}
6 changes: 6 additions & 0 deletions src/System.CommandLine/Invocation/CommandLineAction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,10 @@ private protected CommandLineAction()
/// Indicates that the action terminates a command line invocation, and later actions are skipped.
/// </summary>
public bool Terminating { get; protected init; } = true;

/// <summary>
/// Indicates that the action clears any parse errors associated with symbols other than one that owns the <see cref="CommandLineAction"/>.
/// </summary>
/// <remarks>This property is ignored when <see cref="Terminating"/> is set to <see langword="false"/>.</remarks>
public virtual bool ClearsParseErrors => false;
}
95 changes: 69 additions & 26 deletions src/System.CommandLine/Parsing/ParseOperation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Collections.Generic;
using System.CommandLine.Help;
using System.CommandLine.Invocation;
using System.Linq;

namespace System.CommandLine.Parsing
{
Expand All @@ -17,7 +17,6 @@ internal sealed class ParseOperation

private int _index;
private CommandResult _innermostCommandResult;
private bool _isHelpRequested;
private bool _isTerminatingDirectiveSpecified;
private CommandLineAction? _primaryAction;
private List<CommandLineAction>? _preActions;
Expand Down Expand Up @@ -63,21 +62,7 @@ internal ParseResult Parse()

ValidateAndAddDefaultResults();


if (_isHelpRequested)
{
_symbolResultTree.Errors?.Clear();
}

if (_primaryAction is null)
{
if (_symbolResultTree.ErrorCount > 0)
{
_primaryAction = new ParseErrorAction();
}
}

return new (
return new(
_configuration,
_rootCommandResult,
_innermostCommandResult,
Expand Down Expand Up @@ -197,11 +182,6 @@ private void ParseOption()
// directives have a precedence over --help and --version
if (!_isTerminatingDirectiveSpecified)
{
if (option is HelpOption)
{
_isHelpRequested = true;
}

if (option.Action.Terminating)
{
_primaryAction = option.Action;
Expand Down Expand Up @@ -386,11 +366,74 @@ private void ValidateAndAddDefaultResults()
currentResult = currentResult.Parent as CommandResult;
}

if (_primaryAction is null &&
_innermostCommandResult is { Command: { Action: null, HasSubcommands: true } })
if (_primaryAction is null)
{
_symbolResultTree.InsertFirstError(
new ParseError(LocalizationResources.RequiredCommandWasNotProvided(), _innermostCommandResult));
if (_innermostCommandResult is { Command: { Action: null, HasSubcommands: true } })
{
_symbolResultTree.InsertFirstError(
new ParseError(LocalizationResources.RequiredCommandWasNotProvided(), _innermostCommandResult));
}

if (_innermostCommandResult is { Command.Action.ClearsParseErrors: true } &&
_symbolResultTree.Errors is not null)
{
var errorsNotUnderInnermostCommand = _symbolResultTree
.Errors
.Where(e => e.SymbolResult != _innermostCommandResult)
.ToList();

_symbolResultTree.Errors = errorsNotUnderInnermostCommand;
}
else if (_symbolResultTree.ErrorCount > 0)
{
_primaryAction = new ParseErrorAction();
}
}
else
{
if (_symbolResultTree.ErrorCount > 0 &&
_primaryAction.ClearsParseErrors &&
_symbolResultTree.Errors is not null)
{
foreach (var kvp in _symbolResultTree)
{
var symbol = kvp.Key;
if (symbol is Option { Action: { } optionAction } option)
{
if (_primaryAction == optionAction)
{
var errorsForPrimarySymbol = _symbolResultTree
.Errors
.Where(e => e.SymbolResult is OptionResult r && r.Option == option)
.ToList();

_symbolResultTree.Errors = errorsForPrimarySymbol;

return;
}
}

if (symbol is Command { Action: { } commandAction } command)
{
if (_primaryAction == commandAction)
{
var errorsForPrimarySymbol = _symbolResultTree
.Errors
.Where(e => e.SymbolResult is CommandResult r && r.Command == command)
.ToList();

_symbolResultTree.Errors = errorsForPrimarySymbol;

return;
}
}
}

if (_symbolResultTree.ErrorCount > 0)
{
_symbolResultTree.Errors?.Clear();
}
}
}
}
}
Expand Down
3 changes: 2 additions & 1 deletion src/System.CommandLine/VersionOption.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.CommandLine.Help;
using System.CommandLine.Invocation;
using System.CommandLine.Parsing;
using System.Linq;
Expand Down Expand Up @@ -68,6 +67,8 @@ public override int Invoke(ParseResult parseResult)
parseResult.InvocationConfiguration.Output.WriteLine(RootCommand.ExecutableVersion);
return 0;
}

public override bool ClearsParseErrors => true;
}
}
}