Skip to content
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
41 changes: 41 additions & 0 deletions TrayApp.Test/TrayAppBuilderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,47 @@
Assert.AreEqual("demo", app.DisplayName);
}

[TestMethod]
[DataRow("-s")]
[DataRow("--status")]
[DataRow("-f")]
[DataRow("--for")]
[DataRow("-t")]
[DataRow("--no-tray")]
[DataRow("-h")]
[DataRow("-?")]
[DataRow("-v")]
public void Flag_WithAStandardSwitchAlias_Throws(string alias)
{
// The standard switches are read first, so an option spelled the same way could never be reached.
ArgumentException ex = Assert.ThrowsExactly<ArgumentException>(
() => TrayAppBuilder.Create("demo").Flag(["--quiet", alias], "Quiet.", () => { }));

Assert.Contains($"'{alias}'", ex.Message, StringComparison.Ordinal);
Assert.AreEqual("aliases", ex.ParamName);
}

[TestMethod]
public void Option_WithAStandardSwitchAlias_Throws()
{
ArgumentException ex = Assert.ThrowsExactly<ArgumentException>(
() => TrayAppBuilder.Create("demo").Option(["-f", "--file"], "path", "A file.", _ => { }));

Assert.Contains("'-f'", ex.Message, StringComparison.Ordinal);
}

[TestMethod]
public void Flag_WithAnAliasAnotherOptionTook_Throws()
{
TrayAppBuilder builder = TrayAppBuilder.Create("demo").Option(["-d", "--delay"], "time", "Delay.", _ => { });

ArgumentException ex = Assert.ThrowsExactly<ArgumentException>(
() => builder.Flag(["-d", "--display"], "Display.", () => { }));

Assert.Contains("'-d'", ex.Message, StringComparison.Ordinal);
Assert.AreEqual(1, builder.Build().Options.Count);

Check warning on line 72 in TrayApp.Test/TrayAppBuilderTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.HasCount' instead of 'Assert.AreEqual'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_TrayApp&issues=AaEU2UgkrIqpKK-MGbVH&open=AaEU2UgkrIqpKK-MGbVH&pullRequest=34
}

[TestMethod]
public void Build_CollectsItemsTogglesAndOptions()
{
Expand Down
7 changes: 7 additions & 0 deletions TrayApp/Cli/CommandLineParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@ private enum ReadResult
Failed,
}

/// <summary>
/// Gets the spellings of the standard switches, which an application option may not reuse: they are read
/// first, so an application option spelled the same way could never be reached.
/// </summary>
internal static IReadOnlyList<string> StandardAliases { get; } =
["-t", "--tray", "--no-tray", "-s", "--status", "-h", "--help", "-?", "-v", "--version", "-f", "--for"];

/// <summary>
/// Parses the arguments.
/// </summary>
Expand Down
33 changes: 31 additions & 2 deletions TrayApp/TrayAppBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ namespace ktsu.TrayApp;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Avalonia;
Expand Down Expand Up @@ -247,9 +248,12 @@ public TrayAppBuilder Separator()
/// <param name="onSet">Called once when the switch is present.</param>
/// <returns>The builder, for chaining.</returns>
/// <exception cref="ArgumentNullException">Any argument is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException">
/// An alias is one of the standard switches, or is already taken by another option.
/// </exception>
public TrayAppBuilder Flag(IReadOnlyList<string> aliases, string description, Action onSet)
{
appOptions.Add(AppOption.Flag(aliases, description, onSet));
AddOption(AppOption.Flag(aliases, description, onSet), nameof(aliases));
return this;
}

Expand All @@ -265,9 +269,12 @@ public TrayAppBuilder Flag(IReadOnlyList<string> aliases, string description, Ac
/// </param>
/// <returns>The builder, for chaining.</returns>
/// <exception cref="ArgumentNullException">Any argument is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException">
/// An alias is one of the standard switches, or is already taken by another option.
/// </exception>
public TrayAppBuilder Option(IReadOnlyList<string> aliases, string valueName, string description, Action<string> onValue)
{
appOptions.Add(AppOption.Value(aliases, valueName, description, onValue));
AddOption(AppOption.Value(aliases, valueName, description, onValue), nameof(aliases));
return this;
}

Expand Down Expand Up @@ -660,4 +667,26 @@ internal static int RecoverFromTrayFailure(
return first.IsChecked;
};
}

/// <summary>
/// Registers an application option, refusing an alias that could never be reached: the standard switches
/// are read before any application option, and the first option to claim an alias wins it.
/// </summary>
private void AddOption(AppOption option, string paramName)
{
foreach (string alias in option.Aliases)
{
if (CommandLineParser.StandardAliases.Contains(alias, StringComparer.Ordinal))
{
throw new ArgumentException($"'{alias}' is a standard switch and cannot be reused by an application option.", paramName);
}

if (appOptions.Any(existing => existing.Matches(alias)))
{
throw new ArgumentException($"'{alias}' is already taken by another option.", paramName);
}
}

appOptions.Add(option);
}
}
Loading