Skip to content

Commit 7770aa5

Browse files
committed
Add 'Get-ExperimentalFeature' cmdlet
1 parent 8a5ddbb commit 7770aa5

7 files changed

Lines changed: 249 additions & 47 deletions

File tree

src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,7 @@ private ExperimentalAttribute() {}
286286
/// <summary>
287287
/// An instance that represents the none-value.
288288
/// </summary>
289-
internal readonly static ExperimentalAttribute None = new ExperimentalAttribute();
289+
internal static readonly ExperimentalAttribute None = new ExperimentalAttribute();
290290

291291
internal bool ToHide => EffectiveAction == ExperimentAction.Hide;
292292
internal bool ToShow => EffectiveAction == ExperimentAction.Show;
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.IO;
7+
using System.Linq;
8+
using System.Management.Automation;
9+
using System.Management.Automation.Internal;
10+
11+
namespace Microsoft.PowerShell.Commands
12+
{
13+
/// <summary>
14+
/// Implements Get-ExperimentalFeature cmdlet.
15+
/// </summary>
16+
[Cmdlet(VerbsCommon.Get, "ExperimentalFeature", HelpUri = "")]
17+
public class GetExperimentalFeatureCommand : PSCmdlet
18+
{
19+
/// <summary>
20+
/// Specify the feature names.
21+
/// </summary>
22+
[Parameter(ValueFromPipeline = true, Position = 0)]
23+
[ValidateNotNullOrEmpty]
24+
public string[] Name { get; set; }
25+
26+
/// <summary>
27+
/// Search module paths to find all available experimental features.
28+
/// </summary>
29+
[Parameter()]
30+
public SwitchParameter ListAvailable { get; set; }
31+
32+
/// <summary>
33+
/// ProcessRecord method of this cmdlet.
34+
/// </summary>
35+
protected override void ProcessRecord()
36+
{
37+
const WildcardOptions wildcardOptions = WildcardOptions.IgnoreCase | WildcardOptions.CultureInvariant;
38+
IEnumerable<WildcardPattern> namePatterns = SessionStateUtilities.CreateWildcardsFromStrings(Name, wildcardOptions);
39+
40+
if (ListAvailable)
41+
{
42+
foreach (ExperimentalFeature feature in GetAvailableExperimentalFeatures(namePatterns).OrderBy(GetSortingString))
43+
{
44+
WriteObject(feature);
45+
}
46+
}
47+
else if (ExperimentalFeature.EnabledExperimentalFeatureNames.Count > 0)
48+
{
49+
foreach (ExperimentalFeature feature in GetEnabledExperimentalFeatures(namePatterns).OrderBy(GetSortingString))
50+
{
51+
WriteObject(feature);
52+
}
53+
}
54+
}
55+
56+
/// <summary>
57+
/// Construct the string for sorting experimental feature records.
58+
/// </summary>
59+
/// <remarks>
60+
/// Engine features come before module features.
61+
/// Within engine features and more features, features are ordered by name.
62+
/// </remarks>
63+
private static string GetSortingString(ExperimentalFeature feature)
64+
{
65+
if (ExperimentalFeature.EngineSource.Equals(feature.Source))
66+
{
67+
return "0:" + feature.Name;
68+
}
69+
else
70+
{
71+
return "1:" + feature.Name;
72+
}
73+
}
74+
75+
/// <summary>
76+
/// Get enabled experimental features based on the specified name patterns.
77+
/// </summary>
78+
private IEnumerable<ExperimentalFeature> GetEnabledExperimentalFeatures(IEnumerable<WildcardPattern> namePatterns)
79+
{
80+
var moduleFeatures = new List<string>();
81+
var moduleNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
82+
83+
foreach (string featureName in ExperimentalFeature.EnabledExperimentalFeatureNames)
84+
{
85+
// Only process the feature names that matches any name patterns.
86+
if (SessionStateUtilities.MatchesAnyWildcardPattern(featureName, namePatterns, defaultValue: true))
87+
{
88+
if (ExperimentalFeature.EngineExperimentalFeatureMap.TryGetValue(featureName, out ExperimentalFeature feature))
89+
{
90+
yield return feature;
91+
}
92+
else
93+
{
94+
moduleFeatures.Add(featureName);
95+
int lastDotIndex = featureName.LastIndexOf('.');
96+
moduleNames.Add(featureName.Substring(0, lastDotIndex));
97+
}
98+
}
99+
}
100+
101+
if (moduleFeatures.Count > 0)
102+
{
103+
var featuresFromGivenModules = new Dictionary<string, ExperimentalFeature>(StringComparer.OrdinalIgnoreCase);
104+
foreach (string moduleFile in GetValidModuleFiles(moduleNames))
105+
{
106+
ExperimentalFeature[] features = ModuleIntrinsics.GetExperimentalFeature(moduleFile);
107+
foreach (var feature in features)
108+
{
109+
featuresFromGivenModules.TryAdd(feature.Name, feature);
110+
}
111+
}
112+
113+
foreach (string featureName in moduleFeatures)
114+
{
115+
if (featuresFromGivenModules.TryGetValue(featureName, out ExperimentalFeature feature))
116+
{
117+
yield return feature;
118+
}
119+
else
120+
{
121+
yield return new ExperimentalFeature(featureName, description: null, source: null, isEnabled: true);
122+
}
123+
}
124+
}
125+
}
126+
127+
/// <summary>
128+
/// Get available experimental features based on the specified name patterns.
129+
/// </summary>
130+
private IEnumerable<ExperimentalFeature> GetAvailableExperimentalFeatures(IEnumerable<WildcardPattern> namePatterns)
131+
{
132+
foreach (ExperimentalFeature feature in ExperimentalFeature.EngineExperimentalFeatures)
133+
{
134+
if (SessionStateUtilities.MatchesAnyWildcardPattern(feature.Name, namePatterns, defaultValue: true))
135+
{
136+
yield return feature;
137+
}
138+
}
139+
140+
foreach (string moduleFile in GetValidModuleFiles(moduleNamesToFind: null))
141+
{
142+
ExperimentalFeature[] features = ModuleIntrinsics.GetExperimentalFeature(moduleFile);
143+
foreach (var feature in features)
144+
{
145+
if (SessionStateUtilities.MatchesAnyWildcardPattern(feature.Name, namePatterns, defaultValue: true))
146+
{
147+
yield return feature;
148+
}
149+
}
150+
}
151+
}
152+
153+
/// <summary>
154+
/// Get valid module files from module paths.
155+
/// </summary>
156+
private IEnumerable<string> GetValidModuleFiles(HashSet<string> moduleNamesToFind)
157+
{
158+
var modulePaths = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
159+
foreach (string path in ModuleIntrinsics.GetModulePath(includeSystemModulePath: false, Context))
160+
{
161+
string uniquePath = path.TrimEnd(Utils.Separators.Directory);
162+
if (!modulePaths.Add(uniquePath)) { continue; }
163+
164+
foreach (string moduleFile in ModuleUtils.GetDefaultAvailableModuleFiles(uniquePath))
165+
{
166+
// We only care about module manifest files because that's where experimental features are declared.
167+
if (!moduleFile.EndsWith(StringLiterals.PowerShellDataFileExtension, StringComparison.OrdinalIgnoreCase)) { continue; }
168+
169+
if (moduleNamesToFind != null)
170+
{
171+
string currentModuleName = ModuleIntrinsics.GetModuleName(moduleFile);
172+
if (!moduleNamesToFind.Contains(currentModuleName)) { continue; }
173+
}
174+
175+
yield return moduleFile;
176+
}
177+
}
178+
}
179+
}
180+
}

src/System.Management.Automation/engine/InitialSessionState.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5277,6 +5277,7 @@ private static void InitializeCoreCmdletsAndProviders(
52775277
{"Export-ModuleMember", new SessionStateCmdletEntry("Export-ModuleMember", typeof(ExportModuleMemberCommand), helpFile) },
52785278
{"ForEach-Object", new SessionStateCmdletEntry("ForEach-Object", typeof(ForEachObjectCommand), helpFile) },
52795279
{"Get-Command", new SessionStateCmdletEntry("Get-Command", typeof(GetCommandCommand), helpFile) },
5280+
{"Get-ExperimentalFeature", new SessionStateCmdletEntry("Get-ExperimentalFeature", typeof(GetExperimentalFeatureCommand), helpFile) },
52805281
{"Get-Help", new SessionStateCmdletEntry("Get-Help", typeof(GetHelpCommand), helpFile) },
52815282
{"Get-History", new SessionStateCmdletEntry("Get-History", typeof(GetHistoryCommand), helpFile) },
52825283
{"Get-Job", new SessionStateCmdletEntry("Get-Job", typeof(GetJobCommand), helpFile) },

src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2059,8 +2059,8 @@ internal PSModuleInfo LoadModuleManifest(
20592059
if (ExperimentalFeature.IsModuleFeatureName(featureName, moduleName))
20602060
{
20612061
string featureDescription = feature["Description"] as string;
2062-
expFeatureList.Add(new ExperimentalFeature(featureName, featureDescription, moduleName,
2063-
ExperimentalFeature.HasEnabled(featureName)));
2062+
expFeatureList.Add(new ExperimentalFeature(featureName, featureDescription, moduleManifestPath,
2063+
ExperimentalFeature.HasEnabled(featureName)));
20642064
}
20652065
else
20662066
{

src/System.Management.Automation/engine/Modules/ModuleIntrinsics.cs

Lines changed: 63 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33

4+
using System.Collections;
45
using System.Collections.Generic;
56
using System.IO;
67
using System.Management.Automation.Configuration;
@@ -414,62 +415,90 @@ internal static bool IsModuleMatchingModuleSpec(PSModuleInfo moduleInfo, ModuleS
414415

415416
internal static Version GetManifestModuleVersion(string manifestPath)
416417
{
417-
if (manifestPath != null &&
418-
manifestPath.EndsWith(StringLiterals.PowerShellDataFileExtension, StringComparison.OrdinalIgnoreCase))
418+
try
419419
{
420-
try
421-
{
422-
var dataFileSetting =
423-
PsUtils.GetModuleManifestProperties(
424-
manifestPath,
425-
PsUtils.ManifestModuleVersionPropertyName);
420+
Hashtable dataFileSetting =
421+
PsUtils.GetModuleManifestProperties(
422+
manifestPath,
423+
PsUtils.ManifestModuleVersionPropertyName);
426424

427-
var versionValue = dataFileSetting["ModuleVersion"];
428-
if (versionValue != null)
425+
object versionValue = dataFileSetting["ModuleVersion"];
426+
if (versionValue != null)
427+
{
428+
Version moduleVersion;
429+
if (LanguagePrimitives.TryConvertTo(versionValue, out moduleVersion))
429430
{
430-
Version moduleVersion;
431-
if (LanguagePrimitives.TryConvertTo(versionValue, out moduleVersion))
432-
{
433-
return moduleVersion;
434-
}
431+
return moduleVersion;
435432
}
436433
}
437-
catch (PSInvalidOperationException)
438-
{
439-
}
440434
}
435+
catch (PSInvalidOperationException) { }
441436

442437
return new Version(0, 0);
443438
}
444439

445440
internal static Guid GetManifestGuid(string manifestPath)
446441
{
447-
if (manifestPath != null &&
448-
manifestPath.EndsWith(StringLiterals.PowerShellDataFileExtension, StringComparison.OrdinalIgnoreCase))
442+
try
449443
{
450-
try
444+
Hashtable dataFileSetting =
445+
PsUtils.GetModuleManifestProperties(
446+
manifestPath,
447+
PsUtils.ManifestGuidPropertyName);
448+
449+
object guidValue = dataFileSetting["GUID"];
450+
if (guidValue != null)
451451
{
452-
var dataFileSetting =
453-
PsUtils.GetModuleManifestProperties(
454-
manifestPath,
455-
PsUtils.ManifestGuidPropertyName);
452+
Guid guidID;
453+
if (LanguagePrimitives.TryConvertTo(guidValue, out guidID))
454+
{
455+
return guidID;
456+
}
457+
}
458+
}
459+
catch (PSInvalidOperationException) { }
456460

457-
var guidValue = dataFileSetting["GUID"];
458-
if (guidValue != null)
461+
return new Guid();
462+
}
463+
464+
internal static ExperimentalFeature[] GetExperimentalFeature(string manifestPath)
465+
{
466+
try
467+
{
468+
Hashtable dataFileSetting =
469+
PsUtils.GetModuleManifestProperties(
470+
manifestPath,
471+
PsUtils.ManifestPrivateDataPropertyName);
472+
473+
object privateData = dataFileSetting["PrivateData"];
474+
if (privateData is Hashtable hashData && hashData["PSData"] is Hashtable psData)
475+
{
476+
object expFeatureValue = psData["ExperimentalFeatures"];
477+
if (expFeatureValue != null &&
478+
LanguagePrimitives.TryConvertTo(expFeatureValue, out Hashtable[] features) &&
479+
features.Length > 0)
459480
{
460-
Guid guidID;
461-
if (LanguagePrimitives.TryConvertTo(guidValue, out guidID))
481+
string moduleName = ModuleIntrinsics.GetModuleName(manifestPath);
482+
var expFeatureList = new List<ExperimentalFeature>();
483+
foreach (Hashtable feature in features)
462484
{
463-
return guidID;
485+
string featureName = feature["Name"] as string;
486+
if (String.IsNullOrEmpty(featureName)) { continue; }
487+
488+
if (ExperimentalFeature.IsModuleFeatureName(featureName, moduleName))
489+
{
490+
string featureDescription = feature["Description"] as string;
491+
expFeatureList.Add(new ExperimentalFeature(featureName, featureDescription, manifestPath,
492+
ExperimentalFeature.HasEnabled(featureName)));
493+
}
464494
}
495+
return expFeatureList.ToArray();
465496
}
466497
}
467-
catch (PSInvalidOperationException)
468-
{
469-
}
470498
}
499+
catch (PSInvalidOperationException) { }
471500

472-
return new Guid();
501+
return Utils.EmptyArray<ExperimentalFeature>();
473502
}
474503

475504
// 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.

src/System.Management.Automation/engine/SessionState.cs

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -357,7 +357,6 @@ internal void InitializeFixedVariables()
357357

358358
// $ShellId - if there is no runspace config, use the default string
359359
string shellId = ExecutionContext.ShellID;
360-
361360
v = new PSVariable(SpecialVariables.ShellId, shellId,
362361
ScopedItemOptions.Constant | ScopedItemOptions.AllScope,
363362
RunspaceInit.MshShellIdDescription);
@@ -366,18 +365,10 @@ internal void InitializeFixedVariables()
366365
// $PSHOME
367366
// This depends on the shellId. If we cannot read the application base
368367
// registry key, set the variable to empty string
369-
string applicationBase = string.Empty;
370-
try
371-
{
372-
applicationBase = Utils.GetApplicationBase(shellId);
373-
}
374-
catch (SecurityException)
375-
{
376-
}
368+
string applicationBase = Utils.DefaultPowerShellAppBase;
377369
v = new PSVariable(SpecialVariables.PSHome, applicationBase,
378370
ScopedItemOptions.Constant | ScopedItemOptions.AllScope,
379371
RunspaceInit.PSHOMEDescription);
380-
381372
this.GlobalScope.SetVariable(v.Name, v, false, true, this, CommandOrigin.Internal, fastPath: true);
382373
}
383374

src/System.Management.Automation/utils/PsUtils.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,7 @@ internal static Hashtable EvaluatePowerShellDataFile(
477477
internal static readonly string[] ManifestModuleVersionPropertyName = new[] { "ModuleVersion" };
478478
internal static readonly string[] ManifestGuidPropertyName = new[] { "GUID" };
479479
internal static readonly string[] FastModuleManifestAnalysisPropertyNames = new[] { "AliasesToExport", "CmdletsToExport", "FunctionsToExport", "NestedModules", "RootModule", "ModuleToProcess", "ModuleVersion" };
480+
internal static readonly string[] ManifestPrivateDataPropertyName = new[] { "PrivateData" };
480481

481482
internal static Hashtable GetModuleManifestProperties(string psDataFilePath, string[] keys)
482483
{

0 commit comments

Comments
 (0)