diff --git a/src/ConsoleAppFramework/Command.cs b/src/ConsoleAppFramework/Command.cs index 2cd3eee..2b728dc 100644 --- a/src/ConsoleAppFramework/Command.cs +++ b/src/ConsoleAppFramework/Command.cs @@ -22,6 +22,8 @@ public record class Command public bool IsRootCommand => Name == ""; public required string Name { get; init; } + public required string[] Aliases { get; set; } + public required EquatableArray Parameters { get; init; } public required string Description { get; init; } public required MethodKind MethodKind { get; init; } diff --git a/src/ConsoleAppFramework/CommandHelpBuilder.cs b/src/ConsoleAppFramework/CommandHelpBuilder.cs index 0e7a73e..2a9d7cd 100644 --- a/src/ConsoleAppFramework/CommandHelpBuilder.cs +++ b/src/ConsoleAppFramework/CommandHelpBuilder.cs @@ -217,7 +217,7 @@ static string BuildMethodListMessage(IEnumerable commands, out int maxW var formatted = commands .Select(x => { - return (Command: x.Name, x.Description); + return (Command: string.Join(", ", Array.Empty().Concat([x.Name]).Concat(x.Aliases)), x.Description); }) .ToArray(); maxWidth = formatted.Max(x => x.Command.Length); diff --git a/src/ConsoleAppFramework/ConsoleAppGenerator.cs b/src/ConsoleAppFramework/ConsoleAppGenerator.cs index 14d78c6..99e31de 100644 --- a/src/ConsoleAppFramework/ConsoleAppGenerator.cs +++ b/src/ConsoleAppFramework/ConsoleAppGenerator.cs @@ -144,7 +144,7 @@ internal sealed class ArgumentAttribute : Attribute { } -[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = false)] internal sealed class CommandAttribute : Attribute { public string Command { get; } diff --git a/src/ConsoleAppFramework/Emitter.cs b/src/ConsoleAppFramework/Emitter.cs index 987d8a3..ce5c2b0 100644 --- a/src/ConsoleAppFramework/Emitter.cs +++ b/src/ConsoleAppFramework/Emitter.cs @@ -1,4 +1,5 @@ using Microsoft.CodeAnalysis; +using System.Linq; using System.Reflection.Metadata; namespace ConsoleAppFramework; @@ -447,6 +448,14 @@ void EmitRunBody(ILookup groupedCommands, int depth, bool { var leafCommand = groupedCommands[""].FirstOrDefault(); IDisposable? ifBlcok = null; + if (leafCommand is not null && !leafCommand.Command.Name.Equals("")) + { + // Add or-ing of aliases + foreach (var alias in leafCommand.Command.Aliases) + { + sb.AppendLine($"case \"{alias}\":"); + } + } if (!(groupedCommands.Count == 1 && leafCommand != null)) { ifBlcok = sb.BeginBlock($"if (args.Length == {depth})"); @@ -462,9 +471,15 @@ void EmitRunBody(ILookup groupedCommands, int depth, bool return; } + IEnumerable> aliases = []; + if (leafCommand?.Command.Aliases.Length > 0) + { + aliases = leafCommand.Command.Aliases.SelectMany(lc => commandIds.GroupBy(a => lc)); + } + using (sb.BeginBlock($"switch (args[{depth}])")) { - foreach (var commands in groupedCommands.Where(x => x.Key != "")) + foreach (var commands in groupedCommands.Where(x => x.Key != "").Concat(aliases)) { using (sb.BeginIndent($"case \"{commands.Key}\":")) { diff --git a/src/ConsoleAppFramework/Parser.cs b/src/ConsoleAppFramework/Parser.cs index 019bf15..f37bc91 100644 --- a/src/ConsoleAppFramework/Parser.cs +++ b/src/ConsoleAppFramework/Parser.cs @@ -104,6 +104,15 @@ internal class Parser(DiagnosticReporter context, InvocationExpressionSyntax nod return []; } + List rootAliases = []; + if (type.TypeKind.HasFlag(TypeKind.Class)) + { + foreach (var item in type.GetAttributes().Where(x => x.AttributeClass?.Name == "CommandAttribute")) + { + rootAliases.Add((item.ConstructorArguments[0].Value as string)!); + } + } + var hasIDisposable = type.AllInterfaces.Any(x => SymbolEqualityComparer.Default.Equals(x, wellKnownTypes.IDisposable)); var hasIAsyncDisposable = type.AllInterfaces.Any(x => SymbolEqualityComparer.Default.Equals(x, wellKnownTypes.IAsyncDisposable)); @@ -141,8 +150,9 @@ internal class Parser(DiagnosticReporter context, InvocationExpressionSyntax nod .Select(x => { string commandName; - var commandAttribute = x.GetAttributes().FirstOrDefault(x => x.AttributeClass?.Name == "CommandAttribute"); - if (commandAttribute != null) + var commandAttributes = x.GetAttributes().Where(x => x.AttributeClass?.Name == "CommandAttribute"); + var firstCommandAttribute = commandAttributes.FirstOrDefault(); + if (firstCommandAttribute != null) { commandName = (x.GetAttributes()[0].ConstructorArguments[0].Value as string)!; } @@ -154,6 +164,21 @@ internal class Parser(DiagnosticReporter context, InvocationExpressionSyntax nod var command = ParseFromMethodSymbol(x, false, (commandPath == null) ? commandName : $"{commandPath.Trim()} {commandName}", typeFilters); if (command == null) return null; + List methodAliases = []; + if (commandAttributes.Count() > 0) + { + methodAliases.AddRange(commandAttributes.Select((ca, i) => (x.GetAttributes()[i].ConstructorArguments[0].Value as string)!)); + } + + if (command.IsRootCommand) + { + command.Aliases = methodAliases.Concat(rootAliases).Select(a => NameConverter.ToKebabCase(a)).Distinct().Where(s => !s.Equals(commandName)).ToArray(); + } + else + { + command.Aliases = methodAliases.Select(a => NameConverter.ToKebabCase(a)).Distinct().Where(s => !s.Equals(commandName)).ToArray(); + } + command.CommandMethodInfo = methodInfoBase with { MethodName = x.Name }; return command; }) @@ -367,6 +392,7 @@ internal class Parser(DiagnosticReporter context, InvocationExpressionSyntax nod var cmd = new Command { Name = commandName, + Aliases = [], IsAsync = isAsync, IsVoid = isVoid, Parameters = parameters, @@ -522,6 +548,7 @@ internal class Parser(DiagnosticReporter context, InvocationExpressionSyntax nod var cmd = new Command { Name = commandName, + Aliases = [], IsAsync = isAsync, IsVoid = isVoid, Parameters = parameters, diff --git a/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppBuilderTest.cs b/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppBuilderTest.cs index a6a9bf5..3faa8a9 100644 --- a/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppBuilderTest.cs +++ b/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppBuilderTest.cs @@ -241,6 +241,29 @@ public void Do() verifier.Execute(code, "nomunomu", "yeah"); } + + [Fact] + public void CommandAttrAlias() + { + var code = """ +var builder = ConsoleApp.Create(); +builder.Add(); +builder.Run(args); + +public class MyClass() +{ + [Command("nomunomu")] + [Command("nomunomu2")] + public void Do() + { + Console.Write("yeah"); + } +} +"""; + + verifier.Execute(code, "nomunomu", "yeah"); + verifier.Execute(code, "nomunomu2", "yeah"); + } } diff --git a/tests/ConsoleAppFramework.GeneratorTests/HelpTest.cs b/tests/ConsoleAppFramework.GeneratorTests/HelpTest.cs index 48778a2..3cf938c 100644 --- a/tests/ConsoleAppFramework.GeneratorTests/HelpTest.cs +++ b/tests/ConsoleAppFramework.GeneratorTests/HelpTest.cs @@ -305,4 +305,284 @@ hello my world. """); } + + [Fact] + public void CommandAlias() + { + var code = """ +var app = ConsoleApp.Create(); +app.Add(); +app.Run(args); + +public class MyClass +{ + /// + /// hello my world. + /// + /// -b, my boo is not boo. + /// -f|-fb, my foo is not bar. + [Command("hello-world")] + [Command("hw")] + public void HelloWorld([Argument]int boo, string fooBar) + { + Console.Write("Hello World! " + fooBar); + } +} +"""; + + + verifier.Execute(code, args: "--help", expected: """ +Usage: [command] [-h|--help] [--version] + +Commands: + hello-world, hw hello my world. + +"""); + + + var expectedCommandHelp = """ +Usage: hello-world [arguments...] [options...] [-h|--help] [--version] + +hello my world. + +Arguments: + [0] my boo is not boo. + +Options: + -f|-fb|--foo-bar my foo is not bar. (Required) + +"""; + verifier.Execute(code, args: "hello-world --help", expected: expectedCommandHelp); + verifier.Execute(code, args: "hw --help", expected: expectedCommandHelp); + } + + [Fact] + public void CommandAliasWithRoot() + { + var code = """ +var app = ConsoleApp.Create(); +app.Add(); +app.Run(args); + +public class MyClass +{ + /// + /// hello my world. + /// + /// -b, my boo is not boo. + /// -f|-fb, my foo is not bar. + [Command("")] + [Command("hello-world")] + [Command("hello-world2")] + [Command("hello-world3")] + public void HelloWorld([Argument]int boo, string fooBar) + { + Console.Write("Hello World! " + fooBar); + } + + [Command("hello-our-world")] + public void HelloOurWorld([Argument]int boo, string fooBar) + { + Console.Write("Hello World! " + fooBar); + } +} +"""; + + var expectedCommandHelp = """ +Usage: [command] [arguments...] [options...] [-h|--help] [--version] + +hello my world. + +Arguments: + [0] my boo is not boo. + +Options: + -f|-fb|--foo-bar my foo is not bar. (Required) + +Commands: + hello-our-world + +"""; + + var expectedSubCommand = """ +Usage: [arguments...] [options...] [-h|--help] [--version] + +hello my world. + +Arguments: + [0] my boo is not boo. + +Options: + -f|-fb|--foo-bar my foo is not bar. (Required) + +"""; + + verifier.Execute(code, args: "--help", expected: expectedCommandHelp); + verifier.Execute(code, args: "", expected: expectedSubCommand); + verifier.Execute(code, args: "hello-world --help", expected: expectedSubCommand); + } + + [Fact] + public void CommandAliasClassAttribute() + { + var code = """ +var app = ConsoleApp.Create(); +app.Add(); +app.Run(args); + +[Command("hello-world")] +public class MyClass +{ + /// + /// hello my world. + /// + /// -b, my boo is not boo. + /// -f|-fb, my foo is not bar. + [Command("hw")] + public void HelloWorld([Argument]int boo, string fooBar) + { + Console.Write("Hello World! " + fooBar); + } +} +"""; + + + verifier.Execute(code, args: "--help", expected: """ +Usage: [command] [-h|--help] [--version] + +Commands: + hw hello my world. + +"""); + + + var expectedCommandHelp = """ +Usage: hw [arguments...] [options...] [-h|--help] [--version] + +hello my world. + +Arguments: + [0] my boo is not boo. + +Options: + -f|-fb|--foo-bar my foo is not bar. (Required) + +"""; + verifier.Execute(code, args: "hw --help", expected: expectedCommandHelp); + } + + [Fact] + public void CommandClassAttributeAndRootMethod() + { + var code = """ +var app = ConsoleApp.Create(); +app.Add(); +app.Run(args); + +[Command("hello-world")] +public class MyClass +{ + /// + /// hello my world. + /// + /// -b, my boo is not boo. + /// -f|-fb, my foo is not bar. + [Command("")] + public void HelloWorld([Argument]int boo, string fooBar) + { + Console.Write("Hello World! " + fooBar); + } + + /// + /// hello our world. + /// + /// -b, my boo is not boo. + /// -f|-fb, my foo is not bar. + [Command("hello-our-world")] + public void HelloOurWorld([Argument]int boo, string fooBar) + { + Console.Write("Hello World! " + fooBar); + } +} +"""; + + + verifier.Execute(code, args: "--help", expected: """ +Usage: [command] [arguments...] [options...] [-h|--help] [--version] + +hello my world. + +Arguments: + [0] my boo is not boo. + +Options: + -f|-fb|--foo-bar my foo is not bar. (Required) + +Commands: + hello-our-world hello our world. + +"""); + } + + [Fact] + public void CommandClassAttributeAndMethodCoalescing() + { + var code = """ +var app = ConsoleApp.Create(); +app.Add(); +app.Run(args); + +[Command("hello-world")] +[Command("hw")] +public class MyClass +{ + /// + /// hello my world. + /// + /// -b, my boo is not boo. + /// -f|-fb, my foo is not bar. + public void HelloWorld([Argument]int boo, string fooBar) + { + Console.Write("Hello World! " + fooBar); + } + + /// + /// hello our world. + /// + /// -b, my boo is not boo. + /// -f|-fb, my foo is not bar. + [Command("hello-our-world")] + public void HelloOurWorld([Argument]int boo, string fooBar) + { + Console.Write("Hello World! " + fooBar); + } +} +"""; + + + verifier.Execute(code, args: "--help", expected: """ +Usage: [command] [-h|--help] [--version] + +Commands: + hello-our-world hello our world. + hello-world, hw hello my world. + +"""); + + + var expectedCommandHelp = """ +Usage: hello-world [arguments...] [options...] [-h|--help] [--version] + +hello my world. + +Arguments: + [0] my boo is not boo. + +Options: + -f|-fb|--foo-bar my foo is not bar. (Required) + +"""; + verifier.Execute(code, args: "hello-world --help", expected: expectedCommandHelp); + verifier.Execute(code, args: "hello-our-world --help", expected: expectedCommandHelp); + } }