diff --git a/TrayApp.Test/CommandLineParserTests.cs b/TrayApp.Test/CommandLineParserTests.cs index 6a1fe5a..dbe3ee8 100644 --- a/TrayApp.Test/CommandLineParserTests.cs +++ b/TrayApp.Test/CommandLineParserTests.cs @@ -136,6 +136,32 @@ public void TryParse_WithAnApplicationFlag_RecordsItWithoutRunningIt() Assert.IsTrue(ran); } + [TestMethod] + public void TryParse_WithAFlagGivenUnderSeveralAliases_RunsItsHandlerOnce() + { + int calls = 0; + AppOption display = AppOption.Flag(["-d", "--display"], "Keep the display lit.", () => calls++); + + Assert.IsTrue(CommandLineParser.TryParse(["-d", "--display", "-d"], [display], out CommandLineOptions options, out _)); + Assert.IsTrue(TrayAppBuilder.TryApplyOptions(options, out _)); + + // A handler that toggles would otherwise be flipped back by the second spelling. + Assert.AreEqual(1, calls); + Assert.AreEqual(1, options.Matches.Count); + } + + [TestMethod] + public void TryParse_WithAValueOptionGivenTwice_AppliesBothInOrder() + { + List seen = []; + AppOption reason = AppOption.Value(["-r", "--reason"], "text", "Why.", seen.Add); + + Assert.IsTrue(CommandLineParser.TryParse(["-r", "first", "--reason", "second"], [reason], out CommandLineOptions options, out _)); + Assert.IsTrue(TrayAppBuilder.TryApplyOptions(options, out _)); + + CollectionAssert.AreEqual(new[] { "first", "second" }, seen); + } + [TestMethod] public void TryParse_WithAnApplicationValueOption_TakesTheNextArgument() { diff --git a/TrayApp/Cli/AppOption.cs b/TrayApp/Cli/AppOption.cs index 23ae108..909bf37 100644 --- a/TrayApp/Cli/AppOption.cs +++ b/TrayApp/Cli/AppOption.cs @@ -80,8 +80,10 @@ public static AppOption Flag(IReadOnlyList aliases, string description, /// The placeholder shown in --help, such as text. /// The one-line description shown in --help. /// - /// Called with the value. Throw or from - /// here to reject a value: the message becomes the usage error, and the tool exits 2 without starting. + /// Called with the value, once for each time the option was given and in the order given, so the last + /// one wins for a handler that stores it. Throw or + /// from here to reject a value: the message becomes the usage error, and + /// the tool exits 2 without starting. /// /// The option. /// Any argument is . diff --git a/TrayApp/Cli/CommandLineParser.cs b/TrayApp/Cli/CommandLineParser.cs index 42526b7..dfe77ba 100644 --- a/TrayApp/Cli/CommandLineParser.cs +++ b/TrayApp/Cli/CommandLineParser.cs @@ -168,7 +168,13 @@ private static bool TryReadAppOption( return false; } - state.Matches.Add(new AppOptionMatch(matched, value)); + // A switch runs its handler once however many times it was given, under whichever aliases, so a + // handler that toggles is not undone by "-d --display". A value option keeps every occurrence. + if (matched.TakesValue || !state.Matches.Exists(match => ReferenceEquals(match.Option, matched))) + { + state.Matches.Add(new AppOptionMatch(matched, value)); + } + error = string.Empty; return true; } diff --git a/TrayApp/TrayAppBuilder.cs b/TrayApp/TrayAppBuilder.cs index 85299cf..5d6a48d 100644 --- a/TrayApp/TrayAppBuilder.cs +++ b/TrayApp/TrayAppBuilder.cs @@ -244,7 +244,7 @@ public TrayAppBuilder Separator() /// /// The spellings that select it, such as ["-d", "--display"]. /// The one-line description shown in --help. - /// Called once when the switch is present. + /// Called once when the switch is present, however many times it was given. /// The builder, for chaining. /// Any argument is . public TrayAppBuilder Flag(IReadOnlyList aliases, string description, Action onSet) @@ -260,8 +260,9 @@ public TrayAppBuilder Flag(IReadOnlyList aliases, string description, Ac /// The placeholder shown in --help, such as text. /// The one-line description shown in --help. /// - /// Called with the value. Throw or to - /// reject it: the message becomes the usage error and the tool exits 2. + /// Called with the value, once for each time the option was given and in the order given. Throw + /// or to reject it: the message becomes + /// the usage error and the tool exits 2. /// /// The builder, for chaining. /// Any argument is .