diff --git a/src/Xcaciv.Command.Core/AbstractCommand.cs b/src/Xcaciv.Command.Core/AbstractCommand.cs index 6dcd508..b309d15 100644 --- a/src/Xcaciv.Command.Core/AbstractCommand.cs +++ b/src/Xcaciv.Command.Core/AbstractCommand.cs @@ -35,9 +35,24 @@ public virtual void Help(IIoContext outputContext) /// public virtual void OneLineHelp(IIoContext outputContext) { - var baseCommand = Attribute.GetCustomAttribute(GetType(), typeof(CommandRegisterAttribute)) as CommandRegisterAttribute; - if (baseCommand != null) + var thisType = GetType(); + var baseCommand = Attribute.GetCustomAttribute(thisType, typeof(CommandRegisterAttribute)) as CommandRegisterAttribute; + if (baseCommand == null) + { + outputContext.AddTraceMessage($"no base command for {outputContext.Name}"); + return; + } + + if (Attribute.GetCustomAttribute(thisType, typeof(CommandRootAttribute)) is CommandRootAttribute) + { + outputContext.OutputChunk($"\t{baseCommand.Command,-12} {baseCommand.Description}"); + } + else + { outputContext.OutputChunk($"{baseCommand.Command,-12} {baseCommand.Description}"); + } + + } /// /// create a nicely formated @@ -55,6 +70,8 @@ protected virtual string BuildHelpString() // TODO: extract a help formatter so it can be customized var builder = new StringBuilder(); + if (Attribute.GetCustomAttribute(thisType, typeof(CommandRootAttribute)) is CommandRootAttribute rootCommand) + builder.Append($"{rootCommand.Command} {Environment.NewLine}\t"); builder.AppendLine($"{baseCommand?.Command}:"); builder.AppendLine($" {baseCommand?.Description}"); builder.AppendLine("Usage:"); diff --git a/src/Xcaciv.Command.Core/Xcaciv.Command.Core.csproj b/src/Xcaciv.Command.Core/Xcaciv.Command.Core.csproj index 3286782..30c3729 100644 --- a/src/Xcaciv.Command.Core/Xcaciv.Command.Core.csproj +++ b/src/Xcaciv.Command.Core/Xcaciv.Command.Core.csproj @@ -3,7 +3,7 @@ enable enable - 1.0.4 + 1.0.6 Xcaciv.Command.Core Xcaciv.Command.Core True diff --git a/src/Xcaciv.Command.FileLoader/Crawler.cs b/src/Xcaciv.Command.FileLoader/Crawler.cs index 6839f12..b24c17b 100644 --- a/src/Xcaciv.Command.FileLoader/Crawler.cs +++ b/src/Xcaciv.Command.FileLoader/Crawler.cs @@ -68,13 +68,16 @@ public IDictionary LoadPackageDescriptions(string ba try { var newDescription = CommandParameters.CreatePackageDescription(commandType, packagDesc); - if (newDescription.SubCommands.Count > 0 && commands.TryGetValue(commandType.Name, out ICommandDescription? description)) + + // when it is a sub command, we need to add it to a parent if it already exists + if (newDescription.SubCommands.Count > 0 && commands.TryGetValue(newDescription.BaseCommand, out ICommandDescription? description)) { - var subCommand = description.SubCommands.First().Value; - description.SubCommands.Add(subCommand.BaseCommand, subCommand); + var newSubCommand = newDescription.SubCommands.First().Value; + description.SubCommands[newSubCommand.BaseCommand] = newSubCommand; } else { + // when the parent command does not exist, add it to the list commands[newDescription.BaseCommand] = newDescription; } } @@ -87,6 +90,7 @@ public IDictionary LoadPackageDescriptions(string ba packagDesc.Commands = commands; } + // dont add packages without valid commands if (packagDesc.Commands.Count > 0) packages.TryAdd(key, packagDesc); }); diff --git a/src/Xcaciv.Command.Interface/ICommandController.cs b/src/Xcaciv.Command.Interface/ICommandController.cs index b1eac02..53de5e4 100644 --- a/src/Xcaciv.Command.Interface/ICommandController.cs +++ b/src/Xcaciv.Command.Interface/ICommandController.cs @@ -30,5 +30,26 @@ public interface ICommandController /// /// void GetHelp(string command, IIoContext output); + /// + /// install a single command into the index + /// + /// + void AddCommand(ICommandDescription command); + /// + /// add a command from a loaded type + /// good for commands from internal or linked dlls + /// + /// + /// + /// + void AddCommand(string packageKey, Type commandType, bool modifiesEnvironment = false); + /// + /// add a command from an instance of the command + /// good for commands from internal or linked dlls + /// + /// + /// + /// + void AddCommand(string packageKey, ICommandDelegate command, bool modifiesEnvironment = false); } } \ No newline at end of file diff --git a/src/Xcaciv.Command.Interface/Xcaciv.Command.Interface.csproj b/src/Xcaciv.Command.Interface/Xcaciv.Command.Interface.csproj index 5df5ceb..0d0e9dc 100644 --- a/src/Xcaciv.Command.Interface/Xcaciv.Command.Interface.csproj +++ b/src/Xcaciv.Command.Interface/Xcaciv.Command.Interface.csproj @@ -3,7 +3,7 @@ net8.0 enable enable - 1.1.19 + 1.1.21 True Xcaciv.Command.Interface Xcaciv.Command.Interface diff --git a/src/Xcaciv.Command.Tests/CommandControllerTests.cs b/src/Xcaciv.Command.Tests/CommandControllerTests.cs index ddf7dcb..fbbfff3 100644 --- a/src/Xcaciv.Command.Tests/CommandControllerTests.cs +++ b/src/Xcaciv.Command.Tests/CommandControllerTests.cs @@ -10,6 +10,7 @@ using Xcaciv.Command.FileLoader; using System.IO.Abstractions.TestingHelpers; using Moq; +using Xcaciv.Command.Tests.TestImpementations; namespace Xcaciv.Command.Tests { @@ -74,8 +75,21 @@ public async Task PipeCommandsTestAsync() // verify the output of the first run // by looking at the output of the second output line - Assert.Equal(":d2hhdC13aGF0:-:aXMtaXM=:-:dXAtdXA=:", textio.ToString()); + Assert.Equal(":d2hhdC13aGF0:\r\n:aXMtaXM=:\r\n:dXAtdXA=:", textio.ToString()); } + [Fact()] + public void LoadCommandsTest() + { + var controller = new CommandControllerTestHarness(new Crawler(), @"..\..\..\..\..\"); + controller.AddPackageDirectory(commandPackageDir); + controller.EnableDefaultCommands(); + controller.LoadCommands(string.Empty); + + var commands = controller.GetCommands(); + + Assert.Equal(2, commands["DO"]?.SubCommands.Count); + } + #pragma warning disable CS8602 // Dereference of a possibly null reference. [Fact()] public void LoadDefaultCommandsTest() @@ -89,6 +103,25 @@ public void LoadDefaultCommandsTest() // Note: currently Loader is not unloading assemblies for performance reasons Assert.Contains("REGIF", textio.ToString()); } + [Fact()] + public void HelpCommandsTestAsync() + { + var controller = new CommandControllerTestHarness(new Crawler(), @"..\..\..\..\..\") as Interface.ICommandController; + controller.EnableDefaultCommands(); + + controller.AddPackageDirectory(commandPackageDir); + controller.LoadCommands(string.Empty); + + var textio = new TestImpementations.TestTextIo(); + //var env = new EnvironmentContext(); + controller.GetHelp(string.Empty, textio); + var output = textio.ToString(); + + //BROKEN + + // Note: currently Loader is not unloading assemblies for performance reasons + Assert.Contains("SUB DO say", output); + } #pragma warning restore CS8602 // Dereference of a possibly null reference. } } \ No newline at end of file diff --git a/src/Xcaciv.Command.Tests/Commands/RegifCommandTests.cs b/src/Xcaciv.Command.Tests/Commands/RegifCommandTests.cs index 830d42d..651a57a 100644 --- a/src/Xcaciv.Command.Tests/Commands/RegifCommandTests.cs +++ b/src/Xcaciv.Command.Tests/Commands/RegifCommandTests.cs @@ -40,7 +40,7 @@ public async Task HandleExecutionTestAsync() // verify the output of the first run // by looking at the output of the second output line - Assert.Equal("-is-", textio.ToString()); + Assert.Equal("is", textio.ToString().Trim()); } } } \ No newline at end of file diff --git a/src/Xcaciv.Command.Tests/TestImpementations/CommandControllerTestHarness.cs b/src/Xcaciv.Command.Tests/TestImpementations/CommandControllerTestHarness.cs new file mode 100644 index 0000000..5459acb --- /dev/null +++ b/src/Xcaciv.Command.Tests/TestImpementations/CommandControllerTestHarness.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xcaciv.Command.FileLoader; +using Xcaciv.Command.Interface; + +namespace Xcaciv.Command.Tests.TestImpementations +{ + internal class CommandControllerTestHarness : CommandController + { + /// + /// Command Manger + /// + public CommandControllerTestHarness() : base(new Crawler()) { } + /// + /// Command Manger + /// + public CommandControllerTestHarness(ICrawler crawler) : base(crawler) { } + + /// + /// Command Manager constructor to specify restricted directory + /// + /// + public CommandControllerTestHarness(ICrawler crawler, string restrictedDirectory) : base(crawler, restrictedDirectory) { } + /// + /// Command Manager test constructor + /// + /// + public CommandControllerTestHarness(IVerfiedSourceDirectories packageBinearyDirectories) : base(packageBinearyDirectories) { } + + internal Dictionary GetCommands() + { + return this.Commands; + } + } +} diff --git a/src/Xcaciv.Command.Tests/TestImpementations/TestTextIo.cs b/src/Xcaciv.Command.Tests/TestImpementations/TestTextIo.cs index 2d0e3b1..2da2201 100644 --- a/src/Xcaciv.Command.Tests/TestImpementations/TestTextIo.cs +++ b/src/Xcaciv.Command.Tests/TestImpementations/TestTextIo.cs @@ -90,7 +90,7 @@ public override string ToString() } } - output += string.Join('-', Output); + output += string.Join(Environment.NewLine, Output); return output; } diff --git a/src/Xcaciv.Command/CommandController.cs b/src/Xcaciv.Command/CommandController.cs index 18fd48e..08b7319 100644 --- a/src/Xcaciv.Command/CommandController.cs +++ b/src/Xcaciv.Command/CommandController.cs @@ -127,7 +127,7 @@ public void AddCommand(string packageKey, ICommandDelegate command, bool modifie /// /// /// - public void AddCommand(string packageKey, Type commandType, bool modifiesEnvironment) + public void AddCommand(string packageKey, Type commandType, bool modifiesEnvironment = false) { if (Attribute.GetCustomAttribute(commandType, typeof(CommandRegisterAttribute)) is CommandRegisterAttribute attributes) { @@ -138,7 +138,7 @@ public void AddCommand(string packageKey, Type commandType, bool modifiesEnviron PackageDescription = new PackageDescription() { Name = packageKey, - FullPath = "" + FullPath = commandType.Assembly.Location }, ModifiesEnvironment = modifiesEnvironment }); @@ -299,14 +299,15 @@ protected static ICommandDelegate GetCommandInstance(ICommandDescription command protected static ICommandDelegate GetCommandInstance(string fullTypeName, string packagePath) { + if (String.IsNullOrEmpty(fullTypeName)) throw new InvalidOperationException("Command type name is empty."); Type? executeDeligateType = Type.GetType(fullTypeName); ICommandDelegate commandInstance; if (executeDeligateType == null) { - using (var context = new AssemblyContext(packagePath, basePathRestriction:"*")) // TODO: restrict the path - { - commandInstance = context.CreateInstance(fullTypeName); - } + if (String.IsNullOrEmpty(packagePath)) throw new InvalidOperationException($"Command [{fullTypeName}] is not loaded and no assembly was defined."); + + using var context = new AssemblyContext(packagePath, basePathRestriction: "*"); // TODO: restrict the path + commandInstance = context.CreateInstance(fullTypeName); } else { @@ -332,11 +333,37 @@ protected static async Task ExecuteCommand(IIoContext ioContext, ICommandDelegat public void GetHelp(string command, IIoContext context) { if (String.IsNullOrEmpty(command)) - foreach(var description in Commands) + { + foreach (var description in Commands) { - var cmdInsance = GetCommandInstance(description.Value); - cmdInsance.OneLineHelp(context); + if (String.IsNullOrEmpty(description.Value.FullTypeName)) + { + if (description.Value.SubCommands.Count > 0) + { + // get the first sub command to get the type to get the root command + var subCmd = GetCommandInstance(description.Value.SubCommands.First().Value); + + if (subCmd != null && Attribute.GetCustomAttribute(subCmd.GetType(), typeof(CommandRootAttribute)) is CommandRootAttribute rootAttribute) + { + context.OutputChunk($"{rootAttribute.Command,-12} {rootAttribute.Description}"); + } + + foreach (var subCommand in description.Value.SubCommands) + { + outputOneLineHelp(context, subCommand.Value); + } + } + else + { + context.AddTraceMessage($"No type name registered for command: {description.Key}"); + } + } + else + { + outputOneLineHelp(context, description.Value); + } } + } else { try @@ -361,4 +388,9 @@ public void GetHelp(string command, IIoContext context) } } + protected static void outputOneLineHelp(IIoContext context, ICommandDescription description) + { + var cmdInsance = GetCommandInstance(description); + cmdInsance.OneLineHelp(context); + } } diff --git a/src/Xcaciv.Command/Xcaciv.Command.csproj b/src/Xcaciv.Command/Xcaciv.Command.csproj index 2bf0f1a..a8abadc 100644 --- a/src/Xcaciv.Command/Xcaciv.Command.csproj +++ b/src/Xcaciv.Command/Xcaciv.Command.csproj @@ -2,7 +2,7 @@ enable enable - 1.4.19 + 1.4.23 Xcaciv.Command Xcaciv.Command True diff --git a/src/zTestCommandPackage/EchoDoCommand.cs b/src/zTestCommandPackage/DoEchoCommand.cs similarity index 83% rename from src/zTestCommandPackage/EchoDoCommand.cs rename to src/zTestCommandPackage/DoEchoCommand.cs index 0b8153b..f00f9e1 100644 --- a/src/zTestCommandPackage/EchoDoCommand.cs +++ b/src/zTestCommandPackage/DoEchoCommand.cs @@ -10,8 +10,8 @@ namespace zTestCommandPackage { [CommandRoot("do", "does stuff")] - [CommandRegister("ECHO", "echoes stuff to test subcommands")] - public class EchoDoCommand : AbstractCommand, ICommandDelegate + [CommandRegister("ECHO", "SUB DO echo")] + public class DoEchoCommand : AbstractCommand, ICommandDelegate { public override string HandleExecution(string[] parameters, IEnvironmentContext env) { diff --git a/src/zTestCommandPackage/DoSayCommand.cs b/src/zTestCommandPackage/DoSayCommand.cs new file mode 100644 index 0000000..a263713 --- /dev/null +++ b/src/zTestCommandPackage/DoSayCommand.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xcaciv.Command.Core; +using Xcaciv.Command.Interface; +using Xcaciv.Command.Interface.Attributes; + +namespace zTestCommandPackage +{ + [CommandRoot("do", "does stuff")] + [CommandRegister("SAY", "SUB DO say")] + public class DoSayCommand : AbstractCommand + { + public override string HandleExecution(string[] parameters, IEnvironmentContext env) + { + return String.Join(' ', parameters); + } + + public override string HandlePipedChunk(string pipedChunk, string[] parameters, IEnvironmentContext env) + { + return pipedChunk + String.Join(' ', parameters); + } + } +}