diff --git a/src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs b/src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs index 3c6b2060fb..7a76b0075b 100644 --- a/src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs +++ b/src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs @@ -286,7 +286,7 @@ namespace System.Management.Automation /// /// An instance that represents the none-value. /// - internal readonly static ExperimentalAttribute None = new ExperimentalAttribute(); + internal static readonly ExperimentalAttribute None = new ExperimentalAttribute(); internal bool ToHide => EffectiveAction == ExperimentAction.Hide; internal bool ToShow => EffectiveAction == ExperimentAction.Show; diff --git a/src/System.Management.Automation/engine/ExperimentalFeature/GetExperimentalFeatureCommand.cs b/src/System.Management.Automation/engine/ExperimentalFeature/GetExperimentalFeatureCommand.cs new file mode 100644 index 0000000000..2ab20492d1 --- /dev/null +++ b/src/System.Management.Automation/engine/ExperimentalFeature/GetExperimentalFeatureCommand.cs @@ -0,0 +1,180 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Management.Automation; +using System.Management.Automation.Internal; + +namespace Microsoft.PowerShell.Commands +{ + /// + /// Implements Get-ExperimentalFeature cmdlet. + /// + [Cmdlet(VerbsCommon.Get, "ExperimentalFeature", HelpUri = "")] + public class GetExperimentalFeatureCommand : PSCmdlet + { + /// + /// Specify the feature names. + /// + [Parameter(ValueFromPipeline = true, Position = 0)] + [ValidateNotNullOrEmpty] + public string[] Name { get; set; } + + /// + /// Search module paths to find all available experimental features. + /// + [Parameter()] + public SwitchParameter ListAvailable { get; set; } + + /// + /// ProcessRecord method of this cmdlet. + /// + protected override void ProcessRecord() + { + const WildcardOptions wildcardOptions = WildcardOptions.IgnoreCase | WildcardOptions.CultureInvariant; + IEnumerable namePatterns = SessionStateUtilities.CreateWildcardsFromStrings(Name, wildcardOptions); + + if (ListAvailable) + { + foreach (ExperimentalFeature feature in GetAvailableExperimentalFeatures(namePatterns).OrderBy(GetSortingString)) + { + WriteObject(feature); + } + } + else if (ExperimentalFeature.EnabledExperimentalFeatureNames.Count > 0) + { + foreach (ExperimentalFeature feature in GetEnabledExperimentalFeatures(namePatterns).OrderBy(GetSortingString)) + { + WriteObject(feature); + } + } + } + + /// + /// Construct the string for sorting experimental feature records. + /// + /// + /// Engine features come before module features. + /// Within engine features and more features, features are ordered by name. + /// + private static string GetSortingString(ExperimentalFeature feature) + { + if (ExperimentalFeature.EngineSource.Equals(feature.Source)) + { + return "0:" + feature.Name; + } + else + { + return "1:" + feature.Name; + } + } + + /// + /// Get enabled experimental features based on the specified name patterns. + /// + private IEnumerable GetEnabledExperimentalFeatures(IEnumerable namePatterns) + { + var moduleFeatures = new List(); + var moduleNames = new HashSet(StringComparer.OrdinalIgnoreCase); + + foreach (string featureName in ExperimentalFeature.EnabledExperimentalFeatureNames) + { + // Only process the feature names that matches any name patterns. + if (SessionStateUtilities.MatchesAnyWildcardPattern(featureName, namePatterns, defaultValue: true)) + { + if (ExperimentalFeature.EngineExperimentalFeatureMap.TryGetValue(featureName, out ExperimentalFeature feature)) + { + yield return feature; + } + else + { + moduleFeatures.Add(featureName); + int lastDotIndex = featureName.LastIndexOf('.'); + moduleNames.Add(featureName.Substring(0, lastDotIndex)); + } + } + } + + if (moduleFeatures.Count > 0) + { + var featuresFromGivenModules = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (string moduleFile in GetValidModuleFiles(moduleNames)) + { + ExperimentalFeature[] features = ModuleIntrinsics.GetExperimentalFeature(moduleFile); + foreach (var feature in features) + { + featuresFromGivenModules.TryAdd(feature.Name, feature); + } + } + + foreach (string featureName in moduleFeatures) + { + if (featuresFromGivenModules.TryGetValue(featureName, out ExperimentalFeature feature)) + { + yield return feature; + } + else + { + yield return new ExperimentalFeature(featureName, description: null, source: null, isEnabled: true); + } + } + } + } + + /// + /// Get available experimental features based on the specified name patterns. + /// + private IEnumerable GetAvailableExperimentalFeatures(IEnumerable namePatterns) + { + foreach (ExperimentalFeature feature in ExperimentalFeature.EngineExperimentalFeatures) + { + if (SessionStateUtilities.MatchesAnyWildcardPattern(feature.Name, namePatterns, defaultValue: true)) + { + yield return feature; + } + } + + foreach (string moduleFile in GetValidModuleFiles(moduleNamesToFind: null)) + { + ExperimentalFeature[] features = ModuleIntrinsics.GetExperimentalFeature(moduleFile); + foreach (var feature in features) + { + if (SessionStateUtilities.MatchesAnyWildcardPattern(feature.Name, namePatterns, defaultValue: true)) + { + yield return feature; + } + } + } + } + + /// + /// Get valid module files from module paths. + /// + private IEnumerable GetValidModuleFiles(HashSet moduleNamesToFind) + { + var modulePaths = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (string path in ModuleIntrinsics.GetModulePath(includeSystemModulePath: false, Context)) + { + string uniquePath = path.TrimEnd(Utils.Separators.Directory); + if (!modulePaths.Add(uniquePath)) { continue; } + + foreach (string moduleFile in ModuleUtils.GetDefaultAvailableModuleFiles(uniquePath)) + { + // We only care about module manifest files because that's where experimental features are declared. + if (!moduleFile.EndsWith(StringLiterals.PowerShellDataFileExtension, StringComparison.OrdinalIgnoreCase)) { continue; } + + if (moduleNamesToFind != null) + { + string currentModuleName = ModuleIntrinsics.GetModuleName(moduleFile); + if (!moduleNamesToFind.Contains(currentModuleName)) { continue; } + } + + yield return moduleFile; + } + } + } + } +} \ No newline at end of file diff --git a/src/System.Management.Automation/engine/InitialSessionState.cs b/src/System.Management.Automation/engine/InitialSessionState.cs index c31eba64b5..90bec19566 100644 --- a/src/System.Management.Automation/engine/InitialSessionState.cs +++ b/src/System.Management.Automation/engine/InitialSessionState.cs @@ -5277,6 +5277,7 @@ end {"Export-ModuleMember", new SessionStateCmdletEntry("Export-ModuleMember", typeof(ExportModuleMemberCommand), helpFile) }, {"ForEach-Object", new SessionStateCmdletEntry("ForEach-Object", typeof(ForEachObjectCommand), helpFile) }, {"Get-Command", new SessionStateCmdletEntry("Get-Command", typeof(GetCommandCommand), helpFile) }, + {"Get-ExperimentalFeature", new SessionStateCmdletEntry("Get-ExperimentalFeature", typeof(GetExperimentalFeatureCommand), helpFile) }, {"Get-Help", new SessionStateCmdletEntry("Get-Help", typeof(GetHelpCommand), helpFile) }, {"Get-History", new SessionStateCmdletEntry("Get-History", typeof(GetHistoryCommand), helpFile) }, {"Get-Job", new SessionStateCmdletEntry("Get-Job", typeof(GetJobCommand), helpFile) }, diff --git a/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs b/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs index 67aaf6309e..79ec351b28 100644 --- a/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs +++ b/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs @@ -2059,8 +2059,8 @@ namespace Microsoft.PowerShell.Commands if (ExperimentalFeature.IsModuleFeatureName(featureName, moduleName)) { string featureDescription = feature["Description"] as string; - expFeatureList.Add(new ExperimentalFeature(featureName, featureDescription, moduleName, - ExperimentalFeature.HasEnabled(featureName))); + expFeatureList.Add(new ExperimentalFeature(featureName, featureDescription, moduleManifestPath, + ExperimentalFeature.HasEnabled(featureName))); } else { diff --git a/src/System.Management.Automation/engine/Modules/ModuleIntrinsics.cs b/src/System.Management.Automation/engine/Modules/ModuleIntrinsics.cs index 8451daa80f..99fcdb89eb 100644 --- a/src/System.Management.Automation/engine/Modules/ModuleIntrinsics.cs +++ b/src/System.Management.Automation/engine/Modules/ModuleIntrinsics.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System.Collections; using System.Collections.Generic; using System.IO; using System.Management.Automation.Configuration; @@ -414,64 +415,92 @@ namespace System.Management.Automation internal static Version GetManifestModuleVersion(string manifestPath) { - if (manifestPath != null && - manifestPath.EndsWith(StringLiterals.PowerShellDataFileExtension, StringComparison.OrdinalIgnoreCase)) + try { - try - { - var dataFileSetting = - PsUtils.GetModuleManifestProperties( - manifestPath, - PsUtils.ManifestModuleVersionPropertyName); + Hashtable dataFileSetting = + PsUtils.GetModuleManifestProperties( + manifestPath, + PsUtils.ManifestModuleVersionPropertyName); - var versionValue = dataFileSetting["ModuleVersion"]; - if (versionValue != null) + object versionValue = dataFileSetting["ModuleVersion"]; + if (versionValue != null) + { + Version moduleVersion; + if (LanguagePrimitives.TryConvertTo(versionValue, out moduleVersion)) { - Version moduleVersion; - if (LanguagePrimitives.TryConvertTo(versionValue, out moduleVersion)) - { - return moduleVersion; - } + return moduleVersion; } } - catch (PSInvalidOperationException) - { - } } + catch (PSInvalidOperationException) { } return new Version(0, 0); } internal static Guid GetManifestGuid(string manifestPath) { - if (manifestPath != null && - manifestPath.EndsWith(StringLiterals.PowerShellDataFileExtension, StringComparison.OrdinalIgnoreCase)) + try { - try - { - var dataFileSetting = - PsUtils.GetModuleManifestProperties( - manifestPath, - PsUtils.ManifestGuidPropertyName); + Hashtable dataFileSetting = + PsUtils.GetModuleManifestProperties( + manifestPath, + PsUtils.ManifestGuidPropertyName); - var guidValue = dataFileSetting["GUID"]; - if (guidValue != null) + object guidValue = dataFileSetting["GUID"]; + if (guidValue != null) + { + Guid guidID; + if (LanguagePrimitives.TryConvertTo(guidValue, out guidID)) { - Guid guidID; - if (LanguagePrimitives.TryConvertTo(guidValue, out guidID)) - { - return guidID; - } + return guidID; } } - catch (PSInvalidOperationException) - { - } } + catch (PSInvalidOperationException) { } return new Guid(); } + internal static ExperimentalFeature[] GetExperimentalFeature(string manifestPath) + { + try + { + Hashtable dataFileSetting = + PsUtils.GetModuleManifestProperties( + manifestPath, + PsUtils.ManifestPrivateDataPropertyName); + + object privateData = dataFileSetting["PrivateData"]; + if (privateData is Hashtable hashData && hashData["PSData"] is Hashtable psData) + { + object expFeatureValue = psData["ExperimentalFeatures"]; + if (expFeatureValue != null && + LanguagePrimitives.TryConvertTo(expFeatureValue, out Hashtable[] features) && + features.Length > 0) + { + string moduleName = ModuleIntrinsics.GetModuleName(manifestPath); + var expFeatureList = new List(); + foreach (Hashtable feature in features) + { + string featureName = feature["Name"] as string; + if (String.IsNullOrEmpty(featureName)) { continue; } + + if (ExperimentalFeature.IsModuleFeatureName(featureName, moduleName)) + { + string featureDescription = feature["Description"] as string; + expFeatureList.Add(new ExperimentalFeature(featureName, featureDescription, manifestPath, + ExperimentalFeature.HasEnabled(featureName))); + } + } + return expFeatureList.ToArray(); + } + } + } + catch (PSInvalidOperationException) { } + + return Utils.EmptyArray(); + } + // The extensions of all of the files that can be processed with Import-Module, put the ni.dll in front of .dll to have higher priority to be loaded. internal static string[] PSModuleProcessableExtensions = new string[] { StringLiterals.PowerShellDataFileExtension, diff --git a/src/System.Management.Automation/engine/SessionState.cs b/src/System.Management.Automation/engine/SessionState.cs index 516828c743..4f66c42110 100644 --- a/src/System.Management.Automation/engine/SessionState.cs +++ b/src/System.Management.Automation/engine/SessionState.cs @@ -357,7 +357,6 @@ namespace System.Management.Automation // $ShellId - if there is no runspace config, use the default string string shellId = ExecutionContext.ShellID; - v = new PSVariable(SpecialVariables.ShellId, shellId, ScopedItemOptions.Constant | ScopedItemOptions.AllScope, RunspaceInit.MshShellIdDescription); @@ -366,18 +365,10 @@ namespace System.Management.Automation // $PSHOME // This depends on the shellId. If we cannot read the application base // registry key, set the variable to empty string - string applicationBase = string.Empty; - try - { - applicationBase = Utils.GetApplicationBase(shellId); - } - catch (SecurityException) - { - } + string applicationBase = Utils.DefaultPowerShellAppBase; v = new PSVariable(SpecialVariables.PSHome, applicationBase, ScopedItemOptions.Constant | ScopedItemOptions.AllScope, RunspaceInit.PSHOMEDescription); - this.GlobalScope.SetVariable(v.Name, v, false, true, this, CommandOrigin.Internal, fastPath: true); } diff --git a/src/System.Management.Automation/utils/PsUtils.cs b/src/System.Management.Automation/utils/PsUtils.cs index 3fe10bacba..5734005632 100644 --- a/src/System.Management.Automation/utils/PsUtils.cs +++ b/src/System.Management.Automation/utils/PsUtils.cs @@ -477,6 +477,7 @@ namespace System.Management.Automation internal static readonly string[] ManifestModuleVersionPropertyName = new[] { "ModuleVersion" }; internal static readonly string[] ManifestGuidPropertyName = new[] { "GUID" }; internal static readonly string[] FastModuleManifestAnalysisPropertyNames = new[] { "AliasesToExport", "CmdletsToExport", "FunctionsToExport", "NestedModules", "RootModule", "ModuleToProcess", "ModuleVersion" }; + internal static readonly string[] ManifestPrivateDataPropertyName = new[] { "PrivateData" }; internal static Hashtable GetModuleManifestProperties(string psDataFilePath, string[] keys) {