Skip to content

Commit

Permalink
Closes #18 - update Help Generator with more details for newer featur…
Browse files Browse the repository at this point in the history
…es. Still a little rough around the edges, but more complete
  • Loading branch information
Nick-Lucas committed Dec 28, 2016
1 parent 48cf834 commit 0b1d89e
Show file tree
Hide file tree
Showing 10 changed files with 144 additions and 35 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

namespace EntryPoint.Internals {

internal static class CustomLinqExtensions {
internal static class CustomExtensions {
internal static List<T> Duplicates<T>(this IEnumerable<T> items, IEqualityComparer<T> comparer = null) {
HashSet<T> hash = new HashSet<T>(comparer);
List<T> result = new List<T>();
Expand All @@ -20,6 +20,10 @@ internal static List<T> Duplicates<T>(this IEnumerable<T> items, IEqualityCompar

return result;
}

internal static T IfTrue<T>(this bool b, T show) {
return b ? show : default(T);
}
}

}
91 changes: 82 additions & 9 deletions src/EntryPoint/OptionModel/Help.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -12,26 +14,97 @@ 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) {
builder.AppendLine(model.Help.Detail);
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<ModelOption> options, List<ModelOperand> operands) {
string utilityName = MainAssembly().GetName().Name;
return $" Usage:\n {utilityName} [ -o | --option ] [ -p VALUE | --parameter VALUE ] [ operands ]";
}

static string GenerateBreakdown(List<ModelOption> options, List<ModelOperand> 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} ";
}
}
}

}
4 changes: 4 additions & 0 deletions src/EntryPoint/OptionModel/ModelOperand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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; }
}
}
5 changes: 5 additions & 0 deletions src/EntryPoint/OptionModel/ModelOption.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down
5 changes: 5 additions & 0 deletions src/EntryPoint/Parsing/Mapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@ static void HandleUnusedOptions(Model model, List<TokenGroup> 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
Expand Down
3 changes: 1 addition & 2 deletions src/EntryPoint/Parsing/Parser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@ internal static ParseResult MakeParseResult(List<Token> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
}
}
16 changes: 14 additions & 2 deletions test/EntryPointTests/HelpTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,22 @@ public void Help_CheckRequiredDoesNotThrow_Std() {
EntryPointApi.Parse<HelpWithRequiredArgsModel>(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<HelpWithRequiredArgsModel>(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
Expand All @@ -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
Expand Down
30 changes: 9 additions & 21 deletions test/Example/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ApplicationOptions>(args);
if (a.HelpRequested) {
Console.WriteLine(EntryPointApi.GenerateHelp<ApplicationOptions>());
Console.WriteLine("Enter to exit...");
Console.ReadLine();
return;
}

Console.WriteLine($"a: {a.Option1}");
Console.WriteLine($"b: {a.Option2}");
Expand All @@ -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<ApplicationOptions>(new string[] { "--help" });

Console.Read();
}
}
Expand Down Expand Up @@ -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(
Expand All @@ -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")]
Expand Down
15 changes: 15 additions & 0 deletions test/Example/Properties/launchSettings.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
}

0 comments on commit 0b1d89e

Please sign in to comment.