diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index b3fa77d5cb5c..4e70432bb9a6 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -253,13 +253,13 @@ 2b6ab8d727ce73a78bcbf026ac75ea8a7c804daf - + https://github.com/dotnet/command-line-api - 8374d5fca634a93458c84414b1604c12f765d1ab + 02fe27cd6a9b001c8feb7938e6ef4b3799745759 - + https://github.com/dotnet/command-line-api - 8374d5fca634a93458c84414b1604c12f765d1ab + 02fe27cd6a9b001c8feb7938e6ef4b3799745759 diff --git a/eng/Versions.props b/eng/Versions.props index f2cd910fe14c..a63b3ca95fc0 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -39,7 +39,7 @@ 7.0.2 8.0.0-preview.6.23307.1 4.6.0 - 2.0.0-beta4.22564.1 + 2.0.0-beta4.23307.1 1.0.0-preview.6.23206.1 3.2.2146 diff --git a/src/ApiCompat/Microsoft.DotNet.ApiCompat.Shared/ValidatePackage.cs b/src/ApiCompat/Microsoft.DotNet.ApiCompat.Shared/ValidatePackage.cs index 2b188c5b9d5d..5eda083935cc 100644 --- a/src/ApiCompat/Microsoft.DotNet.ApiCompat.Shared/ValidatePackage.cs +++ b/src/ApiCompat/Microsoft.DotNet.ApiCompat.Shared/ValidatePackage.cs @@ -22,7 +22,7 @@ public static void Run(Func logFactory, bool enableRuleAttributesMustMatch, string[]? excludeAttributesFiles, bool enableRuleCannotChangeParameterName, - string packagePath, + string? packagePath, bool runApiCompat, bool enableStrictModeForCompatibleTfms, bool enableStrictModeForCompatibleFrameworksInPackage, diff --git a/src/ApiCompat/Microsoft.DotNet.ApiCompat.Tool/Program.cs b/src/ApiCompat/Microsoft.DotNet.ApiCompat.Tool/Program.cs index fd5d65184279..1f4089b2ea34 100644 --- a/src/ApiCompat/Microsoft.DotNet.ApiCompat.Tool/Program.cs +++ b/src/ApiCompat/Microsoft.DotNet.ApiCompat.Tool/Program.cs @@ -4,12 +4,10 @@ using System; using System.Collections.Generic; using System.CommandLine; -using System.CommandLine.Invocation; using System.CommandLine.Parsing; using System.IO; using Microsoft.DotNet.ApiCompatibility.Logging; using Microsoft.DotNet.ApiSymbolExtensions.Logging; -using Microsoft.DotNet.PackageValidation; using NuGet.Frameworks; namespace Microsoft.DotNet.ApiCompat.Tool @@ -22,137 +20,168 @@ static int Main(string[] args) // Important: Keep parameters exposed in sync with the msbuild task frontend. // Global options - Option generateSuppressionFileOption = new("--generate-suppression-file", - "If true, generates a compatibility suppression file."); - Option suppressionFilesOption = new("--suppression-file", - "The path to one or more suppression files to read from.") + CliOption generateSuppressionFileOption = new("--generate-suppression-file") { - AllowMultipleArgumentsPerToken= true, + Description = "If true, generates a compatibility suppression file.", + Recursive = true + }; + CliOption suppressionFilesOption = new("--suppression-file") + { + Description = "The path to one or more suppression files to read from.", + AllowMultipleArgumentsPerToken = true, Arity = ArgumentArity.ZeroOrMore, - ArgumentHelpName = "file" + HelpName = "file", + Recursive = true + }; + CliOption suppressionOutputFileOption = new("--suppression-output-file") + { + Description = "The path to a suppression file to write to when --generate-suppression-file is true.", + Recursive = true + }; + CliOption noWarnOption = new("--noWarn") + { + Description = "A NoWarn string that allows to disable specific rules.", + Recursive = true + }; + CliOption respectInternalsOption = new("--respect-internals") + { + Description = "If true, includes both internal and public API.", + Recursive = true + }; + CliOption roslynAssembliesPathOption = new("--roslyn-assemblies-path") + { + Description = "The path to the directory that contains the Microsoft.CodeAnalysis assemblies.", + HelpName = "file", + Recursive = true + }; + CliOption verbosityOption = new("--verbosity", "-v") + { + Description = "Controls the log level verbosity. Allowed values are high, normal, and low.", + DefaultValueFactory = _ => MessageImportance.High, + Recursive = true + }; + CliOption enableRuleAttributesMustMatchOption = new("--enable-rule-attributes-must-match") + { + Description = "If true, enables rule to check that attributes match.", + Recursive = true + }; + CliOption excludeAttributesFilesOption = new("--exclude-attributes-file") + { + Description = "The path to one or more attribute exclusion files with types in DocId format.", + Recursive = true }; - Option suppressionOutputFileOption = new("--suppression-output-file", - "The path to a suppression file to write to when --generate-suppression-file is true."); - Option noWarnOption = new("--noWarn", - "A NoWarn string that allows to disable specific rules."); - Option respectInternalsOption = new("--respect-internals", - "If true, includes both internal and public API."); - Option roslynAssembliesPathOption = new("--roslyn-assemblies-path", - "The path to the directory that contains the Microsoft.CodeAnalysis assemblies.") - { - ArgumentHelpName = "file" + CliOption enableRuleCannotChangeParameterNameOption = new("--enable-rule-cannot-change-parameter-name") + { + Description = "If true, enables rule to check that the parameter names between public methods do not change.", + Recursive = true }; - Option verbosityOption = new(new string[] { "--verbosity", "-v" }, - "Controls the log level verbosity. Allowed values are high, normal, and low."); - verbosityOption.SetDefaultValue(MessageImportance.High); - Option enableRuleAttributesMustMatchOption = new("--enable-rule-attributes-must-match", - "If true, enables rule to check that attributes match."); - Option excludeAttributesFilesOption = new("--exclude-attributes-file", - "The path to one or more attribute exclusion files with types in DocId format."); - Option enableRuleCannotChangeParameterNameOption = new("--enable-rule-cannot-change-parameter-name", - "If true, enables rule to check that the parameter names between public methods do not change."); // Root command - Option leftAssembliesOption = new(new string[] { "--left-assembly", "--left", "-l" }, - description: "The path to one or more assemblies that serve as the left side to compare.", - parseArgument: ParseAssemblyArgument) + CliOption leftAssembliesOption = new("--left-assembly", "--left", "-l") { + Description = "The path to one or more assemblies that serve as the left side to compare.", + CustomParser = ParseAssemblyArgument, AllowMultipleArgumentsPerToken = true, Arity = ArgumentArity.OneOrMore, - IsRequired = true + Required = true }; - Option rightAssembliesOption = new(new string[] { "--right-assembly", "--right", "-r" }, - description: "The path to one or more assemblies that serve as the right side to compare.", - parseArgument: ParseAssemblyArgument) + CliOption rightAssembliesOption = new("--right-assembly", "--right", "-r") { + Description = "The path to one or more assemblies that serve as the right side to compare.", + CustomParser = ParseAssemblyArgument, AllowMultipleArgumentsPerToken = true, Arity = ArgumentArity.OneOrMore, - IsRequired = true + Required = true + }; + CliOption strictModeOption = new("--strict-mode") + { + Description = "If true, performs api compatibility checks in strict mode" }; - Option strictModeOption = new("--strict-mode", - "If true, performs api compatibility checks in strict mode"); - Option leftAssembliesReferencesOption = new(new string[] { "--left-assembly-references", "--lref" }, - description: "Paths to assembly references or the underlying directories for a given left. Values must be separated by commas: ','.", - parseArgument: ParseAssemblyReferenceArgument) + CliOption leftAssembliesReferencesOption = new("--left-assembly-references", "--lref") + { + Description = "Paths to assembly references or the underlying directories for a given left. Values must be separated by commas: ','.", + CustomParser = ParseAssemblyReferenceArgument, AllowMultipleArgumentsPerToken = true, Arity = ArgumentArity.ZeroOrMore, - ArgumentHelpName = "file1,file2,..." + HelpName = "file1,file2,..." }; - Option rightAssembliesReferencesOption = new(new string[] { "--right-assembly-references", "--rref" }, - description: "Paths to assembly references or the underlying directories for a given right. Values must be separated by commas: ','.", - parseArgument: ParseAssemblyReferenceArgument) + CliOption rightAssembliesReferencesOption = new("--right-assembly-references", "--rref") { + Description = "Paths to assembly references or the underlying directories for a given right. Values must be separated by commas: ','.", + CustomParser = ParseAssemblyReferenceArgument, AllowMultipleArgumentsPerToken = true, Arity = ArgumentArity.ZeroOrMore, - ArgumentHelpName = "file1,file2,..." + HelpName = "file1,file2,..." + }; + CliOption createWorkItemPerAssemblyOption = new("--create-work-item-per-assembly") + { + Description = "If true, enqueues a work item per passed in left and right assembly." }; - Option createWorkItemPerAssemblyOption = new("--create-work-item-per-assembly", - "If true, enqueues a work item per passed in left and right assembly."); - Option<(string, string)[]?> leftAssembliesTransformationPatternOption = new("--left-assemblies-transformation-pattern", - description: "A transformation pattern for the left side assemblies.", - parseArgument: ParseTransformationPattern) + CliOption<(string, string)[]?> leftAssembliesTransformationPatternOption = new("--left-assemblies-transformation-pattern") { + Description = "A transformation pattern for the left side assemblies.", + CustomParser = ParseTransformationPattern, AllowMultipleArgumentsPerToken = true, Arity = ArgumentArity.ZeroOrMore }; - Option<(string, string)[]?> rightAssembliesTransformationPatternOption = new("--right-assemblies-transformation-pattern", - description: "A transformation pattern for the right side assemblies.", - parseArgument: ParseTransformationPattern) + CliOption<(string, string)[]?> rightAssembliesTransformationPatternOption = new("--right-assemblies-transformation-pattern") { + Description = "A transformation pattern for the right side assemblies.", + CustomParser = ParseTransformationPattern, AllowMultipleArgumentsPerToken = true, Arity = ArgumentArity.ZeroOrMore }; - RootCommand rootCommand = new("Microsoft.DotNet.ApiCompat v" + Environment.Version.ToString(2)) + CliRootCommand rootCommand = new("Microsoft.DotNet.ApiCompat v" + Environment.Version.ToString(2)) { TreatUnmatchedTokensAsErrors = true }; - rootCommand.AddGlobalOption(generateSuppressionFileOption); - rootCommand.AddGlobalOption(suppressionFilesOption); - rootCommand.AddGlobalOption(suppressionOutputFileOption); - rootCommand.AddGlobalOption(noWarnOption); - rootCommand.AddGlobalOption(respectInternalsOption); - rootCommand.AddGlobalOption(roslynAssembliesPathOption); - rootCommand.AddGlobalOption(verbosityOption); - rootCommand.AddGlobalOption(enableRuleAttributesMustMatchOption); - rootCommand.AddGlobalOption(excludeAttributesFilesOption); - rootCommand.AddGlobalOption(enableRuleCannotChangeParameterNameOption); - - rootCommand.AddOption(leftAssembliesOption); - rootCommand.AddOption(rightAssembliesOption); - rootCommand.AddOption(strictModeOption); - rootCommand.AddOption(leftAssembliesReferencesOption); - rootCommand.AddOption(rightAssembliesReferencesOption); - rootCommand.AddOption(createWorkItemPerAssemblyOption); - rootCommand.AddOption(leftAssembliesTransformationPatternOption); - rootCommand.AddOption(rightAssembliesTransformationPatternOption); - - rootCommand.SetHandler((InvocationContext context) => + rootCommand.Options.Add(generateSuppressionFileOption); + rootCommand.Options.Add(suppressionFilesOption); + rootCommand.Options.Add(suppressionOutputFileOption); + rootCommand.Options.Add(noWarnOption); + rootCommand.Options.Add(respectInternalsOption); + rootCommand.Options.Add(roslynAssembliesPathOption); + rootCommand.Options.Add(verbosityOption); + rootCommand.Options.Add(enableRuleAttributesMustMatchOption); + rootCommand.Options.Add(excludeAttributesFilesOption); + rootCommand.Options.Add(enableRuleCannotChangeParameterNameOption); + + rootCommand.Options.Add(leftAssembliesOption); + rootCommand.Options.Add(rightAssembliesOption); + rootCommand.Options.Add(strictModeOption); + rootCommand.Options.Add(leftAssembliesReferencesOption); + rootCommand.Options.Add(rightAssembliesReferencesOption); + rootCommand.Options.Add(createWorkItemPerAssemblyOption); + rootCommand.Options.Add(leftAssembliesTransformationPatternOption); + rootCommand.Options.Add(rightAssembliesTransformationPatternOption); + + rootCommand.SetAction((ParseResult parseResult) => { // If a roslyn assemblies path isn't provided, use the compiled against version from a subfolder. - string roslynAssembliesPath = context.ParseResult.GetValue(roslynAssembliesPathOption) ?? + string roslynAssembliesPath = parseResult.GetValue(roslynAssembliesPathOption) ?? Path.Combine(AppContext.BaseDirectory, "codeanalysis"); RoslynResolver roslynResolver = RoslynResolver.Register(roslynAssembliesPath); - MessageImportance verbosity = context.ParseResult.GetValue(verbosityOption); - bool generateSuppressionFile = context.ParseResult.GetValue(generateSuppressionFileOption); - string[]? suppressionFiles = context.ParseResult.GetValue(suppressionFilesOption); - string? suppressionOutputFile = context.ParseResult.GetValue(suppressionOutputFileOption); - string? noWarn = context.ParseResult.GetValue(noWarnOption); - bool respectInternals = context.ParseResult.GetValue(respectInternalsOption); - bool enableRuleAttributesMustMatch = context.ParseResult.GetValue(enableRuleAttributesMustMatchOption); - string[]? excludeAttributesFiles = context.ParseResult.GetValue(excludeAttributesFilesOption); - bool enableRuleCannotChangeParameterName = context.ParseResult.GetValue(enableRuleCannotChangeParameterNameOption); - - string[] leftAssemblies = context.ParseResult.GetValue(leftAssembliesOption)!; - string[] rightAssemblies = context.ParseResult.GetValue(rightAssembliesOption)!; - bool strictMode = context.ParseResult.GetValue(strictModeOption); - string[][]? leftAssembliesReferences = context.ParseResult.GetValue(leftAssembliesReferencesOption); - string[][]? rightAssembliesReferences = context.ParseResult.GetValue(rightAssembliesReferencesOption); - bool createWorkItemPerAssembly = context.ParseResult.GetValue(createWorkItemPerAssemblyOption); - (string, string)[]? leftAssembliesTransformationPattern = context.ParseResult.GetValue(leftAssembliesTransformationPatternOption); - (string, string)[]? rightAssembliesTransformationPattern = context.ParseResult.GetValue(rightAssembliesTransformationPatternOption); + MessageImportance verbosity = parseResult.GetValue(verbosityOption); + bool generateSuppressionFile = parseResult.GetValue(generateSuppressionFileOption); + string[]? suppressionFiles = parseResult.GetValue(suppressionFilesOption); + string? suppressionOutputFile = parseResult.GetValue(suppressionOutputFileOption); + string? noWarn = parseResult.GetValue(noWarnOption); + bool respectInternals = parseResult.GetValue(respectInternalsOption); + bool enableRuleAttributesMustMatch = parseResult.GetValue(enableRuleAttributesMustMatchOption); + string[]? excludeAttributesFiles = parseResult.GetValue(excludeAttributesFilesOption); + bool enableRuleCannotChangeParameterName = parseResult.GetValue(enableRuleCannotChangeParameterNameOption); + + string[] leftAssemblies = parseResult.GetValue(leftAssembliesOption)!; + string[] rightAssemblies = parseResult.GetValue(rightAssembliesOption)!; + bool strictMode = parseResult.GetValue(strictModeOption); + string[][]? leftAssembliesReferences = parseResult.GetValue(leftAssembliesReferencesOption); + string[][]? rightAssembliesReferences = parseResult.GetValue(rightAssembliesReferencesOption); + bool createWorkItemPerAssembly = parseResult.GetValue(createWorkItemPerAssemblyOption); + (string, string)[]? leftAssembliesTransformationPattern = parseResult.GetValue(leftAssembliesTransformationPatternOption); + (string, string)[]? rightAssembliesTransformationPattern = parseResult.GetValue(rightAssembliesTransformationPatternOption); Func logFactory = (suppressionEngine) => new(suppressionEngine, verbosity); ValidateAssemblies.Run(logFactory, @@ -177,83 +206,91 @@ static int Main(string[] args) }); // Package command - Argument packageArgument = new("--package", - "The path to the package that should be validated") + CliArgument packageArgument = new("--package") { + Description = "The path to the package that should be validated", Arity = ArgumentArity.ExactlyOne }; - Option runtimeGraphOption = new("--runtime-graph", - "The path to the runtime graph to read from.") + CliOption runtimeGraphOption = new("--runtime-graph") + { + Description = "The path to the runtime graph to read from.", + HelpName = "json" + }; + CliOption runApiCompatOption = new("--run-api-compat") + { + Description = "If true, performs api compatibility checks on the package assets.", + DefaultValueFactory = _ => true + }; + CliOption enableStrictModeForCompatibleTfmsOption = new("--enable-strict-mode-for-compatible-tfms") + { + Description = "Validates api compatibility in strict mode for contract and implementation assemblies for all compatible target frameworks." + }; + CliOption enableStrictModeForCompatibleFrameworksInPackageOption = new("--enable-strict-mode-for-compatible-frameworks-in-package") + { + Description = "Validates api compatibility in strict mode for assemblies that are compatible based on their target framework." + }; + CliOption enableStrictModeForBaselineValidationOption = new("--enable-strict-mode-for-baseline-validation") { - ArgumentHelpName = "json" + Description = "Validates api compatibility in strict mode for package baseline checks." }; - Option runApiCompatOption = new("--run-api-compat", - "If true, performs api compatibility checks on the package assets."); - runApiCompatOption.SetDefaultValue(true); - Option enableStrictModeForCompatibleTfmsOption = new("--enable-strict-mode-for-compatible-tfms", - "Validates api compatibility in strict mode for contract and implementation assemblies for all compatible target frameworks."); - Option enableStrictModeForCompatibleFrameworksInPackageOption = new("--enable-strict-mode-for-compatible-frameworks-in-package", - "Validates api compatibility in strict mode for assemblies that are compatible based on their target framework."); - Option enableStrictModeForBaselineValidationOption = new("--enable-strict-mode-for-baseline-validation", - "Validates api compatibility in strict mode for package baseline checks."); - Option baselinePackageOption = new("--baseline-package", - "The path to a baseline package to validate against the current package.") - { - ArgumentHelpName = "nupkg" + CliOption baselinePackageOption = new("--baseline-package") + { + Description = "The path to a baseline package to validate against the current package.", + HelpName = "nupkg" }; - Option>?> packageAssemblyReferencesOption = new("--package-assembly-references", - description: "Paths to assembly references or their underlying directories for a specific target framework in the package. Values must be separated by commas: ','.", - parseArgument: ParsePackageAssemblyReferenceArgument) + CliOption>?> packageAssemblyReferencesOption = new("--package-assembly-references") { + Description = "Paths to assembly references or their underlying directories for a specific target framework in the package. Values must be separated by commas: ','.", + CustomParser = ParsePackageAssemblyReferenceArgument, AllowMultipleArgumentsPerToken = true, Arity = ArgumentArity.ZeroOrMore, - ArgumentHelpName = "tfm=file1,file2,..." + HelpName = "tfm=file1,file2,..." }; - Option>?> baselinePackageAssemblyReferencesOption = new("--baseline-package-assembly-references", - description: "Paths to assembly references or their underlying directories for a specific target framework in the baseline package. Values must be separated by commas: ','.", - parseArgument: ParsePackageAssemblyReferenceArgument) + CliOption>?> baselinePackageAssemblyReferencesOption = new("--baseline-package-assembly-references") { + Description = "Paths to assembly references or their underlying directories for a specific target framework in the baseline package. Values must be separated by commas: ','.", + CustomParser = ParsePackageAssemblyReferenceArgument, AllowMultipleArgumentsPerToken = true, Arity = ArgumentArity.ZeroOrMore, - ArgumentHelpName = "tfm=file1,file2,..." + HelpName = "tfm=file1,file2,..." }; - Command packageCommand = new("package", "Validates the compatibility of package assets"); - packageCommand.AddArgument(packageArgument); - packageCommand.AddOption(runtimeGraphOption); - packageCommand.AddOption(runApiCompatOption); - packageCommand.AddOption(enableStrictModeForCompatibleTfmsOption); - packageCommand.AddOption(enableStrictModeForCompatibleFrameworksInPackageOption); - packageCommand.AddOption(enableStrictModeForBaselineValidationOption); - packageCommand.AddOption(baselinePackageOption); - packageCommand.AddOption(packageAssemblyReferencesOption); - packageCommand.AddOption(baselinePackageAssemblyReferencesOption); - packageCommand.SetHandler((InvocationContext context) => + CliCommand packageCommand = new("package", "Validates the compatibility of package assets"); + packageCommand.Arguments.Add(packageArgument); + packageCommand.Options.Add(runtimeGraphOption); + packageCommand.Options.Add(runApiCompatOption); + packageCommand.Options.Add(enableStrictModeForCompatibleTfmsOption); + packageCommand.Options.Add(enableStrictModeForCompatibleFrameworksInPackageOption); + packageCommand.Options.Add(enableStrictModeForBaselineValidationOption); + packageCommand.Options.Add(baselinePackageOption); + packageCommand.Options.Add(packageAssemblyReferencesOption); + packageCommand.Options.Add(baselinePackageAssemblyReferencesOption); + packageCommand.SetAction((ParseResult parseResult) => { // If a roslyn assemblies path isn't provided, use the compiled against version from a subfolder. - string roslynAssembliesPath = context.ParseResult.GetValue(roslynAssembliesPathOption) ?? + string roslynAssembliesPath = parseResult.GetValue(roslynAssembliesPathOption) ?? Path.Combine(AppContext.BaseDirectory, "codeanalysis"); RoslynResolver roslynResolver = RoslynResolver.Register(roslynAssembliesPath); - MessageImportance verbosity = context.ParseResult.GetValue(verbosityOption); - bool generateSuppressionFile = context.ParseResult.GetValue(generateSuppressionFileOption); - string[]? suppressionFiles = context.ParseResult.GetValue(suppressionFilesOption); - string? suppressionOutputFile = context.ParseResult.GetValue(suppressionOutputFileOption); - string? noWarn = context.ParseResult.GetValue(noWarnOption); - bool respectInternals = context.ParseResult.GetValue(respectInternalsOption); - bool enableRuleAttributesMustMatch = context.ParseResult.GetValue(enableRuleAttributesMustMatchOption); - string[]? excludeAttributesFiles = context.ParseResult.GetValue(excludeAttributesFilesOption); - bool enableRuleCannotChangeParameterName = context.ParseResult.GetValue(enableRuleCannotChangeParameterNameOption); - - string package = context.ParseResult.GetValue(packageArgument); - bool runApiCompat = context.ParseResult.GetValue(runApiCompatOption); - bool enableStrictModeForCompatibleTfms = context.ParseResult.GetValue(enableStrictModeForCompatibleTfmsOption); - bool enableStrictModeForCompatibleFrameworksInPackage = context.ParseResult.GetValue(enableStrictModeForCompatibleFrameworksInPackageOption); - bool enableStrictModeForBaselineValidation = context.ParseResult.GetValue(enableStrictModeForBaselineValidationOption); - string? baselinePackage = context.ParseResult.GetValue(baselinePackageOption); - string? runtimeGraph = context.ParseResult.GetValue(runtimeGraphOption); - Dictionary>? packageAssemblyReferences = context.ParseResult.GetValue(packageAssemblyReferencesOption); - Dictionary>? baselinePackageAssemblyReferences = context.ParseResult.GetValue(baselinePackageAssemblyReferencesOption); + MessageImportance verbosity = parseResult.GetValue(verbosityOption); + bool generateSuppressionFile = parseResult.GetValue(generateSuppressionFileOption); + string[]? suppressionFiles = parseResult.GetValue(suppressionFilesOption); + string? suppressionOutputFile = parseResult.GetValue(suppressionOutputFileOption); + string? noWarn = parseResult.GetValue(noWarnOption); + bool respectInternals = parseResult.GetValue(respectInternalsOption); + bool enableRuleAttributesMustMatch = parseResult.GetValue(enableRuleAttributesMustMatchOption); + string[]? excludeAttributesFiles = parseResult.GetValue(excludeAttributesFilesOption); + bool enableRuleCannotChangeParameterName = parseResult.GetValue(enableRuleCannotChangeParameterNameOption); + + string? package = parseResult.GetValue(packageArgument); + bool runApiCompat = parseResult.GetValue(runApiCompatOption); + bool enableStrictModeForCompatibleTfms = parseResult.GetValue(enableStrictModeForCompatibleTfmsOption); + bool enableStrictModeForCompatibleFrameworksInPackage = parseResult.GetValue(enableStrictModeForCompatibleFrameworksInPackageOption); + bool enableStrictModeForBaselineValidation = parseResult.GetValue(enableStrictModeForBaselineValidationOption); + string? baselinePackage = parseResult.GetValue(baselinePackageOption); + string? runtimeGraph = parseResult.GetValue(runtimeGraphOption); + Dictionary>? packageAssemblyReferences = parseResult.GetValue(packageAssemblyReferencesOption); + Dictionary>? baselinePackageAssemblyReferences = parseResult.GetValue(baselinePackageAssemblyReferencesOption); Func logFactory = (suppressionEngine) => new(suppressionEngine, verbosity); ValidatePackage.Run(logFactory, @@ -278,14 +315,14 @@ static int Main(string[] args) roslynResolver.Unregister(); }); - rootCommand.AddCommand(packageCommand); - return rootCommand.Invoke(args); + rootCommand.Subcommands.Add(packageCommand); + return rootCommand.Parse(args).Invoke(); } private static string[][] ParseAssemblyReferenceArgument(ArgumentResult argumentResult) { List args = new(); - foreach (Token token in argumentResult.Tokens) + foreach (var token in argumentResult.Tokens) { args.Add(token.Value.Split(',')); } @@ -296,7 +333,7 @@ private static string[][] ParseAssemblyReferenceArgument(ArgumentResult argument private static string[] ParseAssemblyArgument(ArgumentResult argumentResult) { List args = new(); - foreach (Token token in argumentResult.Tokens) + foreach (var token in argumentResult.Tokens) { args.AddRange(token.Value.Split(',')); } @@ -312,7 +349,7 @@ private static (string CaptureGroupPattern, string ReplacementString)[]? ParseTr string[] parts = argumentResult.Tokens[i].Value.Split(';'); if (parts.Length != 2) { - argumentResult.ErrorMessage = "Invalid assemblies transformation pattern. Usage: {regex-pattern};{replacement-string}"; + argumentResult.AddError("Invalid assemblies transformation pattern. Usage: {regex-pattern};{replacement-string}"); continue; } @@ -327,12 +364,12 @@ private static (string CaptureGroupPattern, string ReplacementString)[]? ParseTr const string invalidPackageAssemblyReferenceFormatMessage = "Invalid package assembly reference format {TargetFrameworkMoniker(+TargetPlatformMoniker)=assembly1,assembly2,assembly3,...}"; Dictionary> packageAssemblyReferencesDict = new(argumentResult.Tokens.Count); - foreach (Token token in argumentResult.Tokens) + foreach (var token in argumentResult.Tokens) { string[] parts = token.Value.Split('='); if (parts.Length != 2) { - argumentResult.ErrorMessage = invalidPackageAssemblyReferenceFormatMessage; + argumentResult.AddError(invalidPackageAssemblyReferenceFormatMessage); continue; } @@ -342,7 +379,7 @@ private static (string CaptureGroupPattern, string ReplacementString)[]? ParseTr string[] tfmInformationParts = tfmInformation.Split('+'); if (tfmInformationParts.Length < 1 || tfmInformationParts.Length > 2) { - argumentResult.ErrorMessage = invalidPackageAssemblyReferenceFormatMessage; + argumentResult.AddError(invalidPackageAssemblyReferenceFormatMessage); } string targetFrameworkMoniker = tfmInformationParts[0]; diff --git a/src/BlazorWasmSdk/Tool/Program.cs b/src/BlazorWasmSdk/Tool/Program.cs index 0aeaba796e70..640554c95aa6 100644 --- a/src/BlazorWasmSdk/Tool/Program.cs +++ b/src/BlazorWasmSdk/Tool/Program.cs @@ -4,8 +4,6 @@ using System; using System.Collections.Generic; using System.CommandLine; -using System.CommandLine.Invocation; -using System.CommandLine.Parsing; using System.IO; using System.IO.Compression; using System.Threading.Tasks; @@ -16,23 +14,22 @@ internal static class Program { public static int Main(string[] args) { - var rootCommand = new RootCommand(); - var brotli = new Command("brotli"); + CliRootCommand rootCommand = new(); + CliCommand brotli = new("brotli"); - var compressionLevelOption = new Option( - "-c", - defaultValueFactory: () => CompressionLevel.SmallestSize, - description: "System.IO.Compression.CompressionLevel for the Brotli compression algorithm."); - var sourcesOption = new Option>( - "-s", - description: "A list of files to compress.") + CliOption compressionLevelOption = new("-c") { + DefaultValueFactory = _ => CompressionLevel.SmallestSize, + Description = "System.IO.Compression.CompressionLevel for the Brotli compression algorithm.", + }; + CliOption> sourcesOption = new("-s") + { + Description = "A list of files to compress.", AllowMultipleArgumentsPerToken = false }; - var outputsOption = new Option>( - "-o", - "The filenames to output the compressed file to.") + CliOption> outputsOption = new("-o") { + Description = "The filenames to output the compressed file to.", AllowMultipleArgumentsPerToken = false }; @@ -42,12 +39,11 @@ public static int Main(string[] args) rootCommand.Add(brotli); - brotli.SetHandler((InvocationContext context) => + brotli.SetAction((ParseResult parseResult) => { - var parseResults = context.ParseResult; - var c = parseResults.GetValue(compressionLevelOption); - var s = parseResults.GetValue(sourcesOption); - var o = parseResults.GetValue(outputsOption); + var c = parseResult.GetValue(compressionLevelOption); + var s = parseResult.GetValue(sourcesOption); + var o = parseResult.GetValue(outputsOption); Parallel.For(0, s.Count, i => { @@ -69,7 +65,7 @@ public static int Main(string[] args) }); }); - return rootCommand.InvokeAsync(args).Result; + return rootCommand.Parse(args).Invoke(); } } } diff --git a/src/BuiltInTools/dotnet-watch/CommandLineOptions.cs b/src/BuiltInTools/dotnet-watch/CommandLineOptions.cs index 5bf4f7798baf..b61050e57f06 100644 --- a/src/BuiltInTools/dotnet-watch/CommandLineOptions.cs +++ b/src/BuiltInTools/dotnet-watch/CommandLineOptions.cs @@ -6,10 +6,8 @@ using System; using System.Collections.Generic; using System.CommandLine; -using System.CommandLine.Invocation; +using System.IO; using System.Linq; - -using Microsoft.AspNetCore.Authentication; using Microsoft.DotNet.Watcher.Tools; using Microsoft.Extensions.Tools.Internal; @@ -77,112 +75,131 @@ dotnet watch test public required IReadOnlyList RemainingArguments { get; init; } public RunCommandLineOptions? RunOptions { get; init; } - public static CommandLineOptions? Parse(string[] args, IReporter reporter, out int errorCode, System.CommandLine.IConsole? console = null) + public static CommandLineOptions? Parse(string[] args, IReporter reporter, out int errorCode, TextWriter? output = null, TextWriter? error = null) { - var quietOption = new Option(new[] { "--quiet", "-q" }, "Suppresses all output except warnings and errors"); - var verboseOption = new Option(new[] { "--verbose", "-v" }, "Show verbose output"); + var quietOption = new CliOption("--quiet", "-q") + { + Description = "Suppresses all output except warnings and errors" + }; - verboseOption.AddValidator(v => + var verboseOption = new CliOption("--verbose", "-v") { - if (v.FindResultFor(quietOption) is not null && v.FindResultFor(verboseOption) is not null) + Description = "Show verbose output" + }; + + verboseOption.Validators.Add(v => + { + if (v.GetResult(quietOption) is not null && v.GetResult(verboseOption) is not null) { - v.ErrorMessage = Resources.Error_QuietAndVerboseSpecified; + v.AddError(Resources.Error_QuietAndVerboseSpecified); } }); - var listOption = new Option("--list", "Lists all discovered files without starting the watcher."); - var shortProjectOption = new Option("-p", "The project to watch.") { IsHidden = true }; - var longProjectOption = new Option("--project", "The project to watch"); + var listOption = new CliOption("--list") { Description = "Lists all discovered files without starting the watcher." }; + var shortProjectOption = new CliOption("-p") { Description = "The project to watch.", Hidden = true }; + var longProjectOption = new CliOption("--project") { Description = "The project to watch" }; // launch profile used by dotnet-watch - var launchProfileWatchOption = new Option(new[] { "-lp", LaunchProfileOptionName }, "The launch profile to start the project with (case-sensitive)."); - var noLaunchProfileWatchOption = new Option(new[] { NoLaunchProfileOptionName }, "Do not attempt to use launchSettings.json to configure the application."); + var launchProfileWatchOption = new CliOption(LaunchProfileOptionName, "-lp") + { + Description = "The launch profile to start the project with (case-sensitive)." + }; + var noLaunchProfileWatchOption = new CliOption(NoLaunchProfileOptionName) + { + Description = "Do not attempt to use launchSettings.json to configure the application." + }; // launch profile used by dotnet-run - var launchProfileRunOption = new Option(new[] { "-lp", LaunchProfileOptionName }) { IsHidden = true }; - var noLaunchProfileRunOption = new Option(new[] { NoLaunchProfileOptionName }) { IsHidden = true }; + var launchProfileRunOption = new CliOption(LaunchProfileOptionName, "-lp") { Hidden = true }; + var noLaunchProfileRunOption = new CliOption(NoLaunchProfileOptionName) { Hidden = true }; - var targetFrameworkOption = new Option(new[] { "-f", "--framework" }, "The target framework to run for. The target framework must also be specified in the project file."); - var propertyOption = new Option(new[] { "--property" }, "Properties to be passed to MSBuild."); + var targetFrameworkOption = new CliOption("--framework", "-f") + { + Description = "The target framework to run for. The target framework must also be specified in the project file." + }; + var propertyOption = new CliOption("--property") + { + Description = "Properties to be passed to MSBuild." + }; - propertyOption.AddValidator(v => + propertyOption.Validators.Add(v => { var invalidProperty = v.GetValue(propertyOption)?.FirstOrDefault( property => !(property.IndexOf('=') is > 0 and var index && index < property.Length - 1 && property[..index].Trim().Length > 0)); if (invalidProperty != null) { - v.ErrorMessage = $"Invalid property format: '{invalidProperty}'. Expected 'name=value'."; + v.AddError($"Invalid property format: '{invalidProperty}'. Expected 'name=value'."); } }); - var noHotReloadOption = new Option("--no-hot-reload", "Suppress hot reload for supported apps."); - var nonInteractiveOption = new Option( - "--non-interactive", - "Runs dotnet-watch in non-interactive mode. This option is only supported when running with Hot Reload enabled. " + - "Use this option to prevent console input from being captured."); + var noHotReloadOption = new CliOption("--no-hot-reload") { Description = "Suppress hot reload for supported apps." }; + var nonInteractiveOption = new CliOption("--non-interactive") + { + Description = "Runs dotnet-watch in non-interactive mode. This option is only supported when running with Hot Reload enabled. " + + "Use this option to prevent console input from being captured." + }; - var remainingWatchArgs = new Argument("forwardedArgs", "Arguments to pass to the child dotnet process."); - var remainingRunArgs = new Argument(name: null); + var remainingWatchArgs = new CliArgument("forwardedArgs") { Description = "Arguments to pass to the child dotnet process." }; + var remainingRunArgs = new CliArgument("remainingRunArgs"); - var runCommand = new Command("run") { IsHidden = true }; - var rootCommand = new RootCommand(Description); - addOptions(runCommand); - addOptions(rootCommand); + var runCommand = new CliCommand("run") { Hidden = true }; + var rootCommand = new CliRootCommand(Description); + AddSymbols(runCommand); + AddSymbols(rootCommand); - void addOptions(Command command) + void AddSymbols(CliCommand command) { - command.Add(quietOption); - command.Add(verboseOption); - command.Add(noHotReloadOption); - command.Add(nonInteractiveOption); - command.Add(longProjectOption); - command.Add(shortProjectOption); + command.Options.Add(quietOption); + command.Options.Add(verboseOption); + command.Options.Add(noHotReloadOption); + command.Options.Add(nonInteractiveOption); + command.Options.Add(longProjectOption); + command.Options.Add(shortProjectOption); if (command == runCommand) { - command.Add(launchProfileRunOption); - command.Add(noLaunchProfileRunOption); + command.Options.Add(launchProfileRunOption); + command.Options.Add(noLaunchProfileRunOption); } else { - command.Add(launchProfileWatchOption); - command.Add(noLaunchProfileWatchOption); + command.Options.Add(launchProfileWatchOption); + command.Options.Add(noLaunchProfileWatchOption); } - command.Add(targetFrameworkOption); - command.Add(propertyOption); + command.Options.Add(targetFrameworkOption); + command.Options.Add(propertyOption); - command.Add(listOption); + command.Options.Add(listOption); if (command == runCommand) { - command.Add(remainingRunArgs); + command.Arguments.Add(remainingRunArgs); } else { - command.Add(runCommand); - command.Add(remainingWatchArgs); + command.Subcommands.Add(runCommand); + command.Arguments.Add(remainingWatchArgs); } }; CommandLineOptions? options = null; - runCommand.SetHandler(context => + runCommand.SetAction(parseResult => { - RootHandler(context, new() + RootHandler(parseResult, new() { - LaunchProfileName = context.ParseResult.GetValue(launchProfileRunOption), - NoLaunchProfile = context.ParseResult.GetValue(noLaunchProfileRunOption), - RemainingArguments = context.ParseResult.GetValue(remainingRunArgs), + LaunchProfileName = parseResult.GetValue(launchProfileRunOption), + NoLaunchProfile = parseResult.GetValue(noLaunchProfileRunOption), + RemainingArguments = parseResult.GetValue(remainingRunArgs) ?? Array.Empty(), }); }); - rootCommand.SetHandler(context => RootHandler(context, runOptions: null)); + rootCommand.SetAction(parseResult => RootHandler(parseResult, runOptions: null)); - void RootHandler(InvocationContext context, RunCommandLineOptions? runOptions) + void RootHandler(ParseResult parseResults, RunCommandLineOptions? runOptions) { - var parseResults = context.ParseResult; var projectValue = parseResults.GetValue(longProjectOption); if (string.IsNullOrEmpty(projectValue)) { @@ -207,12 +224,17 @@ void RootHandler(InvocationContext context, RunCommandLineOptions? runOptions) TargetFramework = parseResults.GetValue(targetFrameworkOption), BuildProperties = parseResults.GetValue(propertyOption)? .Select(p => (p[..p.IndexOf('=')].Trim(), p[(p.IndexOf('=') + 1)..])).ToArray(), - RemainingArguments = parseResults.GetValue(remainingWatchArgs), + RemainingArguments = parseResults.GetValue(remainingWatchArgs) ?? Array.Empty(), RunOptions = runOptions, }; } - errorCode = rootCommand.Invoke(args, console); + errorCode = new CliConfiguration(rootCommand) + { + Output = output ?? Console.Out, + Error = error ?? Console.Error + }.Invoke(args); + return options; } diff --git a/src/Cli/Microsoft.DotNet.Cli.Utils/Extensions/StringExtensions.cs b/src/Cli/Microsoft.DotNet.Cli.Utils/Extensions/StringExtensions.cs new file mode 100644 index 000000000000..9c03f3efb233 --- /dev/null +++ b/src/Cli/Microsoft.DotNet.Cli.Utils/Extensions/StringExtensions.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.DotNet.Cli.Utils.Extensions +{ + public static class StringExtensions + { + public static string RemovePrefix(this string name) + { + int prefixLength = GetPrefixLength(name); + + return prefixLength > 0 + ? name.Substring(prefixLength) + : name; + + static int GetPrefixLength(string name) + { + if (name[0] == '-') + { + return name.Length > 1 && name[1] == '-' + ? 2 + : 1; + } + + if (name[0] == '/') + { + return 1; + } + + return 0; + } + } + } +} diff --git a/src/Cli/Microsoft.TemplateEngine.Cli/ChoiceTemplateParameter.cs b/src/Cli/Microsoft.TemplateEngine.Cli/ChoiceTemplateParameter.cs index 9e8dff82f3f8..e24b7928e774 100644 --- a/src/Cli/Microsoft.TemplateEngine.Cli/ChoiceTemplateParameter.cs +++ b/src/Cli/Microsoft.TemplateEngine.Cli/ChoiceTemplateParameter.cs @@ -49,35 +49,38 @@ internal ChoiceTemplateParameter(ChoiceTemplateParameter choiceTemplateParameter { return (context) => { - string standardUsage = HelpBuilder.Default.GetIdentifierSymbolUsageLabel(o.Option, context); + string standardUsage = HelpBuilder.Default.GetOptionUsageLabel(o.Option); if (standardUsage.Length > context.HelpBuilder.MaxWidth / 3) { if (Choices.Count > 2) { - o.Option.ArgumentHelpName = $"{string.Join("|", Choices.Keys.Take(2))}|..."; - string updatedFirstColumn = HelpBuilder.Default.GetIdentifierSymbolUsageLabel(o.Option, context); + o.Option.HelpName = $"{string.Join("|", Choices.Keys.Take(2))}|..."; + string updatedFirstColumn = HelpBuilder.Default.GetOptionUsageLabel(o.Option); if (updatedFirstColumn.Length <= context.HelpBuilder.MaxWidth / 3) { return updatedFirstColumn; } } - o.Option.ArgumentHelpName = HelpStrings.Text_ChoiceArgumentHelpName; - return HelpBuilder.Default.GetIdentifierSymbolUsageLabel(o.Option, context); + o.Option.HelpName = HelpStrings.Text_ChoiceArgumentHelpName; + return HelpBuilder.Default.GetOptionUsageLabel(o.Option); } return standardUsage; }; } - protected override Option GetBaseOption(IReadOnlySet aliases) + protected override CliOption GetBaseOption(IReadOnlySet aliases) { - Option option = new Option( - aliases.ToArray(), - parseArgument: result => GetParseChoiceArgument(this)(result)) + string name = GetName(aliases); + + CliOption option = new(name) { + CustomParser = result => GetParseChoiceArgument(this)(result), Arity = new ArgumentArity(DefaultIfOptionWithoutValue == null ? 1 : 0, AllowMultipleValues ? _choices.Count : 1), AllowMultipleArgumentsPerToken = AllowMultipleValues }; + AddAliases(option, aliases); + // empty string for the explicit unset option option.FromAmongCaseInsensitive(Choices.Keys.ToArray(), allowedHiddenValue: string.Empty); @@ -99,7 +102,7 @@ private static Func GetParseChoiceArgument(ChoiceTemplat if (argumentResult.Tokens.Count == 0) { - if (or.IsImplicit) + if (or.Implicit) { if (!string.IsNullOrWhiteSpace(parameter.DefaultValue)) { @@ -108,16 +111,16 @@ private static Func GetParseChoiceArgument(ChoiceTemplat return defaultValue; } //Cannot parse default value '{0}' for option '{1}' as expected type '{2}': {3}. - argumentResult.ErrorMessage = string.Format( + argumentResult.AddError(string.Format( LocalizableStrings.ParseChoiceTemplateOption_Error_InvalidDefaultValue, parameter.DefaultValue, - or.Token?.Value, + or.IdentifierToken?.Value, "choice", - error); + error)); return string.Empty; } //Default value for argument missing for option: '{0}'. - argumentResult.ErrorMessage = string.Format(LocalizableStrings.ParseTemplateOption_Error_MissingDefaultValue, or.Token?.Value); + argumentResult.AddError(string.Format(LocalizableStrings.ParseTemplateOption_Error_MissingDefaultValue, or.IdentifierToken?.Value)); return string.Empty; } if (parameter.DefaultIfOptionWithoutValue != null) @@ -127,22 +130,22 @@ private static Func GetParseChoiceArgument(ChoiceTemplat return defaultValue; } //Cannot parse default if option without value '{0}' for option '{1}' as expected type '{2}': {3}. - argumentResult.ErrorMessage = string.Format( + argumentResult.AddError(string.Format( LocalizableStrings.ParseChoiceTemplateOption_Error_InvalidDefaultIfNoOptionValue, parameter.DefaultIfOptionWithoutValue, - or.Token?.Value, + or.IdentifierToken?.Value, "choice", - error); + error)); return string.Empty; } //Required argument missing for option: '{0}'. - argumentResult.ErrorMessage = string.Format(LocalizableStrings.ParseTemplateOption_Error_MissingDefaultIfNoOptionValue, or.Token?.Value); + argumentResult.AddError(string.Format(LocalizableStrings.ParseTemplateOption_Error_MissingDefaultIfNoOptionValue, or.IdentifierToken?.Value)); return string.Empty; } else if (!parameter.AllowMultipleValues && argumentResult.Tokens.Count != 1) { //Using more than 1 argument is not allowed for '{0}', used: {1}. - argumentResult.ErrorMessage = string.Format(LocalizableStrings.ParseTemplateOption_Error_InvalidCount, or.Token?.Value, argumentResult.Tokens.Count); + argumentResult.AddError(string.Format(LocalizableStrings.ParseTemplateOption_Error_InvalidCount, or.IdentifierToken?.Value, argumentResult.Tokens.Count)); return string.Empty; } else @@ -150,12 +153,12 @@ private static Func GetParseChoiceArgument(ChoiceTemplat if (!TryConvertValueToChoice(argumentResult.Tokens.Select(t => t.Value), parameter, out string value, out string error)) { //Cannot parse argument '{0}' for option '{1}' as expected type '{2}': {3}. - argumentResult.ErrorMessage = string.Format( + argumentResult.AddError(string.Format( LocalizableStrings.ParseChoiceTemplateOption_Error_InvalidArgument, argumentResult.Tokens[0].Value, - or.Token?.Value, + or.IdentifierToken?.Value, "choice", - error); + error)); return string.Empty; } diff --git a/src/Cli/Microsoft.TemplateEngine.Cli/CliTemplateParameter.cs b/src/Cli/Microsoft.TemplateEngine.Cli/CliTemplateParameter.cs index 5d315915dd6e..5c2b8b70c997 100644 --- a/src/Cli/Microsoft.TemplateEngine.Cli/CliTemplateParameter.cs +++ b/src/Cli/Microsoft.TemplateEngine.Cli/CliTemplateParameter.cs @@ -4,6 +4,7 @@ using System.CommandLine; using System.CommandLine.Help; using System.CommandLine.Parsing; +using System.Diagnostics; using System.Globalization; using System.Text; using Microsoft.TemplateEngine.Abstractions; @@ -134,13 +135,13 @@ internal CliTemplateParameter(CliTemplateParameter other) protected bool AllowMultipleValues { get; private init; } /// - /// Creates for template parameter. + /// Creates for template parameter. /// /// aliases to be used for option. - internal Option GetOption(IReadOnlySet aliases) + internal CliOption GetOption(IReadOnlySet aliases) { - Option option = GetBaseOption(aliases); - option.IsHidden = IsHidden; + CliOption option = GetBaseOption(aliases); + option.Hidden = IsHidden; //if parameter is required, the default value is ignored. //the user should always specify the parameter, so the default value is not even shown. @@ -149,7 +150,34 @@ internal Option GetOption(IReadOnlySet aliases) if (!string.IsNullOrWhiteSpace(DefaultValue) || (Type == ParameterType.String || Type == ParameterType.Choice) && DefaultValue != null) { - option.SetDefaultValue(DefaultValue); + switch (option) + { + case CliOption stringOption: + stringOption.DefaultValueFactory = (_) => DefaultValue; + break; + case CliOption booleanOption: + booleanOption.DefaultValueFactory = (_) => bool.Parse(DefaultValue); + break; + case CliOption integerOption: + if (Type == ParameterType.Hex) + { + integerOption.DefaultValueFactory = (_) => Convert.ToInt64(DefaultValue, 16); + } + else + { + integerOption.DefaultValueFactory = (_) => long.Parse(DefaultValue); + } + break; + case CliOption floatOption: + floatOption.DefaultValueFactory = (_) => float.Parse(DefaultValue); + break; + case CliOption doubleOption: + doubleOption.DefaultValueFactory = (_) => double.Parse(DefaultValue); + break; + default: + Debug.Fail($"Unexpected Option type: {option.GetType()}"); + break; + } } } option.Description = GetOptionDescription(); @@ -176,40 +204,83 @@ internal Option GetOption(IReadOnlySet aliases) }; } - protected virtual Option GetBaseOption(IReadOnlySet aliases) + protected virtual CliOption GetBaseOption(IReadOnlySet aliases) { - return Type switch + string name = GetName(aliases); + CliOption cliOption = Type switch { - ParameterType.Boolean => new Option(aliases.ToArray()) + ParameterType.Boolean => new CliOption(name) { Arity = new ArgumentArity(0, 1) }, - ParameterType.Integer => new Option( - aliases.ToArray(), - parseArgument: result => GetParseArgument(this, ConvertValueToInt)(result)) + ParameterType.Integer => new CliOption(name) { + CustomParser = result => GetParseArgument(this, ConvertValueToInt)(result), Arity = new ArgumentArity(string.IsNullOrWhiteSpace(DefaultIfOptionWithoutValue) ? 1 : 0, 1) }, - ParameterType.String => new Option( - aliases.ToArray(), - parseArgument: result => GetParseArgument(this, ConvertValueToString)(result)) + ParameterType.String => new CliOption(name) { + CustomParser = result => GetParseArgument(this, ConvertValueToString)(result), Arity = new ArgumentArity(DefaultIfOptionWithoutValue == null ? 1 : 0, 1) }, - ParameterType.Float => new Option( - aliases.ToArray(), - parseArgument: result => GetParseArgument(this, ConvertValueToFloat)(result)) + ParameterType.Float => new CliOption(name) { + CustomParser = result => GetParseArgument(this, ConvertValueToFloat)(result), Arity = new ArgumentArity(string.IsNullOrWhiteSpace(DefaultIfOptionWithoutValue) ? 1 : 0, 1) }, - ParameterType.Hex => new Option( - aliases.ToArray(), - parseArgument: result => GetParseArgument(this, ConvertValueToHex)(result)) + ParameterType.Hex => new CliOption(name) { + CustomParser = result => GetParseArgument(this, ConvertValueToHex)(result), Arity = new ArgumentArity(string.IsNullOrWhiteSpace(DefaultIfOptionWithoutValue) ? 1 : 0, 1) }, _ => throw new Exception($"Unexpected value for {nameof(ParameterType)}: {Type}.") }; + AddAliases(cliOption, aliases); + return cliOption; + } + + /// + /// Returns the longest alias without prefix. + /// This is how System.CommandLine used to choose Name from aliases before Name and Aliases separation. + /// + protected string GetName(IReadOnlySet aliases) + { + string name = "-"; + + foreach (string alias in aliases) + { + if ((alias.Length - GetPrefixLength(alias)) > (name.Length - GetPrefixLength(name))) + { + name = alias; + } + } + + return name; + + static int GetPrefixLength(string alias) + { + if (alias[0] == '-') + { + return alias.Length > 1 && alias[1] == '-' ? 2 : 1; + } + else if (alias[0] == '/') + { + return 1; + } + + return 0; + } + } + + protected void AddAliases(CliOption option, IReadOnlySet aliases) + { + foreach (string alias in aliases) + { + if (alias != option.Name) + { + option.Aliases.Add(alias); + } + } } private static string ParameterTypeToString(ParameterType dataType) @@ -251,7 +322,7 @@ private static Func GetParseArgument(CliTemplateParameter if (argumentResult.Tokens.Count == 0) { - if (or.IsImplicit) + if (or.Implicit) { if (parameter.DefaultValue != null) { @@ -262,18 +333,18 @@ private static Func GetParseArgument(CliTemplateParameter } //Cannot parse default value '{0}' for option '{1}' as expected type '{2}'. - argumentResult.ErrorMessage = string.Format( + argumentResult.AddError(string.Format( LocalizableStrings.ParseTemplateOption_Error_InvalidDefaultValue, parameter.DefaultValue, - or.Token?.Value, - typeof(T).Name); + or.IdentifierToken?.Value, + typeof(T).Name)); //https://github.com/dotnet/command-line-api/blob/5eca6545a0196124cc1a66d8bd43db8945f1f1b7/src/System.CommandLine/Argument%7BT%7D.cs#L99-L113 //system-command-line can handle null. return default!; } //Default value for argument missing for option: '{0}'. - argumentResult.ErrorMessage = string.Format(LocalizableStrings.ParseTemplateOption_Error_MissingDefaultValue, or.Token?.Value); + argumentResult.AddError(string.Format(LocalizableStrings.ParseTemplateOption_Error_MissingDefaultValue, or.IdentifierToken?.Value)); return default!; } if (parameter.DefaultIfOptionWithoutValue != null) @@ -284,15 +355,15 @@ private static Func GetParseArgument(CliTemplateParameter return value; } //Cannot parse default if option without value '{0}' for option '{1}' as expected type '{2}'. - argumentResult.ErrorMessage = string.Format( + argumentResult.AddError(string.Format( LocalizableStrings.ParseTemplateOption_Error_InvalidDefaultIfNoOptionValue, parameter.DefaultIfOptionWithoutValue, - or.Token?.Value, - typeof(T).Name); + or.IdentifierToken?.Value, + typeof(T).Name)); return default!; } //Required argument missing for option: '{0}'. - argumentResult.ErrorMessage = string.Format(LocalizableStrings.ParseTemplateOption_Error_MissingDefaultIfNoOptionValue, or.Token?.Value); + argumentResult.AddError(string.Format(LocalizableStrings.ParseTemplateOption_Error_MissingDefaultIfNoOptionValue, or.IdentifierToken?.Value)); return default!; } else if (argumentResult.Tokens.Count == 1) @@ -303,17 +374,17 @@ private static Func GetParseArgument(CliTemplateParameter return value; } //Cannot parse argument '{0}' for option '{1}' as expected type '{2}'. - argumentResult.ErrorMessage = string.Format( + argumentResult.AddError(string.Format( LocalizableStrings.ParseTemplateOption_Error_InvalidArgument, argumentResult.Tokens[0].Value, - or.Token?.Value, - typeof(T).Name); + or.IdentifierToken?.Value, + typeof(T).Name)); return default!; } else { //Using more than 1 argument is not allowed for '{0}', used: {1}. - argumentResult.ErrorMessage = string.Format(LocalizableStrings.ParseTemplateOption_Error_InvalidCount, or.Token?.Value, argumentResult.Tokens.Count); + argumentResult.AddError(string.Format(LocalizableStrings.ParseTemplateOption_Error_InvalidCount, or.IdentifierToken?.Value, argumentResult.Tokens.Count)); return default!; } }; diff --git a/src/Cli/Microsoft.TemplateEngine.Cli/Commands/BaseCommand.cs b/src/Cli/Microsoft.TemplateEngine.Cli/Commands/BaseCommand.cs index db54ccdcdb76..df7a47958b89 100644 --- a/src/Cli/Microsoft.TemplateEngine.Cli/Commands/BaseCommand.cs +++ b/src/Cli/Microsoft.TemplateEngine.Cli/Commands/BaseCommand.cs @@ -3,7 +3,6 @@ using System.CommandLine; using System.CommandLine.Completions; -using System.CommandLine.Invocation; using System.Reflection; using Microsoft.DotNet.Cli.Utils; using Microsoft.TemplateEngine.Abstractions; @@ -12,11 +11,10 @@ using Microsoft.TemplateEngine.Edge; using Microsoft.TemplateEngine.Edge.Settings; using Microsoft.TemplateEngine.Utils; -using Command = System.CommandLine.Command; namespace Microsoft.TemplateEngine.Cli.Commands { - internal abstract class BaseCommand : Command + internal abstract class BaseCommand : CliCommand { private readonly Func _hostBuilder; @@ -49,7 +47,7 @@ protected IEngineEnvironmentSettings CreateEnvironmentSettings(GlobalArgs args, } } - internal abstract class BaseCommand : BaseCommand, ICommandHandler where TArgs : GlobalArgs + internal abstract class BaseCommand : BaseCommand where TArgs : GlobalArgs { internal BaseCommand( Func hostBuilder, @@ -57,70 +55,9 @@ internal BaseCommand( string description) : base(hostBuilder, name, description) { - this.Handler = this; + Action = new CommandAction(this); } - public async Task InvokeAsync(InvocationContext context) - { - TArgs args = ParseContext(context.ParseResult); - using IEngineEnvironmentSettings environmentSettings = CreateEnvironmentSettings(args, context.ParseResult); - using TemplatePackageManager templatePackageManager = new(environmentSettings); - CancellationToken cancellationToken = context.GetCancellationToken(); - - NewCommandStatus returnCode; - - try - { - using (Timing.Over(environmentSettings.Host.Logger, "Execute")) - { - await HandleGlobalOptionsAsync(args, environmentSettings, templatePackageManager, cancellationToken).ConfigureAwait(false); - returnCode = await ExecuteAsync(args, environmentSettings, templatePackageManager, context).ConfigureAwait(false); - } - } - catch (Exception ex) - { - AggregateException? ax = ex as AggregateException; - - while (ax != null && ax.InnerExceptions.Count == 1 && ax.InnerException is not null) - { - ex = ax.InnerException; - ax = ex as AggregateException; - } - - Reporter.Error.WriteLine(ex.Message.Bold().Red()); - - while (ex.InnerException != null) - { - ex = ex.InnerException; - ax = ex as AggregateException; - - while (ax != null && ax.InnerExceptions.Count == 1 && ax.InnerException is not null) - { - ex = ax.InnerException; - ax = ex as AggregateException; - } - - Reporter.Error.WriteLine(ex.Message.Bold().Red()); - } - - if (!string.IsNullOrWhiteSpace(ex.StackTrace)) - { - Reporter.Error.WriteLine(ex.StackTrace.Bold().Red()); - } - returnCode = NewCommandStatus.Unexpected; - } - - if (returnCode != NewCommandStatus.Success) - { - Reporter.Error.WriteLine(); - Reporter.Error.WriteLine(LocalizableStrings.BaseCommand_ExitCodeHelp, (int)returnCode); - } - - return (int)returnCode; - } - - public int Invoke(InvocationContext context) => InvokeAsync(context).GetAwaiter().GetResult(); - public override IEnumerable GetCompletions(CompletionContext context) { if (context.ParseResult == null) @@ -147,7 +84,7 @@ protected internal static async Task CheckTemplatesWithSubCommandName( CancellationToken cancellationToken) { IReadOnlyList availableTemplates = await templatePackageManager.GetTemplatesAsync(cancellationToken).ConfigureAwait(false); - string usedCommandAlias = args.ParseResult.CommandResult.Token.Value; + string usedCommandAlias = args.ParseResult.CommandResult.IdentifierToken.Value; if (!availableTemplates.Any(t => t.ShortNameList.Any(sn => string.Equals(sn, usedCommandAlias, StringComparison.OrdinalIgnoreCase)))) { return; @@ -158,7 +95,9 @@ protected internal static async Task CheckTemplatesWithSubCommandName( Reporter.Output.WriteLine(); } - protected static void PrintDeprecationMessage(ParseResult parseResult, Option? additionalOption = null) where TDepr : Command where TNew : Command + protected static void PrintDeprecationMessage(ParseResult parseResult, CliOption? additionalOption = null) + where TDepr : CliCommand + where TNew : CliCommand { var newCommandExample = Example.For(parseResult); if (additionalOption != null) @@ -176,22 +115,22 @@ protected static void PrintDeprecationMessage(ParseResult parseResu Reporter.Output.WriteLine(); } - protected abstract Task ExecuteAsync(TArgs args, IEngineEnvironmentSettings environmentSettings, TemplatePackageManager templatePackageManager, InvocationContext context); + protected abstract Task ExecuteAsync(TArgs args, IEngineEnvironmentSettings environmentSettings, TemplatePackageManager templatePackageManager, ParseResult parseResult, CancellationToken cancellationToken); protected abstract TArgs ParseContext(ParseResult parseResult); - protected virtual Option GetFilterOption(FilterOptionDefinition def) + protected virtual CliOption GetFilterOption(FilterOptionDefinition def) { return def.OptionFactory(); } - protected IReadOnlyDictionary SetupFilterOptions(IReadOnlyList filtersToSetup) + protected IReadOnlyDictionary SetupFilterOptions(IReadOnlyList filtersToSetup) { - Dictionary options = new(); + Dictionary options = new(); foreach (FilterOptionDefinition filterDef in filtersToSetup) { - Option newOption = GetFilterOption(filterDef); - this.AddOption(newOption); + CliOption newOption = GetFilterOption(filterDef); + this.Options.Add(newOption); options[filterDef] = newOption; } return options; @@ -202,8 +141,8 @@ protected IReadOnlyDictionary SetupFilterOptions /// protected void SetupTabularOutputOptions(ITabularOutputCommand command) { - this.AddOption(command.ColumnsAllOption); - this.AddOption(command.ColumnsOption); + this.Options.Add(command.ColumnsAllOption); + this.Options.Add(command.ColumnsOption); } private static async Task HandleGlobalOptionsAsync( @@ -279,5 +218,72 @@ private static void HandleDebugShowConfig(TArgs args, IEngineEnvironmentSettings Reporter.Output.WriteLine(generatorsFormatter.Layout()); Reporter.Output.WriteLine(); } + + private sealed class CommandAction : CliAction + { + private readonly BaseCommand _command; + + public CommandAction(BaseCommand command) => _command = command; + + public override async Task InvokeAsync(ParseResult parseResult, CancellationToken cancellationToken) + { + TArgs args = _command.ParseContext(parseResult); + using IEngineEnvironmentSettings environmentSettings = _command.CreateEnvironmentSettings(args, parseResult); + using TemplatePackageManager templatePackageManager = new(environmentSettings); + + NewCommandStatus returnCode; + + try + { + using (Timing.Over(environmentSettings.Host.Logger, "Execute")) + { + await HandleGlobalOptionsAsync(args, environmentSettings, templatePackageManager, cancellationToken).ConfigureAwait(false); + returnCode = await _command.ExecuteAsync(args, environmentSettings, templatePackageManager, parseResult, cancellationToken).ConfigureAwait(false); + } + } + catch (Exception ex) + { + AggregateException? ax = ex as AggregateException; + + while (ax != null && ax.InnerExceptions.Count == 1 && ax.InnerException is not null) + { + ex = ax.InnerException; + ax = ex as AggregateException; + } + + Reporter.Error.WriteLine(ex.Message.Bold().Red()); + + while (ex.InnerException != null) + { + ex = ex.InnerException; + ax = ex as AggregateException; + + while (ax != null && ax.InnerExceptions.Count == 1 && ax.InnerException is not null) + { + ex = ax.InnerException; + ax = ex as AggregateException; + } + + Reporter.Error.WriteLine(ex.Message.Bold().Red()); + } + + if (!string.IsNullOrWhiteSpace(ex.StackTrace)) + { + Reporter.Error.WriteLine(ex.StackTrace.Bold().Red()); + } + returnCode = NewCommandStatus.Unexpected; + } + + if (returnCode != NewCommandStatus.Success) + { + Reporter.Error.WriteLine(); + Reporter.Error.WriteLine(LocalizableStrings.BaseCommand_ExitCodeHelp, (int)returnCode); + } + + return (int)returnCode; + } + + public override int Invoke(ParseResult parseResult) => InvokeAsync(parseResult, CancellationToken.None).GetAwaiter().GetResult(); + } } } diff --git a/src/Cli/Microsoft.TemplateEngine.Cli/Commands/BaseFilterableArgs.cs b/src/Cli/Microsoft.TemplateEngine.Cli/Commands/BaseFilterableArgs.cs index 10e26ef2da00..8fb691a304d3 100644 --- a/src/Cli/Microsoft.TemplateEngine.Cli/Commands/BaseFilterableArgs.cs +++ b/src/Cli/Microsoft.TemplateEngine.Cli/Commands/BaseFilterableArgs.cs @@ -47,7 +47,7 @@ internal string GetFilterValue(FilterOptionDefinition filter) /// Token or null when token cannot be evaluated. internal string? GetFilterToken(FilterOptionDefinition filter) { - return _filters[filter].Token?.Value; + return _filters[filter].IdentifierToken?.Value; } private static IReadOnlyDictionary ParseFilters(IFilterableCommand filterableCommand, ParseResult parseResult) @@ -55,7 +55,7 @@ private static IReadOnlyDictionary ParseFi Dictionary filterValues = new(); foreach (var filter in filterableCommand.Filters) { - OptionResult? value = parseResult.FindResultFor(filter.Value); + OptionResult? value = parseResult.GetResult(filter.Value); if (value != null) { filterValues[filter.Key] = value; diff --git a/src/Cli/Microsoft.TemplateEngine.Cli/Commands/CommandLineUtils.cs b/src/Cli/Microsoft.TemplateEngine.Cli/Commands/CommandLineUtils.cs index a12cd8081ac5..657f2a6451fa 100644 --- a/src/Cli/Microsoft.TemplateEngine.Cli/Commands/CommandLineUtils.cs +++ b/src/Cli/Microsoft.TemplateEngine.Cli/Commands/CommandLineUtils.cs @@ -10,7 +10,7 @@ internal class CommandLineUtils { // This code is from System.CommandLine, HelpBuilder class. // Ideally those methods are exposed, we may switch to use them. - internal static string FormatArgumentUsage(IReadOnlyList arguments) + internal static string FormatArgumentUsage(IReadOnlyList arguments) { var sb = new StringBuilder(); var end = default(Stack); @@ -18,7 +18,7 @@ internal static string FormatArgumentUsage(IReadOnlyList arguments) for (var i = 0; i < arguments.Count; i++) { var argument = arguments[i]; - if (argument.IsHidden) + if (argument.Hidden) { continue; } @@ -57,20 +57,20 @@ internal static string FormatArgumentUsage(IReadOnlyList arguments) } return sb.ToString(); - bool IsMultiParented(Argument argument) => + bool IsMultiParented(CliArgument argument) => argument.Parents.Count() > 1; - bool IsOptional(Argument argument) => + bool IsOptional(CliArgument argument) => IsMultiParented(argument) || argument.Arity.MinimumNumberOfValues == 0; } - internal static string FormatArgumentUsage(Argument argument) => FormatArgumentUsage(new[] { argument }); + internal static string FormatArgumentUsage(CliArgument argument) => FormatArgumentUsage(new[] { argument }); - internal static string FormatArgumentUsage(Option option) => FormatArgumentUsage(new[] { option }); + internal static string FormatArgumentUsage(CliOption option) => FormatArgumentUsage(new[] { option }); // separate instance as Option.Argument is internal. - internal static string FormatArgumentUsage(IReadOnlyList