From 0b1d89ec6e5903ce11f55b7da554ba35e8951c5c Mon Sep 17 00:00:00 2001 From: Nick Lucas Date: Wed, 28 Dec 2016 20:10:09 +0000 Subject: [PATCH] Closes #18 - update Help Generator with more details for newer features. Still a little rough around the edges, but more complete --- ...mLinqExtensions.cs => CustomExtensions.cs} | 6 +- src/EntryPoint/OptionModel/Help.cs | 91 +++++++++++++++++-- src/EntryPoint/OptionModel/ModelOperand.cs | 4 + src/EntryPoint/OptionModel/ModelOption.cs | 5 + src/EntryPoint/Parsing/Mapper.cs | 5 + src/EntryPoint/Parsing/Parser.cs | 3 +- .../HelpWithRequiredArgsModel.cs | 4 + test/EntryPointTests/HelpTests.cs | 16 +++- test/Example/Program.cs | 30 ++---- test/Example/Properties/launchSettings.json | 15 +++ 10 files changed, 144 insertions(+), 35 deletions(-) rename src/EntryPoint/Internals/{CustomLinqExtensions.cs => CustomExtensions.cs} (79%) create mode 100644 test/Example/Properties/launchSettings.json diff --git a/src/EntryPoint/Internals/CustomLinqExtensions.cs b/src/EntryPoint/Internals/CustomExtensions.cs similarity index 79% rename from src/EntryPoint/Internals/CustomLinqExtensions.cs rename to src/EntryPoint/Internals/CustomExtensions.cs index 1dc036a..ac9e2e4 100644 --- a/src/EntryPoint/Internals/CustomLinqExtensions.cs +++ b/src/EntryPoint/Internals/CustomExtensions.cs @@ -5,7 +5,7 @@ namespace EntryPoint.Internals { - internal static class CustomLinqExtensions { + internal static class CustomExtensions { internal static List Duplicates(this IEnumerable items, IEqualityComparer comparer = null) { HashSet hash = new HashSet(comparer); List result = new List(); @@ -20,6 +20,10 @@ internal static List Duplicates(this IEnumerable items, IEqualityCompar return result; } + + internal static T IfTrue(this bool b, T show) { + return b ? show : default(T); + } } } diff --git a/src/EntryPoint/OptionModel/Help.cs b/src/EntryPoint/OptionModel/Help.cs index bba03e6..19936a0 100644 --- a/src/EntryPoint/OptionModel/Help.cs +++ b/src/EntryPoint/OptionModel/Help.cs @@ -1,7 +1,9 @@ -using EntryPoint.Parsing; +using EntryPoint.Internals; +using EntryPoint.Parsing; using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; using System.Text; using System.Threading.Tasks; @@ -12,9 +14,14 @@ internal static class Help { // based on information provided by the ApplicationModel public static string Generate(Model model) { StringBuilder builder = new StringBuilder(); - + var options = model.Options.OrderBy(mo => mo.Definition.ShortName).ToList(); + var operands = model.Operands.OrderBy(mo => mo.Definition.Position).ToList(); + + // Header section if (model.ApplicationOptions.UtilityName.Length > 0) { - builder.AppendLine(model.ApplicationOptions.UtilityName); + string utilityName = model.ApplicationOptions.UtilityName; + string version = MainAssembly().GetName().Version.ToString().TrimEnd('.', '0'); + builder.AppendLine($"{utilityName} v{version} Documentation"); builder.AppendLine(); } if (model.Help.Detail.Length > 0) { @@ -22,16 +29,82 @@ public static string Generate(Model model) { builder.AppendLine(); } - foreach (var option in model.Options.OrderBy(mo => mo.Definition.ShortName)) { - builder.AppendLine($" -{option.Definition.ShortName} --{option.Definition.LongName}"); - builder.AppendLine($" {option.Help.Detail}"); - builder.AppendLine(); - } - builder.AppendLine("Generated by EntryPoint"); + builder.AppendLine(GenerateUsageSummary(options, operands)); + builder.AppendLine(); + builder.AppendLine(GenerateBreakdown(options, operands)); return builder.ToString(); } + static Assembly MainAssembly() { + return Assembly.GetEntryAssembly(); + } + + static string GenerateUsageSummary(List options, List operands) { + string utilityName = MainAssembly().GetName().Name; + return $" Usage:\n {utilityName} [ -o | --option ] [ -p VALUE | --parameter VALUE ] [ operands ]"; + } + + static string GenerateBreakdown(List options, List operands) { + StringBuilder breakdown = new StringBuilder(); + + // For Options + foreach (var option in options) { + string shortName = $"-{option.Definition.ShortName.ToString()} "; + string longName = $"--{option.Definition.LongName} "; + string parameterString = GetParameterString(option); + string requiredString = option.Required.IfTrue("REQUIRED"); + + breakdown.AppendLine($" {shortName}{longName}{parameterString}{requiredString}"); + breakdown.AppendLine($" {option.Help.Detail}"); + breakdown.AppendLine(); + } + + // For Operands + foreach (var operand in operands) { + string position = $"{operand.Definition.Position.ToString()} "; + string type = GetOperandTypeString(operand); + breakdown.AppendLine($" [Operand {position}{type}]"); + breakdown.AppendLine($" {operand.Help.Detail}"); + } + + return breakdown.ToString(); + } + + static string GetParameterString(ModelOption option) { + if (option.TakesParameter) { + Type type = option.Property.PropertyType; + return $"[{GetTypeSummary(type)}] "; + } + return ""; + } + static string GetOperandTypeString(ModelOperand operand) { + Type type = operand.Property.PropertyType; + return $"[{GetTypeSummary(type)}] "; + } + static string GetTypeSummary(Type type) { + if (type == typeof(Enum)) { + var names = Enum + .GetNames(type) + .Select(s => s.ToUpper()); + return string.Join("|", names); + } else { + return type.Name.ToUpper(); + } + } + + static string GetNamesSummary(ModelOption option) { + bool hasShort = option.Definition.ShortName > char.MinValue; + bool hasLong = option.Definition.LongName.Length > 0; + + if (hasShort && hasLong) { + return $"-{option.Definition.ShortName} | --{option.Definition.LongName} "; + } else if (hasShort) { + return $"-{option.Definition.ShortName} "; + } else { + return $"--{option.Definition.LongName} "; + } + } } } diff --git a/src/EntryPoint/OptionModel/ModelOperand.cs b/src/EntryPoint/OptionModel/ModelOperand.cs index 3d19f73..825cd50 100644 --- a/src/EntryPoint/OptionModel/ModelOperand.cs +++ b/src/EntryPoint/OptionModel/ModelOperand.cs @@ -14,6 +14,7 @@ public ModelOperand(PropertyInfo property) { Property = property; Definition = property.GetOperandDefinition(); Required = property.HasRequiredAttribute(); + Help = property.GetHelp(); } // The original property on the ApplicationOptions implementation @@ -27,5 +28,8 @@ public ModelOperand(PropertyInfo property) { // Whether the Option is required public bool Required { get; private set; } + + // Help attribute + public HelpAttribute Help { get; internal set; } } } diff --git a/src/EntryPoint/OptionModel/ModelOption.cs b/src/EntryPoint/OptionModel/ModelOption.cs index d3cd79f..c376487 100644 --- a/src/EntryPoint/OptionModel/ModelOption.cs +++ b/src/EntryPoint/OptionModel/ModelOption.cs @@ -24,6 +24,11 @@ internal ModelOption(PropertyInfo property) { // Option configuration public BaseOptionAttribute Definition { get; private set; } + public bool TakesParameter { + get { + return Definition is OptionParameterAttribute; + } + } // Provided documentation public HelpAttribute Help { get; private set; } diff --git a/src/EntryPoint/Parsing/Mapper.cs b/src/EntryPoint/Parsing/Mapper.cs index 82c755b..a79c40d 100644 --- a/src/EntryPoint/Parsing/Mapper.cs +++ b/src/EntryPoint/Parsing/Mapper.cs @@ -68,6 +68,11 @@ static void HandleUnusedOptions(Model model, List usedOptions) { } static void HandleUnusedOperands(Model model, ParseResult parseResult) { + if (model.ApplicationOptions.HelpRequested) { + // If the help flag is set, then Required parameters are irrelevant + return; + } + int providedOperandsCount = parseResult.Operands.Count; var requiredOperand = model.Operands diff --git a/src/EntryPoint/Parsing/Parser.cs b/src/EntryPoint/Parsing/Parser.cs index 0af637e..6713f6f 100644 --- a/src/EntryPoint/Parsing/Parser.cs +++ b/src/EntryPoint/Parsing/Parser.cs @@ -18,8 +18,7 @@ internal static ParseResult MakeParseResult(List tokens, Model model) { if (token.IsOption) { queue.Dequeue(); - // TODO: refactor out knowledge of OptionParameterAttribute - bool takesParameter = model.FindOptionByToken(token).Definition is OptionParameterAttribute; + bool takesParameter = model.FindOptionByToken(token).TakesParameter; Token argument = null; if (takesParameter) { AssertParameterExists(token, queue); diff --git a/test/EntryPointTests/AppOptionModels/HelpWithRequiredArgsModel.cs b/test/EntryPointTests/AppOptionModels/HelpWithRequiredArgsModel.cs index 411ef71..c3ebcae 100644 --- a/test/EntryPointTests/AppOptionModels/HelpWithRequiredArgsModel.cs +++ b/test/EntryPointTests/AppOptionModels/HelpWithRequiredArgsModel.cs @@ -20,5 +20,9 @@ public HelpWithRequiredArgsModel() : base("APP_NAME") { } ShortName = 'o')] [Help("PARAM_OPTIONAL_HELP_STRING")] public string StringOption { get; set; } + + [Required] + [Operand(1)] + public string OneOperand { get; set; } } } diff --git a/test/EntryPointTests/HelpTests.cs b/test/EntryPointTests/HelpTests.cs index 66b2e76..03b62a0 100644 --- a/test/EntryPointTests/HelpTests.cs +++ b/test/EntryPointTests/HelpTests.cs @@ -20,10 +20,22 @@ public void Help_CheckRequiredDoesNotThrow_Std() { EntryPointApi.Parse(args); } + [Fact] + public void Help_CheckRequiredDoesNotThrow_Operand() { + string[] args = new string[] { + "-r", "1", "--help" + }; + + // Check this doesn't throw because of Required validation + // Also check it doesn't throw because of an option being included + // Behaviour: --help will take control + EntryPointApi.Parse(args); + } + [Fact] public void Help_CheckRequiredDoesNotThrow_OtherParams() { string[] args = new string[] { - "-o", "name", "--help" + "-o", "name", "--help", "operand_value" }; // Check this doesn't throw because of Required validation @@ -35,7 +47,7 @@ public void Help_CheckRequiredDoesNotThrow_OtherParams() { [Fact] public void Help_CheckRequiredDoesNotThrow_RequiredProvided() { string[] args = new string[] { - "-r", "1", "--help" + "-r", "1", "--help", "operand_value" }; // Check this doesn't throw because of an option being included diff --git a/test/Example/Program.cs b/test/Example/Program.cs index 8cf0a74..45218af 100644 --- a/test/Example/Program.cs +++ b/test/Example/Program.cs @@ -9,26 +9,16 @@ public static void Main(string[] args) { // CLI command: // ApplicationName -oq -re FirstItem --string Bob -n=1.2 "the operand" Console.WriteLine("For the following command: "); - Console.WriteLine("ApplicationName -oq -re FirstItem --string Bob -n=1.2 \"the operand\"\n"); - if (!args.Any()) { - args = new string[] { - // short options can be grouped - "-ab", - - // the last short option in a group - // can be an option parameter - "-ce", "FirstItem", - - // option parameters can be whitespace or = separated - "--string", "Bob", - "-n=1.2", - - "the operand" - }; - } + Console.WriteLine("ApplicationName " + string.Join(" ", args)); // Parses arguments based on a declarative BaseApplicationOptions implementation (below) ApplicationOptions a = EntryPointApi.Parse(args); + if (a.HelpRequested) { + Console.WriteLine(EntryPointApi.GenerateHelp()); + Console.WriteLine("Enter to exit..."); + Console.ReadLine(); + return; + } Console.WriteLine($"a: {a.Option1}"); Console.WriteLine($"b: {a.Option2}"); @@ -39,10 +29,6 @@ public static void Main(string[] args) { Console.WriteLine($"first operand: {a.Operand1}"); Console.WriteLine($"other operands: {string.Join(" : ", a.Operands)}"); - // Contains a built in documentation generator - //Console.WriteLine("\n\nHelp Documentation: \n"); - //EntryPointApi.Parse(new string[] { "--help" }); - Console.Read(); } } @@ -75,6 +61,7 @@ public ApplicationOptions() : base("Example Project") { } public decimal DecimalArg { get; set; } // Also supports named and numbered enums + [Required] [OptionParameter( ShortName = 'e', LongName = "app-enum")] [Help( @@ -91,6 +78,7 @@ public ApplicationOptions() : base("Example Project") { } // Operands are always dumped into the BaseApplicationModel.Operands list // But Positional Operands can also be mapped directly + [Required] [Operand(position: 1)] [Help( "The first Operand after all Options and OptionParameters")] diff --git a/test/Example/Properties/launchSettings.json b/test/Example/Properties/launchSettings.json new file mode 100644 index 0000000..e8925bf --- /dev/null +++ b/test/Example/Properties/launchSettings.json @@ -0,0 +1,15 @@ +{ + "profiles": { + "Example": { + "commandName": "Project" + }, + "Help": { + "commandName": "Project", + "commandLineArgs": "--help" + }, + "TestArgs": { + "commandName": "Project", + "commandLineArgs": "-oq -re FirstItem --string Bob -n=1.2 \"the operand\" \n" + } + } +} \ No newline at end of file