From 4243a6045bd9c30e9ca475869a1cb18ebbb53f5f Mon Sep 17 00:00:00 2001 From: Andrey Akinshin Date: Fri, 7 Jul 2023 14:24:46 +0200 Subject: [PATCH] Improve build script option parsing --- .../CommandLineParser.cs | 359 +++++++++++------- build/BenchmarkDotNet.Build/HelpInfo.cs | 8 + build/BenchmarkDotNet.Build/IHelpProvider.cs | 6 + build/BenchmarkDotNet.Build/Program.cs | 14 +- 4 files changed, 244 insertions(+), 143 deletions(-) create mode 100644 build/BenchmarkDotNet.Build/HelpInfo.cs create mode 100644 build/BenchmarkDotNet.Build/IHelpProvider.cs diff --git a/build/BenchmarkDotNet.Build/CommandLineParser.cs b/build/BenchmarkDotNet.Build/CommandLineParser.cs index 6f8af08a8c..122dffa1a8 100644 --- a/build/BenchmarkDotNet.Build/CommandLineParser.cs +++ b/build/BenchmarkDotNet.Build/CommandLineParser.cs @@ -8,8 +8,89 @@ namespace BenchmarkDotNet.Build; public class CommandLineParser { + private const string ScriptName = "build.cmd"; + public static readonly CommandLineParser Instance = new(); + public string[]? Parse(string[]? args) + { + if (args == null || args.Length == 0 || (args.Length == 1 && Is(args[0], "help", "--help", "-h"))) + { + PrintHelp(); + return null; + } + + if (Is(args[0], "cake")) + return args.Skip(1).ToArray(); + + var argsToProcess = new Queue(args); + + var taskName = argsToProcess.Dequeue(); + if (Is(taskName, "-t", "--target") && argsToProcess.Any()) + taskName = argsToProcess.Dequeue(); + + taskName = taskName.Replace("-", ""); + + var taskNames = GetTaskNames(); + if (!taskNames.Contains(taskName)) + { + PrintError($"'{taskName}' is not a task"); + return null; + } + + if (argsToProcess.Count == 1 && Is(argsToProcess.Peek(), "-h", "--help")) + { + PrintTaskHelp(taskName); + return null; + } + + var cakeArgs = new List + { + "--target", + taskName + }; + while (argsToProcess.Any()) + { + var arg = argsToProcess.Dequeue(); + + var matched = false; + foreach (var option in options) + { + if (Is(arg, option.ShortName, option.FullName)) + { + matched = true; + cakeArgs.Add(option.CakeOption); + if (option.Arg != "") + { + if (!argsToProcess.Any()) + { + PrintError(option.FullName + " is not specified"); + return null; + } + + cakeArgs.Add(argsToProcess.Dequeue()); + } + } + } + + if (arg.StartsWith("/p:")) + { + matched = true; + cakeArgs.Add("--msbuild"); + cakeArgs.Add(arg[3..]); + } + + if (!matched) + { + PrintError("Unknown option: " + arg); + return null; + } + } + + return cakeArgs.ToArray(); + } + + private record Option(string ShortName, string FullName, string Arg, string Description, string CakeOption); private readonly Option[] options = @@ -23,37 +104,45 @@ private record Option(string ShortName, string FullName, string Arg, string Desc "--exclusive", "", "Executes the target task without any dependencies", - "--exclusive") + "--exclusive"), + new("-h", + "--help", + "", + "Prints help information for the target task", + "") }; - private void PrintHelp(bool skipWelcome = false) + private void PrintHelp() { - const string scriptName = "build.cmd"; - if (!skipWelcome) - { - WriteHeader("Welcome to the BenchmarkDotNet build script!"); - WriteLine(); - } + WriteHeader("Description:"); + + WritePrefix(); + WriteLine("BenchmarkDotNet build script"); + + WritePrefix(); + WriteLine("Task names are case-insensitive, dashes are ignored"); + + WriteLine(); - WriteHeader("USAGE:"); + WriteHeader("Usage:"); WritePrefix(); - Write(scriptName + " "); + Write(ScriptName + " "); WriteTask(" "); WriteOption("[OPTIONS]"); WriteLine(); WriteLine(); - WriteHeader("EXAMPLES:"); + WriteHeader("Examples:"); WritePrefix(); - Write(scriptName + " "); + Write(ScriptName + " "); WriteTask("restore"); WriteLine(); WritePrefix(); - Write(scriptName + " "); + Write(ScriptName + " "); WriteTask("build "); WriteOption("/p:"); WriteArg("Configuration"); @@ -62,24 +151,24 @@ private void PrintHelp(bool skipWelcome = false) WriteLine(); WritePrefix(); - Write(scriptName + " "); + Write(ScriptName + " "); WriteTask("pack "); WriteOption("/p:"); WriteArg("Version"); WriteOption("="); WriteArg("0.1.1729-preview"); WriteLine(); - + WritePrefix(); - Write(scriptName + " "); + Write(ScriptName + " "); WriteTask("unittests "); WriteOption("--exclusive --verbosity "); WriteArg("Diagnostic"); WriteLine(); WritePrefix(); - Write(scriptName + " "); - WriteTask("docsupdate "); + Write(ScriptName + " "); + WriteTask("docs-update "); WriteOption("/p:"); WriteArg("Depth"); WriteOption("="); @@ -88,7 +177,35 @@ private void PrintHelp(bool skipWelcome = false) WriteLine(); - WriteLine("OPTIONS:", ConsoleColor.DarkCyan); + PrintCommonOptions(); + + WriteLine(); + + WriteHeader("Tasks:"); + var taskWidth = GetTaskNames().Max(name => name.Length) + 3; + foreach (var (taskName, taskDescription) in GetTasks()) + { + if (taskName.Equals("Default", StringComparison.OrdinalIgnoreCase)) + continue; + + if (taskDescription.StartsWith("OBSOLETE", StringComparison.OrdinalIgnoreCase)) + { + WriteObsolete(" " + taskName.PadRight(taskWidth)); + WriteObsolete(taskDescription); + } + else + { + WriteTask(" " + taskName.PadRight(taskWidth)); + Write(taskDescription); + } + + WriteLine(); + } + } + + private void PrintCommonOptions() + { + WriteLine("Options:", ConsoleColor.DarkCyan); var shortNameWidth = options.Max(it => it.ShortName.Length); var targetWidth = options.Max(it => it.FullName.Length + it.Arg.Length); @@ -114,7 +231,7 @@ private void PrintHelp(bool skipWelcome = false) WriteLine(); } - + WritePrefix(); WriteOption("/p:"); WriteArg(""); @@ -123,57 +240,72 @@ private void PrintHelp(bool skipWelcome = false) Write(new string(' ', targetWidth + shortNameWidth - 11)); Write("Passes custom properties to MSBuild"); WriteLine(); + } - WriteLine(); + private void PrintTaskHelp(string taskName) + { + var taskType = typeof(BuildContext).Assembly + .GetTypes() + .Where(type => type.IsSubclassOf(typeof(FrostingTask)) && !type.IsAbstract) + .First(type => Is(type.GetCustomAttribute()?.Name, taskName)); + taskName = taskType.GetCustomAttribute()!.Name; + var taskDescription = taskType.GetCustomAttribute()?.Description ?? ""; + var taskInstance = Activator.CreateInstance(taskType); + var helpInfo = taskInstance is IHelpProvider helpProvider ? helpProvider.GetHelp() : new HelpInfo(); - WriteHeader("TASKS:"); - var taskWidth = GetTaskNames().Max(name => name.Length) + 3; - foreach (var (taskName, taskDescription) in GetTasks()) - { - if (taskName.Equals("Default", StringComparison.OrdinalIgnoreCase)) - continue; + WriteHeader("Description:"); - if (taskDescription.StartsWith("OBSOLETE", StringComparison.OrdinalIgnoreCase)) - { - WriteObsolete(" " + taskName.PadRight(taskWidth)); - WriteObsolete(taskDescription); - } - else - { - WriteTask(" " + taskName.PadRight(taskWidth)); - Write(taskDescription); - } + WritePrefix(); + WriteLine($"Task '{taskName}'"); + if (!string.IsNullOrWhiteSpace(taskDescription)) + { + WritePrefix(); + WriteLine(taskDescription); + } - WriteLine(); + foreach (var line in helpInfo.Description) + { + WritePrefix(); + WriteLine(line); } - return; + WriteLine(); - void WritePrefix() => Write(" "); - void WriteTask(string message) => Write(message, ConsoleColor.Green); - void WriteOption(string message) => Write(message, ConsoleColor.Blue); - void WriteArg(string message) => Write(message, ConsoleColor.DarkYellow); - void WriteObsolete(string message) => Write(message, ConsoleColor.Gray); + WriteHeader("Usage:"); - void WriteHeader(string message) - { - WriteLine(message, ConsoleColor.DarkCyan); - } + WritePrefix(); + Write(ScriptName + " "); + WriteTask(taskName + " "); + WriteOption("[OPTIONS]"); + WriteLine(); - void Write(string message, ConsoleColor? color = null) + WriteLine(); + + WriteHeader("Examples:"); + + WritePrefix(); + Write(ScriptName + " "); + WriteTask(taskName); + WriteLine(); + + if (taskName.StartsWith("docs", StringComparison.OrdinalIgnoreCase)) { - if (color != null) - Console.ForegroundColor = color.Value; - Console.Write(message); - if (color != null) - Console.ResetColor(); + WritePrefix(); + Write(ScriptName + " "); + WriteTask("docs-" + taskName[4..].ToLowerInvariant()); + WriteLine(); } - - void WriteLine(string message = "", ConsoleColor? color = null) + else { - Write(message, color); - Console.WriteLine(); + WritePrefix(); + Write(ScriptName + " "); + WriteTask(taskName.ToLowerInvariant()); + WriteLine(); } + + WriteLine(); + + PrintCommonOptions(); } private static HashSet GetTaskNames() @@ -194,98 +326,41 @@ private static HashSet GetTaskNames() .ToList(); } + private static bool Is(string? arg, params string[] values) => + values.Any(value => value.Equals(arg, StringComparison.OrdinalIgnoreCase)); - public string[]? Parse(string[]? args) + private void PrintError(string text) { - if (args == null || args.Length == 0) - { - PrintHelp(); - return null; - } - - if (args.Length == 1) - { - if (IsOneOf(args[0], "help")) - { - PrintHelp(); - return null; - } - - if (IsOneOf(args[0], "help-cake")) - { - new CakeHost().UseContext().Run(new[] { "--help" }); - return null; - } - } - - var argsToProcess = new Queue(args); - - var taskName = argsToProcess.Dequeue(); - if (IsOneOf(taskName, "-t", "--target") && argsToProcess.Any()) - taskName = argsToProcess.Dequeue(); - - var taskNames = GetTaskNames(); - if (!taskNames.Contains(taskName)) - { - PrintError($"'{taskName}' is not a task"); - return null; - } - - var cakeArgs = new List - { - "--target", - taskName - }; - while (argsToProcess.Any()) - { - var arg = argsToProcess.Dequeue(); - - var matched = false; - foreach (var option in options) - { - if (IsOneOf(arg, option.ShortName, option.FullName)) - { - matched = true; - cakeArgs.Add(option.CakeOption); - if (option.Arg != "") - { - if (!argsToProcess.Any()) - { - PrintError(option.FullName + " is not specified"); - return null; - } - - cakeArgs.Add(argsToProcess.Dequeue()); - } - } - } + Console.ForegroundColor = ConsoleColor.Red; + Console.Error.WriteLine("ERROR: " + text); + Console.WriteLine(); + Console.ResetColor(); + PrintHelp(); + } - if (arg.StartsWith("/p:")) - { - matched = true; - cakeArgs.Add("--msbuild"); - cakeArgs.Add(arg[3..]); - } + private void WritePrefix() => Write(" "); + private void WriteTask(string message) => Write(message, ConsoleColor.Green); + private void WriteOption(string message) => Write(message, ConsoleColor.Blue); + private void WriteArg(string message) => Write(message, ConsoleColor.DarkYellow); + private void WriteObsolete(string message) => Write(message, ConsoleColor.Gray); - if (!matched) - { - PrintError("Unknown option: " + arg); - return null; - } - } - - return cakeArgs.ToArray(); + private void WriteHeader(string message) + { + WriteLine(message, ConsoleColor.DarkCyan); } - bool IsOneOf(string arg, params string[] values) => - values.Any(value => value.Equals(arg, StringComparison.OrdinalIgnoreCase)); + private void Write(string message, ConsoleColor? color = null) + { + if (color != null) + Console.ForegroundColor = color.Value; + Console.Write(message); + if (color != null) + Console.ResetColor(); + } - void PrintError(string text) + private void WriteLine(string message = "", ConsoleColor? color = null) { - Console.ForegroundColor = ConsoleColor.Red; - Console.Error.WriteLine("ERROR: " + text); + Write(message, color); Console.WriteLine(); - Console.ResetColor(); - PrintHelp(true); } } \ No newline at end of file diff --git a/build/BenchmarkDotNet.Build/HelpInfo.cs b/build/BenchmarkDotNet.Build/HelpInfo.cs new file mode 100644 index 0000000000..8f93af63f9 --- /dev/null +++ b/build/BenchmarkDotNet.Build/HelpInfo.cs @@ -0,0 +1,8 @@ +using System; + +namespace BenchmarkDotNet.Build; + +public class HelpInfo +{ + public string[] Description { get; init; } = Array.Empty(); +} \ No newline at end of file diff --git a/build/BenchmarkDotNet.Build/IHelpProvider.cs b/build/BenchmarkDotNet.Build/IHelpProvider.cs new file mode 100644 index 0000000000..6fff1c061f --- /dev/null +++ b/build/BenchmarkDotNet.Build/IHelpProvider.cs @@ -0,0 +1,6 @@ +namespace BenchmarkDotNet.Build; + +public interface IHelpProvider +{ + HelpInfo GetHelp(); +} \ No newline at end of file diff --git a/build/BenchmarkDotNet.Build/Program.cs b/build/BenchmarkDotNet.Build/Program.cs index 4f9138bf08..715c37e3ae 100644 --- a/build/BenchmarkDotNet.Build/Program.cs +++ b/build/BenchmarkDotNet.Build/Program.cs @@ -1,3 +1,4 @@ +using BenchmarkDotNet.Build.Meta; using Cake.Common; using Cake.Frosting; @@ -84,9 +85,20 @@ public class CiTask : FrostingTask [TaskName("DocsUpdate")] [TaskDescription("Update generated documentation files")] -public class DocsUpdateTask : FrostingTask +public class DocsUpdateTask : FrostingTask, IHelpProvider { public override void Run(BuildContext context) => context.DocumentationRunner.Update(); + + public HelpInfo GetHelp() + { + return new HelpInfo + { + Description = new[] + { + $"Requires environment variable '{GitHubCredentials.TokenVariableName}'" + } + }; + } } [TaskName("DocsPrepare")]