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/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/CommandParsing/CommandParserSupport.cs b/src/Microsoft.TemplateEngine.Cli/CommandParsing/CommandParserSupport.cs index 14e1ac2a932..08d584c1a6a 100644 --- a/src/Microsoft.TemplateEngine.Cli/CommandParsing/CommandParserSupport.cs +++ b/src/Microsoft.TemplateEngine.Cli/CommandParsing/CommandParserSupport.cs @@ -53,15 +53,15 @@ private static Option[] NewCommandVisibleArgs .ZeroOrOneArgument() .And(ArgumentCannotStartWithDashRule) .With(LocalizableStrings.ListsTemplates, "PARTIAL_NAME")), - Create.Option("-n|--name", LocalizableStrings.NameOfOutput, Accept.ExactlyOneArgument()), - Create.Option("-o|--output", LocalizableStrings.OutputPath, Accept.ExactlyOneArgument()), + Create.Option("-n|--name", LocalizableStrings.OptionDescriptionName, Accept.ExactlyOneArgument()), + Create.Option("-o|--output", LocalizableStrings.OptionDescriptionOutput, Accept.ExactlyOneArgument()), Create.Option("-i|--install", LocalizableStrings.InstallHelp, Accept.OneOrMoreArguments()), Create.Option("-u|--uninstall", LocalizableStrings.UninstallHelp, Accept.ZeroOrMoreArguments()), Create.Option("--interactive", LocalizableStrings.OptionDescriptionInteractive, Accept.NoArguments()), Create.Option("--nuget-source|--add-source", LocalizableStrings.OptionDescriptionNuGetSource, Accept.OneOrMoreArguments()), Create.Option("--type", LocalizableStrings.OptionDescriptionTypeFilter, Accept.ExactlyOneArgument()), - Create.Option("--dry-run", LocalizableStrings.DryRunDescription, Accept.NoArguments()), - Create.Option("--force", LocalizableStrings.ForcesTemplateCreation, Accept.NoArguments()), + Create.Option("--dry-run", LocalizableStrings.OptionDescriptionDryRun, Accept.NoArguments()), + Create.Option("--force", LocalizableStrings.OptionDescriptionForce, Accept.NoArguments()), Create.Option("-lang|--language", LocalizableStrings.OptionDescriptionLanguageFilter, Accept.ExactlyOneArgument()), Create.Option("--update-check", LocalizableStrings.UpdateCheckCommandHelp, Accept.NoArguments()), Create.Option("--update-apply", LocalizableStrings.UpdateApplyCommandHelp, Accept.NoArguments()), @@ -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/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/src/Microsoft.TemplateEngine.Cli/Commands/BaseCommand.cs b/src/Microsoft.TemplateEngine.Cli/Commands/BaseCommand.cs index 9d15d891e90..2c05bcc8419 100644 --- a/src/Microsoft.TemplateEngine.Cli/Commands/BaseCommand.cs +++ b/src/Microsoft.TemplateEngine.Cli/Commands/BaseCommand.cs @@ -17,8 +17,10 @@ namespace Microsoft.TemplateEngine.Cli.Commands { internal abstract class BaseCommand : Command { - protected BaseCommand(string name, string? description = null) : base(name, description) + protected BaseCommand(ITelemetryLogger logger, NewCommandCallbacks callbacks, string name, string? description = null) : base(name, description) { + TelemetryLogger = logger; + Callbacks = callbacks; } internal Option DebugCustomSettingsLocationOption { get; } = new("--debug:custom-hive", "Sets custom settings location") @@ -50,21 +52,30 @@ protected BaseCommand(string name, string? description = null) : base(name, desc { IsHidden = true }; + + internal ITelemetryLogger TelemetryLogger { get; } + + internal NewCommandCallbacks Callbacks { get; } } internal abstract class BaseCommand : BaseCommand, ICommandHandler where TArgs : GlobalArgs { private static readonly Guid _entryMutexGuid = new Guid("5CB26FD1-32DB-4F4C-B3DC-49CFD61633D2"); - private readonly ITemplateEngineHost _host; + private readonly ITemplateEngineHost? _host; internal BaseCommand(ITemplateEngineHost host, ITelemetryLogger logger, NewCommandCallbacks callbacks, string name, string? description = null) - : base(name, description) + : this(logger, callbacks, name, description) { _host = host; - TelemetryLogger = logger; - Callbacks = callbacks; this.Handler = this; + } + + //command called via this constructor is not invokable + internal BaseCommand(BaseCommand parent, string name, string? description = null) : this(parent.TelemetryLogger, parent.Callbacks, name, description) { } + + private BaseCommand(ITelemetryLogger logger, NewCommandCallbacks callbacks, string name, string? description = null) : base(logger, callbacks, name, description) + { this.AddOption(DebugCustomSettingsLocationOption); this.AddOption(DebugVirtualizeSettingsOption); this.AddOption(DebugAttachOption); @@ -73,10 +84,6 @@ internal BaseCommand(ITemplateEngineHost host, ITelemetryLogger logger, NewComma this.AddOption(DebugShowConfigOption); } - internal ITelemetryLogger TelemetryLogger { get; } - - internal NewCommandCallbacks Callbacks { get; } - public async Task InvokeAsync(InvocationContext context) { TArgs args = ParseContext(context.ParseResult); @@ -146,19 +153,28 @@ protected virtual IEnumerable GetSuggestions(TArgs args, IEngineEnvironm return base.GetSuggestions(args.ParseResult, textToMatch); } - protected IEngineEnvironmentSettings CreateEnvironmentSettings(TArgs args) + private IEngineEnvironmentSettings CreateEnvironmentSettings(TArgs args) { - string? outputPath = (args as InstantiateCommandArgs)?.OutputPath; + //TODO: replace with reparse + //string? outputPath = (args as InstantiateCommandArgs)?.OutputPath; + + if (_host is null) + { + throw new ArgumentException("The method should not be used if host is not available."); + } IEngineEnvironmentSettings environmentSettings = new EngineEnvironmentSettings( - new CliTemplateEngineHost(_host, outputPath), + new CliTemplateEngineHost(_host, string.Empty), + //new CliTemplateEngineHost(_host, outputPath), settingsLocation: args.DebugCustomSettingsLocation, virtualizeSettings: args.DebugVirtualizeSettings, environment: new CliEnvironment()); return environmentSettings; } +#pragma warning disable SA1202 // Elements should be ordered by access protected abstract Task ExecuteAsync(TArgs args, IEngineEnvironmentSettings environmentSettings, InvocationContext context); +#pragma warning restore SA1202 // Elements should be ordered by access protected abstract TArgs ParseContext(ParseResult parseResult); 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/InstantiateCommand.cs b/src/Microsoft.TemplateEngine.Cli/Commands/InstantiateCommand.cs index 66da7c26718..9d84f576d0e 100644 --- a/src/Microsoft.TemplateEngine.Cli/Commands/InstantiateCommand.cs +++ b/src/Microsoft.TemplateEngine.Cli/Commands/InstantiateCommand.cs @@ -5,42 +5,197 @@ using System.CommandLine; using System.CommandLine.Invocation; +using System.CommandLine.IO; using System.CommandLine.Parsing; using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Edge.Settings; namespace Microsoft.TemplateEngine.Cli.Commands { internal class InstantiateCommand : BaseCommand { - internal InstantiateCommand(ITemplateEngineHost host, ITelemetryLogger logger, NewCommandCallbacks callbacks) : base(host, logger, callbacks, "create") + private NewCommand? _parentCommand; + + internal InstantiateCommand(ITemplateEngineHost host, ITelemetryLogger logger, NewCommandCallbacks callbacks) : base(host, logger, callbacks, "create", "TODO") + { + this.AddArgument(ShortNameArgument); + this.AddArgument(RemainingArguments); + this.AddOption(HelpOption); + } + + private InstantiateCommand(NewCommand parentCommand, string name, string? description = null) : base(parentCommand, name, description) + { + _parentCommand = parentCommand; + this.AddArgument(ShortNameArgument); + this.AddArgument(RemainingArguments); + this.AddOption(HelpOption); + } + + internal Argument ShortNameArgument { get; } = new Argument("template-short-name") + { + Arity = new ArgumentArity(0, 1) + }; + + internal Argument RemainingArguments { get; } = new Argument("template-args") + { + Arity = new ArgumentArity(0, 999) + }; + + internal Option HelpOption { get; } = new Option(new string[] { "-h", "--help", "-?" }) { - InstantiateCommandArgs.AddToCommand(this); + IsHidden = true + }; + internal static InstantiateCommand FromNewCommand(NewCommand parentCommand) + { + return new InstantiateCommand(parentCommand, parentCommand.Name, parentCommand.Description); } - protected override Task ExecuteAsync(InstantiateCommandArgs args, IEngineEnvironmentSettings environmentSettings, InvocationContext context) => throw new NotImplementedException(); + internal Task ExecuteAsync(ParseResult parseResult, IEngineEnvironmentSettings environmentSettings, InvocationContext context) + { + return ExecuteAsync(ParseContext(parseResult), environmentSettings, context); + } + + internal HashSet GetTemplateCommand( + InstantiateCommandArgs 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.RemainingArguments ?? Array.Empty()); + if (!parseResult.Errors.Any()) + { + candidates.Add(command); + } + } + if (!candidates.Any()) + { + continue; + } + return candidates; + } + return new HashSet(); + } + + protected async override Task ExecuteAsync(InstantiateCommandArgs instantiateArgs, IEngineEnvironmentSettings environmentSettings, InvocationContext context) + { + if (string.IsNullOrWhiteSpace(instantiateArgs.ShortName) && instantiateArgs.HelpRequested) + { + context.HelpBuilder.Write( + context.ParseResult.CommandResult.Command, + StandardStreamWriter.Create(context.Console.Out), + context.ParseResult); + return NewCommandStatus.Success; + } + using TemplatePackageManager templatePackageManager = new TemplatePackageManager(environmentSettings); + var hostSpecificDataLoader = new HostSpecificDataLoader(environmentSettings); + if (string.IsNullOrWhiteSpace(instantiateArgs.ShortName)) + { + TemplateListCoordinator templateListCoordinator = new TemplateListCoordinator( + environmentSettings, + templatePackageManager, + hostSpecificDataLoader, + TelemetryLogger); - protected override InstantiateCommandArgs ParseContext(ParseResult parseResult) => throw new NotImplementedException(); + return await templateListCoordinator.DisplayCommandDescriptionAsync(instantiateArgs, default).ConfigureAwait(false); + } + + var templates = await templatePackageManager.GetTemplatesAsync(context.GetCancellationToken()).ConfigureAwait(false); + var templateGroups = TemplateGroup.FromTemplateList(CliTemplateInfo.FromTemplateInfo(templates, hostSpecificDataLoader)); + + //TODO: decide what to do if there are more than 1 group. + var selectedTemplateGroup = templateGroups.FirstOrDefault(template => template.ShortNames.Contains(instantiateArgs.ShortName)); + + if (selectedTemplateGroup == null) + { + Reporter.Error.WriteLine( + string.Format(LocalizableStrings.NoTemplatesMatchingInputParameters, instantiateArgs.ShortName).Bold().Red()); + Reporter.Error.WriteLine(); + + Reporter.Error.WriteLine(LocalizableStrings.ListTemplatesCommand); + Reporter.Error.WriteCommand(CommandExamples.ListCommandExample(instantiateArgs.CommandName)); + + Reporter.Error.WriteLine(LocalizableStrings.SearchTemplatesCommand); + Reporter.Error.WriteCommand(CommandExamples.SearchCommandExample(instantiateArgs.CommandName, instantiateArgs.ShortName)); + Reporter.Error.WriteLine(); + return NewCommandStatus.NotFound; + } + return await HandleTemplateInstantationAsync(instantiateArgs, environmentSettings, templatePackageManager, selectedTemplateGroup).ConfigureAwait(false); + } + + protected override InstantiateCommandArgs ParseContext(ParseResult parseResult) => new(this, parseResult); + + private async Task HandleTemplateInstantationAsync( + InstantiateCommandArgs args, + IEngineEnvironmentSettings environmentSettings, + TemplatePackageManager templatePackageManager, + TemplateGroup templateGroup) + { + HashSet candidates = GetTemplateCommand(args, environmentSettings, templatePackageManager, templateGroup); + if (candidates.Count == 1) + { + Command commandToRun = _parentCommand is null ? this : _parentCommand; + + commandToRun.AddCommand(candidates.First()); + return (NewCommandStatus)await commandToRun.InvokeAsync(args.TokensToInvoke).ConfigureAwait(false); + } + else if (candidates.Any()) + { + 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(); } internal class InstantiateCommandArgs : GlobalArgs { public InstantiateCommandArgs(InstantiateCommand command, ParseResult parseResult) : base(command, parseResult) { - OutputPath = parseResult.GetValueForOption(OutputPathOption); + RemainingArguments = parseResult.GetValueForArgument(command.RemainingArguments) ?? Array.Empty(); + ShortName = parseResult.GetValueForArgument(command.ShortNameArgument); + HelpRequested = parseResult.GetValueForOption(command.HelpOption); + + var tokens = new List(); + if (!string.IsNullOrWhiteSpace(ShortName)) + { + tokens.Add(ShortName); + } + tokens.AddRange(RemainingArguments); + if (HelpRequested) + { + tokens.Add(command.HelpOption.Aliases.First()); + } + TokensToInvoke = tokens.ToArray(); + } - public string? OutputPath { get; } + internal string? ShortName { get; } - private static Option OutputPathOption { get; } = new(new[] { "-o", "--output" }) - { - Description = "Location to place the generated output. The default is the current directory." - }; + internal string[] RemainingArguments { get; } - internal static void AddToCommand(Command command) - { - command.AddOption(OutputPathOption); - } + internal bool HelpRequested { get; } + internal string[] TokensToInvoke { get; } } } diff --git a/src/Microsoft.TemplateEngine.Cli/Commands/NewCommand.cs b/src/Microsoft.TemplateEngine.Cli/Commands/NewCommand.cs index 10971d5ab36..da979e5748f 100644 --- a/src/Microsoft.TemplateEngine.Cli/Commands/NewCommand.cs +++ b/src/Microsoft.TemplateEngine.Cli/Commands/NewCommand.cs @@ -5,7 +5,6 @@ using System.CommandLine; using System.CommandLine.Invocation; -using System.CommandLine.IO; using System.CommandLine.Parsing; using Microsoft.TemplateEngine.Abstractions; using Microsoft.TemplateEngine.Edge.Settings; @@ -24,12 +23,8 @@ internal class NewCommand : BaseCommand FilterOptionDefinition.PackageFilter }; - private readonly string _commandName; - internal NewCommand(string commandName, ITemplateEngineHost host, ITelemetryLogger telemetryLogger, NewCommandCallbacks callbacks) : base(host, telemetryLogger, callbacks, commandName, LocalizableStrings.CommandDescription) { - _commandName = commandName; - this.AddArgument(ShortNameArgument); this.AddArgument(RemainingArguments); this.AddOption(HelpOption); @@ -65,6 +60,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,90 +128,50 @@ 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(); - - 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; - } - } - } + return Array.Empty(); - 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) + protected override Task ExecuteAsync(NewCommandArgs args, IEngineEnvironmentSettings environmentSettings, InvocationContext context) { - if (string.IsNullOrWhiteSpace(args.ShortName) && args.HelpRequested) - { - context.HelpBuilder.Write( - context.ParseResult.CommandResult.Command, - StandardStreamWriter.Create(context.Console.Out), - context.ParseResult); - - return NewCommandStatus.Success; - } - - using TemplatePackageManager templatePackageManager = new TemplatePackageManager(environmentSettings); - - if (string.IsNullOrWhiteSpace(args.ShortName)) - { - TemplateListCoordinator templateListCoordinator = new TemplateListCoordinator( - environmentSettings, - templatePackageManager, - new HostSpecificDataLoader(environmentSettings), - 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)); - - if (template == null) - { - Reporter.Error.WriteLine($"Template {args.ShortName} doesn't exist."); - return NewCommandStatus.NotFound; - } - - //var dotnet = new Command("dotnet") - //{ - // TreatUnmatchedTokensAsErrors = false - //}; - var newC = new Command(_commandName) - { - TreatUnmatchedTokensAsErrors = false - }; - //dotnet.AddCommand(newC); - newC.AddCommand(new TemplateGroupCommand(this, environmentSettings, template)); - - return (NewCommandStatus)newC.Invoke(context.ParseResult.Tokens.Select(s => s.Value).ToArray()); + InstantiateCommand command = InstantiateCommand.FromNewCommand(this); + ParseResult reparseResult = TemplateParserFactory.CreateParser(command).Parse(args.Tokens); + return command.ExecuteAsync(reparseResult, environmentSettings, context); } protected override NewCommandArgs ParseContext(ParseResult parseResult) => new(this, parseResult); @@ -268,5 +224,6 @@ protected override async Task ExecuteAsync(NewCommandArgs args } return null; } + } } diff --git a/src/Microsoft.TemplateEngine.Cli/Commands/NewCommandArgs.cs b/src/Microsoft.TemplateEngine.Cli/Commands/NewCommandArgs.cs index ae19b22a2fb..993ef07d920 100644 --- a/src/Microsoft.TemplateEngine.Cli/Commands/NewCommandArgs.cs +++ b/src/Microsoft.TemplateEngine.Cli/Commands/NewCommandArgs.cs @@ -11,15 +11,9 @@ internal class NewCommandArgs : GlobalArgs { public NewCommandArgs(NewCommand command, ParseResult parseResult) : base(command, parseResult) { - Arguments = parseResult.GetValueForArgument(command.RemainingArguments); - ShortName = parseResult.GetValueForArgument(command.ShortNameArgument); - HelpRequested = parseResult.GetValueForOption(command.HelpOption); + Tokens = parseResult.Tokens.Select(t => t.Value).ToArray(); } - internal string? ShortName { get; } - - internal string[]? Arguments { get; } - - internal bool HelpRequested { get; } + internal string[] Tokens { 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..1d294e88202 --- /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, 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 = command.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..758d17e4b13 --- /dev/null +++ b/src/Microsoft.TemplateEngine.Cli/Commands/TemplateCommand.cs @@ -0,0 +1,251 @@ +// 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 InstantiateCommand _instantiateCommand; + private readonly TemplateGroup _templateGroup; + private readonly CliTemplateInfo _template; + private Dictionary _templateSpecificOptions = new Dictionary(); + + public TemplateCommand( + InstantiateCommand instantiateCommand, + IEngineEnvironmentSettings environmentSettings, + TemplatePackageManager templatePackageManager, + TemplateGroup templateGroup, + CliTemplateInfo template) + : base( + templateGroup.ShortNames[0], + template.Name + Environment.NewLine + template.Description) + { + _instantiateCommand = instantiateCommand; + _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.OptionDescriptionOutput, + Arity = new ArgumentArity(0, 1) + }; + + internal Option NameOption { get; } = new Option(new string[] { "-n", "--name" }) + { + Description = LocalizableStrings.OptionDescriptionName, + Arity = new ArgumentArity(0, 1) + }; + + internal Option DryRunOption { get; } = new Option("--dry-run") + { + Description = LocalizableStrings.OptionDescriptionDryRun, + Arity = new ArgumentArity(0, 1) + }; + + internal Option ForceOption { get; } = new Option("--force") + { + Description = LocalizableStrings.OptionDescriptionForce, + 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 = LocalizableStrings.OptionDescriptionAllowScripts, + IsHidden = true, + Arity = new ArgumentArity(0, 1) + }; + + internal Option? LanguageOption { get; } + + internal Option? TypeOption { get; } + + internal Option? BaselineOption { get; } + + internal IReadOnlyDictionary TemplateOptions => _templateSpecificOptions; + + internal CliTemplateInfo Template => _template; + + public async Task InvokeAsync(InvocationContext context) + { + TemplateArgs args = new TemplateArgs(this, context.ParseResult); + + TemplateInvoker invoker = new TemplateInvoker(_environmentSettings, _instantiateCommand.TelemetryLogger, () => Console.ReadLine() ?? string.Empty, _instantiateCommand.Callbacks); + if (!args.NoUpdateCheck) + { + TemplatePackageCoordinator packageCoordinator = new TemplatePackageCoordinator(_instantiateCommand.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, args.NewCommandName); + } + // 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