From 7af2c3cfa5d7b539b2beca6cbd98989f08660b1e Mon Sep 17 00:00:00 2001 From: Vlada Shubina Date: Wed, 3 Nov 2021 19:33:45 +0100 Subject: [PATCH 1/9] fixed parsing of Priority enum when reading from cache --- src/Microsoft.TemplateEngine.Utils/TemplateParameter.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.TemplateEngine.Utils/TemplateParameter.cs b/src/Microsoft.TemplateEngine.Utils/TemplateParameter.cs index 999f7687862..a8f17347b3f 100644 --- a/src/Microsoft.TemplateEngine.Utils/TemplateParameter.cs +++ b/src/Microsoft.TemplateEngine.Utils/TemplateParameter.cs @@ -31,7 +31,12 @@ public TemplateParameter(JObject jObject) Type = jObject.ToString(nameof(Type)) ?? "parameter"; DataType = jObject.ToString(nameof(DataType)) ?? "string"; Description = jObject.ToString(nameof(Description)); - Priority = Enum.TryParse(jObject.ToString(nameof(Priority)), out TemplateParameterPriority value) ? value : default; + + int priority = jObject.ToInt32(nameof(Priority)); + Priority = Enum.IsDefined(typeof(TemplateParameterPriority), priority) ? (TemplateParameterPriority)priority : default; + + Priority = (TemplateParameterPriority)jObject.ToInt32(nameof(Priority)); + DefaultValue = jObject.ToString(nameof(DefaultValue)); DefaultIfOptionWithoutValue = jObject.ToString(nameof(DefaultIfOptionWithoutValue)); DisplayName = jObject.ToString(nameof(DisplayName)); From b39d33e3be93f1e658ab5a3bedfa7310ed9e5a49 Mon Sep 17 00:00:00 2001 From: Vlada Shubina Date: Wed, 3 Nov 2021 19:34:44 +0100 Subject: [PATCH 2/9] disabled help on pre-parsing --- src/dotnet-new3/ParserFactory.cs | 16 ++++++++++------ src/dotnet-new3/Program.cs | 2 +- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/dotnet-new3/ParserFactory.cs b/src/dotnet-new3/ParserFactory.cs index 572f3438866..53ad240f52a 100644 --- a/src/dotnet-new3/ParserFactory.cs +++ b/src/dotnet-new3/ParserFactory.cs @@ -11,17 +11,21 @@ namespace Dotnet_new3 { internal static class ParserFactory { - internal static Parser CreateParser(Command command) + internal static Parser CreateParser(Command command, bool disableHelp = false) { - return new CommandLineBuilder(command) + var builder = new CommandLineBuilder(command) //.UseExceptionHandler(ExceptionHandler) - .UseHelp() - //.UseHelpBuilder(context => DotnetHelpBuilder.Instance.Value) //.UseLocalizationResources(new CommandLineValidationMessages()) .UseParseDirective() .UseSuggestDirective() - .DisablePosixBinding() - .Build(); + .DisablePosixBinding(); + + if (!disableHelp) + { + builder = builder.UseHelp(); + //.UseHelpBuilder(context => DotnetHelpBuilder.Instance.Value) + } + return builder.Build(); } private static CommandLineBuilder DisablePosixBinding(this CommandLineBuilder builder) diff --git a/src/dotnet-new3/Program.cs b/src/dotnet-new3/Program.cs index 7553e147787..4326c0b7798 100644 --- a/src/dotnet-new3/Program.cs +++ b/src/dotnet-new3/Program.cs @@ -24,7 +24,7 @@ public static class Program public static int Main(string[] args) { Command new3Command = new New3Command(); - ParseResult preParseResult = ParserFactory.CreateParser(new3Command).Parse(args); + ParseResult preParseResult = ParserFactory.CreateParser(new3Command, disableHelp: true).Parse(args); DefaultTemplateEngineHost host = CreateHost(preParseResult.GetValueForOption(New3Command.DebugDisableBuiltInTemplatesOption)); ITelemetryLogger telemetryLogger = new TelemetryLogger(null, preParseResult.GetValueForOption(New3Command.DebugEmitTelemetryOption)); From e297c6fbb3d192a5a8a5a7c0d094a20475343d69 Mon Sep 17 00:00:00 2001 From: Vlada Shubina Date: Wed, 3 Nov 2021 19:35:40 +0100 Subject: [PATCH 3/9] refactored AliasAssignmentCoordinator subject to discussion whether we need this logic or not --- .../AliasAssignmentCoordinator.cs | 187 ----------- .../Commands/AliasAssignmentCoordinator.cs | 195 +++++++++++ .../AliasAssignmentTests.cs | 309 +++++++----------- 3 files changed, 307 insertions(+), 384 deletions(-) delete mode 100644 src/Microsoft.TemplateEngine.Cli/CommandParsing/AliasAssignmentCoordinator.cs create mode 100644 src/Microsoft.TemplateEngine.Cli/Commands/AliasAssignmentCoordinator.cs diff --git a/src/Microsoft.TemplateEngine.Cli/CommandParsing/AliasAssignmentCoordinator.cs b/src/Microsoft.TemplateEngine.Cli/CommandParsing/AliasAssignmentCoordinator.cs deleted file mode 100644 index d48f1e627a7..00000000000 --- a/src/Microsoft.TemplateEngine.Cli/CommandParsing/AliasAssignmentCoordinator.cs +++ /dev/null @@ -1,187 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.TemplateEngine.Abstractions; - -namespace Microsoft.TemplateEngine.Cli.CommandParsing -{ - internal class AliasAssignmentCoordinator - { - private IReadOnlyList _parameterDefinitions; - private IDictionary _longNameOverrides; - private IDictionary _shortNameOverrides; - private HashSet _takenAliases; - private Dictionary _longAssignments; - private Dictionary _shortAssignments; - private HashSet _invalidParams; - private bool _calculatedAssignments; - - internal AliasAssignmentCoordinator(IReadOnlyList parameterDefinitions, IDictionary longNameOverrides, IDictionary shortNameOverrides, HashSet takenAliases) - { - _parameterDefinitions = parameterDefinitions; - _longNameOverrides = longNameOverrides; - _shortNameOverrides = shortNameOverrides; - _takenAliases = takenAliases; - _longAssignments = new Dictionary(); - _shortAssignments = new Dictionary(); - _invalidParams = new HashSet(); - _calculatedAssignments = false; - } - - internal IReadOnlyDictionary LongNameAssignments - { - get - { - EnsureAliasAssignments(); - return _longAssignments; - } - } - - internal IReadOnlyDictionary ShortNameAssignments - { - get - { - EnsureAliasAssignments(); - return _shortAssignments; - } - } - - internal HashSet InvalidParams - { - get - { - EnsureAliasAssignments(); - return _invalidParams; - } - } - - internal HashSet TakenAliases - { - get - { - EnsureAliasAssignments(); - return _takenAliases; - } - } - - private void EnsureAliasAssignments() - { - if (_calculatedAssignments) - { - return; - } - - Dictionary> aliasAssignments = new Dictionary>(); - Dictionary paramNamesNeedingAssignment = _parameterDefinitions.Where(x => x.Priority != TemplateParameterPriority.Implicit) - .ToDictionary(x => x.Name, x => x); - - SetupAssignmentsFromLongOverrides(paramNamesNeedingAssignment); - SetupAssignmentsFromShortOverrides(paramNamesNeedingAssignment); - SetupAssignmentsWithoutOverrides(paramNamesNeedingAssignment); - - _calculatedAssignments = true; - } - - private void SetupAssignmentsFromLongOverrides(IReadOnlyDictionary paramNamesNeedingAssignment) - { - foreach (KeyValuePair canonicalAndLong in _longNameOverrides.Where(x => paramNamesNeedingAssignment.ContainsKey(x.Key))) - { - string canonical = canonicalAndLong.Key; - string longOverride = canonicalAndLong.Value; - if (CommandAliasAssigner.TryAssignAliasesForParameter((x) => _takenAliases.Contains(x), canonical, longOverride, null, out IReadOnlyList assignedAliases)) - { - // only deal with the long here, ignore the short for now - string longParam = assignedAliases.FirstOrDefault(x => x.StartsWith("--")); - if (!string.IsNullOrEmpty(longParam)) - { - _longAssignments.Add(canonical, longParam); - _takenAliases.Add(longParam); - } - } - else - { - _invalidParams.Add(canonical); - } - } - } - - private void SetupAssignmentsFromShortOverrides(IReadOnlyDictionary paramNamesNeedingAssignment) - { - foreach (KeyValuePair canonicalAndShort in _shortNameOverrides.Where(x => paramNamesNeedingAssignment.ContainsKey(x.Key))) - { - string canonical = canonicalAndShort.Key; - string shortOverride = canonicalAndShort.Value; - - if (shortOverride == string.Empty) - { - // it was explicitly empty string in the host file. If it wasn't specified, it'll be null - // this means there should be no short version - continue; - } - - if (CommandAliasAssigner.TryAssignAliasesForParameter((x) => _takenAliases.Contains(x), canonical, null, shortOverride, out IReadOnlyList assignedAliases)) - { - string shortParam = assignedAliases.FirstOrDefault(x => x.StartsWith("-") && !x.StartsWith("--")); - if (!string.IsNullOrEmpty(shortParam)) - { - _shortAssignments.Add(canonical, shortParam); - _takenAliases.Add(shortParam); - } - } - else - { - _invalidParams.Add(canonical); - } - } - } - - private void SetupAssignmentsWithoutOverrides(IReadOnlyDictionary paramNamesNeedingAssignment) - { - foreach (ITemplateParameter parameterInfo in paramNamesNeedingAssignment.Values) - { - if (_longAssignments.ContainsKey(parameterInfo.Name) && _shortAssignments.ContainsKey(parameterInfo.Name)) - { - // already fully assigned - continue; - } - - _longNameOverrides.TryGetValue(parameterInfo.Name, out string longOverride); - _shortNameOverrides.TryGetValue(parameterInfo.Name, out string shortOverride); - - if (CommandAliasAssigner.TryAssignAliasesForParameter((x) => _takenAliases.Contains(x), parameterInfo.Name, longOverride, shortOverride, out IReadOnlyList assignedAliases)) - { - if (shortOverride != string.Empty) - { - // explicit empty string in the host file means there should be no short name. - // but thats not the case here. - if (!_shortAssignments.ContainsKey(parameterInfo.Name)) - { - // still needs a short version - string shortParam = assignedAliases.FirstOrDefault(x => x.StartsWith("-") && !x.StartsWith("--")); - if (!string.IsNullOrEmpty(shortParam)) - { - _shortAssignments.Add(parameterInfo.Name, shortParam); - _takenAliases.Add(shortParam); - } - } - } - - if (!_longAssignments.ContainsKey(parameterInfo.Name)) - { - // still needs a long version - string longParam = assignedAliases.FirstOrDefault(x => x.StartsWith("--")); - if (!string.IsNullOrEmpty(longParam)) - { - _longAssignments.Add(parameterInfo.Name, longParam); - _takenAliases.Add(longParam); - } - } - } - else - { - _invalidParams.Add(parameterInfo.Name); - } - } - } - } -} diff --git a/src/Microsoft.TemplateEngine.Cli/Commands/AliasAssignmentCoordinator.cs b/src/Microsoft.TemplateEngine.Cli/Commands/AliasAssignmentCoordinator.cs new file mode 100644 index 00000000000..3706dfd5ccf --- /dev/null +++ b/src/Microsoft.TemplateEngine.Cli/Commands/AliasAssignmentCoordinator.cs @@ -0,0 +1,195 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable enable + +namespace Microsoft.TemplateEngine.Cli.Commands +{ + internal class AliasAssignmentCoordinator + { + internal static IEnumerable<(CliTemplateParameter Parameter, IEnumerable Aliases, IEnumerable Errors)> AssignAliasesForParameter(IEnumerable parameters, HashSet takenAliases) + { + List<(CliTemplateParameter Parameter, IEnumerable Aliases, IEnumerable Errors)> result = new(); + + List predefinedLongOverrides = parameters.SelectMany(p => p.LongNameOverrides).Where(n => !string.IsNullOrEmpty(n)).Select(n => $"--{n}").ToList(); + List predefinedShortOverrides = parameters.SelectMany(p => p.ShortNameOverrides).Where(n => !string.IsNullOrEmpty(n)).Select(n => $"-{n}").ToList(); + + Func isAliasTaken = (s) => takenAliases.Contains(s); + Func isLongNamePredefined = (s) => predefinedLongOverrides.Contains(s); + Func isShortNamePredefined = (s) => predefinedShortOverrides.Contains(s); + + foreach (var parameter in parameters) + { + List aliases = new List(); + List errors = new List(); + if (parameter.Name.Contains(':')) + { + // Colon is reserved, template param names cannot have any. + errors.Add($"Parameter name '{parameter.Name}' contains colon, which is forbidden."); + result.Add((parameter, aliases, errors)); + continue; + } + + HandleLongOverrides(takenAliases, aliases, errors, isAliasTaken, isLongNamePredefined, parameter); + HandleShortOverrides(takenAliases, aliases, errors, isAliasTaken, parameter); + + //if there is already short name override defined, do not generate new one + if (parameter.ShortNameOverrides.Any()) + { + result.Add((parameter, aliases, errors)); + continue; + } + + GenerateShortName(takenAliases, aliases, errors, isAliasTaken, isShortNamePredefined, parameter); + result.Add((parameter, aliases, errors)); + } + return result; + } + + private static void HandleShortOverrides( + HashSet takenAliases, + List aliases, + List errors, + Func isAliasTaken, + CliTemplateParameter parameter) + { + foreach (string shortNameOverride in parameter.ShortNameOverrides) + { + if (shortNameOverride == string.Empty) + { + // it was explicitly empty string in the host file. + continue; + } + if (!string.IsNullOrEmpty(shortNameOverride)) + { + // short name starting point was explicitly specified + string fullShortNameOverride = "-" + shortNameOverride; + if (!isAliasTaken(shortNameOverride)) + { + aliases.Add(fullShortNameOverride); + takenAliases.Add(fullShortNameOverride); + continue; + } + + //if taken, we append prefix + string qualifiedShortNameOverride = "-p:" + shortNameOverride; + if (!isAliasTaken(qualifiedShortNameOverride)) + { + aliases.Add(qualifiedShortNameOverride); + takenAliases.Add(qualifiedShortNameOverride); + continue; + } + errors.Add($"Failed to assign short option name from {parameter.Name}, tried: '{fullShortNameOverride}'; '{qualifiedShortNameOverride}.'"); + } + } + } + + private static void HandleLongOverrides( + HashSet takenAliases, + List aliases, + List errors, + Func isAliasTaken, + Func isLongNamePredefined, + CliTemplateParameter parameter) + { + bool noLongOverrideDefined = false; + IEnumerable longNameOverrides = parameter.LongNameOverrides; + + //if no long override define, we use parameter name + if (!longNameOverrides.Any()) + { + longNameOverrides = new[] { parameter.Name }; + noLongOverrideDefined = true; + } + + foreach (string longName in longNameOverrides) + { + string optionName = "--" + longName; + if ((!noLongOverrideDefined && !isAliasTaken(optionName)) + //if we use parameter name, we should also check if there is any other parameter which defines this long name. + //in case it is, then we should give precedence to other parameter to use it. + || (noLongOverrideDefined && !isAliasTaken(optionName) && !isLongNamePredefined(optionName))) + { + aliases.Add(optionName); + takenAliases.Add(optionName); + continue; + } + + // if paramater name is taken + optionName = "--param:" + longName; + if (!isAliasTaken(optionName)) + { + aliases.Add(optionName); + takenAliases.Add(optionName); + continue; + } + errors.Add($"Failed to assign long option name from {parameter.Name}, tried: '--{longName}'; '--param:{longName}.'"); + } + } + + private static void GenerateShortName( + HashSet takenAliases, + List aliases, + List errors, + Func isAliasTaken, + Func isShortNamePredefined, + CliTemplateParameter parameter) + { + //use long override as base, if exists + string flagFullText = parameter.LongNameOverrides.Count > 0 ? parameter.LongNameOverrides[0] : parameter.Name; + + // try to generate un-prefixed name, if not taken. + string shortName = GetFreeShortName(s => isAliasTaken(s) || (isShortNamePredefined(s)), flagFullText); + if (!isAliasTaken(shortName)) + { + aliases.Add(shortName); + takenAliases.Add(shortName); + return; + } + + // try to generate prefixed name, as the fallback + string qualifiedShortName = GetFreeShortName(s => isAliasTaken(s) || (isShortNamePredefined(s)), flagFullText, "p:"); + if (!isAliasTaken(qualifiedShortName)) + { + aliases.Add(qualifiedShortName); + return; + } + errors.Add($"Failed to assign short option name from {parameter.Name}, tried: '{shortName}'; '{qualifiedShortName}.'"); + } + + private static string GetFreeShortName(Func isAliasTaken, string name, string prefix = "") + { + string[] parts = name.Split(new[] { '-' }, StringSplitOptions.RemoveEmptyEntries); + string[] buckets = new string[parts.Length]; + + for (int i = 0; i < buckets.Length; ++i) + { + buckets[i] = parts[i].Substring(0, 1); + } + + int lastBucket = parts.Length - 1; + while (isAliasTaken("-" + prefix + string.Join("", buckets))) + { + //Find the next thing we can take a character from + bool first = true; + int end = (lastBucket + 1) % parts.Length; + int i = (lastBucket + 1) % parts.Length; + for (; first || i != end; first = false, i = (i + 1) % parts.Length) + { + if (parts[i].Length > buckets[i].Length) + { + buckets[i] = parts[i].Substring(0, buckets[i].Length + 1); + break; + } + } + + if (i == end) + { + break; + } + } + + return "-" + prefix + string.Join("", buckets); + } + } +} diff --git a/test/Microsoft.TemplateEngine.Cli.UnitTests/AliasAssignmentTests.cs b/test/Microsoft.TemplateEngine.Cli.UnitTests/AliasAssignmentTests.cs index f9f8e170954..770fc625d26 100644 --- a/test/Microsoft.TemplateEngine.Cli.UnitTests/AliasAssignmentTests.cs +++ b/test/Microsoft.TemplateEngine.Cli.UnitTests/AliasAssignmentTests.cs @@ -1,9 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Microsoft.TemplateEngine.Abstractions; -using Microsoft.TemplateEngine.Cli.CommandParsing; -using Microsoft.TemplateEngine.Utils; +using Microsoft.TemplateEngine.Cli.Commands; using Xunit; namespace Microsoft.TemplateEngine.Cli.UnitTests @@ -44,246 +42,163 @@ private static HashSet InitiallyTakenAliases [Fact(DisplayName = nameof(LongNameOverrideTakesPrecendence))] public void LongNameOverrideTakesPrecendence() { - IReadOnlyList paramNameList = new List() + IReadOnlyList paramList = new List() { - "foo", - "bar", + new CliTemplateParameter("foo"), + new CliTemplateParameter("bar", longNameOverrides: new[] { "foo" }) }; - IReadOnlyList parameters = ParameterNamesToParametersTransform(paramNameList); - IDictionary longNameOverrides = new Dictionary() - { - { "bar", "foo" } // bar explicitly wants foo for its long form - }; - IDictionary shortNameOverrides = new Dictionary(); + var result = AliasAssignmentCoordinator.AssignAliasesForParameter(paramList, InitiallyTakenAliases).ToDictionary(r => r.Parameter.Name, r => r); - AliasAssignmentCoordinator assignmentCoordinator = new AliasAssignmentCoordinator(parameters, longNameOverrides, shortNameOverrides, InitiallyTakenAliases); - - Assert.Equal("--param:foo", assignmentCoordinator.LongNameAssignments["foo"]); - Assert.Equal("-f", assignmentCoordinator.ShortNameAssignments["foo"]); - Assert.Equal("--foo", assignmentCoordinator.LongNameAssignments["bar"]); - Assert.Equal("-fo", assignmentCoordinator.ShortNameAssignments["bar"]); // the short name is based on the long name override if it exists - Assert.Empty(assignmentCoordinator.InvalidParams); + Assert.Contains("--param:foo", result["foo"].Aliases); + Assert.Contains("-f", result["foo"].Aliases); + Assert.Contains("--foo", result["bar"].Aliases); + Assert.Contains("-fo", result["bar"].Aliases); // the short name is based on the long name override if it exists + Assert.DoesNotContain(result, r => r.Value.Errors.Any()); } [Fact(DisplayName = nameof(ShortNameOverrideTakesPrecedence))] public void ShortNameOverrideTakesPrecedence() { - IReadOnlyList paramNameList = new List() - { - "foo", - "bar", - }; - IReadOnlyList parameters = ParameterNamesToParametersTransform(paramNameList); - - IDictionary longNameOverrides = new Dictionary(); - IDictionary shortNameOverrides = new Dictionary() + IReadOnlyList paramList = new List() { - { "bar", "f" } // bar explicitly wants f for its short form + new CliTemplateParameter("foo"), + new CliTemplateParameter("bar", shortNameOverrides: new[] { "f" }) }; - AliasAssignmentCoordinator assignmentCoordinator = new AliasAssignmentCoordinator(parameters, longNameOverrides, shortNameOverrides, InitiallyTakenAliases); + var result = AliasAssignmentCoordinator.AssignAliasesForParameter(paramList, InitiallyTakenAliases).ToDictionary(r => r.Parameter.Name, r => r); - Assert.Equal("--foo", assignmentCoordinator.LongNameAssignments["foo"]); - Assert.Equal("-fo", assignmentCoordinator.ShortNameAssignments["foo"]); - Assert.Equal("--bar", assignmentCoordinator.LongNameAssignments["bar"]); - Assert.Equal("-f", assignmentCoordinator.ShortNameAssignments["bar"]); - Assert.Empty(assignmentCoordinator.InvalidParams); + Assert.Contains("--foo", result["foo"].Aliases); + Assert.Contains("-fo", result["foo"].Aliases); + Assert.Contains("--bar", result["bar"].Aliases); + Assert.Contains("-f", result["bar"].Aliases); + Assert.DoesNotContain(result, r => r.Value.Errors.Any()); } [Fact(DisplayName = nameof(ShortNameExcludedWithEmptyStringOverride))] public void ShortNameExcludedWithEmptyStringOverride() { - IReadOnlyList paramNameList = new List() + IReadOnlyList paramList = new List() { - "foo", - "bar", + new CliTemplateParameter("foo"), + new CliTemplateParameter("bar", shortNameOverrides: new[] { "" }) }; - IReadOnlyList parameters = ParameterNamesToParametersTransform(paramNameList); - - IDictionary longNameOverrides = new Dictionary(); - IDictionary shortNameOverrides = new Dictionary() - { - { "bar", "" } // bar explicitly wants f for its short form - }; - - AliasAssignmentCoordinator assignmentCoordinator = new AliasAssignmentCoordinator(parameters, longNameOverrides, shortNameOverrides, InitiallyTakenAliases); - - Assert.Equal("--foo", assignmentCoordinator.LongNameAssignments["foo"]); - Assert.Equal("-f", assignmentCoordinator.ShortNameAssignments["foo"]); - Assert.Equal("--bar", assignmentCoordinator.LongNameAssignments["bar"]); - Assert.False(assignmentCoordinator.ShortNameAssignments.TryGetValue("bar", out string placeholder)); - Assert.Empty(assignmentCoordinator.InvalidParams); + + var result = AliasAssignmentCoordinator.AssignAliasesForParameter(paramList, InitiallyTakenAliases).ToDictionary(r => r.Parameter.Name, r => r); + + Assert.Contains("--foo", result["foo"].Aliases); + Assert.Contains("-f", result["foo"].Aliases); + Assert.Contains("--bar", result["bar"].Aliases); + Assert.Single(result["bar"].Aliases); + Assert.DoesNotContain(result, r => r.Value.Errors.Any()); } [Fact(DisplayName = nameof(ParameterNameCannotContainColon))] public void ParameterNameCannotContainColon() { - IReadOnlyList paramNameList = new List() + IReadOnlyList paramList = new List() { - "foo:bar", + new CliTemplateParameter("foo:bar"), }; - IReadOnlyList parameters = ParameterNamesToParametersTransform(paramNameList); - - IDictionary longNameOverrides = new Dictionary(); - IDictionary shortNameOverrides = new Dictionary(); - AliasAssignmentCoordinator assignmentCoordinator = new AliasAssignmentCoordinator(parameters, longNameOverrides, shortNameOverrides, InitiallyTakenAliases); - - Assert.Equal(0, assignmentCoordinator.LongNameAssignments.Count); - Assert.Equal(0, assignmentCoordinator.ShortNameAssignments.Count); - Assert.Single(assignmentCoordinator.InvalidParams); - Assert.Contains("foo:bar", assignmentCoordinator.InvalidParams); + var result = AliasAssignmentCoordinator.AssignAliasesForParameter(paramList, InitiallyTakenAliases).ToDictionary(r => r.Parameter.Name, r => r); + Assert.Empty(result["foo:bar"].Aliases); + Assert.Single(result["foo:bar"].Errors); + Assert.Contains("Parameter name 'foo:bar' contains colon, which is forbidden.", result["foo:bar"].Errors); } [Fact(DisplayName = nameof(ShortNameGetPrependedPColonIfNeeded))] public void ShortNameGetPrependedPColonIfNeeded() { - IReadOnlyList paramNameList = new List() - { - "bar", - "f" - }; - IReadOnlyList parameters = ParameterNamesToParametersTransform(paramNameList); - - IDictionary longNameOverrides = new Dictionary(); - IDictionary shortNameOverrides = new Dictionary() + IReadOnlyList paramList = new List() { - { "bar", "f" } + new CliTemplateParameter("bar", shortNameOverrides: new [] { "f" }), + new CliTemplateParameter("f") }; - AliasAssignmentCoordinator assignmentCoordinator = new AliasAssignmentCoordinator(parameters, longNameOverrides, shortNameOverrides, InitiallyTakenAliases); + var result = AliasAssignmentCoordinator.AssignAliasesForParameter(paramList, InitiallyTakenAliases).ToDictionary(r => r.Parameter.Name, r => r); - Assert.Equal("--bar", assignmentCoordinator.LongNameAssignments["bar"]); - Assert.Equal("-f", assignmentCoordinator.ShortNameAssignments["bar"]); - Assert.Equal("--f", assignmentCoordinator.LongNameAssignments["f"]); - Assert.Equal("-p:f", assignmentCoordinator.ShortNameAssignments["f"]); + Assert.Contains("--bar", result["bar"].Aliases); + Assert.Contains("-f", result["bar"].Aliases); + Assert.Contains("--f", result["f"].Aliases); + Assert.Contains("-p:f", result["f"].Aliases); + Assert.DoesNotContain(result, r => r.Value.Errors.Any()); } // This reflects the MVC 2.0 tempalte as of May 24, 2017 [Fact(DisplayName = nameof(CheckAliasAssignmentsMvc20))] public void CheckAliasAssignmentsMvc20() { - IReadOnlyList paramNameList = new List() - { - "auth", - "AAdB2CInstance", - "SignUpSignInPolicyId", - "ResetPasswordPolicyId", - "EditProfilePolicyId", - "AADInstance", - "ClientId", - "Domain", - "TenantId", - "CallbackPath", - "OrgReadAccess", - "UserSecretsId", - "IncludeLaunchSettings", - "HttpsPort", - "KestrelPort", - "IISExpressPort", - "UseLocalDB", - "TargetFrameworkOverride", - "Framework", - "NoTools", - "skipRestore", - }; - IReadOnlyList parameters = ParameterNamesToParametersTransform(paramNameList); - - IDictionary longNameOverrides = new Dictionary() + IReadOnlyList paramList = new List() { - { "TargetFrameworkOverride", "target-framework-override" }, - { "UseLocalDB", "use-local-db" }, - { "AADInstance", "aad-instance" }, - { "AAdB2CInstance", "aad-b2c-instance" }, - { "SignUpSignInPolicyId", "susi-policy-id" }, - { "ResetPasswordPolicyId", "reset-password-policy-id" }, - { "EditProfilePolicyId", "edit-profile-policy-id" }, - { "OrgReadAccess", "org-read-access" }, - { "ClientId", "client-id" }, - { "CallbackPath", "callback-path" }, - { "Domain", "domain" }, - { "TenantId", "tenant-id" }, - { "Framework", "framework" }, - { "NoTools", "no-tools" }, - { "skipRestore", "no-restore" }, + new CliTemplateParameter("auth"), + new CliTemplateParameter("AAdB2CInstance", longNameOverrides: new [] { "aad-b2c-instance" }, shortNameOverrides: new [] { "" }), + new CliTemplateParameter("SignUpSignInPolicyId", longNameOverrides: new [] { "susi-policy-id" }, shortNameOverrides: new [] { "ssp" }), + new CliTemplateParameter("ResetPasswordPolicyId", longNameOverrides: new [] { "reset-password-policy-id" }, shortNameOverrides: new [] { "rp" }), + new CliTemplateParameter("EditProfilePolicyId", longNameOverrides: new [] { "edit-profile-policy-id" }, shortNameOverrides: new [] { "ep" }), + new CliTemplateParameter("AADInstance", longNameOverrides: new [] { "aad-instance" }, shortNameOverrides: new [] { "" } ), + new CliTemplateParameter("ClientId", longNameOverrides: new [] { "client-id" }, shortNameOverrides: new [] { "" }), + new CliTemplateParameter("Domain", longNameOverrides: new [] { "domain" }, shortNameOverrides: new [] { "" }), + new CliTemplateParameter("TenantId", longNameOverrides: new [] { "tenant-id" }, shortNameOverrides: new [] { "" }), + new CliTemplateParameter("CallbackPath", longNameOverrides: new [] { "callback-path" }, shortNameOverrides: new [] { "" }), + new CliTemplateParameter("OrgReadAccess", longNameOverrides: new [] { "org-read-access" }, shortNameOverrides: new [] { "r" }), + new CliTemplateParameter("UserSecretsId"), + new CliTemplateParameter("IncludeLaunchSettings"), + new CliTemplateParameter("HttpsPort"), + new CliTemplateParameter("KestrelPort"), + new CliTemplateParameter("IISExpressPort"), + new CliTemplateParameter("UseLocalDB", longNameOverrides: new [] { "use-local-db" }), + new CliTemplateParameter("TargetFrameworkOverride", longNameOverrides: new [] { "target-framework-override" }, shortNameOverrides: new [] { "" }), + new CliTemplateParameter("Framework", longNameOverrides: new [] { "framework" }), + new CliTemplateParameter("NoTools", longNameOverrides: new [] { "no-tools" }), + new CliTemplateParameter("skipRestore", longNameOverrides: new [] { "no-restore" }, shortNameOverrides: new [] { "" }) }; - IDictionary shortNameOverrides = new Dictionary() - { - { "TargetFrameworkOverride", "" }, - { "AADInstance", "" }, - { "AAdB2CInstance", "" }, - { "SignUpSignInPolicyId", "ssp" }, - { "ResetPasswordPolicyId", "rp" }, - { "EditProfilePolicyId", "ep" }, - { "OrgReadAccess", "r" }, - { "ClientId", "" }, - { "CallbackPath", "" }, - { "Domain", "" }, - { "TenantId", "" }, - { "skipRestore", "" }, - }; - - AliasAssignmentCoordinator assignmentCoordinator = new AliasAssignmentCoordinator(parameters, longNameOverrides, shortNameOverrides, InitiallyTakenAliases); - - Assert.Equal("-au", assignmentCoordinator.ShortNameAssignments["auth"]); - Assert.Equal("--auth", assignmentCoordinator.LongNameAssignments["auth"]); - Assert.False(assignmentCoordinator.ShortNameAssignments.TryGetValue("AAdB2CInstance", out string placeholder)); - Assert.Equal("--aad-b2c-instance", assignmentCoordinator.LongNameAssignments["AAdB2CInstance"]); - Assert.Equal("-ssp", assignmentCoordinator.ShortNameAssignments["SignUpSignInPolicyId"]); - Assert.Equal("--susi-policy-id", assignmentCoordinator.LongNameAssignments["SignUpSignInPolicyId"]); - Assert.Equal("-rp", assignmentCoordinator.ShortNameAssignments["ResetPasswordPolicyId"]); - Assert.Equal("--reset-password-policy-id", assignmentCoordinator.LongNameAssignments["ResetPasswordPolicyId"]); - Assert.Equal("-ep", assignmentCoordinator.ShortNameAssignments["EditProfilePolicyId"]); - Assert.Equal("--edit-profile-policy-id", assignmentCoordinator.LongNameAssignments["EditProfilePolicyId"]); - Assert.False(assignmentCoordinator.ShortNameAssignments.TryGetValue("AADInstance", out placeholder)); - Assert.Equal("--aad-instance", assignmentCoordinator.LongNameAssignments["AADInstance"]); - Assert.False(assignmentCoordinator.ShortNameAssignments.TryGetValue("ClientId", out placeholder)); - Assert.Equal("--client-id", assignmentCoordinator.LongNameAssignments["ClientId"]); - Assert.False(assignmentCoordinator.ShortNameAssignments.TryGetValue("Domain", out placeholder)); - Assert.Equal("--domain", assignmentCoordinator.LongNameAssignments["Domain"]); - Assert.False(assignmentCoordinator.ShortNameAssignments.TryGetValue("TenantId", out placeholder)); - Assert.Equal("--tenant-id", assignmentCoordinator.LongNameAssignments["TenantId"]); - Assert.False(assignmentCoordinator.ShortNameAssignments.TryGetValue("CallbackPath", out placeholder)); - Assert.Equal("--callback-path", assignmentCoordinator.LongNameAssignments["CallbackPath"]); - Assert.Equal("-r", assignmentCoordinator.ShortNameAssignments["OrgReadAccess"]); - Assert.Equal("--org-read-access", assignmentCoordinator.LongNameAssignments["OrgReadAccess"]); - Assert.Equal("-U", assignmentCoordinator.ShortNameAssignments["UserSecretsId"]); - Assert.Equal("--UserSecretsId", assignmentCoordinator.LongNameAssignments["UserSecretsId"]); - Assert.Equal("-I", assignmentCoordinator.ShortNameAssignments["IncludeLaunchSettings"]); - Assert.Equal("--IncludeLaunchSettings", assignmentCoordinator.LongNameAssignments["IncludeLaunchSettings"]); - Assert.Equal("-H", assignmentCoordinator.ShortNameAssignments["HttpsPort"]); - Assert.Equal("--HttpsPort", assignmentCoordinator.LongNameAssignments["HttpsPort"]); - Assert.Equal("-K", assignmentCoordinator.ShortNameAssignments["KestrelPort"]); - Assert.Equal("--KestrelPort", assignmentCoordinator.LongNameAssignments["KestrelPort"]); - Assert.Equal("-II", assignmentCoordinator.ShortNameAssignments["IISExpressPort"]); - Assert.Equal("--IISExpressPort", assignmentCoordinator.LongNameAssignments["IISExpressPort"]); - Assert.Equal("-uld", assignmentCoordinator.ShortNameAssignments["UseLocalDB"]); - Assert.Equal("--use-local-db", assignmentCoordinator.LongNameAssignments["UseLocalDB"]); - Assert.False(assignmentCoordinator.ShortNameAssignments.TryGetValue("TargetFrameworkOverride", out placeholder)); - Assert.Equal("--target-framework-override", assignmentCoordinator.LongNameAssignments["TargetFrameworkOverride"]); - Assert.Equal("-f", assignmentCoordinator.ShortNameAssignments["Framework"]); - Assert.Equal("--framework", assignmentCoordinator.LongNameAssignments["Framework"]); - Assert.Equal("-nt", assignmentCoordinator.ShortNameAssignments["NoTools"]); - Assert.Equal("--no-tools", assignmentCoordinator.LongNameAssignments["NoTools"]); - Assert.False(assignmentCoordinator.ShortNameAssignments.TryGetValue("SkipRestore", out placeholder)); - Assert.Equal("--no-restore", assignmentCoordinator.LongNameAssignments["skipRestore"]); - Assert.Empty(assignmentCoordinator.InvalidParams); - } - - // fills in enough of the parameter info for alias assignment - private static IReadOnlyList ParameterNamesToParametersTransform(IReadOnlyList paramNameList) - { - List parameterList = new List(); - - foreach (string paramName in paramNameList) - { - ITemplateParameter parameter = new TemplateParameter(paramName, type: "parameter", datatype: "string", priority: TemplateParameterPriority.Required); - parameterList.Add(parameter); - } - - return parameterList; + var result = AliasAssignmentCoordinator.AssignAliasesForParameter(paramList, InitiallyTakenAliases).ToDictionary(r => r.Parameter.Name, r => r); + + Assert.Contains("-au", result["auth"].Aliases); + Assert.Contains("--auth", result["auth"].Aliases); + Assert.Single(result["AAdB2CInstance"].Aliases); + Assert.Contains("--aad-b2c-instance", result["AAdB2CInstance"].Aliases); + Assert.Contains("-ssp", result["SignUpSignInPolicyId"].Aliases); + Assert.Contains("--susi-policy-id", result["SignUpSignInPolicyId"].Aliases); + Assert.Contains("-rp", result["ResetPasswordPolicyId"].Aliases); + Assert.Contains("--reset-password-policy-id", result["ResetPasswordPolicyId"].Aliases); + Assert.Contains("-ep", result["EditProfilePolicyId"].Aliases); + Assert.Contains("--edit-profile-policy-id", result["EditProfilePolicyId"].Aliases); + Assert.Single(result["AADInstance"].Aliases); + Assert.Contains("--aad-instance", result["AADInstance"].Aliases); + Assert.Single(result["ClientId"].Aliases); + Assert.Contains("--client-id", result["ClientId"].Aliases); + Assert.Single(result["Domain"].Aliases); + Assert.Contains("--domain", result["Domain"].Aliases); + Assert.Single(result["TenantId"].Aliases); + Assert.Contains("--tenant-id", result["TenantId"].Aliases); + Assert.Single(result["CallbackPath"].Aliases); + Assert.Contains("--callback-path", result["CallbackPath"].Aliases); + Assert.Contains("-r", result["OrgReadAccess"].Aliases); + Assert.Contains("--org-read-access", result["OrgReadAccess"].Aliases); + Assert.Contains("-U", result["UserSecretsId"].Aliases); + Assert.Contains("--UserSecretsId", result["UserSecretsId"].Aliases); + Assert.Contains("-I", result["IncludeLaunchSettings"].Aliases); + Assert.Contains("--IncludeLaunchSettings", result["IncludeLaunchSettings"].Aliases); + Assert.Contains("-H", result["HttpsPort"].Aliases); + Assert.Contains("--HttpsPort", result["HttpsPort"].Aliases); + Assert.Contains("-K", result["KestrelPort"].Aliases); + Assert.Contains("--KestrelPort", result["KestrelPort"].Aliases); + Assert.Contains("-II", result["IISExpressPort"].Aliases); + Assert.Contains("--IISExpressPort", result["IISExpressPort"].Aliases); + Assert.Contains("-uld", result["UseLocalDB"].Aliases); + Assert.Contains("--use-local-db", result["UseLocalDB"].Aliases); + Assert.Single(result["TargetFrameworkOverride"].Aliases); + Assert.Contains("--target-framework-override", result["TargetFrameworkOverride"].Aliases); + Assert.Contains("-f", result["Framework"].Aliases); + Assert.Contains("--framework", result["Framework"].Aliases); + Assert.Contains("-nt", result["NoTools"].Aliases); + Assert.Contains("--no-tools", result["NoTools"].Aliases); + Assert.Single(result["skipRestore"].Aliases); + Assert.Contains("--no-restore", result["skipRestore"].Aliases); + Assert.DoesNotContain(result, r => r.Value.Errors.Any()); } } } From d12e28aa281e9d5028453573624e17055df10ffa Mon Sep 17 00:00:00 2001 From: Vlada Shubina Date: Wed, 3 Nov 2021 19:36:27 +0100 Subject: [PATCH 4/9] lost chunk of work for TemplatePackageCoordinator --- .../TemplatePackageCoordinator.cs | 57 +++++++------------ 1 file changed, 21 insertions(+), 36 deletions(-) diff --git a/src/Microsoft.TemplateEngine.Cli/TemplatePackageCoordinator.cs b/src/Microsoft.TemplateEngine.Cli/TemplatePackageCoordinator.cs index 65e1693f440..9d9a162e6df 100644 --- a/src/Microsoft.TemplateEngine.Cli/TemplatePackageCoordinator.cs +++ b/src/Microsoft.TemplateEngine.Cli/TemplatePackageCoordinator.cs @@ -6,7 +6,6 @@ using Microsoft.TemplateEngine.Abstractions; using Microsoft.TemplateEngine.Abstractions.Installer; using Microsoft.TemplateEngine.Abstractions.TemplatePackage; -using Microsoft.TemplateEngine.Cli.CommandParsing; using Microsoft.TemplateEngine.Cli.Commands; using Microsoft.TemplateEngine.Cli.NuGet; using Microsoft.TemplateEngine.Cli.TabularOutput; @@ -39,21 +38,13 @@ internal TemplatePackageCoordinator( /// Checks if there is an update for the package containing the . /// /// template to check the update for. - /// /// /// Task for checking the update or null when check for update is not possible. - internal async Task CheckUpdateForTemplate(ITemplateInfo template, INewCommandInput commandInput, CancellationToken cancellationToken = default) + internal async Task CheckUpdateForTemplate(ITemplateInfo template, CancellationToken cancellationToken = default) { _ = template ?? throw new ArgumentNullException(nameof(template)); - _ = commandInput ?? throw new ArgumentNullException(nameof(commandInput)); cancellationToken.ThrowIfCancellationRequested(); - if (commandInput.NoUpdateCheck) - { - Reporter.Verbose.WriteLine("The check for update is skipped by user."); - return null; - } - ITemplatePackage templatePackage; try { @@ -70,15 +61,13 @@ internal TemplatePackageCoordinator( //update is not supported - built-in or optional workload source return null; } - - InitializeNuGetCredentialService(commandInput); + InitializeNuGetCredentialService(interactive: false); return (await managedTemplatePackage.ManagedProvider.GetLatestVersionsAsync(new[] { managedTemplatePackage }, cancellationToken).ConfigureAwait(false)).Single(); } - internal void DisplayUpdateCheckResult(CheckUpdateResult versionCheckResult, INewCommandInput commandInput) + internal void DisplayUpdateCheckResult(CheckUpdateResult versionCheckResult, string commandName) { _ = versionCheckResult ?? throw new ArgumentNullException(nameof(versionCheckResult)); - _ = commandInput ?? throw new ArgumentNullException(nameof(commandInput)); if (versionCheckResult.Success) { @@ -89,7 +78,7 @@ internal void DisplayUpdateCheckResult(CheckUpdateResult versionCheckResult, INe Reporter.Output.WriteLine(LocalizableStrings.TemplatePackageCoordinator_Update_Info_UpdateSingleCommandHeader); Reporter.Output.WriteCommand(CommandExamples.InstallCommandExample( - commandInput.CommandName, + commandName, packageID: versionCheckResult.TemplatePackage.Identifier, version: versionCheckResult.LatestVersion)); } @@ -100,29 +89,10 @@ internal void DisplayUpdateCheckResult(CheckUpdateResult versionCheckResult, INe } } - private static void InitializeNuGetCredentialService(INewCommandInput commandInput) - { - _ = commandInput ?? throw new ArgumentNullException(nameof(commandInput)); - - try - { - DefaultCredentialServiceUtility.SetupDefaultCredentialService(new CliNuGetLogger(), !commandInput.IsInteractiveFlagSpecified); - } - catch (Exception ex) - { - Reporter.Verbose.WriteLine( - string.Format( - LocalizableStrings.TemplatePackageCoordinator_Verbose_NuGetCredentialServiceError, - ex.ToString())); - } - } - /// /// Install the template package(s) flow (--install, -i). /// -#pragma warning disable SA1202 // Elements should be ordered by access internal async Task EnterInstallFlowAsync(InstallCommandArgs args, CancellationToken cancellationToken) -#pragma warning restore SA1202 // Elements should be ordered by access { _ = args ?? throw new ArgumentNullException(nameof(args)); _ = args.TemplatePackages ?? throw new ArgumentNullException(nameof(args.TemplatePackages)); @@ -131,6 +101,7 @@ internal async Task EnterInstallFlowAsync(InstallCommandArgs a throw new ArgumentException($"{nameof(args.TemplatePackages)} should have at least one item to continue.", nameof(args.TemplatePackages)); } cancellationToken.ThrowIfCancellationRequested(); + InitializeNuGetCredentialService(args.Interactive); NewCommandStatus resultStatus = NewCommandStatus.Success; _telemetryLogger.TrackEvent(args.CommandName + TelemetryConstants.InstallEventSuffix, new Dictionary { { TelemetryConstants.ToInstallCount, args.TemplatePackages.Count.ToString() } }); @@ -209,6 +180,7 @@ internal async Task EnterUpdateFlowAsync(UpdateCommandArgs com { _ = commandArgs ?? throw new ArgumentNullException(nameof(commandArgs)); cancellationToken.ThrowIfCancellationRequested(); + InitializeNuGetCredentialService(commandArgs.Interactive); bool applyUpdates = !commandArgs.CheckOnly; bool allTemplatesUpToDate = true; @@ -264,9 +236,7 @@ internal async Task EnterUpdateFlowAsync(UpdateCommandArgs com /// /// Uninstall the template package(s) flow (--uninstall, -u). /// -#pragma warning disable SA1202 // Elements should be ordered by access internal async Task EnterUninstallFlowAsync(UninstallCommandArgs args, CancellationToken cancellationToken) -#pragma warning restore SA1202 // Elements should be ordered by access { _ = args ?? throw new ArgumentNullException(nameof(args)); cancellationToken.ThrowIfCancellationRequested(); @@ -304,6 +274,21 @@ internal async Task EnterUninstallFlowAsync(UninstallCommandAr return result; } + private static void InitializeNuGetCredentialService(bool interactive) + { + try + { + DefaultCredentialServiceUtility.SetupDefaultCredentialService(new CliNuGetLogger(), !interactive); + } + catch (Exception ex) + { + Reporter.Verbose.WriteLine( + string.Format( + LocalizableStrings.TemplatePackageCoordinator_Verbose_NuGetCredentialServiceError, + ex.ToString())); + } + } + private async Task<(NewCommandStatus, Dictionary>)> DetermineSourcesToUninstallAsync(UninstallCommandArgs commandArgs, CancellationToken cancellationToken) { _ = commandArgs ?? throw new ArgumentNullException(nameof(commandArgs)); From 1db357bc803c9b47ec8288740c0a1ce5712e6406 Mon Sep 17 00:00:00 2001 From: Vlada Shubina Date: Wed, 3 Nov 2021 20:18:43 +0100 Subject: [PATCH 5/9] TemplateInvoker refactoring --- .../TelemetryHelper.cs | 22 +-- .../TemplateInvocationCoordinator.cs | 83 --------- .../TemplateInvoker.cs | 172 +++++++----------- .../TelemetryHelperTests.cs | 10 +- 4 files changed, 78 insertions(+), 209 deletions(-) delete mode 100644 src/Microsoft.TemplateEngine.Cli/TemplateInvocationCoordinator.cs diff --git a/src/Microsoft.TemplateEngine.Cli/TelemetryHelper.cs b/src/Microsoft.TemplateEngine.Cli/TelemetryHelper.cs index 8d3232abf69..260006e6798 100644 --- a/src/Microsoft.TemplateEngine.Cli/TelemetryHelper.cs +++ b/src/Microsoft.TemplateEngine.Cli/TelemetryHelper.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#nullable enable + using System.Security.Cryptography; using System.Text; using Microsoft.TemplateEngine.Abstractions; @@ -14,45 +16,35 @@ internal static class TelemetryHelper // For this to return other than the defaultValue, one of these must occur: // - the input value must either exactly match one of the choices (case-insensitive) // - there must be exactly one choice value starting with the input value (case-insensitive). - internal static string GetCanonicalValueForChoiceParamOrDefault(ITemplateInfo template, string paramName, string inputParamValue, string defaultValue = null) + internal static string? GetCanonicalValueForChoiceParamOrDefault(ITemplateInfo template, string paramName, string? inputParamValue, string? defaultValue = null) { if (string.IsNullOrEmpty(paramName) || string.IsNullOrEmpty(inputParamValue)) { return defaultValue; } - - ITemplateParameter parameter = template.Parameters.FirstOrDefault(x => string.Equals(x.Name, paramName, StringComparison.Ordinal)); + ITemplateParameter? parameter = template.Parameters.FirstOrDefault(x => string.Equals(x.Name, paramName, StringComparison.Ordinal)); if (parameter == null || parameter.Choices == null || parameter.Choices.Count == 0) { return defaultValue; } - // This is a case-insensitive key lookup, because that is how Choices is initialized. if (parameter.Choices.ContainsKey(inputParamValue)) { return inputParamValue; } - - IReadOnlyList startsWithChoices = parameter.Choices.Where(x => x.Key.StartsWith(inputParamValue, StringComparison.OrdinalIgnoreCase)).Select(x => x.Key).ToList(); - - if (startsWithChoices.Count == 1) - { - return startsWithChoices[0]; - } - return defaultValue; } /// /// // The hashed mac address needs to be the same hashed value as produced by the other distinct sources given the same input. (e.g. VsCode). /// - internal static string Hash(string text) + internal static string? Hash(string? text) { var sha256 = SHA256.Create(); return HashInFormat(sha256, text); } - internal static string HashWithNormalizedCasing(string text) + internal static string? HashWithNormalizedCasing(string? text) { if (text == null) { @@ -62,7 +54,7 @@ internal static string HashWithNormalizedCasing(string text) return Hash(text.ToUpper()); } - private static string HashInFormat(SHA256 sha256, string text) + private static string? HashInFormat(SHA256 sha256, string? text) { if (text == null) { diff --git a/src/Microsoft.TemplateEngine.Cli/TemplateInvocationCoordinator.cs b/src/Microsoft.TemplateEngine.Cli/TemplateInvocationCoordinator.cs deleted file mode 100644 index 47a6b08d637..00000000000 --- a/src/Microsoft.TemplateEngine.Cli/TemplateInvocationCoordinator.cs +++ /dev/null @@ -1,83 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -#nullable enable - -using Microsoft.TemplateEngine.Abstractions; -using Microsoft.TemplateEngine.Cli.CommandParsing; -using Microsoft.TemplateEngine.Cli.HelpAndUsage; -using Microsoft.TemplateEngine.Cli.TemplateResolution; -using Microsoft.TemplateEngine.Edge.Settings; - -namespace Microsoft.TemplateEngine.Cli -{ - internal class TemplateInvocationCoordinator - { - private readonly IEngineEnvironmentSettings _environment; - private readonly ITelemetryLogger _telemetryLogger; - private readonly string? _defaultLanguage; - private readonly Func _inputGetter; - private readonly NewCommandCallbacks _callbacks; - private readonly TemplatePackageManager _templatePackageManager; - private readonly TemplateInformationCoordinator _templateInformationCoordinator; - private readonly IHostSpecificDataLoader _hostSpecificDataLoader; - private readonly TemplateInvoker _invoker; - - internal TemplateInvocationCoordinator( - IEngineEnvironmentSettings environment, - TemplatePackageManager templatePackageManager, - TemplateInformationCoordinator templateInformationCoordinator, - IHostSpecificDataLoader hostSpecificDataLoader, - ITelemetryLogger telemetryLogger, - string? defaultLanguage, - Func inputGetter, - NewCommandCallbacks callbacks) - { - _environment = environment ?? throw new ArgumentNullException(nameof(environment)); - _telemetryLogger = telemetryLogger ?? throw new ArgumentNullException(nameof(telemetryLogger)); - _defaultLanguage = defaultLanguage; - _inputGetter = inputGetter ?? throw new ArgumentNullException(nameof(inputGetter)); - _callbacks = callbacks ?? throw new ArgumentNullException(nameof(callbacks)); - _templatePackageManager = templatePackageManager ?? throw new ArgumentNullException(nameof(templatePackageManager)); - _templateInformationCoordinator = templateInformationCoordinator ?? throw new ArgumentNullException(nameof(templateInformationCoordinator)); - _hostSpecificDataLoader = hostSpecificDataLoader ?? throw new ArgumentNullException(nameof(hostSpecificDataLoader)); - _invoker = new TemplateInvoker(_environment, _telemetryLogger, _inputGetter, _callbacks, _hostSpecificDataLoader); - } - - internal async Task CoordinateInvocationAsync(INewCommandInput commandInput, CancellationToken cancellationToken) - { - InstantiateTemplateResolver resolver = new InstantiateTemplateResolver(_templatePackageManager, _hostSpecificDataLoader); - TemplateResolutionResult templateResolutionResult = await resolver.ResolveTemplatesAsync(commandInput, _defaultLanguage, default).ConfigureAwait(false); - - if (templateResolutionResult.ResolutionStatus == TemplateResolutionResult.Status.SingleMatch && templateResolutionResult.TemplateToInvoke != null) - { - throw new NotImplementedException(); - //TemplatePackageCoordinator packageCoordinator = new TemplatePackageCoordinator(_telemetryLogger, _environment, _templatePackageManager, _templateInformationCoordinator); - - //// start checking for updates - //var checkForUpdateTask = packageCoordinator.CheckUpdateForTemplate(templateResolutionResult.TemplateToInvoke.Value.Template, commandInput, cancellationToken); - //// start creation of template - //var templateCreationTask = _invoker.InvokeTemplate( - // templateResolutionResult.TemplateToInvoke.Value.Template, - // templateResolutionResult.TemplateToInvoke.Value.Parameters, - // commandInput); - - //// await for both tasks to finish - //await Task.WhenAll(checkForUpdateTask, templateCreationTask).ConfigureAwait(false); - - //if (checkForUpdateTask.Result != null) - //{ - // // print if there is update for this template - // packageCoordinator.DisplayUpdateCheckResult(checkForUpdateTask.Result, commandInput); - //} - - //// return creation result - //return templateCreationTask.Result; - } - else - { - return await _templateInformationCoordinator.CoordinateAmbiguousTemplateResolutionDisplayAsync(templateResolutionResult, commandInput, default).ConfigureAwait(false); - } - } - } -} diff --git a/src/Microsoft.TemplateEngine.Cli/TemplateInvoker.cs b/src/Microsoft.TemplateEngine.Cli/TemplateInvoker.cs index 44812f4f788..e91356ecacc 100644 --- a/src/Microsoft.TemplateEngine.Cli/TemplateInvoker.cs +++ b/src/Microsoft.TemplateEngine.Cli/TemplateInvoker.cs @@ -15,55 +15,52 @@ namespace Microsoft.TemplateEngine.Cli { internal class TemplateInvoker { - private readonly IEngineEnvironmentSettings _environment; + private readonly IEngineEnvironmentSettings _environmentSettings; private readonly ITelemetryLogger _telemetryLogger; private readonly Func _inputGetter; private readonly NewCommandCallbacks _callbacks; - private readonly TemplateCreator _templateCreator; - private readonly IHostSpecificDataLoader _hostDataLoader; private readonly PostActionDispatcher _postActionDispatcher; internal TemplateInvoker( IEngineEnvironmentSettings environment, ITelemetryLogger telemetryLogger, Func inputGetter, - NewCommandCallbacks callbacks, - IHostSpecificDataLoader hostDataLoader) + NewCommandCallbacks callbacks) { - _environment = environment; + _environmentSettings = environment; _telemetryLogger = telemetryLogger; _inputGetter = inputGetter; _callbacks = callbacks; - _templateCreator = new TemplateCreator(_environment); - _hostDataLoader = hostDataLoader; - _postActionDispatcher = new PostActionDispatcher(_environment, _callbacks, _inputGetter); + _templateCreator = new TemplateCreator(_environmentSettings); + _postActionDispatcher = new PostActionDispatcher(_environmentSettings, _callbacks, _inputGetter); } - internal async Task InvokeTemplate(TemplateGroupArgs templateGroupArgs) + internal async Task InvokeTemplateAsync(TemplateArgs templateArgs, CancellationToken cancellationToken) { - var templateToInvoke = templateGroupArgs.Template; - string? templateLanguage = templateToInvoke.GetLanguage(); - bool isMicrosoftAuthored = string.Equals(templateToInvoke.Author, "Microsoft", StringComparison.OrdinalIgnoreCase); + cancellationToken.ThrowIfCancellationRequested(); + + string? templateLanguage = templateArgs.Template.GetLanguage(); + bool isMicrosoftAuthored = string.Equals(templateArgs.Template.Author, "Microsoft", StringComparison.OrdinalIgnoreCase); string? framework = null; string? auth = null; - string templateName = TelemetryHelper.HashWithNormalizedCasing(templateToInvoke.Identity); + string? templateName = TelemetryHelper.HashWithNormalizedCasing(templateArgs.Template.Identity); if (isMicrosoftAuthored) { - templateGroupArgs.TemplateSpecificOptions.TryGetValue("Framework", out string? inputFrameworkValue); - framework = TelemetryHelper.HashWithNormalizedCasing(TelemetryHelper.GetCanonicalValueForChoiceParamOrDefault(templateToInvoke, "Framework", inputFrameworkValue)); + templateArgs.TemplateParameters.TryGetValue("Framework", out string? inputFrameworkValue); + framework = TelemetryHelper.HashWithNormalizedCasing(TelemetryHelper.GetCanonicalValueForChoiceParamOrDefault(templateArgs.Template, "Framework", inputFrameworkValue)); - templateGroupArgs.TemplateSpecificOptions.TryGetValue("auth", out string? inputAuthValue); - auth = TelemetryHelper.HashWithNormalizedCasing(TelemetryHelper.GetCanonicalValueForChoiceParamOrDefault(templateToInvoke, "auth", inputAuthValue)); + templateArgs.TemplateParameters.TryGetValue("auth", out string? inputAuthValue); + auth = TelemetryHelper.HashWithNormalizedCasing(TelemetryHelper.GetCanonicalValueForChoiceParamOrDefault(templateArgs.Template, "auth", inputAuthValue)); } bool success = true; try { - return await CreateTemplateAsync(templateToInvoke, templateGroupArgs).ConfigureAwait(false); + return await CreateTemplateAsync(templateArgs, cancellationToken).ConfigureAwait(false); } catch (ContentGenerationException cx) { @@ -83,8 +80,7 @@ internal async Task InvokeTemplate(TemplateGroupArgs templateG } finally { - //TODO: undo harded "new" below - _telemetryLogger.TrackEvent("new" + TelemetryConstants.CreateEventSuffix, new Dictionary + _telemetryLogger.TrackEvent(templateArgs.NewCommandName + TelemetryConstants.CreateEventSuffix, new Dictionary { { TelemetryConstants.Language, templateLanguage }, { TelemetryConstants.ArgError, "False" }, @@ -112,15 +108,12 @@ private static string GetChangeString(ChangeKind kind) }; } - // Attempts to invoke the template. - // Warning: The _commandInput cannot be assumed to be in a state that is parsed for the template being invoked. - // So be sure to only get template-agnostic information from it. Anything specific to the template must be gotten from the ITemplateMatchInfo - // Or do a reparse if necessary (currently occurs in one error case). - private async Task CreateTemplateAsync(ITemplateInfo template, TemplateGroupArgs commandInput) + private async Task CreateTemplateAsync(TemplateArgs templateArgs, CancellationToken cancellationToken) { + cancellationToken.ThrowIfCancellationRequested(); char[] invalidChars = Path.GetInvalidFileNameChars(); - if (commandInput.Name != null && commandInput.Name.IndexOfAny(invalidChars) > -1) + if (templateArgs.Name != null && templateArgs.Name.IndexOfAny(invalidChars) > -1) { string printableChars = string.Join(", ", invalidChars.Where(x => !char.IsControl(x)).Select(x => $"'{x}'")); string nonPrintableChars = string.Join(", ", invalidChars.Where(char.IsControl).Select(x => $"char({(int)x})")); @@ -129,8 +122,8 @@ private async Task CreateTemplateAsync(ITemplateInfo template, } string? fallbackName = new DirectoryInfo( - !string.IsNullOrWhiteSpace(commandInput.OutputPath) - ? commandInput.OutputPath + !string.IsNullOrWhiteSpace(templateArgs.OutputPath) + ? templateArgs.OutputPath : Directory.GetCurrentDirectory()) .Name; @@ -150,19 +143,20 @@ private async Task CreateTemplateAsync(ITemplateInfo template, } } - Edge.Template.ITemplateCreationResult instantiateResult; + ITemplateCreationResult instantiateResult; try { instantiateResult = await _templateCreator.InstantiateAsync( - template, - commandInput.Name, + templateArgs.Template, + templateArgs.Name, fallbackName, - commandInput.OutputPath, - commandInput.TemplateSpecificOptions, - commandInput.IsForceFlagSpecified, - commandInput.BaselineName, - commandInput.IsDryRun) + templateArgs.OutputPath, + templateArgs.TemplateParameters, + templateArgs.IsForceFlagSpecified, + templateArgs.BaselineName, + templateArgs.IsDryRun, + cancellationToken) .ConfigureAwait(false); } catch (ContentGenerationException cx) @@ -181,12 +175,12 @@ private async Task CreateTemplateAsync(ITemplateInfo template, return NewCommandStatus.CreateFailed; } - string resultTemplateName = string.IsNullOrEmpty(instantiateResult.TemplateFullName) ? commandInput.Template.Name : instantiateResult.TemplateFullName; + string resultTemplateName = string.IsNullOrEmpty(instantiateResult.TemplateFullName) ? templateArgs.Template.Name : instantiateResult.TemplateFullName; switch (instantiateResult.Status) { case CreationResultStatus.Success: - if (!commandInput.IsDryRun) + if (!templateArgs.IsDryRun) { Reporter.Output.WriteLine(string.Format(LocalizableStrings.CreateSuccessful, resultTemplateName)); } @@ -202,50 +196,37 @@ private async Task CreateTemplateAsync(ITemplateInfo template, } } - if (!string.IsNullOrEmpty(template.ThirdPartyNotices)) + if (!string.IsNullOrEmpty(templateArgs.Template.ThirdPartyNotices)) { - Reporter.Output.WriteLine(string.Format(LocalizableStrings.ThirdPartyNotices, template.ThirdPartyNotices)); + Reporter.Output.WriteLine(string.Format(LocalizableStrings.ThirdPartyNotices, templateArgs.Template.ThirdPartyNotices)); } - return HandlePostActions(instantiateResult, commandInput); + return HandlePostActions(instantiateResult, templateArgs); case CreationResultStatus.CreateFailed: Reporter.Error.WriteLine(string.Format(LocalizableStrings.CreateFailed, resultTemplateName, instantiateResult.ErrorMessage).Bold().Red()); return NewCommandStatus.CreateFailed; - //TODO: needed? - //case CreationResultStatus.MissingMandatoryParam: - // if (!string.IsNullOrWhiteSpace(instantiateResult.ErrorMessage)) - // { - // // TODO: rework to avoid having to reparse. - // // The canonical info could be in the ITemplateMatchInfo, but currently isn't. - // TemplateCommandInput reparsedCommand = TemplateCommandInput.ParseForTemplate(template, commandInput, _hostDataLoader.ReadHostSpecificTemplateData(template)); - // IReadOnlyList missingParamNamesCanonical = instantiateResult.ErrorMessage.Split(new[] { ',' }) - // .Select(x => reparsedCommand.VariantsForCanonical(x.Trim()) - // .DefaultIfEmpty(x.Trim()).First()) - // .ToList(); - // string fixedMessage = string.Join(", ", missingParamNamesCanonical); - // Reporter.Error.WriteLine(string.Format(LocalizableStrings.MissingRequiredParameter, fixedMessage, resultTemplateName).Bold().Red()); - // } - // return New3CommandStatus.MissingMandatoryParam; + //this is unlikely case as these errors are caught on parse level now + //TODO: discuss if we need better handling here, then enhance core to return canonical names as array and not parse them from error message + case CreationResultStatus.MissingMandatoryParam: + if (!string.IsNullOrWhiteSpace(instantiateResult.ErrorMessage)) + { + IReadOnlyList missingParamNamesCanonical = instantiateResult.ErrorMessage.Split(new[] { ',' }) + .Select(x => templateArgs.TryGetAliasForCanonicalName(x, out string? alias) ? alias! : x) + .ToList(); + string fixedMessage = string.Join(", ", missingParamNamesCanonical); + Reporter.Error.WriteLine(string.Format(LocalizableStrings.MissingRequiredParameter, fixedMessage, resultTemplateName).Bold().Red()); + } + return NewCommandStatus.MissingMandatoryParam; case CreationResultStatus.NotFound: - Reporter.Error.WriteLine(string.Format(LocalizableStrings.MissingTemplateContentDetected, commandInput).Bold().Red()); + Reporter.Error.WriteLine(string.Format(LocalizableStrings.MissingTemplateContentDetected, templateArgs).Bold().Red()); return NewCommandStatus.NotFound; - //TODO: needed? - //case CreationResultStatus.InvalidParamValues: - // TemplateUsageInformation? usageInformation = await TemplateUsageHelp.GetTemplateUsageInformationAsync(template, _environment, commandInput, _hostDataLoader, _templateCreator, default).ConfigureAwait(false); - - // if (usageInformation != null) - // { - // string invalidParamsError = InvalidParameterInfo.InvalidParameterListToString(usageInformation.Value.InvalidParameters); - // Reporter.Error.WriteLine(invalidParamsError.Bold().Red()); - // Reporter.Error.WriteLine(LocalizableStrings.RunHelpForInformationAboutAcceptedParameters); - // Reporter.Error.WriteCommand(commandInput.HelpCommandExample()); - // } - // else - // { - // Reporter.Error.WriteLine(string.Format(LocalizableStrings.MissingTemplateContentDetected, commandInput.CommandName).Bold().Red()); - // return New3CommandStatus.NotFound; - // } - // return New3CommandStatus.InvalidParamValues; + //this is unlikely case as these errors are caught on parse level now, so rely on proper error message from core. + //TODO: discuss if we need better handling here, then enhance core to return canonical names as array and not parse them from error message + case CreationResultStatus.InvalidParamValues: + Reporter.Error.WriteLine($"{LocalizableStrings.InvalidCommandOptions}: {instantiateResult.ErrorMessage}".Bold().Red()); + Reporter.Error.WriteLine(LocalizableStrings.RunHelpForInformationAboutAcceptedParameters); + Reporter.Error.WriteCommand(CommandExamples.HelpCommandExample(templateArgs.NewCommandName, templateArgs.Template.ShortNameList[0])); + return NewCommandStatus.InvalidParamValues; case CreationResultStatus.DestructiveChangesDetected: Reporter.Error.WriteLine(LocalizableStrings.DestructiveChangesNotification.Bold().Red()); if (instantiateResult.CreationEffects != null) @@ -268,39 +249,18 @@ private async Task CreateTemplateAsync(ITemplateInfo template, } } - private NewCommandStatus HandlePostActions(ITemplateCreationResult creationResult, TemplateGroupArgs commandInput) + private NewCommandStatus HandlePostActions(ITemplateCreationResult creationResult, TemplateArgs args) { - return NewCommandStatus.Success; - //TODO: DO - //AllowRunScripts scriptRunSettings; + PostActionExecutionStatus result = _postActionDispatcher.Process(creationResult, args.IsDryRun, args.AllowScripts ?? AllowRunScripts.Prompt); - //if (string.IsNullOrEmpty(commandInput.AllowScriptsToRun) || string.Equals(commandInput.AllowScriptsToRun, "prompt", StringComparison.OrdinalIgnoreCase)) - //{ - // scriptRunSettings = AllowRunScripts.Prompt; - //} - //else if (string.Equals(commandInput.AllowScriptsToRun, "yes", StringComparison.OrdinalIgnoreCase)) - //{ - // scriptRunSettings = AllowRunScripts.Yes; - //} - //else if (string.Equals(commandInput.AllowScriptsToRun, "no", StringComparison.OrdinalIgnoreCase)) - //{ - // scriptRunSettings = AllowRunScripts.No; - //} - //else - //{ - // scriptRunSettings = AllowRunScripts.Prompt; - //} - - //PostActionExecutionStatus result = _postActionDispatcher.Process(creationResult, commandInput.IsDryRun, scriptRunSettings); - - //return result switch - //{ - // PostActionExecutionStatus.Success => New3CommandStatus.Success, - // PostActionExecutionStatus.Failure => New3CommandStatus.PostActionFailed, - // PostActionExecutionStatus.Cancelled => New3CommandStatus.Cancelled, - // PostActionExecutionStatus.Failure | PostActionExecutionStatus.Cancelled => New3CommandStatus.PostActionFailed, - // _ => New3CommandStatus.UnexpectedResult - //}; + return result switch + { + PostActionExecutionStatus.Success => NewCommandStatus.Success, + PostActionExecutionStatus.Failure => NewCommandStatus.PostActionFailed, + PostActionExecutionStatus.Cancelled => NewCommandStatus.Cancelled, + PostActionExecutionStatus.Failure | PostActionExecutionStatus.Cancelled => NewCommandStatus.PostActionFailed, + _ => NewCommandStatus.UnexpectedResult + }; } } } diff --git a/test/Microsoft.TemplateEngine.Cli.UnitTests/TelemetryHelperTests.cs b/test/Microsoft.TemplateEngine.Cli.UnitTests/TelemetryHelperTests.cs index 17ff162b8d0..bc7973f7e7f 100644 --- a/test/Microsoft.TemplateEngine.Cli.UnitTests/TelemetryHelperTests.cs +++ b/test/Microsoft.TemplateEngine.Cli.UnitTests/TelemetryHelperTests.cs @@ -78,8 +78,8 @@ public void ValidChoiceForParameterIsItsOwnCanonicalValueTest() Assert.Equal("foo", canonical); } - [Fact(DisplayName = nameof(UniqueStartsWithValueResolvesCanonicalValueTest))] - public void UniqueStartsWithValueResolvesCanonicalValueTest() + [Fact] + public void UniqueStartsWithValueDoNotResolvesCanonicalValueTest() { ITemplateParameter param = new TemplateParameter( name: "TestName", @@ -95,7 +95,7 @@ public void UniqueStartsWithValueResolvesCanonicalValueTest() A.CallTo(() => templateInfo.Parameters).Returns(new List() { param }); string canonical = TelemetryHelper.GetCanonicalValueForChoiceParamOrDefault(templateInfo, "TestName", "f"); - Assert.Equal("foo", canonical); + Assert.Null(canonical); } [Fact(DisplayName = nameof(AmbiguousStartsWithValueHasNullCanonicalValueTest))] @@ -127,7 +127,7 @@ public void ChoiceValueCaseDifferenceIsAMatchTest() name: "TestName", type: "parameter", datatype: "choice", - choices: new Dictionary() + choices: new Dictionary(StringComparer.OrdinalIgnoreCase) { { "foo", new ParameterChoice("Foo", "Foo value") }, { "bar", new ParameterChoice("Bar", "Bar value") } @@ -137,7 +137,7 @@ public void ChoiceValueCaseDifferenceIsAMatchTest() A.CallTo(() => templateInfo.Parameters).Returns(new List() { param }); string canonical = TelemetryHelper.GetCanonicalValueForChoiceParamOrDefault(templateInfo, "TestName", "FOO"); - Assert.Equal("foo", canonical); + Assert.Equal("FOO", canonical); } [Fact(DisplayName = nameof(ChoiceValueCaseDifferencesContributeToAmbiguousMatchTest))] From 73bee910bbcd60dc5f6474b17124f3e78f7af0d0 Mon Sep 17 00:00:00 2001 From: Vlada Shubina Date: Wed, 3 Nov 2021 20:21:38 +0100 Subject: [PATCH 6/9] introduced CliTemplateInfo and CliTemplateParameter extending core implemenation with host data --- .../CliTemplateInfo.cs | 108 ++++++++++++++++++ .../CliTemplateParameter.cs | 107 +++++++++++++++++ .../TabularOutput/TemplateGroupDisplay.cs | 1 - .../{TemplateResolution => }/TemplateGroup.cs | 62 ++++++++-- .../BaseTemplateResolver.cs | 4 +- .../TemplateSearch/CliSearchFiltersFactory.cs | 2 +- 6 files changed, 273 insertions(+), 11 deletions(-) create mode 100644 src/Microsoft.TemplateEngine.Cli/CliTemplateInfo.cs create mode 100644 src/Microsoft.TemplateEngine.Cli/CliTemplateParameter.cs rename src/Microsoft.TemplateEngine.Cli/{TemplateResolution => }/TemplateGroup.cs (71%) diff --git a/src/Microsoft.TemplateEngine.Cli/CliTemplateInfo.cs b/src/Microsoft.TemplateEngine.Cli/CliTemplateInfo.cs new file mode 100644 index 00000000000..0cbd3610e4d --- /dev/null +++ b/src/Microsoft.TemplateEngine.Cli/CliTemplateInfo.cs @@ -0,0 +1,108 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable enable + +using Microsoft.TemplateEngine.Abstractions; + +namespace Microsoft.TemplateEngine.Cli +{ + /// + /// + . + /// + internal class CliTemplateInfo : ITemplateInfo + { + private readonly ITemplateInfo _templateInfo; + private readonly HostSpecificTemplateData _cliData; + + internal CliTemplateInfo(ITemplateInfo templateInfo, HostSpecificTemplateData cliData) + { + _templateInfo = templateInfo ?? throw new ArgumentNullException(nameof(templateInfo)); + _cliData = cliData ?? throw new ArgumentNullException(nameof(cliData)); + } + + public string? Author => _templateInfo.Author; + + public string? Description => _templateInfo.Description; + + public IReadOnlyList Classifications => _templateInfo.Classifications; + + public string? DefaultName => _templateInfo.DefaultName; + + public string Identity => _templateInfo.Identity; + + public Guid GeneratorId => _templateInfo.GeneratorId; + + public string? GroupIdentity => _templateInfo.GroupIdentity; + + public int Precedence => _templateInfo.Precedence; + + public string Name => _templateInfo.Name; + + [Obsolete] + public string ShortName => _templateInfo.ShortName; + + [Obsolete] + public IReadOnlyDictionary Tags => _templateInfo.Tags; + + public IReadOnlyDictionary TagsCollection => _templateInfo.TagsCollection; + + [Obsolete] + public IReadOnlyDictionary CacheParameters => _templateInfo.CacheParameters; + + public IReadOnlyList Parameters => _templateInfo.Parameters; + + public string MountPointUri => _templateInfo.MountPointUri; + + public string ConfigPlace => _templateInfo.ConfigPlace; + + public string? LocaleConfigPlace => _templateInfo.LocaleConfigPlace; + + public string? HostConfigPlace => _templateInfo.HostConfigPlace; + + public string? ThirdPartyNotices => _templateInfo.ThirdPartyNotices; + + public IReadOnlyDictionary BaselineInfo => _templateInfo.BaselineInfo; + + [Obsolete] + public bool HasScriptRunningPostActions { get => _templateInfo.HasScriptRunningPostActions; set => _templateInfo.HasScriptRunningPostActions = value; } + + public IReadOnlyList ShortNameList => _templateInfo.ShortNameList; + + internal HostSpecificTemplateData CliData => _cliData; + + internal bool IsHidden => _cliData.IsHidden; + + internal static IEnumerable FromTemplateInfo(IEnumerable templateInfos, IHostSpecificDataLoader hostSpecificDataLoader) + { + if (templateInfos is null) + { + throw new ArgumentNullException(nameof(templateInfos)); + } + + if (hostSpecificDataLoader is null) + { + throw new ArgumentNullException(nameof(hostSpecificDataLoader)); + } + + return templateInfos.Select(templateInfo => new CliTemplateInfo(templateInfo, hostSpecificDataLoader.ReadHostSpecificTemplateData(templateInfo))); + } + + internal IEnumerable GetParameters() + { + HashSet processedParameters = new HashSet(); + List parameters = new List(); + + foreach (ITemplateParameter parameter in Parameters.Where(param => param.Type == "parameter")) + { + if (!processedParameters.Add(parameter.Name)) + { + //TODO: + throw new Exception($"Template {Identity} defines {parameter.Name} twice."); + } + parameters.Add(new CliTemplateParameter(parameter, CliData)); + } + return parameters; + } + } +} diff --git a/src/Microsoft.TemplateEngine.Cli/CliTemplateParameter.cs b/src/Microsoft.TemplateEngine.Cli/CliTemplateParameter.cs new file mode 100644 index 00000000000..1b91b74a761 --- /dev/null +++ b/src/Microsoft.TemplateEngine.Cli/CliTemplateParameter.cs @@ -0,0 +1,107 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable enable + +using Microsoft.TemplateEngine.Abstractions; + +namespace Microsoft.TemplateEngine.Cli +{ + internal enum ParameterType + { + Boolean, + Choice, + Float, + Integer, + Hex, + String + } + + internal class CliTemplateParameter + { + private List _shortNameOverrides = new List(); + + private List _longNameOverrides = new List(); + + private Dictionary _choices = new Dictionary(StringComparer.OrdinalIgnoreCase); + + internal CliTemplateParameter(ITemplateParameter parameter, HostSpecificTemplateData data) + { + Name = parameter.Name; + Description = parameter.Description ?? string.Empty; + Type = ParseType(parameter.DataType); + DefaultValue = parameter.DefaultValue; + DefaultIfOptionWithoutValue = parameter.DefaultIfOptionWithoutValue ?? string.Empty; + IsRequired = parameter.Priority == TemplateParameterPriority.Required && parameter.DefaultValue == null; + IsHidden = parameter.Priority == TemplateParameterPriority.Implicit || data.HiddenParameterNames.Contains(parameter.Name); + + if (parameter.Choices != null) + { + _choices = parameter.Choices.ToDictionary(kvp => kvp.Key, kvp => kvp.Value, StringComparer.OrdinalIgnoreCase); + } + if (data.ShortNameOverrides.ContainsKey(parameter.Name)) + { + _shortNameOverrides.Add(data.ShortNameOverrides[parameter.Name]); + } + if (data.LongNameOverrides.ContainsKey(parameter.Name)) + { + _longNameOverrides.Add(data.LongNameOverrides[parameter.Name]); + } + } + + /// + /// Unit test constructor. + /// + internal CliTemplateParameter( + string name, + ParameterType type = ParameterType.String, + IEnumerable? shortNameOverrides = null, + IEnumerable? longNameOverrides = null, + int precedence = 0) + { + Name = name; + Type = type; + _shortNameOverrides = shortNameOverrides?.ToList() ?? new List(); + _longNameOverrides = longNameOverrides?.ToList() ?? new List(); + + Description = string.Empty; + DefaultValue = string.Empty; + DefaultIfOptionWithoutValue = string.Empty; + } + + internal string Name { get; private set; } + + internal string Description { get; private set; } + + internal ParameterType Type { get; private set; } + + internal string? DefaultValue { get; private set; } + + internal bool IsRequired { get; private set; } + + internal bool IsHidden { get; private set; } + + internal IReadOnlyDictionary? Choices => _choices; + + internal IReadOnlyList ShortNameOverrides => _shortNameOverrides; + + internal IReadOnlyList LongNameOverrides => _longNameOverrides; + + //TODO: decide if we handle it + internal string DefaultIfOptionWithoutValue { get; private set; } + + private static ParameterType ParseType(string dataType) + { + return dataType switch + { + "bool" => ParameterType.Boolean, + "boolean" => ParameterType.Boolean, + "choice" => ParameterType.Choice, + "float" => ParameterType.Float, + "int" => ParameterType.Integer, + "integer" => ParameterType.Integer, + _ => ParameterType.String + }; + } + } +} diff --git a/src/Microsoft.TemplateEngine.Cli/TabularOutput/TemplateGroupDisplay.cs b/src/Microsoft.TemplateEngine.Cli/TabularOutput/TemplateGroupDisplay.cs index 9a4754795d1..4a328335b2d 100644 --- a/src/Microsoft.TemplateEngine.Cli/TabularOutput/TemplateGroupDisplay.cs +++ b/src/Microsoft.TemplateEngine.Cli/TabularOutput/TemplateGroupDisplay.cs @@ -6,7 +6,6 @@ using Microsoft.TemplateEngine.Abstractions; using Microsoft.TemplateEngine.Cli.Commands; using Microsoft.TemplateEngine.Cli.Extensions; -using Microsoft.TemplateEngine.Cli.TemplateResolution; using Microsoft.TemplateEngine.Utils; namespace Microsoft.TemplateEngine.Cli.TabularOutput diff --git a/src/Microsoft.TemplateEngine.Cli/TemplateResolution/TemplateGroup.cs b/src/Microsoft.TemplateEngine.Cli/TemplateGroup.cs similarity index 71% rename from src/Microsoft.TemplateEngine.Cli/TemplateResolution/TemplateGroup.cs rename to src/Microsoft.TemplateEngine.Cli/TemplateGroup.cs index 47783c27bc2..5a617eeae84 100644 --- a/src/Microsoft.TemplateEngine.Cli/TemplateResolution/TemplateGroup.cs +++ b/src/Microsoft.TemplateEngine.Cli/TemplateGroup.cs @@ -6,7 +6,7 @@ using Microsoft.TemplateEngine.Abstractions; using Microsoft.TemplateEngine.Utils; -namespace Microsoft.TemplateEngine.Cli.TemplateResolution +namespace Microsoft.TemplateEngine.Cli { /// /// The class represents template group. Templates in single group:
@@ -23,7 +23,7 @@ internal sealed class TemplateGroup /// the templates of the template group. /// when is null. /// when is empty or don't have same defined. - internal TemplateGroup(IEnumerable templates) + internal TemplateGroup(IEnumerable templates) { _ = templates ?? throw new ArgumentNullException(paramName: nameof(templates)); if (!templates.Any()) @@ -74,12 +74,47 @@ internal IReadOnlyList Languages { get { - HashSet shortNames = new HashSet(StringComparer.OrdinalIgnoreCase); + HashSet language = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (ITemplateInfo template in Templates) { - shortNames.Add(template.GetLanguage()); + language.Add(template.GetLanguage()); } - return shortNames.ToList(); + return language.ToList(); + } + } + + /// + /// Returns the list of types defined for templates in the group. + /// + internal IReadOnlyList Types + { + get + { + HashSet type = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (ITemplateInfo template in Templates) + { + type.Add(template.GetTemplateType()); + } + return type.ToList(); + } + } + + /// + /// Returns the list of baselines defined for templates in the group. + /// + internal IReadOnlyList Baselines + { + get + { + HashSet baselines = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (ITemplateInfo template in Templates) + { + foreach (var baseline in template.BaselineInfo) + { + baselines.Add(baseline.Key); + } + } + return baselines.ToList(); } } @@ -96,6 +131,19 @@ internal string Name } } + /// + /// Returns the description of template group + /// Template group name is the name of highest precedence template in the group. + /// If multiple templates have the maximum precedence, the name of first one is returned. + /// + internal string Description + { + get + { + return GetHighestPrecedenceTemplates().First().Description ?? string.Empty; + } + } + /// /// Returns true when is not null or empty. /// @@ -109,9 +157,9 @@ internal string Name /// /// Returns the list of templates in the group. /// - internal IReadOnlyList Templates { get; private set; } + internal IReadOnlyList Templates { get; private set; } - internal static IEnumerable FromTemplateList (IEnumerable templates) + internal static IEnumerable FromTemplateList (IEnumerable templates) { return templates .GroupBy(x => x.GroupIdentity, x => !string.IsNullOrEmpty(x.GroupIdentity), StringComparer.OrdinalIgnoreCase) diff --git a/src/Microsoft.TemplateEngine.Cli/TemplateResolution/BaseTemplateResolver.cs b/src/Microsoft.TemplateEngine.Cli/TemplateResolution/BaseTemplateResolver.cs index 8445dda35c3..7d063cdf8e1 100644 --- a/src/Microsoft.TemplateEngine.Cli/TemplateResolution/BaseTemplateResolver.cs +++ b/src/Microsoft.TemplateEngine.Cli/TemplateResolution/BaseTemplateResolver.cs @@ -56,10 +56,10 @@ protected async Task> GetTemplateGroupsAsync(Cancella } else { - throw new Exception($"Both {nameof(_templateList)} and {nameof(_templatePackageManager)} cannot be null"); + throw new Exception($"Both {nameof(_templateList)} and {nameof(_templatePackageManager)} cannot be null."); } templates = templates.Where(x => !x.IsHiddenByHostFile(HostSpecificDataLoader)); - return TemplateGroup.FromTemplateList(templates); + return TemplateGroup.FromTemplateList(CliTemplateInfo.FromTemplateInfo(templates, HostSpecificDataLoader)); } } diff --git a/src/Microsoft.TemplateEngine.Cli/TemplateSearch/CliSearchFiltersFactory.cs b/src/Microsoft.TemplateEngine.Cli/TemplateSearch/CliSearchFiltersFactory.cs index 05ec37e91e5..1127fb83a52 100644 --- a/src/Microsoft.TemplateEngine.Cli/TemplateSearch/CliSearchFiltersFactory.cs +++ b/src/Microsoft.TemplateEngine.Cli/TemplateSearch/CliSearchFiltersFactory.cs @@ -54,7 +54,7 @@ internal static Func> Ge { InMemoryHostSpecificDataLoader hostDataLoader = new InMemoryHostSpecificDataLoader(templatePackageSearchData); IEnumerable templates = templatePackageSearchData.Templates.Where(template => IsNotHiddenBySearchFile(template)); - IEnumerable templateGroups = TemplateGroup.FromTemplateList(templates); + IEnumerable templateGroups = TemplateGroup.FromTemplateList(CliTemplateInfo.FromTemplateInfo(templates, hostDataLoader)); IEnumerable> groupFilters = new[] { CliFilters.NameTemplateGroupFilter(commandArgs.SearchNameCriteria) From f31a2bff2897c4491e7c38484e856e13a0492ee0 Mon Sep 17 00:00:00 2001 From: Vlada Shubina Date: Wed, 3 Nov 2021 20:33:39 +0100 Subject: [PATCH 7/9] template subcommand initial implementation --- .../CommandParsing/CommandParserSupport.cs | 83 +++--- .../Commands/Extensions.cs | 44 ++++ .../Commands/GlobalArgs.cs | 12 +- .../Commands/NewCommand.cs | 154 +++++++---- .../Commands/NewCommandArgs.cs | 3 + .../Commands/TemplateArgs.cs | 96 +++++++ .../Commands/TemplateCommand.cs | 249 ++++++++++++++++++ .../Commands/TemplateGroupArgs.cs | 89 ------- .../Commands/TemplateGroupCommand.cs | 48 ---- .../Commands/TemplateParserFactory.cs | 31 +++ 10 files changed, 568 insertions(+), 241 deletions(-) create mode 100644 src/Microsoft.TemplateEngine.Cli/Commands/Extensions.cs create mode 100644 src/Microsoft.TemplateEngine.Cli/Commands/TemplateArgs.cs create mode 100644 src/Microsoft.TemplateEngine.Cli/Commands/TemplateCommand.cs delete mode 100644 src/Microsoft.TemplateEngine.Cli/Commands/TemplateGroupArgs.cs delete mode 100644 src/Microsoft.TemplateEngine.Cli/Commands/TemplateGroupCommand.cs create mode 100644 src/Microsoft.TemplateEngine.Cli/Commands/TemplateParserFactory.cs diff --git a/src/Microsoft.TemplateEngine.Cli/CommandParsing/CommandParserSupport.cs b/src/Microsoft.TemplateEngine.Cli/CommandParsing/CommandParserSupport.cs index 14e1ac2a932..65a59be86d4 100644 --- a/src/Microsoft.TemplateEngine.Cli/CommandParsing/CommandParserSupport.cs +++ b/src/Microsoft.TemplateEngine.Cli/CommandParsing/CommandParserSupport.cs @@ -147,47 +147,48 @@ internal static Command CreateNewCommandWithArgsForTemplate( HashSet initiallyTakenAliases = ArgsForBuiltInCommands; Dictionary> canonicalToVariantMap = new Dictionary>(); - AliasAssignmentCoordinator assignmentCoordinator = new AliasAssignmentCoordinator(parameterDefinitions, longNameOverrides, shortNameOverrides, initiallyTakenAliases); - - if (assignmentCoordinator.InvalidParams.Count > 0) - { - string unusableDisplayList = string.Join(", ", assignmentCoordinator.InvalidParams); - throw new Exception($"Template is malformed. The following parameter names are invalid: {unusableDisplayList}"); - } - - foreach (ITemplateParameter parameter in parameterDefinitions.Where(x => x.Priority != TemplateParameterPriority.Implicit)) - { - Option option; - IList aliasesForParam = new List(); - - if (assignmentCoordinator.LongNameAssignments.TryGetValue(parameter.Name, out string longVersion)) - { - aliasesForParam.Add(longVersion); - } - - if (assignmentCoordinator.ShortNameAssignments.TryGetValue(parameter.Name, out string shortVersion)) - { - aliasesForParam.Add(shortVersion); - } - - if (!string.IsNullOrEmpty(parameter.DefaultIfOptionWithoutValue)) - { - // This switch can be provided with or without a value. - // If the user doesn't specify a value, it gets the value of DefaultIfOptionWithoutValue - option = Create.Option(string.Join("|", aliasesForParam), parameter.Description, Accept.ZeroOrOneArgument()); - } - else - { - // User must provide a value if this switch is specified. - option = Create.Option(string.Join("|", aliasesForParam), parameter.Description, Accept.ExactlyOneArgument()); - } - - paramOptionList.Add(option); // add the option - canonicalToVariantMap.Add(parameter.Name, aliasesForParam.ToList()); // map the template canonical name to its aliases. - } - - templateParamMap = canonicalToVariantMap; - return GetNewCommandForTemplate(commandName, templateName, NewCommandVisibleArgs, NewCommandHiddenArgs, DebuggingCommandArgs, paramOptionList.ToArray()); + throw new NotImplementedException(); + //AliasAssignmentCoordinator assignmentCoordinator = new AliasAssignmentCoordinator(parameterDefinitions, longNameOverrides, shortNameOverrides, initiallyTakenAliases); + + //if (assignmentCoordinator.InvalidParams.Count > 0) + //{ + // string unusableDisplayList = string.Join(", ", assignmentCoordinator.InvalidParams); + // throw new Exception($"Template is malformed. The following parameter names are invalid: {unusableDisplayList}"); + //} + + //foreach (ITemplateParameter parameter in parameterDefinitions.Where(x => x.Priority != TemplateParameterPriority.Implicit)) + //{ + // Option option; + // IList aliasesForParam = new List(); + + // if (assignmentCoordinator.LongNameAssignments.TryGetValue(parameter.Name, out string longVersion)) + // { + // aliasesForParam.Add(longVersion); + // } + + // if (assignmentCoordinator.ShortNameAssignments.TryGetValue(parameter.Name, out string shortVersion)) + // { + // aliasesForParam.Add(shortVersion); + // } + + // if (!string.IsNullOrEmpty(parameter.DefaultIfOptionWithoutValue)) + // { + // // This switch can be provided with or without a value. + // // If the user doesn't specify a value, it gets the value of DefaultIfOptionWithoutValue + // option = Create.Option(string.Join("|", aliasesForParam), parameter.Description, Accept.ZeroOrOneArgument()); + // } + // else + // { + // // User must provide a value if this switch is specified. + // option = Create.Option(string.Join("|", aliasesForParam), parameter.Description, Accept.ExactlyOneArgument()); + // } + + // paramOptionList.Add(option); // add the option + // canonicalToVariantMap.Add(parameter.Name, aliasesForParam.ToList()); // map the template canonical name to its aliases. + //} + + //templateParamMap = canonicalToVariantMap; + //return GetNewCommandForTemplate(commandName, templateName, NewCommandVisibleArgs, NewCommandHiddenArgs, DebuggingCommandArgs, paramOptionList.ToArray()); } internal static Command CreateNewCommandWithoutTemplateInfo(string commandName) diff --git a/src/Microsoft.TemplateEngine.Cli/Commands/Extensions.cs b/src/Microsoft.TemplateEngine.Cli/Commands/Extensions.cs new file mode 100644 index 00000000000..8b45b6528d9 --- /dev/null +++ b/src/Microsoft.TemplateEngine.Cli/Commands/Extensions.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable enable + +using System.CommandLine; +using System.CommandLine.Parsing; + +namespace Microsoft.TemplateEngine.Cli.Commands +{ + internal static class Extensions + { + internal static string? GetValueForOptionOrNull(this ParseResult parseResult, IOption option) + { + OptionResult? result = parseResult.FindResultFor(option); + if (result == null) + { + return null; + } + if (result.Token is null) + { + return null; + } + return result.GetValueOrDefault()?.ToString(); + } + + /// + /// Gets name of from . + /// might be result for subcommand, then method will traverse up until is found. + /// + /// + /// + internal static string GetNewCommandName(this ParseResult parseResult) + { + var command = parseResult.CommandResult.Command; + + while (command != null && command is not NewCommand) + { + command = (parseResult.CommandResult.Parent as CommandResult)?.Command; + } + return command?.Name ?? string.Empty; + } + } +} diff --git a/src/Microsoft.TemplateEngine.Cli/Commands/GlobalArgs.cs b/src/Microsoft.TemplateEngine.Cli/Commands/GlobalArgs.cs index a84a4fdae94..18662d14175 100644 --- a/src/Microsoft.TemplateEngine.Cli/Commands/GlobalArgs.cs +++ b/src/Microsoft.TemplateEngine.Cli/Commands/GlobalArgs.cs @@ -17,7 +17,7 @@ public GlobalArgs(BaseCommand command, ParseResult parseResult) DebugReinit = parseResult.GetValueForOption(command.DebugReinitOption); DebugRebuildCache = parseResult.GetValueForOption(command.DebugRebuildCacheOption); DebugShowConfig = parseResult.GetValueForOption(command.DebugShowConfigOption); - CommandName = GetNewCommandName(parseResult); + CommandName = parseResult.GetNewCommandName(); ParseResult = parseResult; } @@ -42,15 +42,5 @@ protected static (bool, IReadOnlyList?) ParseTabularOutputSettings(ITabu return (parseResult.GetValueForOption(command.ColumnsAllOption), parseResult.GetValueForOption(command.ColumnsOption)); } - private string GetNewCommandName(ParseResult parseResult) - { - var command = parseResult.CommandResult.Command; - - while (command != null && command is not NewCommand) - { - command = (parseResult.CommandResult.Parent as CommandResult)?.Command; - } - return command?.Name ?? string.Empty; - } } } diff --git a/src/Microsoft.TemplateEngine.Cli/Commands/NewCommand.cs b/src/Microsoft.TemplateEngine.Cli/Commands/NewCommand.cs index 10971d5ab36..4d557fef01d 100644 --- a/src/Microsoft.TemplateEngine.Cli/Commands/NewCommand.cs +++ b/src/Microsoft.TemplateEngine.Cli/Commands/NewCommand.cs @@ -65,6 +65,7 @@ internal NewCommand(string commandName, ITemplateEngineHost host, ITelemetryLogg this.Add(new ListCommand(this, host, telemetryLogger, callbacks)); this.Add(new LegacyListCommand(this, host, telemetryLogger, callbacks)); + } internal Argument ShortNameArgument { get; } = new Argument("template-short-name") @@ -132,41 +133,43 @@ protected override IEnumerable GetSuggestions(NewCommandArgs args, IEngi using TemplatePackageManager templatePackageManager = new TemplatePackageManager(environmentSettings); var templates = templatePackageManager.GetTemplatesAsync(CancellationToken.None).Result; - //TODO: implement correct logic - if (!string.IsNullOrEmpty(args.ShortName)) - { - var matchingTemplates = templates.Where(template => template.ShortNameList.Contains(args.ShortName)); - HashSet distinctSuggestions = new HashSet(); + return Array.Empty(); - foreach (var template in matchingTemplates) - { - var templateGroupCommand = new TemplateGroupCommand(this, environmentSettings, template); - var parsed = templateGroupCommand.Parse(args.Arguments ?? Array.Empty()); - foreach (var suggestion in templateGroupCommand.GetSuggestions(parsed, textToMatch)) - { - if (distinctSuggestions.Add(suggestion)) - { - yield return suggestion; - } - } - } - yield break; - } - else - { - foreach (var template in templates) - { - foreach (var suggestion in template.ShortNameList) - { - yield return suggestion; - } - } - } - - foreach (var suggestion in base.GetSuggestions(args, environmentSettings, textToMatch)) - { - yield return suggestion; - } + //TODO: implement correct logic + //if (!string.IsNullOrEmpty(args.ShortName)) + //{ + // var matchingTemplates = templates.Where(template => template.ShortNameList.Contains(args.ShortName)); + // HashSet distinctSuggestions = new HashSet(); + + // foreach (var template in matchingTemplates) + // { + // var templateGroupCommand = new TemplateGroupCommand(this, environmentSettings, template); + // var parsed = templateGroupCommand.Parse(args.Arguments ?? Array.Empty()); + // foreach (var suggestion in templateGroupCommand.GetSuggestions(parsed, textToMatch)) + // { + // if (distinctSuggestions.Add(suggestion)) + // { + // yield return suggestion; + // } + // } + // } + // yield break; + //} + //else + //{ + // foreach (var template in templates) + // { + // foreach (var suggestion in template.ShortNameList) + // { + // yield return suggestion; + // } + // } + //} + + //foreach (var suggestion in base.GetSuggestions(args, environmentSettings, textToMatch)) + //{ + // yield return suggestion; + //} } protected override async Task ExecuteAsync(NewCommandArgs args, IEngineEnvironmentSettings environmentSettings, InvocationContext context) @@ -182,40 +185,40 @@ protected override async Task ExecuteAsync(NewCommandArgs args } using TemplatePackageManager templatePackageManager = new TemplatePackageManager(environmentSettings); + var hostSpecificDataLoader = new HostSpecificDataLoader(environmentSettings); if (string.IsNullOrWhiteSpace(args.ShortName)) { TemplateListCoordinator templateListCoordinator = new TemplateListCoordinator( environmentSettings, templatePackageManager, - new HostSpecificDataLoader(environmentSettings), + hostSpecificDataLoader, TelemetryLogger); - //TODO: we need to await, otherwise templatePackageManager will be disposed. return await templateListCoordinator.DisplayCommandDescriptionAsync(args, default).ConfigureAwait(false); } var templates = await templatePackageManager.GetTemplatesAsync(context.GetCancellationToken()).ConfigureAwait(false); - var template = templates.FirstOrDefault(template => template.ShortNameList.Contains(args.ShortName)); + var templateGroups = TemplateGroup.FromTemplateList(CliTemplateInfo.FromTemplateInfo(templates, hostSpecificDataLoader)); - if (template == null) - { - Reporter.Error.WriteLine($"Template {args.ShortName} doesn't exist."); - return NewCommandStatus.NotFound; - } + //TODO: decide what to do if there are more than 1 group. + var selectedTemplateGroup = templateGroups.FirstOrDefault(template => template.ShortNames.Contains(args.ShortName)); - //var dotnet = new Command("dotnet") - //{ - // TreatUnmatchedTokensAsErrors = false - //}; - var newC = new Command(_commandName) + if (selectedTemplateGroup == null) { - TreatUnmatchedTokensAsErrors = false - }; - //dotnet.AddCommand(newC); - newC.AddCommand(new TemplateGroupCommand(this, environmentSettings, template)); + Reporter.Error.WriteLine( + string.Format(LocalizableStrings.NoTemplatesMatchingInputParameters, args.ShortName).Bold().Red()); + Reporter.Error.WriteLine(); + + Reporter.Error.WriteLine(LocalizableStrings.ListTemplatesCommand); + Reporter.Error.WriteCommand(CommandExamples.ListCommandExample(args.CommandName)); - return (NewCommandStatus)newC.Invoke(context.ParseResult.Tokens.Select(s => s.Value).ToArray()); + Reporter.Error.WriteLine(LocalizableStrings.SearchTemplatesCommand); + Reporter.Error.WriteCommand(CommandExamples.SearchCommandExample(args.CommandName, args.ShortName)); + Reporter.Error.WriteLine(); + return NewCommandStatus.NotFound; + } + return await HandleTemplateInstantationAsync(args, environmentSettings, templatePackageManager, selectedTemplateGroup).ConfigureAwait(false); } protected override NewCommandArgs ParseContext(ParseResult parseResult) => new(this, parseResult); @@ -268,5 +271,52 @@ protected override async Task ExecuteAsync(NewCommandArgs args } return null; } + + private async Task HandleTemplateInstantationAsync( + NewCommandArgs args, + IEngineEnvironmentSettings environmentSettings, + TemplatePackageManager templatePackageManager, + TemplateGroup templateGroup) + { + foreach (IGrouping templateGrouping in templateGroup.Templates.GroupBy(g => g.Precedence).OrderByDescending(g => g.Key)) + { + HashSet candidates = new HashSet(); + foreach (CliTemplateInfo template in templateGrouping) + { + TemplateCommand command = new TemplateCommand(this, environmentSettings, templatePackageManager, templateGroup, template); + Parser parser = TemplateParserFactory.CreateParser(command); + ParseResult parseResult = parser.Parse(args.Arguments ?? Array.Empty()); + if (!parseResult.Errors.Any()) + { + candidates.Add(command); + } + } + if (!candidates.Any()) + { + continue; + } + if (candidates.Count == 1) + { + this.AddCommand(candidates.First()); + return (NewCommandStatus)await this.InvokeAsync(args.InitialTokens).ConfigureAwait(false); + } + return HandleAmbuguousResult(); + } + + //TODO: handle it better + Reporter.Error.WriteLine( + string.Format(LocalizableStrings.NoTemplatesMatchingInputParameters, args.ShortName).Bold().Red()); + Reporter.Error.WriteLine(); + + Reporter.Error.WriteLine(LocalizableStrings.ListTemplatesCommand); + Reporter.Error.WriteCommand(CommandExamples.ListCommandExample(args.CommandName)); + + Reporter.Error.WriteLine(LocalizableStrings.SearchTemplatesCommand); + Reporter.Error.WriteCommand(CommandExamples.SearchCommandExample(args.CommandName, args.ShortName)); + Reporter.Error.WriteLine(); + return NewCommandStatus.NotFound; + } + + private NewCommandStatus HandleAmbuguousResult() => throw new NotImplementedException(); } } diff --git a/src/Microsoft.TemplateEngine.Cli/Commands/NewCommandArgs.cs b/src/Microsoft.TemplateEngine.Cli/Commands/NewCommandArgs.cs index ae19b22a2fb..37b037fdea6 100644 --- a/src/Microsoft.TemplateEngine.Cli/Commands/NewCommandArgs.cs +++ b/src/Microsoft.TemplateEngine.Cli/Commands/NewCommandArgs.cs @@ -14,6 +14,7 @@ public NewCommandArgs(NewCommand command, ParseResult parseResult) : base(comman Arguments = parseResult.GetValueForArgument(command.RemainingArguments); ShortName = parseResult.GetValueForArgument(command.ShortNameArgument); HelpRequested = parseResult.GetValueForOption(command.HelpOption); + InitialTokens = parseResult.Tokens.Select(t => t.Value).ToArray(); } internal string? ShortName { get; } @@ -21,5 +22,7 @@ public NewCommandArgs(NewCommand command, ParseResult parseResult) : base(comman internal string[]? Arguments { get; } internal bool HelpRequested { get; } + + internal string[] InitialTokens { get; } } } diff --git a/src/Microsoft.TemplateEngine.Cli/Commands/TemplateArgs.cs b/src/Microsoft.TemplateEngine.Cli/Commands/TemplateArgs.cs new file mode 100644 index 00000000000..03bfd438fb8 --- /dev/null +++ b/src/Microsoft.TemplateEngine.Cli/Commands/TemplateArgs.cs @@ -0,0 +1,96 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable enable + +using System.CommandLine.Parsing; + +namespace Microsoft.TemplateEngine.Cli.Commands +{ + internal class TemplateArgs + { + private readonly ParseResult _parseResult; + private readonly TemplateCommand _command; + private Dictionary _templateOptions = new Dictionary(); + + public TemplateArgs(TemplateCommand command, CliTemplateInfo template, ParseResult parseResult) + { + _parseResult = parseResult ?? throw new ArgumentNullException(nameof(parseResult)); + _command = command ?? throw new ArgumentNullException(nameof(command)); + + Name = parseResult.GetValueForOptionOrNull(command.NameOption); + OutputPath = parseResult.GetValueForOptionOrNull(command.OutputOption); + IsForceFlagSpecified = parseResult.GetValueForOption(command.ForceOption); + IsDryRun = parseResult.GetValueForOption(command.DryRunOption); + NoUpdateCheck = parseResult.GetValueForOption(command.NoUpdateCheckOption); + AllowScripts = parseResult.GetValueForOption(command.AllowScriptsOption); + + if (command.LanguageOption != null) + { + Language = parseResult.GetValueForOptionOrNull(command.LanguageOption); + } + if (command.TypeOption != null) + { + Type = parseResult.GetValueForOptionOrNull(command.TypeOption); + } + if (command.BaselineOption != null) + { + BaselineName = parseResult.GetValueForOptionOrNull(command.BaselineOption); + } + + foreach (var opt in command.TemplateOptions) + { + if (parseResult.FindResultFor(opt.Value) is { } result) + { + _templateOptions[opt.Key] = result; + } + } + Template = template ?? throw new ArgumentNullException(nameof(template)); + NewCommandName = parseResult.GetNewCommandName(); + } + + public string? Name { get; } + + public string? OutputPath { get; } + + public bool IsForceFlagSpecified { get; } + + public string? Language { get; } + + public string? Type { get; } + + public string? BaselineName { get; } + + public bool IsDryRun { get; } + + public bool NoUpdateCheck { get; } + + public AllowRunScripts? AllowScripts { get; } + + public CliTemplateInfo Template { get; } + + public IReadOnlyDictionary TemplateParameters + { + get + { + return _templateOptions.Select(o => (o.Key, _parseResult.GetValueForOptionOrNull(o.Value.Option))) + .Where(kvp => kvp.Item2 != null) + .ToDictionary(kvp => kvp.Key, kvp => kvp.Item2); + } + + } + + internal string NewCommandName { get; private set; } + + public bool TryGetAliasForCanonicalName(string canonicalName, out string? alias) + { + if (_command.TemplateOptions.ContainsKey(canonicalName)) + { + alias = _command.TemplateOptions[canonicalName].Aliases.First(); + return true; + } + alias = null; + return false; + } + } +} diff --git a/src/Microsoft.TemplateEngine.Cli/Commands/TemplateCommand.cs b/src/Microsoft.TemplateEngine.Cli/Commands/TemplateCommand.cs new file mode 100644 index 00000000000..6b1a0f84b27 --- /dev/null +++ b/src/Microsoft.TemplateEngine.Cli/Commands/TemplateCommand.cs @@ -0,0 +1,249 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable enable + +using System.CommandLine; +using System.CommandLine.Invocation; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Installer; +using Microsoft.TemplateEngine.Cli.Extensions; +using Microsoft.TemplateEngine.Edge.Settings; +using Microsoft.TemplateEngine.Utils; + +namespace Microsoft.TemplateEngine.Cli.Commands +{ + internal class TemplateCommand : Command, ICommandHandler + { + private readonly TemplatePackageManager _templatePackageManager; + private readonly IEngineEnvironmentSettings _environmentSettings; + private readonly NewCommand _newCommand; + private readonly TemplateGroup _templateGroup; + private readonly CliTemplateInfo _template; + private Dictionary _templateSpecificOptions = new Dictionary(); + + public TemplateCommand( + NewCommand newCommand, + IEngineEnvironmentSettings environmentSettings, + TemplatePackageManager templatePackageManager, + TemplateGroup templateGroup, + CliTemplateInfo template) + : base( + templateGroup.ShortNames[0], + template.Name + Environment.NewLine + template.Description) + { + _newCommand = newCommand; + _environmentSettings = environmentSettings; + _templatePackageManager = templatePackageManager; + _templateGroup = templateGroup; + _template = template; + foreach (var item in templateGroup.ShortNames.Skip(1)) + { + AddAlias(item); + } + + this.AddOption(OutputOption); + this.AddOption(NameOption); + this.AddOption(DryRunOption); + this.AddOption(ForceOption); + this.AddOption(NoUpdateCheckOption); + this.AddOption(AllowScriptsOption); + + string? templateLanguage = template.GetLanguage(); + + if (!string.IsNullOrWhiteSpace(templateLanguage)) + { + LanguageOption = SharedOptionsFactory.CreateLanguageOption(); + LanguageOption.FromAmong(templateLanguage); + if (templateGroup.Languages.Count > 1) + { + LanguageOption.SetDefaultValue(environmentSettings.GetDefaultLanguage()); + LanguageOption.AddValidator(optionResult => + { + var value = optionResult.GetValueOrDefault(); + if (value != template.GetLanguage()) + { + return "Languages don't match"; + } + return null; + } + ); + } + this.AddOption(LanguageOption); + } + + string? templateType = template.GetTemplateType(); + + if (!string.IsNullOrWhiteSpace(templateType)) + { + TypeOption = SharedOptionsFactory.CreateTypeOption(); + TypeOption.FromAmong(templateType); + this.AddOption(TypeOption); + } + + if (template.BaselineInfo.Any(b => string.IsNullOrWhiteSpace(b.Key))) + { + BaselineOption = SharedOptionsFactory.CreateBaselineOption(); + BaselineOption.FromAmong(template.BaselineInfo.Select(b => b.Key).Where(b => !string.IsNullOrWhiteSpace(b)).ToArray()); + this.AddOption(BaselineOption); + } + + AddTemplateOptionsToCommand(template); + this.Handler = this; + } + + internal Option OutputOption { get; } = new Option(new string[] { "-o", "--output" }) + { + Description = LocalizableStrings.OutputPath, + Arity = new ArgumentArity(0, 1) + }; + + internal Option NameOption { get; } = new Option(new string[] { "-n", "--name" }) + { + Description = LocalizableStrings.NameOfOutput, + Arity = new ArgumentArity(0, 1) + }; + + internal Option DryRunOption { get; } = new Option("--dry-run") + { + Description = LocalizableStrings.DryRunDescription, + Arity = new ArgumentArity(0, 1) + }; + + internal Option ForceOption { get; } = new Option("--force") + { + Description = LocalizableStrings.ForcesTemplateCreation, + Arity = new ArgumentArity(0, 1) + }; + + internal Option NoUpdateCheckOption { get; } = new Option("--no-update-check") + { + Description = LocalizableStrings.OptionDescriptionNoUpdateCheck, + Arity = new ArgumentArity(0, 1) + }; + + internal Option AllowScriptsOption { get; } = new Option("--allow-scripts") + { + Description = "TODO", + IsHidden = true, + Arity = new ArgumentArity(0, 1) + }; + + internal Option? LanguageOption { get; } + + internal Option? TypeOption { get; } + + internal Option? BaselineOption { get; } + + internal IReadOnlyDictionary TemplateOptions => _templateSpecificOptions; + + public async Task InvokeAsync(InvocationContext context) + { + TemplateArgs args = new TemplateArgs(this, _template, context.ParseResult); + + TemplateInvoker invoker = new TemplateInvoker(_environmentSettings, _newCommand.TelemetryLogger, () => Console.ReadLine() ?? string.Empty, _newCommand.Callbacks); + if (!args.NoUpdateCheck) + { + TemplatePackageCoordinator packageCoordinator = new TemplatePackageCoordinator(_newCommand.TelemetryLogger, _environmentSettings, _templatePackageManager); + Task checkForUpdateTask = packageCoordinator.CheckUpdateForTemplate(args.Template, context.GetCancellationToken()); + Task instantiateTask = invoker.InvokeTemplateAsync(args, context.GetCancellationToken()); + await Task.WhenAll(checkForUpdateTask, instantiateTask).ConfigureAwait(false); + + if (checkForUpdateTask?.Result != null) + { + // print if there is update for this template + packageCoordinator.DisplayUpdateCheckResult(checkForUpdateTask.Result, _newCommand.Name); + } + // return creation result + return (int)instantiateTask.Result; + } + else + { + return (int)await invoker.InvokeTemplateAsync(args, context.GetCancellationToken()).ConfigureAwait(false); + } + } + + private static ArgumentArity GetOptionArity(CliTemplateParameter parameter) => new ArgumentArity(parameter.IsRequired ? 1 : 0, 1); + + private HashSet GetReservedAliases() + { + HashSet reservedAliases = new HashSet(); + foreach (string alias in this.Children.OfType