diff --git a/src/PowerShell.Core.Instrumentation/PowerShell.Core.Instrumentation.man b/src/PowerShell.Core.Instrumentation/PowerShell.Core.Instrumentation.man
index 33c0a5f519a..a058072272f 100644
--- a/src/PowerShell.Core.Instrumentation/PowerShell.Core.Instrumentation.man
+++ b/src/PowerShell.Core.Instrumentation/PowerShell.Core.Instrumentation.man
@@ -98,6 +98,29 @@
value="0xD003"
version="1"
/>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
GetFormatData()
"System.Management.Automation.PSModuleInfo",
ViewsOf_System_Management_Automation_PSModuleInfo());
+ yield return new ExtendedTypeDefinition(
+ "System.Management.Automation.ExperimentalFeature",
+ ViewsOf_System_Management_Automation_ExperimentalFeature());
+
var td46 = new ExtendedTypeDefinition(
"Microsoft.PowerShell.Commands.BasicHtmlWebResponseObject",
ViewsOf_Microsoft_PowerShell_Commands_BasicHtmlWebResponseObject());
@@ -1240,6 +1244,33 @@ private static IEnumerable ViewsOf_System_Management_Autom
.EndList());
}
+ private static IEnumerable ViewsOf_System_Management_Automation_ExperimentalFeature()
+ {
+ yield return new FormatViewDefinition("ExperimentalFeature",
+ TableControl.Create()
+ .AddHeader(Alignment.Left, width: 35)
+ .AddHeader(Alignment.Right, width: 7)
+ .AddHeader(Alignment.Left, width: 35)
+ .AddHeader(Alignment.Left)
+ .StartRowDefinition()
+ .AddPropertyColumn("Name")
+ .AddPropertyColumn("Enabled")
+ .AddPropertyColumn("Source")
+ .AddPropertyColumn("Description")
+ .EndRowDefinition()
+ .EndTable());
+
+ yield return new FormatViewDefinition("ExperimentalFeature",
+ ListControl.Create()
+ .StartEntry()
+ .AddItemProperty("Name")
+ .AddItemProperty("Enabled")
+ .AddItemProperty("Source")
+ .AddItemProperty("Description")
+ .EndEntry()
+ .EndList());
+ }
+
private static IEnumerable ViewsOf_Microsoft_PowerShell_Commands_BasicHtmlWebResponseObject()
{
yield return new FormatViewDefinition("Microsoft.PowerShell.Commands.BasicHtmlWebResponseObject",
diff --git a/src/System.Management.Automation/engine/Attributes.cs b/src/System.Management.Automation/engine/Attributes.cs
index efa3d5c76f2..309bee7a9b2 100644
--- a/src/System.Management.Automation/engine/Attributes.cs
+++ b/src/System.Management.Automation/engine/Attributes.cs
@@ -620,12 +620,55 @@ public ParameterAttribute()
{
}
+ ///
+ /// Initializes a new instance that is associated with an experimental feature.
+ ///
+ public ParameterAttribute(string experimentName, ExperimentAction experimentAction)
+ {
+ ExperimentalAttribute.ValidateArguments(experimentName, experimentAction);
+ ExperimentName = experimentName;
+ ExperimentAction = experimentAction;
+ }
+
private string _parameterSetName = ParameterAttribute.AllParameterSets;
private string _helpMessage;
private string _helpMessageBaseName;
private string _helpMessageResourceId;
+ #region Experimental Feature Related Properties
+
+ ///
+ /// Get name of the experimental feature this attribute is associated with.
+ ///
+ public string ExperimentName { get; }
+
+ ///
+ /// Get action for engine to take when the experimental feature is enabled.
+ ///
+ public ExperimentAction ExperimentAction { get; }
+
+ internal bool ToHide => EffectiveAction == ExperimentAction.Hide;
+ internal bool ToShow => EffectiveAction == ExperimentAction.Show;
+
+ ///
+ /// Get effective action to take at run time.
+ ///
+ private ExperimentAction EffectiveAction
+ {
+ get
+ {
+ if (_effectiveAction == ExperimentAction.None)
+ {
+ _effectiveAction = ExperimentalFeature.GetActionToTake(ExperimentName, ExperimentAction);
+ }
+ return _effectiveAction;
+ }
+ }
+ private ExperimentAction _effectiveAction = default(ExperimentAction);
+
+ #endregion
+
///
/// Gets and sets the parameter position. If not set, the parameter is named.
///
diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs
index 4f8debce7c0..e0f1ecbbbd1 100644
--- a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs
+++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs
@@ -1756,13 +1756,15 @@ private List GetResultForAttributeArgument(CompletionContext c
{
PropertyInfo[] propertyInfos = attributeType.GetProperties(BindingFlags.Public | BindingFlags.Instance);
List result = new List();
- foreach (PropertyInfo pro in propertyInfos)
+ foreach (PropertyInfo property in propertyInfos)
{
- //Ignore TypeId (all attributes inherit it)
- if (pro.Name != "TypeId" && (pro.Name.StartsWith(argName, StringComparison.OrdinalIgnoreCase)))
+ // Ignore getter-only properties, including 'TypeId' (all attributes inherit it).
+ if (!property.CanWrite) { continue; }
+
+ if (property.Name.StartsWith(argName, StringComparison.OrdinalIgnoreCase))
{
- result.Add(new CompletionResult(pro.Name, pro.Name, CompletionResultType.Property,
- pro.PropertyType.ToString() + " " + pro.Name));
+ result.Add(new CompletionResult(property.Name, property.Name, CompletionResultType.Property,
+ property.PropertyType.ToString() + " " + property.Name));
}
}
return result;
diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs
index cf88b76bca7..5bac5a9ae86 100644
--- a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs
+++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs
@@ -1698,12 +1698,10 @@ private static void ProcessParameter(
foreach (ValidateArgumentsAttribute att in parameter.Parameter.ValidationAttributes)
{
- if (att is ValidateSetAttribute)
+ if (att is ValidateSetAttribute setAtt)
{
RemoveLastNullCompletionResult(result);
- var setAtt = (ValidateSetAttribute)att;
-
string wordToComplete = context.WordToComplete;
string quote = HandleDoubleAndSingleQuote(ref wordToComplete);
@@ -1712,6 +1710,8 @@ private static void ProcessParameter(
foreach (string value in setAtt.ValidValues)
{
+ if (value == string.Empty) { continue; }
+
if (wordToComplete.Equals(value, StringComparison.OrdinalIgnoreCase))
{
string completionText = quote == string.Empty ? value : quote + value + quote;
diff --git a/src/System.Management.Automation/engine/CommandProcessor.cs b/src/System.Management.Automation/engine/CommandProcessor.cs
index 72f7e10c9e5..fdfeee97641 100644
--- a/src/System.Management.Automation/engine/CommandProcessor.cs
+++ b/src/System.Management.Automation/engine/CommandProcessor.cs
@@ -18,7 +18,7 @@ namespace System.Management.Automation
///
internal class CommandProcessor : CommandProcessorBase
{
-#region ctor
+ #region ctor
static CommandProcessor()
{
@@ -87,9 +87,10 @@ internal CommandProcessor(IScriptCommandInfo scriptCommandInfo, ExecutionContext
// CommandProcessor
-#endregion ctor
+ #endregion ctor
+
+ #region internal members
-#region internal members
///
/// Returns a CmdletParameterBinderController for the specified command
///
@@ -393,9 +394,9 @@ internal override void ProcessRecord()
}
}
-#endregion public_methods
+ #endregion public_methods
-#region helper_methods
+ #region helper_methods
///
/// Tells whether it is the first call to Read
@@ -849,7 +850,7 @@ internal override bool IsHelpRequested(out string helpTarget, out HelpCategory h
return base.IsHelpRequested(out helpTarget, out helpCategory);
}
-#endregion helper_methods
+ #endregion helper_methods
}
}
diff --git a/src/System.Management.Automation/engine/CommandProcessorBase.cs b/src/System.Management.Automation/engine/CommandProcessorBase.cs
index 562f6a9572e..38c364ea2be 100644
--- a/src/System.Management.Automation/engine/CommandProcessorBase.cs
+++ b/src/System.Management.Automation/engine/CommandProcessorBase.cs
@@ -34,14 +34,31 @@ internal CommandProcessorBase()
/// The metadata about the command to run.
///
///
- internal CommandProcessorBase(
- CommandInfo commandInfo)
+ internal CommandProcessorBase(CommandInfo commandInfo)
{
if (commandInfo == null)
{
throw PSTraceSource.NewArgumentNullException("commandInfo");
}
+ if (commandInfo is IScriptCommandInfo scriptCommand)
+ {
+ ExperimentalAttribute expAttribute = scriptCommand.ScriptBlock.ExperimentalAttribute;
+ if (expAttribute != null && expAttribute.ToHide)
+ {
+ string errorTemplate = expAttribute.ExperimentAction == ExperimentAction.Hide
+ ? DiscoveryExceptions.ScriptDisabledWhenFeatureOn
+ : DiscoveryExceptions.ScriptDisabledWhenFeatureOff;
+ string errorMsg = StringUtil.Format(errorTemplate, expAttribute.ExperimentName);
+ ErrorRecord errorRecord = new ErrorRecord(
+ new InvalidOperationException(errorMsg),
+ "ScriptCommandDisabled",
+ ErrorCategory.InvalidOperation,
+ commandInfo);
+ throw new CmdletInvocationException(errorRecord);
+ }
+ }
+
CommandInfo = commandInfo;
}
diff --git a/src/System.Management.Automation/engine/CompiledCommandParameter.cs b/src/System.Management.Automation/engine/CompiledCommandParameter.cs
index 97422f46d4a..070abdf42f6 100644
--- a/src/System.Management.Automation/engine/CompiledCommandParameter.cs
+++ b/src/System.Management.Automation/engine/CompiledCommandParameter.cs
@@ -64,6 +64,18 @@ internal CompiledCommandParameter(RuntimeDefinedParameter runtimeDefinedParamete
// First, process attributes that aren't type conversions
foreach (Attribute attribute in runtimeDefinedParameter.Attributes)
{
+ if (processingDynamicParameters)
+ {
+ // When processing dynamic parameters, the attribute list may contain experimental attributes
+ // and disabled parameter attributes. We should ignore those attributes.
+ // When processing non-dynamic parameters, the experimental attributes and disabled parameter
+ // attributes have already been filtered out when constructing the RuntimeDefinedParameter.
+ if (attribute is ExperimentalAttribute || attribute is ParameterAttribute param && param.ToHide)
+ {
+ continue;
+ }
+ }
+
if (!(attribute is ArgumentTypeConverterAttribute))
{
ProcessAttribute(runtimeDefinedParameter.Name, attribute, ref validationAttributes, ref argTransformationAttributes, ref aliases);
@@ -78,7 +90,7 @@ internal CompiledCommandParameter(RuntimeDefinedParameter runtimeDefinedParamete
}
// Now process type converters
- foreach (ArgumentTypeConverterAttribute attribute in runtimeDefinedParameter.Attributes.OfType())
+ foreach (var attribute in runtimeDefinedParameter.Attributes.OfType())
{
ProcessAttribute(runtimeDefinedParameter.Name, attribute, ref validationAttributes, ref argTransformationAttributes, ref aliases);
}
@@ -169,7 +181,15 @@ internal CompiledCommandParameter(MemberInfo member, bool processingDynamicParam
foreach (Attribute attr in memberAttributes)
{
- ProcessAttribute(member.Name, attr, ref validationAttributes, ref argTransformationAttributes, ref aliases);
+ switch (attr)
+ {
+ case ExperimentalAttribute _:
+ case ParameterAttribute param when param.ToHide:
+ break;
+ default:
+ ProcessAttribute(member.Name, attr, ref validationAttributes, ref argTransformationAttributes, ref aliases);
+ break;
+ }
}
this.ValidationAttributes = validationAttributes == null
@@ -436,7 +456,6 @@ private void ProcessAttribute(
ref Collection argTransformationAttributes,
ref string[] aliases)
{
- // NTRAID#Windows Out Of Band Releases-926374-2005/12/22-JonN
if (attribute == null)
return;
diff --git a/src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs b/src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs
new file mode 100644
index 00000000000..776768339e5
--- /dev/null
+++ b/src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs
@@ -0,0 +1,338 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Management.Automation.Configuration;
+using System.Management.Automation.Internal;
+using System.Management.Automation.Tracing;
+using System.Runtime.CompilerServices;
+
+namespace System.Management.Automation
+{
+ ///
+ /// Support experimental features in PowerShell.
+ ///
+ public class ExperimentalFeature
+ {
+ #region Const Members
+
+ internal const string EngineSource = "PSEngine";
+
+ #endregion
+
+ #region Instance Members
+
+ ///
+ /// Name of an experimental feature.
+ ///
+ public string Name { get; }
+
+ ///
+ /// Description of an experimental feature.
+ ///
+ public string Description { get; }
+
+ ///
+ /// Source of an experimental feature.
+ ///
+ public string Source { get; }
+
+ ///
+ /// Indicate whether the feature is enabled.
+ ///
+ public bool Enabled { get; private set; }
+
+ ///
+ /// Constructor for ExperimentalFeature.
+ ///
+ internal ExperimentalFeature(string name, string description, string source, bool isEnabled)
+ {
+ Name = name;
+ Description = description;
+ Source = source;
+ Enabled = isEnabled;
+ }
+
+ #endregion
+
+ #region Static Members
+
+ ///
+ /// All available engine experimental features.
+ ///
+ internal static readonly ReadOnlyCollection EngineExperimentalFeatures;
+
+ ///
+ /// A dictionary of all available engine experimental features. Feature name is the key.
+ ///
+ internal static readonly ReadOnlyDictionary EngineExperimentalFeatureMap;
+
+ ///
+ /// Experimental feature names that are enabled in the config file.
+ ///
+ internal static readonly ImmutableHashSet EnabledExperimentalFeatureNames;
+
+ ///
+ /// Type initializer. Initialize the engine experimental feature list.
+ ///
+ static ExperimentalFeature()
+ {
+ // Initialize the readonly collection 'EngineExperimentalFeatures'.
+ var engineFeatures = new ExperimentalFeature[] {
+ /* Register engine experimental features here. Follow the same pattern as the example:
+ new ExperimentalFeature(name: "PSFileSystemProviderV2",
+ description: "Replace the old FileSystemProvider with cleaner design and faster code",
+ source: EngineSource,
+ isEnabled: false),
+ */
+ };
+ EngineExperimentalFeatures = new ReadOnlyCollection(engineFeatures);
+
+ // Initialize the readonly dictionary 'EngineExperimentalFeatureMap'.
+ var engineExpFeatureMap = engineFeatures.ToDictionary(f => f.Name, StringComparer.OrdinalIgnoreCase);
+ EngineExperimentalFeatureMap = new ReadOnlyDictionary(engineExpFeatureMap);
+
+ // Initialize the immutable hashset 'EnabledExperimentalFeatureNames'.
+ // The initialization of 'EnabledExperimentalFeatureNames' is deliberately made in the type initializer so that:
+ // 1. 'EnabledExperimentalFeatureNames' can be declared as readonly;
+ // 2. No need to deal with initialization from multiple threads;
+ // 3. We don't need to decide where/when to read the config file for the enabled experimental features,
+ // instead, it will be done when the type is used for the first time, which is always earlier than
+ // any experimental features take effect.
+ string[] enabledFeatures = Utils.EmptyArray();
+ try
+ {
+ enabledFeatures = PowerShellConfig.Instance.GetExperimentalFeatures();
+ }
+ catch (Exception e) when (LogException(e)) { }
+
+ EnabledExperimentalFeatureNames = ProcessEnabledFeatures(enabledFeatures);
+ }
+
+ ///
+ /// Process the array of enabled feature names retrieved from configuration.
+ /// Ignore invalid feature names and unavailable engine feature names, and
+ /// return an ImmutableHashSet of the valid enabled feature names.
+ ///
+ private static ImmutableHashSet ProcessEnabledFeatures(string[] enabledFeatures)
+ {
+ if (enabledFeatures.Length == 0) { return ImmutableHashSet.Empty; }
+
+ var list = new List(enabledFeatures.Length);
+ foreach (string name in enabledFeatures)
+ {
+ if (IsModuleFeatureName(name))
+ {
+ list.Add(name);
+ }
+ else if (IsEngineFeatureName(name))
+ {
+ if (EngineExperimentalFeatureMap.TryGetValue(name, out ExperimentalFeature feature))
+ {
+ feature.Enabled = true;
+ list.Add(name);
+ }
+ else
+ {
+ string message = StringUtil.Format(Logging.EngineExperimentalFeatureNotFound, name);
+ LogError(PSEventId.ExperimentalFeature_InvalidName, name, message);
+ }
+ }
+ else
+ {
+ string message = StringUtil.Format(Logging.InvalidExperimentalFeatureName, name);
+ LogError(PSEventId.ExperimentalFeature_InvalidName, name, message);
+ }
+ }
+ return ImmutableHashSet.CreateRange(StringComparer.OrdinalIgnoreCase, list);
+ }
+
+ ///
+ /// Log the exception without rewinding the stack.
+ ///
+ private static bool LogException(Exception e)
+ {
+ LogError(PSEventId.ExperimentalFeature_ReadConfig_Error, e.GetType().FullName, e.Message, e.StackTrace);
+ return false;
+ }
+
+ ///
+ /// Log an error message.
+ ///
+ private static void LogError(PSEventId eventId, params object[] args)
+ {
+ PSEtwLog.LogOperationalError(eventId, PSOpcode.Constructor, PSTask.ExperimentalFeature, PSKeyword.UseAlwaysOperational, args);
+ }
+
+ ///
+ /// Check if the name follows the engine experimental feature name convention.
+ /// Convention: prefix 'PS' to the feature name -- 'PSFeatureName'.
+ ///
+ internal static bool IsEngineFeatureName(string featureName)
+ {
+ return featureName.Length > 2 && featureName.IndexOf('.') == -1 && featureName.StartsWith("PS", StringComparison.Ordinal);
+ }
+
+ ///
+ /// Check if the name follows the module experimental feature name convention.
+ /// Convention: prefix the module name to the feature name -- 'ModuleName.FeatureName'.
+ ///
+ /// The feature name to check.
+ /// When specified, we check if the feature name matches the module name.
+ internal static bool IsModuleFeatureName(string featureName, string moduleName = null)
+ {
+ // Feature names cannot start with a dot
+ if (featureName.StartsWith('.'))
+ {
+ return false;
+ }
+
+ // Feature names must contain a dot, but not at the end
+ int lastDotIndex = featureName.LastIndexOf('.');
+ if (lastDotIndex == -1 || lastDotIndex == featureName.Length - 1)
+ {
+ return false;
+ }
+
+ if (moduleName == null)
+ {
+ return true;
+ }
+
+ // If the module name is given, it must match the prefix of the feature name (up to the last dot).
+ var moduleNamePart = featureName.AsSpan(0, lastDotIndex);
+ return moduleNamePart.Equals(moduleName.AsSpan(), StringComparison.OrdinalIgnoreCase);
+ }
+
+ ///
+ /// Determine the action to take for the specified experiment name and action.
+ ///
+ internal static ExperimentAction GetActionToTake(string experimentName, ExperimentAction experimentAction)
+ {
+ if (experimentName == null || experimentAction == ExperimentAction.None)
+ {
+ // If either the experiment name or action is not defined, then return 'Show' by default.
+ // This could happen to 'ParameterAttribute' when no experimental related field is declared.
+ return ExperimentAction.Show;
+ }
+
+ ExperimentAction action = experimentAction;
+ if (!IsEnabled(experimentName))
+ {
+ action = (action == ExperimentAction.Hide) ? ExperimentAction.Show : ExperimentAction.Hide;
+ }
+ return action;
+ }
+
+ ///
+ /// Check if the specified experimental feature has been enabled.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool IsEnabled(string featureName)
+ {
+ return EnabledExperimentalFeatureNames.Contains(featureName);
+ }
+
+ #endregion
+ }
+
+ ///
+ /// Indicates the action to take on the cmdlet/parameter that has the attribute declared.
+ ///
+ public enum ExperimentAction
+ {
+ ///
+ /// Represent an undefined action, used as the default value.
+ ///
+ None = 0,
+
+ ///
+ /// Hide the cmdlet/parameter when the corresponding experimental feature is enabled.
+ ///
+ Hide = 1,
+
+ ///
+ /// Show the cmdlet/parameter when the corresponding experimental feature is enabled.
+ ///
+ Show = 2
+ }
+
+ ///
+ /// The attribute that applies to cmdlet/function/parameter to define what the engine should do with it.
+ ///
+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Field | AttributeTargets.Property)]
+ public sealed class ExperimentalAttribute : ParsingBaseAttribute
+ {
+ ///
+ /// Get name of the experimental feature this attribute is associated with.
+ ///
+ public string ExperimentName { get; }
+
+ ///
+ /// Get action for engine to take when the experimental feature is enabled.
+ ///
+ public ExperimentAction ExperimentAction { get; }
+
+ ///
+ /// Initializes a new instance of the ExperimentalAttribute class.
+ ///
+ public ExperimentalAttribute(string experimentName, ExperimentAction experimentAction)
+ {
+ ValidateArguments(experimentName, experimentAction);
+ ExperimentName = experimentName;
+ ExperimentAction = experimentAction;
+ }
+
+ ///
+ /// Initialize an instance that represents the none-value.
+ ///
+ private ExperimentalAttribute() {}
+
+ ///
+ /// An instance that represents the none-value.
+ ///
+ internal static readonly ExperimentalAttribute None = new ExperimentalAttribute();
+
+ ///
+ /// Validate arguments for the constructor.
+ ///
+ internal static void ValidateArguments(string experimentName, ExperimentAction experimentAction)
+ {
+ if (string.IsNullOrEmpty(experimentName))
+ {
+ string paramName = nameof(experimentName);
+ throw PSTraceSource.NewArgumentNullException(paramName, Metadata.ArgumentNullOrEmpty, paramName);
+ }
+
+ if (experimentAction == ExperimentAction.None)
+ {
+ string paramName = nameof(experimentAction);
+ string invalidMember = ExperimentAction.None.ToString();
+ string validMembers = StringUtil.Format("{0}, {1}", ExperimentAction.Hide, ExperimentAction.Show);
+ throw PSTraceSource.NewArgumentException(paramName, Metadata.InvalidEnumArgument, invalidMember, paramName, validMembers);
+ }
+ }
+
+ internal bool ToHide => EffectiveAction == ExperimentAction.Hide;
+ internal bool ToShow => EffectiveAction == ExperimentAction.Show;
+
+ ///
+ /// Get effective action to take at run time.
+ ///
+ private ExperimentAction EffectiveAction
+ {
+ get
+ {
+ if (_effectiveAction == ExperimentAction.None)
+ {
+ _effectiveAction = ExperimentalFeature.GetActionToTake(ExperimentName, ExperimentAction);
+ }
+ return _effectiveAction;
+ }
+ }
+ private ExperimentAction _effectiveAction = ExperimentAction.None;
+ }
+}
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 00000000000..7affc3f118b
--- /dev/null
+++ b/src/System.Management.Automation/engine/ExperimentalFeature/GetExperimentalFeatureCommand.cs
@@ -0,0 +1,174 @@
+// 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
+ {
+ ///
+ /// Get and set the feature names.
+ ///
+ [Parameter(ValueFromPipeline = true, Position = 0)]
+ [ValidateNotNullOrEmpty]
+ public string[] Name { get; set; }
+
+ ///
+ /// Get and set the switch flag to 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 module features, features are ordered by name.
+ ///
+ private static (int, string) GetSortingString(ExperimentalFeature feature)
+ {
+ return ExperimentalFeature.EngineSource.Equals(feature.Source, StringComparison.OrdinalIgnoreCase)
+ ? (0, feature.Name)
+ : (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))
+ {
+ foreach (var feature in ModuleIntrinsics.GetExperimentalFeature(moduleFile))
+ {
+ 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;
+ }
+ }
+ }
+ }
+}
diff --git a/src/System.Management.Automation/engine/InitialSessionState.cs b/src/System.Management.Automation/engine/InitialSessionState.cs
index 320961e1674..b480ade349d 100644
--- a/src/System.Management.Automation/engine/InitialSessionState.cs
+++ b/src/System.Management.Automation/engine/InitialSessionState.cs
@@ -13,12 +13,13 @@
using System.Management.Automation.Provider;
using System.Management.Automation.Language;
using System.Reflection;
+using System.Runtime.CompilerServices;
using System.Threading;
using Microsoft.PowerShell.Commands;
-using Debug = System.Management.Automation.Diagnostics;
using System.Management.Automation.Host;
using System.Text;
using System.Threading.Tasks;
+using Debug = System.Management.Automation.Diagnostics;
namespace System.Management.Automation.Runspaces
{
@@ -3752,7 +3753,7 @@ internal PSSnapInInfo ImportPSSnapIn(PSSnapInInfo psSnapInInfo, out PSSnapInExce
{
s_PSSnapInTracer.WriteLine("Loading assembly for psSnapIn {0}", psSnapInInfo.Name);
- assembly = PSSnapInHelpers.LoadPSSnapInAssembly(psSnapInInfo, out cmdlets, out providers);
+ assembly = PSSnapInHelpers.LoadPSSnapInAssembly(psSnapInInfo);
if (assembly == null)
{
@@ -3763,7 +3764,7 @@ internal PSSnapInInfo ImportPSSnapIn(PSSnapInInfo psSnapInInfo, out PSSnapInExce
s_PSSnapInTracer.WriteLine("Loading assembly for psSnapIn {0} succeeded", psSnapInInfo.Name);
- PSSnapInHelpers.AnalyzePSSnapInAssembly(assembly, psSnapInInfo.Name, psSnapInInfo, null, true, out cmdlets, out aliases, out providers, out helpFile);
+ PSSnapInHelpers.AnalyzePSSnapInAssembly(assembly, psSnapInInfo.Name, psSnapInInfo, moduleInfo: null, out cmdlets, out aliases, out providers, out helpFile);
}
// We skip checking if the file exists when it's in $PSHOME because of magic
@@ -4052,8 +4053,7 @@ internal void ImportCmdletsFromAssembly(Assembly assembly, PSModuleInfo module)
Dictionary providers = null;
string assemblyPath = assembly.Location;
- string throwAwayHelpFile = null;
- PSSnapInHelpers.AnalyzePSSnapInAssembly(assembly, assemblyPath, null, module, true, out cmdlets, out aliases, out providers, out throwAwayHelpFile);
+ PSSnapInHelpers.AnalyzePSSnapInAssembly(assembly, assemblyPath, psSnapInInfo: null, module, out cmdlets, out aliases, out providers, helpFile: out _);
// If this is an in-memory assembly, don't added it to the list of AssemblyEntries
// since it can't be loaded by path or name
@@ -4897,13 +4897,9 @@ internal static string GetNestedModuleDllName(string moduleName)
internal static class PSSnapInHelpers
{
[SuppressMessage("Microsoft.Reliability", "CA2001:AvoidCallingProblematicMethods", MessageId = "System.Reflection.Assembly.LoadFrom")]
- internal static Assembly LoadPSSnapInAssembly(PSSnapInInfo psSnapInInfo,
- out Dictionary cmdlets, out Dictionary providers)
+ internal static Assembly LoadPSSnapInAssembly(PSSnapInInfo psSnapInInfo)
{
Assembly assembly = null;
- cmdlets = null;
- providers = null;
-
s_PSSnapInTracer.WriteLine("Loading assembly from GAC. Assembly Name: {0}", psSnapInInfo.AssemblyName);
try
@@ -4960,23 +4956,21 @@ internal static Assembly LoadPSSnapInAssembly(PSSnapInInfo psSnapInInfo,
return assembly;
}
- private static T GetCustomAttribute(Type decoratedType) where T : Attribute
+ private static bool TryGetCustomAttribute(Type decoratedType, out T attribute) where T : Attribute
{
- var attributes = decoratedType.GetCustomAttributes(false);
- var customAttrs = attributes.ToArray();
-
- Debug.Assert(customAttrs.Length <= 1, "CmdletAttribute and/or CmdletProviderAttribute cannot normally appear more than once");
- return customAttrs.Length == 0 ? null : customAttrs[0];
+ var attributes = decoratedType.GetCustomAttributes(inherit: false);
+ attribute = attributes.FirstOrDefault();
+ return attribute != null;
}
- internal static void AnalyzePSSnapInAssembly(Assembly assembly, string name, PSSnapInInfo psSnapInInfo, PSModuleInfo moduleInfo, bool isModuleLoad,
+ internal static void AnalyzePSSnapInAssembly(Assembly assembly, string name, PSSnapInInfo psSnapInInfo, PSModuleInfo moduleInfo,
out Dictionary cmdlets, out Dictionary> aliases,
out Dictionary providers, out string helpFile)
{
helpFile = null;
if (assembly == null)
{
- throw new ArgumentNullException("assembly");
+ throw new ArgumentNullException(nameof(assembly));
}
cmdlets = null;
@@ -4988,8 +4982,8 @@ internal static void AnalyzePSSnapInAssembly(Assembly assembly, string name, PSS
Dictionary>> cachedCmdlets;
if (s_cmdletCache.Value.TryGetValue(assembly, out cachedCmdlets))
{
- cmdlets = new Dictionary(s_cmdletCache.Value.Count, StringComparer.OrdinalIgnoreCase);
- aliases = new Dictionary>(StringComparer.OrdinalIgnoreCase);
+ cmdlets = new Dictionary(cachedCmdlets.Count, StringComparer.OrdinalIgnoreCase);
+ aliases = new Dictionary>(cachedCmdlets.Count, StringComparer.OrdinalIgnoreCase);
foreach (var pair in cachedCmdlets)
{
@@ -5051,7 +5045,6 @@ internal static void AnalyzePSSnapInAssembly(Assembly assembly, string name, PSS
}
string assemblyPath = assembly.Location;
- Type[] assemblyTypes;
if (cmdlets != null || providers != null)
{
if (!s_assembliesWithModuleInitializerCache.Value.ContainsKey(assembly))
@@ -5062,19 +5055,15 @@ internal static void AnalyzePSSnapInAssembly(Assembly assembly, string name, PSS
else
{
s_PSSnapInTracer.WriteLine("Executing IModuleAssemblyInitializer.Import for {0}", assemblyPath);
- assemblyTypes = GetAssemblyTypes(assembly, name);
- ExecuteModuleInitializer(assembly, assemblyTypes, isModuleLoad);
+ var assemblyTypes = GetAssemblyTypes(assembly, name);
+ ExecuteModuleInitializer(assembly, assemblyTypes);
return;
}
}
s_PSSnapInTracer.WriteLine("Analyzing assembly {0} for cmdlet and providers", assemblyPath);
-
helpFile = GetHelpFile(assemblyPath);
- Type randomCmdletToCheckLinkDemand = null;
- Type randomProviderToCheckLinkDemand = null;
-
if (psSnapInInfo != null && psSnapInInfo.Name.Equals(InitialSessionState.CoreSnapin, StringComparison.OrdinalIgnoreCase))
{
InitializeCoreCmdletsAndProviders(psSnapInInfo, out cmdlets, out providers, helpFile);
@@ -5086,13 +5075,10 @@ internal static void AnalyzePSSnapInAssembly(Assembly assembly, string name, PSS
Dictionary cmdletsCheck = null;
Dictionary providersCheck = null;
Dictionary> aliasesCheck = null;
- Type unused1 = null;
- Type unused2 = null;
- AnalyzeModuleAssemblyWithReflection(assembly, name, psSnapInInfo, moduleInfo, isModuleLoad,
- ref cmdletsCheck, ref aliasesCheck, ref providersCheck, helpFile, ref unused1, ref unused2);
+ AnalyzeModuleAssemblyWithReflection(assembly, name, psSnapInInfo, moduleInfo, helpFile, ref cmdletsCheck, ref aliasesCheck, ref providersCheck);
Diagnostics.Assert(aliasesCheck == null, "InitializeCoreCmdletsAndProviders assumes no aliases are defined in System.Management.Automation.dll");
- Diagnostics.Assert(providersCheck.Keys.Count == providers.Keys.Count, "new Provider added to System.Management.Automation.dll - update InitializeCoreCmdletsAndProviders");
+ Diagnostics.Assert(providersCheck.Count == providers.Count, "new Provider added to System.Management.Automation.dll - update InitializeCoreCmdletsAndProviders");
foreach (var pair in providersCheck)
{
SessionStateProviderEntry other;
@@ -5110,7 +5096,7 @@ internal static void AnalyzePSSnapInAssembly(Assembly assembly, string name, PSS
Diagnostics.Assert(false, "Missing provider: " + pair.Key);
}
}
- Diagnostics.Assert(cmdletsCheck.Keys.Count == cmdlets.Keys.Count, "new Cmdlet added to System.Management.Automation.dll - update InitializeCoreCmdletsAndProviders");
+ Diagnostics.Assert(cmdletsCheck.Count == cmdlets.Count, "new Cmdlet added to System.Management.Automation.dll - update InitializeCoreCmdletsAndProviders");
foreach (var pair in cmdletsCheck)
{
@@ -5133,39 +5119,7 @@ internal static void AnalyzePSSnapInAssembly(Assembly assembly, string name, PSS
}
else
{
- AnalyzeModuleAssemblyWithReflection(assembly, name, psSnapInInfo, moduleInfo, isModuleLoad,
- ref cmdlets, ref aliases, ref providers, helpFile, ref randomCmdletToCheckLinkDemand, ref randomProviderToCheckLinkDemand);
- }
-
- // force a LinkDemand check to get an explicit exception if
- // Cmdlet[Provider]Attributes are silently swallowed by Type.GetCustomAttributes
- // bug Win7:705573
- if ((providers == null || providers.Count == 0) && (cmdlets == null || cmdlets.Count == 0))
- {
- try
- {
- if (randomCmdletToCheckLinkDemand != null)
- {
- ConstructorInfo constructor = randomCmdletToCheckLinkDemand.GetConstructor(PSTypeExtensions.EmptyTypes);
- if (constructor != null)
- {
- constructor.Invoke(null); // this is how we artificially force a LinkDemand check
- }
- }
-
- if (randomProviderToCheckLinkDemand != null)
- {
- ConstructorInfo constructor = randomProviderToCheckLinkDemand.GetConstructor(PSTypeExtensions.EmptyTypes);
- if (constructor != null)
- {
- constructor.Invoke(null); // this is how we artificially force a LinkDemand check
- }
- }
- }
- catch (TargetInvocationException e)
- {
- throw e.InnerException;
- }
+ AnalyzeModuleAssemblyWithReflection(assembly, name, psSnapInInfo, moduleInfo, helpFile, ref cmdlets, ref aliases, ref providers);
}
// Cache the cmdlet and provider info for this assembly...
@@ -5208,122 +5162,86 @@ internal static void AnalyzePSSnapInAssembly(Assembly assembly, string name, PSS
}
private static void AnalyzeModuleAssemblyWithReflection(Assembly assembly, string name, PSSnapInInfo psSnapInInfo,
- PSModuleInfo moduleInfo, bool isModuleLoad,
+ PSModuleInfo moduleInfo, string helpFile,
ref Dictionary cmdlets,
ref Dictionary> aliases,
- ref Dictionary providers,
- string helpFile,
- ref Type randomCmdletToCheckLinkDemand,
- ref Type randomProviderToCheckLinkDemand)
+ ref Dictionary providers)
{
var assemblyTypes = GetAssemblyTypes(assembly, name);
-
- ExecuteModuleInitializer(assembly, assemblyTypes, isModuleLoad);
+ ExecuteModuleInitializer(assembly, assemblyTypes);
foreach (Type type in assemblyTypes)
{
- if (!(type.IsPublic || type.IsNestedPublic) || type.IsAbstract)
- continue;
+ if (!HasDefaultConstructor(type)) { continue; }
// Check for cmdlets
- if (IsCmdletClass(type) && HasDefaultConstructor(type))
+ if (IsCmdletClass(type) && TryGetCustomAttribute(type, out CmdletAttribute cmdletAttribute))
{
- randomCmdletToCheckLinkDemand = type;
-
- CmdletAttribute cmdletAttribute = GetCustomAttribute(type);
- if (cmdletAttribute == null)
- {
- continue;
- }
- string cmdletName = GetCmdletName(cmdletAttribute);
- if (string.IsNullOrEmpty(cmdletName))
+ if (TryGetCustomAttribute(type, out ExperimentalAttribute expAttribute) && expAttribute.ToHide)
{
+ // If 'ExperimentalAttribute' is specified on the cmdlet type and the
+ // effective action at run time is 'Hide', then we ignore the type.
continue;
}
+ string cmdletName = cmdletAttribute.VerbName + "-" + cmdletAttribute.NounName;
if (cmdlets != null && cmdlets.ContainsKey(cmdletName))
{
string message = StringUtil.Format(ConsoleInfoErrorStrings.PSSnapInDuplicateCmdlets, cmdletName, name);
-
s_PSSnapInTracer.TraceError(message);
-
throw new PSSnapInException(name, message);
}
SessionStateCmdletEntry cmdlet = new SessionStateCmdletEntry(cmdletName, type, helpFile);
- cmdlet.SetPSSnapIn(psSnapInInfo);
- if (cmdlets == null)
- {
- cmdlets = new Dictionary(StringComparer.OrdinalIgnoreCase);
- }
+ if (psSnapInInfo != null) { cmdlet.SetPSSnapIn(psSnapInInfo); }
+ if (moduleInfo != null) { cmdlet.SetModule(moduleInfo); }
+
+ cmdlets = cmdlets ?? new Dictionary(StringComparer.OrdinalIgnoreCase);
cmdlets.Add(cmdletName, cmdlet);
- var aliasAttribute = GetCustomAttribute(type);
- if (aliasAttribute != null)
+ if (TryGetCustomAttribute(type, out AliasAttribute aliasAttribute))
{
- if (aliases == null)
- {
- aliases = new Dictionary>(StringComparer.OrdinalIgnoreCase);
- }
+ aliases = aliases ?? new Dictionary>(StringComparer.OrdinalIgnoreCase);
var aliasList = new List();
foreach (var alias in aliasAttribute.AliasNames)
{
- // Alias declared by AliasAttribute is set with the option 'ScopedItemOptions.None',
- // because we believe a user of the cmdlet, instead of the author of it,
- // should be the one to decide the option
- // ('ScopedItemOptions.ReadOnly' and/or 'ScopedItemOptions.AllScopes') of the alias usage."
- var aliasEntry = new SessionStateAliasEntry(alias, cmdletName, string.Empty, ScopedItemOptions.None);
- if (psSnapInInfo != null)
- {
- aliasEntry.SetPSSnapIn(psSnapInInfo);
- }
+ // Alias declared by 'AliasAttribute' is set with the option 'ScopedItemOptions.None', because we believe
+ // the users of the cmdlet, instead of the author, should have control of what options applied to an alias
+ // ('ScopedItemOptions.ReadOnly' and/or 'ScopedItemOptions.AllScopes').
+ var aliasEntry = new SessionStateAliasEntry(alias, cmdletName, description: string.Empty, ScopedItemOptions.None);
+ if (psSnapInInfo != null) { aliasEntry.SetPSSnapIn(psSnapInInfo); }
+ if (moduleInfo != null) { aliasEntry.SetModule(moduleInfo); }
aliasList.Add(aliasEntry);
}
aliases.Add(cmdletName, aliasList);
}
s_PSSnapInTracer.WriteLine("{0} from type {1} is added as a cmdlet. ", cmdletName, type.FullName);
- continue;
}
-
// Check for providers
- if (IsProviderClass(type) && HasDefaultConstructor(type))
+ else if (IsProviderClass(type) && TryGetCustomAttribute(type, out CmdletProviderAttribute providerAttribute))
{
- randomProviderToCheckLinkDemand = type;
-
- CmdletProviderAttribute providerAttribute = GetCustomAttribute(type);
- if (providerAttribute == null)
- {
- continue;
- }
- string providerName = GetProviderName(providerAttribute);
- if (string.IsNullOrEmpty(providerName))
+ if (TryGetCustomAttribute(type, out ExperimentalAttribute expAttribute) && expAttribute.ToHide)
{
+ // If 'ExperimentalAttribute' is specified on the provider type and
+ // the effective action at run time is 'Hide', then we ignore the type.
continue;
}
+ string providerName = providerAttribute.ProviderName;
if (providers != null && providers.ContainsKey(providerName))
{
string message = StringUtil.Format(ConsoleInfoErrorStrings.PSSnapInDuplicateProviders, providerName, psSnapInInfo.Name);
-
s_PSSnapInTracer.TraceError(message);
-
throw new PSSnapInException(psSnapInInfo.Name, message);
}
SessionStateProviderEntry provider = new SessionStateProviderEntry(providerName, type, helpFile);
- provider.SetPSSnapIn(psSnapInInfo);
+ if (psSnapInInfo != null) { provider.SetPSSnapIn(psSnapInInfo); }
+ if (moduleInfo != null) { provider.SetModule(moduleInfo); }
- // After converting core snapins to load as modules, the providers will have Module property populated
- if (moduleInfo != null)
- {
- provider.SetModule(moduleInfo);
- }
- if (providers == null)
- {
- providers = new Dictionary(StringComparer.OrdinalIgnoreCase);
- }
+ providers = providers ?? new Dictionary(StringComparer.OrdinalIgnoreCase);
providers.Add(providerName, provider);
s_PSSnapInTracer.WriteLine("{0} from type {1} is added as a provider. ", providerName, type.FullName);
@@ -5367,6 +5285,7 @@ private static void InitializeCoreCmdletsAndProviders(
{"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) },
@@ -5431,39 +5350,29 @@ private static void InitializeCoreCmdletsAndProviders(
}
}
- private static void ExecuteModuleInitializer(Assembly assembly, Type[] assemblyTypes, bool isModuleLoad)
+ private static void ExecuteModuleInitializer(Assembly assembly, IEnumerable assemblyTypes)
{
- for (int i = 0; i < assemblyTypes.Length; i++)
+ foreach (Type type in assemblyTypes)
{
- Type type = assemblyTypes[i];
- if (!(type.IsPublic || type.IsNestedPublic) || type.IsAbstract) { continue; }
-
- if (isModuleLoad && typeof(IModuleAssemblyInitializer).IsAssignableFrom(type) && type != typeof(IModuleAssemblyInitializer))
+ if (typeof(IModuleAssemblyInitializer).IsAssignableFrom(type))
{
s_assembliesWithModuleInitializerCache.Value[assembly] = true;
- IModuleAssemblyInitializer moduleInitializer = (IModuleAssemblyInitializer)Activator.CreateInstance(type, true);
+ var moduleInitializer = (IModuleAssemblyInitializer)Activator.CreateInstance(type, true);
moduleInitializer.OnImport();
}
}
}
- internal static Type[] GetAssemblyTypes(Assembly assembly, string name)
+ internal static IEnumerable GetAssemblyTypes(Assembly assembly, string name)
{
- Type[] assemblyTypes = null;
-
try
{
- var exportedTypes = assembly.ExportedTypes;
- assemblyTypes = exportedTypes as Type[] ?? exportedTypes.ToArray();
+ // Return types that are public, non-abstract, non-interface and non-valueType.
+ return assembly.ExportedTypes.Where(t => !t.IsAbstract && !t.IsInterface && !t.IsValueType);
}
catch (ReflectionTypeLoadException e)
{
- string message;
-
- message = e.Message;
-
- message += "\nLoader Exceptions: \n";
-
+ string message = e.Message + "\nLoader Exceptions: \n";
if (e.LoaderExceptions != null)
{
foreach (Exception exception in e.LoaderExceptions)
@@ -5473,10 +5382,8 @@ internal static Type[] GetAssemblyTypes(Assembly assembly, string name)
}
s_PSSnapInTracer.TraceError(message);
-
throw new PSSnapInException(name, message);
}
- return assemblyTypes;
}
// cmdletCache holds the list of cmdlets along with its aliases per each assembly.
@@ -5487,51 +5394,25 @@ internal static Type[] GetAssemblyTypes(Assembly assembly, string name)
// Using a ConcurrentDictionary for this so that we can avoid having a private lock variable. We use only the keys for checking.
private static Lazy> s_assembliesWithModuleInitializerCache = new Lazy>();
- private static string GetCmdletName(CmdletAttribute cmdletAttribute)
- {
- string verb = cmdletAttribute.VerbName;
-
- string noun = cmdletAttribute.NounName;
-
- return verb + "-" + noun;
- }
-
- private static string GetProviderName(CmdletProviderAttribute providerAttribute)
- {
- return providerAttribute.ProviderName;
- }
-
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsCmdletClass(Type type)
{
- if (type == null)
- return false;
-
return type.IsSubclassOf(typeof(System.Management.Automation.Cmdlet));
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsProviderClass(Type type)
{
- if (type == null)
- return false;
-
return type.IsSubclassOf(typeof(System.Management.Automation.Provider.CmdletProvider));
}
- internal static bool IsModuleAssemblyInitializerClass(Type type)
- {
- if (type == null)
- {
- return false;
- }
-
- return type.IsSubclassOf(typeof(System.Management.Automation.IModuleAssemblyInitializer));
- }
-
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool HasDefaultConstructor(Type type)
{
return !(type.GetConstructor(PSTypeExtensions.EmptyTypes) == null);
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static string GetHelpFile(string assemblyPath)
{
// Help files exist only for original module assemblies, not for generated Ngen binaries
diff --git a/src/System.Management.Automation/engine/Modules/AnalysisCache.cs b/src/System.Management.Automation/engine/Modules/AnalysisCache.cs
index 48e9fb15774..b39d52488ed 100644
--- a/src/System.Management.Automation/engine/Modules/AnalysisCache.cs
+++ b/src/System.Management.Automation/engine/Modules/AnalysisCache.cs
@@ -5,10 +5,12 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
+using System.Globalization;
using System.IO;
using System.Management.Automation.Internal;
using System.Management.Automation.Language;
using System.Management.Automation.Runspaces;
+using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
@@ -1025,12 +1027,53 @@ private AnalysisCacheData()
static AnalysisCacheData()
{
- s_cacheStoreLocation =
- Environment.GetEnvironmentVariable("PSModuleAnalysisCachePath") ??
+ // If user defines a custom cache path, then use that.
+ string userDefinedCachePath = Environment.GetEnvironmentVariable("PSModuleAnalysisCachePath");
+ if (!string.IsNullOrEmpty(userDefinedCachePath))
+ {
+ s_cacheStoreLocation = userDefinedCachePath;
+ return;
+ }
+
+ string cacheFileName = "ModuleAnalysisCache";
+ if (ExperimentalFeature.EnabledExperimentalFeatureNames.Count > 0)
+ {
+ // If any experimental features are enabled, we cannot use the default cache file because those
+ // features may expose commands that are not available in a regular powershell session, and we
+ // should not cache those commands in the default cache file because that will result in wrong
+ // auto-completion suggestions when the default cache file is used in another powershell session.
+ //
+ // Here we will generate a cache file name that represent the combination of enabled feature names.
+ // We first convert enabled feature names to lower case, then we sort the feature names, and then
+ // compute an SHA1 hash from the sorted feature names. We will use a short SHA name (first 8 chars)
+ // to generate the cache file name.
+ int index = 0;
+ string[] featureNames = new string[ExperimentalFeature.EnabledExperimentalFeatureNames.Count];
+ foreach (string featureName in ExperimentalFeature.EnabledExperimentalFeatureNames)
+ {
+ featureNames[index++] = featureName.ToLowerInvariant();
+ }
+
+ Array.Sort(featureNames);
+ string allNames = string.Join(Environment.NewLine, featureNames);
+
+ // Use SHA1 because it's faster.
+ // It's very unlikely to get collision from hashing the combinations of enabled features names.
+ byte[] hashBytes;
+ using (var sha1 = SHA1.Create())
+ {
+ hashBytes = sha1.ComputeHash(Encoding.UTF8.GetBytes(allNames));
+ }
+
+ // Use the first 8 characters of the hash string for a short SHA.
+ string stringVal = BitConverter.ToString(hashBytes, startIndex: 0, length: 4).Replace("-", String.Empty);
+ cacheFileName = String.Format(CultureInfo.InvariantCulture, "{0}-{1}", cacheFileName, stringVal);
+ }
+
#if UNIX
- Path.Combine(Platform.SelectProductNameForDirectory(Platform.XDG_Type.CACHE), "ModuleAnalysisCache");
+ s_cacheStoreLocation = Path.Combine(Platform.SelectProductNameForDirectory(Platform.XDG_Type.CACHE), cacheFileName);
#else
- Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"Microsoft\PowerShell\ModuleAnalysisCache");
+ s_cacheStoreLocation = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"Microsoft\PowerShell", cacheFileName);
#endif
}
}
@@ -1044,4 +1087,4 @@ internal class ModuleCacheEntry
public ConcurrentDictionary Commands;
public ConcurrentDictionary Types;
}
-} // System.Management.Automation
+}
diff --git a/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs b/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs
index 9d019f95b0d..f7d2fc73c22 100644
--- a/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs
+++ b/src/System.Management.Automation/engine/Modules/ModuleCmdletBase.cs
@@ -418,7 +418,7 @@ private Hashtable LoadModuleManifestData(
///
/// Extra variables that are allowed to be referenced in module manifest file
///
- private static readonly string[] s_extraAllowedVariables = new string[] { "PSScriptRoot", "PSEdition" };
+ private static readonly string[] s_extraAllowedVariables = new string[] { SpecialVariables.PSScriptRoot, SpecialVariables.PSEdition, SpecialVariables.EnabledExperimentalFeatures };
///
/// Load and execute the manifest psd1 file or a localized manifest psd1 file.
@@ -2027,12 +2027,73 @@ internal PSModuleInfo LoadModuleManifest(
Array.Clear(tmpNestedModules, 0, tmpNestedModules.Length);
}
- // Set the private data member for the module if the manifest contains
- // this member
- object privateData = null;
- if (data.Contains("PrivateData"))
+ // Set the private data member for the module if the manifest contains this member
+ object privateData = data["PrivateData"];
+
+ // Validate the 'ExperimentalFeatures' member of the manifest
+ List expFeatureList = null;
+ if (privateData is Hashtable hashData && hashData["PSData"] is Hashtable psData)
{
- privateData = data["PrivateData"];
+ if (!GetScalarFromData(psData, moduleManifestPath, "ExperimentalFeatures", manifestProcessingFlags, out Hashtable[] features))
+ {
+ containedErrors = true;
+ if (bailOnFirstError) return null;
+ }
+
+ if (features != null && features.Length > 0)
+ {
+ bool nameMissingOrEmpty = false;
+ var invalidNames = new List();
+ string moduleName = ModuleIntrinsics.GetModuleName(moduleManifestPath);
+ expFeatureList = new List(features.Length);
+
+ foreach (Hashtable feature in features)
+ {
+ string featureName = feature["Name"] as string;
+ if (string.IsNullOrEmpty(featureName))
+ {
+ nameMissingOrEmpty = true;
+ }
+ else if (ExperimentalFeature.IsModuleFeatureName(featureName, moduleName))
+ {
+ string featureDescription = feature["Description"] as string;
+ expFeatureList.Add(new ExperimentalFeature(featureName, featureDescription, moduleManifestPath,
+ ExperimentalFeature.IsEnabled(featureName)));
+ }
+ else
+ {
+ invalidNames.Add(featureName);
+ }
+ }
+
+ if (nameMissingOrEmpty)
+ {
+ if (writingErrors)
+ {
+ WriteError(new ErrorRecord(new ArgumentException(Modules.ExperimentalFeatureNameMissingOrEmpty),
+ "Modules_ExperimentalFeatureNameMissingOrEmpty",
+ ErrorCategory.InvalidData, null));
+ }
+
+ containedErrors = true;
+ if (bailOnFirstError) { return null; }
+ }
+
+ if (invalidNames.Count > 0)
+ {
+ if (writingErrors)
+ {
+ string invalidNameStr = String.Join(", ", invalidNames);
+ string errorMsg = StringUtil.Format(Modules.InvalidExperimentalFeatureName, invalidNameStr);
+ WriteError(new ErrorRecord(new ArgumentException(errorMsg),
+ "Modules_InvalidExperimentalFeatureName",
+ ErrorCategory.InvalidData, null));
+ }
+
+ containedErrors = true;
+ if (bailOnFirstError) { return null; }
+ }
+ }
}
// Process all of the exports...
@@ -2413,6 +2474,12 @@ internal PSModuleInfo LoadModuleManifest(
manifestInfo.PowerShellVersion = powerShellVersion;
manifestInfo.ProcessorArchitecture = requiredProcessorArchitecture;
manifestInfo.Prefix = resolvedCommandPrefix;
+
+ if (expFeatureList != null)
+ {
+ manifestInfo.ExperimentalFeatures = new ReadOnlyCollection(expFeatureList);
+ }
+
if (assemblyList != null)
{
foreach (var a in assemblyList)
@@ -3009,6 +3076,7 @@ internal PSModuleInfo LoadModuleManifest(
newManifestInfo.LicenseUri = manifestInfo.LicenseUri;
newManifestInfo.IconUri = manifestInfo.IconUri;
newManifestInfo.RepositorySourceLocation = manifestInfo.RepositorySourceLocation;
+ newManifestInfo.ExperimentalFeatures = manifestInfo.ExperimentalFeatures;
// If we are in module discovery, then fix the path.
if (ss == null)
@@ -4672,7 +4740,7 @@ internal void RemoveModule(PSModuleInfo module, string moduleNameInRemoveModuleC
var exportedTypes = PSSnapInHelpers.GetAssemblyTypes(module.ImplementingAssembly, module.Name);
foreach (var type in exportedTypes)
{
- if (typeof(IModuleAssemblyCleanup).IsAssignableFrom(type) && type != typeof(IModuleAssemblyCleanup))
+ if (typeof(IModuleAssemblyCleanup).IsAssignableFrom(type))
{
var moduleCleanup = (IModuleAssemblyCleanup)Activator.CreateInstance(type, true);
moduleCleanup.OnRemove(module);
diff --git a/src/System.Management.Automation/engine/Modules/ModuleIntrinsics.cs b/src/System.Management.Automation/engine/Modules/ModuleIntrinsics.cs
index 8451daa80f1..d2187f6aca8 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,62 +415,90 @@ internal static bool IsModuleMatchingModuleSpec(PSModuleInfo moduleInfo, ModuleS
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
+ Hashtable dataFileSetting =
+ PsUtils.GetModuleManifestProperties(
+ manifestPath,
+ PsUtils.ManifestGuidPropertyName);
+
+ object guidValue = dataFileSetting["GUID"];
+ if (guidValue != null)
{
- var dataFileSetting =
- PsUtils.GetModuleManifestProperties(
- manifestPath,
- PsUtils.ManifestGuidPropertyName);
+ Guid guidID;
+ if (LanguagePrimitives.TryConvertTo(guidValue, out guidID))
+ {
+ return guidID;
+ }
+ }
+ }
+ catch (PSInvalidOperationException) { }
- var guidValue = dataFileSetting["GUID"];
- if (guidValue != null)
+ 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)
{
- Guid guidID;
- if (LanguagePrimitives.TryConvertTo(guidValue, out guidID))
+ string moduleName = ModuleIntrinsics.GetModuleName(manifestPath);
+ var expFeatureList = new List();
+ foreach (Hashtable feature in features)
{
- return guidID;
+ 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.IsEnabled(featureName)));
+ }
}
+ return expFeatureList.ToArray();
}
}
- catch (PSInvalidOperationException)
- {
- }
}
+ catch (PSInvalidOperationException) { }
- return new Guid();
+ 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.
diff --git a/src/System.Management.Automation/engine/Modules/PSModuleInfo.cs b/src/System.Management.Automation/engine/Modules/PSModuleInfo.cs
index d0e13d9a6ad..3bf5c0d9a07 100644
--- a/src/System.Management.Automation/engine/Modules/PSModuleInfo.cs
+++ b/src/System.Management.Automation/engine/Modules/PSModuleInfo.cs
@@ -301,49 +301,37 @@ private void SetPSDataPropertiesFromPrivateData()
ProjectUri = null;
IconUri = null;
- var privateDataHashTable = _privateData as Hashtable;
- if (privateDataHashTable != null)
+ if (_privateData is Hashtable hashData && hashData["PSData"] is Hashtable psData)
{
- var psData = privateDataHashTable["PSData"] as Hashtable;
- if (psData != null)
+ var tagsValue = psData["Tags"];
+ if (tagsValue is object[] tags && tags.Length > 0)
{
- object tagsValue = psData["Tags"];
- if (tagsValue != null)
+ foreach (var tagString in tags.OfType())
{
- var tags = tagsValue as object[];
- if (tags != null && tags.Any())
- {
- foreach (var tagString in tags.OfType())
- {
- AddToTags(tagString);
- }
- }
- else
- {
- AddToTags(tagsValue.ToString());
- }
- }
-
- var licenseUri = psData["LicenseUri"] as string;
- if (licenseUri != null)
- {
- LicenseUri = GetUriFromString(licenseUri);
+ AddToTags(tagString);
}
+ }
+ else if (tagsValue is string tag)
+ {
+ AddToTags(tag);
+ }
- var projectUri = psData["ProjectUri"] as string;
- if (projectUri != null)
- {
- ProjectUri = GetUriFromString(projectUri);
- }
+ if (psData["LicenseUri"] is string licenseUri)
+ {
+ LicenseUri = GetUriFromString(licenseUri);
+ }
- var iconUri = psData["IconUri"] as string;
- if (iconUri != null)
- {
- IconUri = GetUriFromString(iconUri);
- }
+ if (psData["ProjectUri"] is string projectUri)
+ {
+ ProjectUri = GetUriFromString(projectUri);
+ }
- ReleaseNotes = psData["ReleaseNotes"] as string;
+ if (psData["IconUri"] is string iconUri)
+ {
+ IconUri = GetUriFromString(iconUri);
}
+
+ ReleaseNotes = psData["ReleaseNotes"] as string;
}
}
@@ -360,6 +348,11 @@ private static Uri GetUriFromString(string uriString)
return uri;
}
+ ///
+ /// Get the experimental features declared in this module.
+ ///
+ public IEnumerable ExperimentalFeatures { get; internal set; } = Utils.EmptyReadOnlyCollection();
+
///
/// Tags of this module.
///
diff --git a/src/System.Management.Automation/engine/PSConfiguration.cs b/src/System.Management.Automation/engine/PSConfiguration.cs
index 18d7814a2ff..7f3597f8405 100644
--- a/src/System.Management.Automation/engine/PSConfiguration.cs
+++ b/src/System.Management.Automation/engine/PSConfiguration.cs
@@ -193,6 +193,14 @@ internal void SetDisablePromptToUpdateHelp(bool prompt)
WriteValueToFile(ConfigScope.SystemWide, "DisablePromptToUpdateHelp", prompt);
}
+ ///
+ /// Get the names of experimental features enabled in the config file.
+ ///
+ internal string[] GetExperimentalFeatures()
+ {
+ return ReadValueFromFile(ConfigScope.SystemWide, "ExperimentalFeatures", Utils.EmptyArray());
+ }
+
///
/// Corresponding settings of the original Group Policies
///
diff --git a/src/System.Management.Automation/engine/PseudoParameters.cs b/src/System.Management.Automation/engine/PseudoParameters.cs
index 50b8ff4ef2e..06d4a0cca67 100644
--- a/src/System.Management.Automation/engine/PseudoParameters.cs
+++ b/src/System.Management.Automation/engine/PseudoParameters.cs
@@ -164,7 +164,36 @@ public object Value
/// This can be any attribute that can be applied to a normal parameter.
///
public Collection Attributes { get; } = new Collection();
- } // class RuntimeDefinedParameter
+
+ ///
+ /// Check if the parameter is disabled due to the associated experimental feature.
+ ///
+ internal bool IsDisabled()
+ {
+ bool hasParameterAttribute = false;
+ bool hasEnabledParamAttribute = false;
+ bool hasSeenExpAttribute = false;
+
+ foreach (Attribute attr in Attributes)
+ {
+ if (!hasSeenExpAttribute && attr is ExperimentalAttribute expAttribute)
+ {
+ if (expAttribute.ToHide) { return true; }
+ hasSeenExpAttribute = true;
+ }
+ else if (attr is ParameterAttribute paramAttribute)
+ {
+ hasParameterAttribute = true;
+ if (paramAttribute.ToHide) { continue; }
+ hasEnabledParamAttribute = true;
+ }
+ }
+
+ // If one or more parameter attributes are declared but none is enabled,
+ // then we consider the parameter is disabled.
+ return hasParameterAttribute && !hasEnabledParamAttribute;
+ }
+ }
///
/// Represents a collection of runtime-defined parameters that are keyed based on the name
diff --git a/src/System.Management.Automation/engine/ScriptCommandProcessor.cs b/src/System.Management.Automation/engine/ScriptCommandProcessor.cs
index e14f0616f7a..2ba86c67254 100644
--- a/src/System.Management.Automation/engine/ScriptCommandProcessor.cs
+++ b/src/System.Management.Automation/engine/ScriptCommandProcessor.cs
@@ -17,10 +17,9 @@ namespace System.Management.Automation
internal abstract class ScriptCommandProcessorBase : CommandProcessorBase
{
protected ScriptCommandProcessorBase(ScriptBlock scriptBlock, ExecutionContext context, bool useLocalScope, CommandOrigin origin, SessionStateInternal sessionState)
+ : base(new ScriptInfo(String.Empty, scriptBlock, context))
{
this._dontUseScopeCommandOrigin = false;
- this.CommandInfo = new ScriptInfo(String.Empty, scriptBlock, context);
-
this._fromScriptFile = false;
CommonInitialization(scriptBlock, context, useLocalScope, origin, sessionState);
diff --git a/src/System.Management.Automation/engine/SessionState.cs b/src/System.Management.Automation/engine/SessionState.cs
index 516828c743d..64a4838a63b 100644
--- a/src/System.Management.Automation/engine/SessionState.cs
+++ b/src/System.Management.Automation/engine/SessionState.cs
@@ -301,7 +301,7 @@ internal void InitializeFixedVariables()
ExecutionContext.EngineHostInterface,
ScopedItemOptions.Constant | ScopedItemOptions.AllScope,
RunspaceInit.PSHostDescription);
- this.GlobalScope.SetVariable(v.Name, v, false, true, this, CommandOrigin.Internal, fastPath: true);
+ this.GlobalScope.SetVariable(v.Name, v, asValue: false, force: true, this, CommandOrigin.Internal, fastPath: true);
// $HOME - indicate where a user's home directory is located in the file system.
// -- %USERPROFILE% on windows
@@ -311,28 +311,28 @@ internal void InitializeFixedVariables()
home,
ScopedItemOptions.ReadOnly | ScopedItemOptions.AllScope,
RunspaceInit.HOMEDescription);
- this.GlobalScope.SetVariable(v.Name, v, false, true, this, CommandOrigin.Internal, fastPath: true);
+ this.GlobalScope.SetVariable(v.Name, v, asValue: false, force: true, this, CommandOrigin.Internal, fastPath: true);
// $ExecutionContext
v = new PSVariable(SpecialVariables.ExecutionContext,
ExecutionContext.EngineIntrinsics,
ScopedItemOptions.Constant | ScopedItemOptions.AllScope,
RunspaceInit.ExecutionContextDescription);
- this.GlobalScope.SetVariable(v.Name, v, false, true, this, CommandOrigin.Internal, fastPath: true);
+ this.GlobalScope.SetVariable(v.Name, v, asValue: false, force: true, this, CommandOrigin.Internal, fastPath: true);
// $PSVersionTable
v = new PSVariable(SpecialVariables.PSVersionTable,
PSVersionInfo.GetPSVersionTable(),
ScopedItemOptions.Constant | ScopedItemOptions.AllScope,
RunspaceInit.PSVersionTableDescription);
- this.GlobalScope.SetVariable(v.Name, v, false, true, this, CommandOrigin.Internal, fastPath: true);
+ this.GlobalScope.SetVariable(v.Name, v, asValue: false, force: true, this, CommandOrigin.Internal, fastPath: true);
// $PSEdition
v = new PSVariable(SpecialVariables.PSEdition,
PSVersionInfo.PSEditionValue,
ScopedItemOptions.Constant | ScopedItemOptions.AllScope,
RunspaceInit.PSEditionDescription);
- this.GlobalScope.SetVariable(v.Name, v, false, true, this, CommandOrigin.Internal, fastPath: true);
+ this.GlobalScope.SetVariable(v.Name, v, asValue: false, force: true, this, CommandOrigin.Internal, fastPath: true);
// $PID
Process currentProcess = Process.GetCurrentProcess();
@@ -341,15 +341,15 @@ internal void InitializeFixedVariables()
currentProcess.Id,
ScopedItemOptions.Constant | ScopedItemOptions.AllScope,
RunspaceInit.PIDDescription);
- this.GlobalScope.SetVariable(v.Name, v, false, true, this, CommandOrigin.Internal, fastPath: true);
+ this.GlobalScope.SetVariable(v.Name, v, asValue: false, force: true, this, CommandOrigin.Internal, fastPath: true);
// $PSCulture
v = new PSCultureVariable();
- this.GlobalScope.SetVariable(v.Name, v, false, true, this, CommandOrigin.Internal, fastPath: true);
+ this.GlobalScope.SetVariable(v.Name, v, asValue: false, force: true, this, CommandOrigin.Internal, fastPath: true);
// $PSUICulture
v = new PSUICultureVariable();
- this.GlobalScope.SetVariable(v.Name, v, false, true, this, CommandOrigin.Internal, fastPath: true);
+ this.GlobalScope.SetVariable(v.Name, v, asValue: false, force: true, this, CommandOrigin.Internal, fastPath: true);
// $?
v = new QuestionMarkVariable(this.ExecutionContext);
@@ -357,28 +357,24 @@ internal void InitializeFixedVariables()
// $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);
- this.GlobalScope.SetVariable(v.Name, v, false, true, this, CommandOrigin.Internal, fastPath: true);
+ this.GlobalScope.SetVariable(v.Name, v, asValue: false, force: true, this, CommandOrigin.Internal, fastPath: true);
// $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);
+ this.GlobalScope.SetVariable(v.Name, v, asValue: false, force: true, this, CommandOrigin.Internal, fastPath: true);
+
+ // $EnabledExperimentalFeatures
+ v = new PSVariable(SpecialVariables.EnabledExperimentalFeatures,
+ ExperimentalFeature.EnabledExperimentalFeatureNames,
+ ScopedItemOptions.Constant | ScopedItemOptions.AllScope,
+ RunspaceInit.EnabledExperimentalFeatures);
+ this.GlobalScope.SetVariable(v.Name, v, asValue: false, force: true, this, CommandOrigin.Internal, fastPath: true);
}
///
diff --git a/src/System.Management.Automation/engine/SpecialVariables.cs b/src/System.Management.Automation/engine/SpecialVariables.cs
index f1a2c32315a..c95689e5ba7 100644
--- a/src/System.Management.Automation/engine/SpecialVariables.cs
+++ b/src/System.Management.Automation/engine/SpecialVariables.cs
@@ -225,20 +225,7 @@ internal static class SpecialVariables
internal const string PSVersionTable = "PSVersionTable";
internal const string PSEdition = "PSEdition";
internal const string ShellId = "ShellId";
-
- internal static List AllScopeSessionVariables = new List
- {
- ExecutionContext,
- Home,
- Host,
- PID,
- PSCulture,
- PSHome,
- PSUICulture,
- PSVersionTable,
- PSEdition,
- ShellId
- };
+ internal const string EnabledExperimentalFeatures = "EnabledExperimentalFeatures";
#endregion AllScope variables created in every session
@@ -304,6 +291,7 @@ internal static class SpecialVariables
SpecialVariables.PSEdition,
SpecialVariables.ShellId,
SpecialVariables.True,
+ SpecialVariables.EnabledExperimentalFeatures,
};
private static readonly HashSet s_classMethodsAccessibleVariables = new HashSet
diff --git a/src/System.Management.Automation/engine/TypeMetadata.cs b/src/System.Management.Automation/engine/TypeMetadata.cs
index 4cf45a6dd05..b07501d7dd8 100644
--- a/src/System.Management.Automation/engine/TypeMetadata.cs
+++ b/src/System.Management.Automation/engine/TypeMetadata.cs
@@ -1308,10 +1308,12 @@ private void ConstructCompiledParametersUsingRuntimeDefinedParameters(
foreach (RuntimeDefinedParameter parameterDefinition in runtimeDefinedParameters.Values)
{
// Create the compiled parameter and add it to the bindable parameters collection
-
- // NTRAID#Windows Out Of Band Releases-926374-2005/12/22-JonN
- if (parameterDefinition == null)
- continue;
+ if (processingDynamicParameters)
+ {
+ // When processing dynamic parameters, parameter definitions come from the user,
+ // Invalid data could be passed in, or the parameter could be actually disabled.
+ if (parameterDefinition == null || parameterDefinition.IsDisabled()) { continue; }
+ }
CompiledCommandParameter parameter = new CompiledCommandParameter(parameterDefinition, processingDynamicParameters);
AddParameter(parameter, checkNames);
@@ -1379,7 +1381,6 @@ private void CheckForReservedParameter(string name)
}
}
- // NTRAID#Windows Out Of Band Releases-906345-2005/06/30-JeffJon
// This call verifies that the parameter is unique or
// can be deemed unique. If not, an exception is thrown.
// If it is unique (or deemed unique), then it is added
@@ -1458,7 +1459,6 @@ private void AddParameter(CompiledCommandParameter parameter, bool checkNames)
foreach (string alias in parameter.Aliases)
{
- // NTRAID#Windows Out Of Band Releases-917356-JonN
if (AliasedParameters.ContainsKey(alias))
{
throw new MetadataException(
@@ -1501,16 +1501,22 @@ private void RemoveParameter(CompiledCommandParameter parameter)
///
private static bool IsMemberAParameter(MemberInfo member)
{
- bool result = false;
-
try
{
- // MemberInfo.GetCustomAttributes returns IEnumerable in CoreCLR
- var attributes = member.GetCustomAttributes(typeof(ParameterAttribute), false);
- if (attributes.Any())
+ var expAttribute = member.GetCustomAttributes(inherit: false).FirstOrDefault();
+ if (expAttribute != null && expAttribute.ToHide) { return false; }
+
+ var hasAnyVisibleParamAttributes = false;
+ var paramAttributes = member.GetCustomAttributes(inherit: false);
+ foreach (var paramAttribute in paramAttributes)
{
- result = true;
+ if (!paramAttribute.ToHide)
+ {
+ hasAnyVisibleParamAttributes = true;
+ break;
+ }
}
+ return hasAnyVisibleParamAttributes;
}
catch (MetadataException metadataException)
{
@@ -1530,8 +1536,6 @@ private static bool IsMemberAParameter(MemberInfo member)
member.Name,
argumentException.Message);
}
-
- return result;
} // IsMemberAParameter
#endregion helper methods
diff --git a/src/System.Management.Automation/engine/parser/Compiler.cs b/src/System.Management.Automation/engine/parser/Compiler.cs
index 3426d326a9f..c6159b1fe85 100644
--- a/src/System.Management.Automation/engine/parser/Compiler.cs
+++ b/src/System.Management.Automation/engine/parser/Compiler.cs
@@ -733,6 +733,7 @@ static Compiler()
}
s_builtinAttributeGenerator.Add(typeof(CmdletBindingAttribute), NewCmdletBindingAttribute);
+ s_builtinAttributeGenerator.Add(typeof(ExperimentalAttribute), NewExperimentalAttribute);
s_builtinAttributeGenerator.Add(typeof(ParameterAttribute), NewParameterAttribute);
s_builtinAttributeGenerator.Add(typeof(OutputTypeAttribute), NewOutputTypeAttribute);
s_builtinAttributeGenerator.Add(typeof(AliasAttribute), NewAliasAttribute);
@@ -1000,24 +1001,26 @@ internal static Expression ConvertValue(Expression expr, List
internal static RuntimeDefinedParameterDictionary GetParameterMetaData(ReadOnlyCollection parameters, bool automaticPositions, ref bool usesCmdletBinding)
{
- var md = new RuntimeDefinedParameterDictionary();
- var listMd = new List();
+ var runtimeDefinedParamDict = new RuntimeDefinedParameterDictionary();
+ var runtimeDefinedParamList = new List(parameters.Count);
var customParameterSet = false;
for (int index = 0; index < parameters.Count; index++)
{
var param = parameters[index];
var rdp = GetRuntimeDefinedParameter(param, ref customParameterSet, ref usesCmdletBinding);
-
- listMd.Add(rdp);
- md.Add(param.Name.VariablePath.UserPath, rdp);
+ if (rdp != null)
+ {
+ runtimeDefinedParamList.Add(rdp);
+ runtimeDefinedParamDict.Add(param.Name.VariablePath.UserPath, rdp);
+ }
}
int pos = 0;
if (automaticPositions && !customParameterSet)
{
- for (int index = 0; index < listMd.Count; index++)
+ for (int index = 0; index < runtimeDefinedParamList.Count; index++)
{
- var rdp = listMd[index];
+ var rdp = runtimeDefinedParamList[index];
var paramAttribute = (ParameterAttribute)rdp.Attributes.First(attr => attr is ParameterAttribute);
if (!(rdp.ParameterType == typeof(SwitchParameter)))
{
@@ -1026,8 +1029,8 @@ internal static RuntimeDefinedParameterDictionary GetParameterMetaData(ReadOnlyC
}
}
- md.Data = listMd.ToArray();
- return md;
+ runtimeDefinedParamDict.Data = runtimeDefinedParamList.ToArray();
+ return runtimeDefinedParamDict;
}
private static readonly Dictionary s_attributeGeneratorCache = new Dictionary();
@@ -1067,6 +1070,9 @@ private static Delegate GetAttributeGenerator(CallInfo callInfo)
CallSite>.Create(PSConvertBinder.Get(typeof(ConfirmImpact)));
private static readonly CallSite> s_attrArgToRemotingCapabilityConverter =
CallSite>.Create(PSConvertBinder.Get(typeof(RemotingCapability)));
+ private static readonly CallSite> s_attrArgToExperimentActionConverter =
+ CallSite>.Create(PSConvertBinder.Get(typeof(ExperimentAction)));
+ private static readonly ConstantValueVisitor s_cvv = new ConstantValueVisitor { AttributeArgument = true };
private static void CheckNoPositionalArgs(AttributeAst ast)
{
@@ -1091,17 +1097,25 @@ private static void CheckNoNamedArgs(AttributeAst ast)
}
}
+ private static (string, ExperimentAction) GetFeatureNameAndAction(AttributeAst ast)
+ {
+ var argValue0 = ast.PositionalArguments[0].Accept(s_cvv);
+ var argValue1 = ast.PositionalArguments[1].Accept(s_cvv);
+
+ var featureName = _attrArgToStringConverter.Target(_attrArgToStringConverter, argValue0);
+ var action = s_attrArgToExperimentActionConverter.Target(s_attrArgToExperimentActionConverter, argValue1);
+ return (featureName, action);
+ }
+
private static Attribute NewCmdletBindingAttribute(AttributeAst ast)
{
CheckNoPositionalArgs(ast);
- var cvv = new ConstantValueVisitor { AttributeArgument = true };
-
var result = new CmdletBindingAttribute();
foreach (var namedArg in ast.NamedArguments)
{
- var argValue = namedArg.Argument.Accept(cvv);
+ var argValue = namedArg.Argument.Accept(s_cvv);
var argumentName = namedArg.ArgumentName;
if (argumentName.Equals("DefaultParameterSetName", StringComparison.OrdinalIgnoreCase))
{
@@ -1146,17 +1160,40 @@ private static Attribute NewCmdletBindingAttribute(AttributeAst ast)
return result;
}
- private static Attribute NewParameterAttribute(AttributeAst ast)
+ private static Attribute NewExperimentalAttribute(AttributeAst ast)
{
- CheckNoPositionalArgs(ast);
+ int positionalArgCount = ast.PositionalArguments.Count;
+ if (positionalArgCount != 2)
+ {
+ throw InterpreterError.NewInterpreterException(targetObject: null, typeof(MethodException), ast.Extent,
+ "MethodCountCouldNotFindBest", ExtendedTypeSystem.MethodArgumentCountException, ".ctor", positionalArgCount);
+ }
- var cvv = new ConstantValueVisitor { AttributeArgument = true };
+ (string name, ExperimentAction action) = GetFeatureNameAndAction(ast);
+ return new ExperimentalAttribute(name, action);
+ }
- var result = new ParameterAttribute();
+ private static Attribute NewParameterAttribute(AttributeAst ast)
+ {
+ ParameterAttribute result;
+ int positionalArgCount = ast.PositionalArguments.Count;
+ switch (positionalArgCount)
+ {
+ case 0:
+ result = new ParameterAttribute();
+ break;
+ case 2:
+ (string name, ExperimentAction action) = GetFeatureNameAndAction(ast);
+ result = new ParameterAttribute(name, action);
+ break;
+ default:
+ throw InterpreterError.NewInterpreterException(targetObject: null, typeof(MethodException), ast.Extent,
+ "MethodCountCouldNotFindBest", ExtendedTypeSystem.MethodArgumentCountException, ".ctor", positionalArgCount);
+ }
foreach (var namedArg in ast.NamedArguments)
{
- var argValue = namedArg.Argument.Accept(cvv);
+ var argValue = namedArg.Argument.Accept(s_cvv);
var argumentName = namedArg.ArgumentName;
if (argumentName.Equals("Position", StringComparison.OrdinalIgnoreCase))
@@ -1212,8 +1249,6 @@ private static Attribute NewParameterAttribute(AttributeAst ast)
private static Attribute NewOutputTypeAttribute(AttributeAst ast)
{
- var cvv = new ConstantValueVisitor { AttributeArgument = true };
-
OutputTypeAttribute result;
if (ast.PositionalArguments.Count == 0)
{
@@ -1229,7 +1264,7 @@ private static Attribute NewOutputTypeAttribute(AttributeAst ast)
}
else
{
- var argValue = ast.PositionalArguments[0].Accept(cvv);
+ var argValue = ast.PositionalArguments[0].Accept(s_cvv);
result = new OutputTypeAttribute(_attrArgToStringConverter.Target(_attrArgToStringConverter, argValue));
}
}
@@ -1242,7 +1277,7 @@ private static Attribute NewOutputTypeAttribute(AttributeAst ast)
var typeArg = positionalArgument as TypeExpressionAst;
args[i] = typeArg != null
? TypeOps.ResolveTypeName(typeArg.TypeName, typeArg.Extent)
- : positionalArgument.Accept(cvv);
+ : positionalArgument.Accept(s_cvv);
}
if (args[0] is Type)
@@ -1257,7 +1292,7 @@ private static Attribute NewOutputTypeAttribute(AttributeAst ast)
foreach (var namedArg in ast.NamedArguments)
{
- var argValue = namedArg.Argument.Accept(cvv);
+ var argValue = namedArg.Argument.Accept(s_cvv);
var argumentName = namedArg.ArgumentName;
if (argumentName.Equals("ParameterSetName", StringComparison.OrdinalIgnoreCase))
@@ -1283,12 +1318,11 @@ private static Attribute NewAliasAttribute(AttributeAst ast)
{
CheckNoNamedArgs(ast);
- var cvv = new ConstantValueVisitor { AttributeArgument = true };
var args = new string[ast.PositionalArguments.Count];
for (int i = 0; i < ast.PositionalArguments.Count; i++)
{
args[i] = _attrArgToStringConverter.Target(_attrArgToStringConverter,
- ast.PositionalArguments[i].Accept(cvv));
+ ast.PositionalArguments[i].Accept(s_cvv));
}
return new AliasAttribute(args);
}
@@ -1296,7 +1330,6 @@ private static Attribute NewAliasAttribute(AttributeAst ast)
private static Attribute NewValidateSetAttribute(AttributeAst ast)
{
ValidateSetAttribute result;
- var cvv = new ConstantValueVisitor { AttributeArgument = true };
// 'ValidateSet([CustomGeneratorType], IgnoreCase=$false)' is supported in scripts.
if (ast.PositionalArguments.Count == 1 && ast.PositionalArguments[0] is TypeExpressionAst generatorTypeAst)
@@ -1320,7 +1353,7 @@ private static Attribute NewValidateSetAttribute(AttributeAst ast)
for (int i = 0; i < ast.PositionalArguments.Count; i++)
{
args[i] = _attrArgToStringConverter.Target(_attrArgToStringConverter,
- ast.PositionalArguments[i].Accept(cvv));
+ ast.PositionalArguments[i].Accept(s_cvv));
}
result = new ValidateSetAttribute(args);
@@ -1328,7 +1361,7 @@ private static Attribute NewValidateSetAttribute(AttributeAst ast)
foreach (var namedArg in ast.NamedArguments)
{
- var argValue = namedArg.Argument.Accept(cvv);
+ var argValue = namedArg.Argument.Accept(s_cvv);
var argumentName = namedArg.ArgumentName;
if (argumentName.Equals("IgnoreCase", StringComparison.OrdinalIgnoreCase))
{
@@ -1393,17 +1426,16 @@ internal static Attribute GetAttribute(AttributeAst attributeAst)
var delegateArgs = new object[totalArgCount + 1];
delegateArgs[0] = attributeType;
- var cvv = new ConstantValueVisitor { AttributeArgument = true };
int i = 1;
for (int index = 0; index < attributeAst.PositionalArguments.Count; index++)
{
var posArg = attributeAst.PositionalArguments[index];
- delegateArgs[i++] = posArg.Accept(cvv);
+ delegateArgs[i++] = posArg.Accept(s_cvv);
}
for (int index = 0; index < attributeAst.NamedArguments.Count; index++)
{
var namedArg = attributeAst.NamedArguments[index];
- delegateArgs[i++] = namedArg.Argument.Accept(cvv);
+ delegateArgs[i++] = namedArg.Argument.Accept(s_cvv);
}
try
@@ -1447,28 +1479,47 @@ internal static Attribute GetAttribute(TypeConstraintAst typeConstraintAst)
private static RuntimeDefinedParameter GetRuntimeDefinedParameter(ParameterAst parameterAst, ref bool customParameterSet, ref bool usesCmdletBinding)
{
- List attributes = new List();
+ var attributes = new List(parameterAst.Attributes.Count);
bool hasParameterAttribute = false;
+ bool hasEnabledParamAttribute = false;
+ bool hasSeenExpAttribute = false;
+
for (int index = 0; index < parameterAst.Attributes.Count; index++)
{
var attributeAst = parameterAst.Attributes[index];
var attribute = attributeAst.GetAttribute();
- attributes.Add(attribute);
- var parameterAttribute = attribute as ParameterAttribute;
- if (parameterAttribute != null)
+ if (attribute is ExperimentalAttribute expAttribute)
+ {
+ // Only honor the first seen experimental attribute, ignore the others.
+ if (!hasSeenExpAttribute && expAttribute.ToHide) { return null; }
+
+ // Do not add experimental attributes to the attribute list.
+ hasSeenExpAttribute = true;
+ continue;
+ }
+ else if (attribute is ParameterAttribute paramAttribute)
{
hasParameterAttribute = true;
+ if (paramAttribute.ToHide) { continue; }
+
+ hasEnabledParamAttribute = true;
usesCmdletBinding = true;
- if (parameterAttribute.Position != int.MinValue ||
- !parameterAttribute.ParameterSetName.Equals(ParameterAttribute.AllParameterSets,
- StringComparison.OrdinalIgnoreCase))
+ if (paramAttribute.Position != int.MinValue ||
+ !paramAttribute.ParameterSetName.Equals(ParameterAttribute.AllParameterSets,
+ StringComparison.OrdinalIgnoreCase))
{
customParameterSet = true;
}
}
+
+ attributes.Add(attribute);
}
+ // If all 'ParameterAttribute' declared for the parameter are hidden due to
+ // an experimental feature, then the parameter should be ignored.
+ if (hasParameterAttribute && !hasEnabledParamAttribute) { return null; }
+
attributes.Reverse();
if (!hasParameterAttribute)
{
@@ -1476,7 +1527,7 @@ private static RuntimeDefinedParameter GetRuntimeDefinedParameter(ParameterAst p
}
var result = new RuntimeDefinedParameter(parameterAst.Name.VariablePath.UserPath, parameterAst.StaticType,
- new Collection(attributes.ToArray()));
+ new Collection(attributes));
if (parameterAst.DefaultValue != null)
{
diff --git a/src/System.Management.Automation/engine/parser/TypeResolver.cs b/src/System.Management.Automation/engine/parser/TypeResolver.cs
index e63a53d2eef..2be41133853 100644
--- a/src/System.Management.Automation/engine/parser/TypeResolver.cs
+++ b/src/System.Management.Automation/engine/parser/TypeResolver.cs
@@ -731,7 +731,10 @@ internal static class CoreTypes
{ typeof(DateTime), new[] { "datetime" } },
{ typeof(decimal), new[] { "decimal" } },
{ typeof(double), new[] { "double" } },
- { typeof(DscResourceAttribute), new[] { "DscResource"} },
+ { typeof(DscResourceAttribute), new[] { "DscResource" } },
+ { typeof(ExperimentAction), new[] { "ExperimentAction" } },
+ { typeof(ExperimentalAttribute), new[] { "Experimental" } },
+ { typeof(ExperimentalFeature), new[] { "ExperimentalFeature" } },
{ typeof(float), new[] { "float", "single" } },
{ typeof(Guid), new[] { "guid" } },
{ typeof(Hashtable), new[] { "hashtable" } },
diff --git a/src/System.Management.Automation/engine/parser/ast.cs b/src/System.Management.Automation/engine/parser/ast.cs
index 46cf2d9da31..10221af3a8b 100644
--- a/src/System.Management.Automation/engine/parser/ast.cs
+++ b/src/System.Management.Automation/engine/parser/ast.cs
@@ -55,6 +55,7 @@ internal interface IParameterMetadataProvider
bool HasAnyScriptBlockAttributes();
RuntimeDefinedParameterDictionary GetParameterMetadata(bool automaticPositions, ref bool usesCmdletBinding);
IEnumerable GetScriptBlockAttributes();
+ IEnumerable GetExperimentalAttributes();
bool UsesCmdletBinding();
ReadOnlyCollection Parameters { get; }
@@ -1370,6 +1371,64 @@ IEnumerable IParameterMetadataProvider.GetScriptBlockAttributes()
}
}
+ IEnumerable IParameterMetadataProvider.GetExperimentalAttributes()
+ {
+ for (int index = 0; index < Attributes.Count; index++)
+ {
+ AttributeAst attributeAst = Attributes[index];
+ ExperimentalAttribute expAttr = GetExpAttributeHelper(attributeAst);
+ if (expAttr != null) { yield return expAttr; }
+ }
+
+ if (ParamBlock != null)
+ {
+ for (int index = 0; index < ParamBlock.Attributes.Count; index++)
+ {
+ var attributeAst = ParamBlock.Attributes[index];
+ var expAttr = GetExpAttributeHelper(attributeAst);
+ if (expAttr != null) { yield return expAttr; }
+ }
+ }
+
+ ExperimentalAttribute GetExpAttributeHelper(AttributeAst attributeAst)
+ {
+ AttributeAst potentialExpAttr = null;
+ string expAttrTypeName = typeof(ExperimentalAttribute).FullName;
+ string attrAstTypeName = attributeAst.TypeName.Name;
+
+ if (TypeAccelerators.Get.TryGetValue(attrAstTypeName, out Type attrType) && attrType == typeof(ExperimentalAttribute))
+ {
+ potentialExpAttr = attributeAst;
+ }
+ else if (expAttrTypeName.EndsWith(attrAstTypeName, StringComparison.OrdinalIgnoreCase))
+ {
+ // Handle two cases:
+ // 1. declare the attribute using full type name;
+ // 2. declare the attribute using partial type name due to 'using namespace'.
+ int expAttrLength = expAttrTypeName.Length;
+ int attrAstLength = attrAstTypeName.Length;
+ if (expAttrLength == attrAstLength || expAttrTypeName[expAttrLength - attrAstLength - 1] == '.')
+ {
+ potentialExpAttr = attributeAst;
+ }
+ }
+
+ if (potentialExpAttr != null)
+ {
+ try
+ {
+ return Compiler.GetAttribute(potentialExpAttr) as ExperimentalAttribute;
+ }
+ catch (Exception)
+ {
+ // catch all and assume it's not a declaration of ExperimentalAttribute
+ }
+ }
+
+ return null;
+ }
+ }
+
ReadOnlyCollection IParameterMetadataProvider.Parameters
{
get { return (ParamBlock != null) ? this.ParamBlock.Parameters : null; }
@@ -3299,6 +3358,11 @@ IEnumerable IParameterMetadataProvider.GetScriptBlockAttributes()
return ((IParameterMetadataProvider)_functionDefinitionAst).GetScriptBlockAttributes();
}
+ IEnumerable IParameterMetadataProvider.GetExperimentalAttributes()
+ {
+ return ((IParameterMetadataProvider)_functionDefinitionAst).GetExperimentalAttributes();
+ }
+
bool IParameterMetadataProvider.UsesCmdletBinding()
{
return ((IParameterMetadataProvider)_functionDefinitionAst).UsesCmdletBinding();
@@ -3412,6 +3476,11 @@ public IEnumerable GetScriptBlockAttributes()
return ((IParameterMetadataProvider)Body).GetScriptBlockAttributes();
}
+ public IEnumerable GetExperimentalAttributes()
+ {
+ return ((IParameterMetadataProvider)Body).GetExperimentalAttributes();
+ }
+
public bool UsesCmdletBinding()
{
return false;
@@ -3693,6 +3762,11 @@ IEnumerable IParameterMetadataProvider.GetScriptBlockAttributes()
return ((IParameterMetadataProvider)Body).GetScriptBlockAttributes();
}
+ IEnumerable IParameterMetadataProvider.GetExperimentalAttributes()
+ {
+ return ((IParameterMetadataProvider)Body).GetExperimentalAttributes();
+ }
+
ReadOnlyCollection IParameterMetadataProvider.Parameters
{
get { return Parameters ?? (Body.ParamBlock != null ? Body.ParamBlock.Parameters : null); }
diff --git a/src/System.Management.Automation/engine/remoting/common/PSETWTracer.cs b/src/System.Management.Automation/engine/remoting/common/PSETWTracer.cs
index 05164306f5f..b898725769b 100644
--- a/src/System.Management.Automation/engine/remoting/common/PSETWTracer.cs
+++ b/src/System.Management.Automation/engine/remoting/common/PSETWTracer.cs
@@ -159,6 +159,10 @@ internal enum PSEventId : int
Settings = 0x1F04,
Engine_Trace = 0x1F06,
+ // Experimental Features
+ ExperimentalFeature_InvalidName = 0x3001,
+ ExperimentalFeature_ReadConfig_Error = 0x3002,
+
// Scheduled Jobs
ScheduledJob_Start = 0xD001,
ScheduledJob_Complete = 0xD002,
@@ -232,6 +236,7 @@ internal enum PSTask : int
ProviderStart = 0x68,
ProviderStop = 0x69,
ExecutePipeline = 0x6A,
+ ExperimentalFeature = 0x6B,
ScheduledJob = 0x6E,
NamedPipe = 0x6F,
ISEOperation = 0x78
diff --git a/src/System.Management.Automation/engine/runtime/CompiledScriptBlock.cs b/src/System.Management.Automation/engine/runtime/CompiledScriptBlock.cs
index 47e7c3ff2fd..72e223a873a 100644
--- a/src/System.Management.Automation/engine/runtime/CompiledScriptBlock.cs
+++ b/src/System.Management.Automation/engine/runtime/CompiledScriptBlock.cs
@@ -348,6 +348,26 @@ internal ObsoleteAttribute ObsoleteAttribute
}
}
+ internal ExperimentalAttribute ExperimentalAttribute
+ {
+ get
+ {
+ if (_expAttribute == ExperimentalAttribute.None)
+ {
+ lock (this)
+ {
+ if (_expAttribute == ExperimentalAttribute.None)
+ {
+ _expAttribute = Ast.GetExperimentalAttributes().FirstOrDefault();
+ }
+ }
+ }
+
+ return _expAttribute;
+ }
+ }
+ private ExperimentalAttribute _expAttribute = ExperimentalAttribute.None;
+
public MergedCommandParameterMetadata GetParameterMetadata(ScriptBlock scriptBlock)
{
if (_parameterMetadata == null)
@@ -1228,6 +1248,11 @@ internal ObsoleteAttribute ObsoleteAttribute
get { return _scriptBlockData.ObsoleteAttribute; }
}
+ internal ExperimentalAttribute ExperimentalAttribute
+ {
+ get { return _scriptBlockData.ExperimentalAttribute; }
+ }
+
internal bool Compile(bool optimized)
{
return _scriptBlockData.Compile(optimized);
diff --git a/src/System.Management.Automation/engine/runtime/Operations/MiscOps.cs b/src/System.Management.Automation/engine/runtime/Operations/MiscOps.cs
index 74b0210e761..ddd148c72df 100644
--- a/src/System.Management.Automation/engine/runtime/Operations/MiscOps.cs
+++ b/src/System.Management.Automation/engine/runtime/Operations/MiscOps.cs
@@ -1212,8 +1212,12 @@ internal static void DefineFunction(ExecutionContext context,
ScriptBlock scriptBlock = scriptBlockExpressionWrapper.GetScriptBlock(
context, functionDefinitionAst.IsFilter);
- context.EngineSessionState.SetFunctionRaw(functionDefinitionAst.Name,
- scriptBlock, context.EngineSessionState.CurrentScope.ScopeOrigin);
+ var expAttribute = scriptBlock.ExperimentalAttribute;
+ if (expAttribute == null || expAttribute.ToShow)
+ {
+ context.EngineSessionState.SetFunctionRaw(functionDefinitionAst.Name,
+ scriptBlock, context.EngineSessionState.CurrentScope.ScopeOrigin);
+ }
}
catch (Exception exception)
{
diff --git a/src/System.Management.Automation/resources/DiscoveryExceptions.resx b/src/System.Management.Automation/resources/DiscoveryExceptions.resx
index 7e1a3bcf578..795a23a6327 100644
--- a/src/System.Management.Automation/resources/DiscoveryExceptions.resx
+++ b/src/System.Management.Automation/resources/DiscoveryExceptions.resx
@@ -218,4 +218,10 @@ The #requires statement must be in one of the following formats:
The ShowCommandInfo and Syntax parameters cannot be specified together.
+
+ This script command is disabled when the experimental feature '{0}' has been turned on.
+
+
+ This script command is disabled when the experimental feature '{0}' has been turned off.
+
diff --git a/src/System.Management.Automation/resources/EventResource.resx b/src/System.Management.Automation/resources/EventResource.resx
index 5e15cdbf4c6..87e12dda7bb 100644
--- a/src/System.Management.Automation/resources/EventResource.resx
+++ b/src/System.Management.Automation/resources/EventResource.resx
@@ -81,6 +81,16 @@
InnerException: {3}
+
+ Experimental Feature Initialization: Ignore the experimental feature '{0}' from the config file. {1}
+
+
+ Experimental Feature Initialization: Failed to read the config file.
+ Exception: {0}
+ Message: {1}
+ StackTrace: {2}
+
+
Workflow plugin loaded.
EndpointName: {0}
diff --git a/src/System.Management.Automation/resources/Logging.resx b/src/System.Management.Automation/resources/Logging.resx
index 7a4d98e5009..b8cb1b29476 100644
--- a/src/System.Management.Automation/resources/Logging.resx
+++ b/src/System.Management.Automation/resources/Logging.resx
@@ -288,4 +288,13 @@ AdditionalInfo:
UNKNOWN
+
+ The engine experimental feature '{0}' declared in the config file is not registered in the current PowerShell.
+
+
+ The experimental feature '{0}' declared in the config file is invalid.
+The name of an experimental feature should follow the convention below:
+ Engine Feature Name: 'PS[FeatureName]'
+ Module Feature Name: '[ModuleName].[FeatureName]'
+
diff --git a/src/System.Management.Automation/resources/Metadata.resx b/src/System.Management.Automation/resources/Metadata.resx
index f4ce375cfea..f8a0d753788 100644
--- a/src/System.Management.Automation/resources/Metadata.resx
+++ b/src/System.Management.Automation/resources/Metadata.resx
@@ -243,4 +243,10 @@
The path argument has no root drive. Supply a full path argument with a root drive.
+
+ The argument value for the parameter '{0}' cannot be null or an empty string.
+
+
+ The Enum member '{0}' is not a valid value for the parameter '{1}'. Specify one of the following members and try again: {2}.
+
diff --git a/src/System.Management.Automation/resources/Modules.resx b/src/System.Management.Automation/resources/Modules.resx
index af889e15561..11bf9e47769 100644
--- a/src/System.Management.Automation/resources/Modules.resx
+++ b/src/System.Management.Automation/resources/Modules.resx
@@ -606,4 +606,10 @@
This prerequisite is valid for the PowerShell Desktop edition only.
+
+ A non-empty string value should be specified for an experimental feature declared in the module manifest.
+
+
+ One or more invalid experimental feature names found: {0}. A module experimental feature name should follow this convention: 'ModuleName.FeatureName'.
+
diff --git a/src/System.Management.Automation/resources/RunspaceInit.resx b/src/System.Management.Automation/resources/RunspaceInit.resx
index b8aee86a251..c4a485121c7 100644
--- a/src/System.Management.Automation/resources/RunspaceInit.resx
+++ b/src/System.Management.Automation/resources/RunspaceInit.resx
@@ -117,6 +117,9 @@
System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+ Variable to hold the enabled experimental feature names
+
Parent folder of the host application of the current runspace
diff --git a/src/System.Management.Automation/utils/PsUtils.cs b/src/System.Management.Automation/utils/PsUtils.cs
index 3fe10bacba2..57340056327 100644
--- a/src/System.Management.Automation/utils/PsUtils.cs
+++ b/src/System.Management.Automation/utils/PsUtils.cs
@@ -477,6 +477,7 @@ internal static Hashtable EvaluatePowerShellDataFile(
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)
{
diff --git a/test/powershell/Language/Parser/TypeAccelerator.Tests.ps1 b/test/powershell/Language/Parser/TypeAccelerator.Tests.ps1
index c7b2cb0770d..b6d0ab534c1 100644
--- a/test/powershell/Language/Parser/TypeAccelerator.Tests.ps1
+++ b/test/powershell/Language/Parser/TypeAccelerator.Tests.ps1
@@ -70,6 +70,18 @@ Describe "Type accelerators" -Tags "CI" {
Accelerator = 'DscResource'
Type = [System.Management.Automation.DscResourceAttribute]
}
+ @{
+ Accelerator = 'ExperimentAction'
+ Type = [System.Management.Automation.ExperimentAction]
+ }
+ @{
+ Accelerator = 'Experimental'
+ Type = [System.Management.Automation.ExperimentalAttribute]
+ }
+ @{
+ Accelerator = 'ExperimentalFeature'
+ Type = [System.Management.Automation.ExperimentalFeature]
+ }
@{
Accelerator = 'float'
Type = [System.Single]
@@ -378,11 +390,11 @@ Describe "Type accelerators" -Tags "CI" {
if ( !$IsWindows )
{
- $totalAccelerators = 91
+ $totalAccelerators = 94
}
else
{
- $totalAccelerators = 96
+ $totalAccelerators = 99
$extraFullPSAcceleratorTestCases = @(
@{
diff --git a/test/powershell/engine/Basic/DefaultCommands.Tests.ps1 b/test/powershell/engine/Basic/DefaultCommands.Tests.ps1
index 64b2b441c75..0fcdef96c70 100644
--- a/test/powershell/engine/Basic/DefaultCommands.Tests.ps1
+++ b/test/powershell/engine/Basic/DefaultCommands.Tests.ps1
@@ -258,6 +258,7 @@ Describe "Verify approved aliases list" -Tags "CI" {
"Cmdlet", "Get-EventLog", , $($FullCLR )
"Cmdlet", "Get-EventSubscriber", , $($FullCLR -or $CoreWindows -or $CoreUnix)
"Cmdlet", "Get-ExecutionPolicy", , $($FullCLR -or $CoreWindows -or $CoreUnix)
+"Cmdlet", "Get-ExperimentalFeature", , $( $CoreWindows -or $CoreUnix)
"Cmdlet", "Get-FileHash", , $( $CoreWindows -or $CoreUnix)
"Cmdlet", "Get-FormatData", , $($FullCLR -or $CoreWindows -or $CoreUnix)
"Cmdlet", "Get-Help", , $($FullCLR -or $CoreWindows -or $CoreUnix)
diff --git a/test/powershell/engine/ExperimentalFeature/ExperimentalFeature.Basic.Tests.ps1 b/test/powershell/engine/ExperimentalFeature/ExperimentalFeature.Basic.Tests.ps1
new file mode 100644
index 00000000000..84757e49097
--- /dev/null
+++ b/test/powershell/engine/ExperimentalFeature/ExperimentalFeature.Basic.Tests.ps1
@@ -0,0 +1,447 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+Describe "Experimental Feature Basic Tests - Feature-Disabled" -tags "CI" {
+
+ BeforeAll {
+ $skipTest = $EnabledExperimentalFeatures.Contains('ExpTest.FeatureOne')
+
+ if ($skipTest) {
+ Write-Verbose "Test Suite Skipped. The test suite requires the experimental feature 'ExpTest.FeatureOne' to be disabled." -Verbose
+ $originalDefaultParameterValues = $PSDefaultParameterValues.Clone()
+ $PSDefaultParameterValues["it:skip"] = $true
+ } else {
+ ## Common parameters are defined in the type 'CommonParameters' as public properties.
+ $CommonParameterCount = [System.Management.Automation.Internal.CommonParameters].GetProperties().Length
+ $TestModule = Join-Path $PSScriptRoot "assets" "ExpTest"
+ $AssemblyPath = Join-Path $TestModule "ExpTest.dll"
+ if (-not (Test-Path $AssemblyPath)) {
+ ## When using $SourcePath directly, 'Add-Type' fails in AppVeyor CI runs with an 'access denied' error.
+ ## It turns out Pester doesn't handle an exception like this from 'BeforeAll'. It causes the Pester to
+ ## be somehow corrupted, and results in random failures in other tests.
+ ## To work around this issue, we copy the source file to 'TestDrive' before calling 'Add-Type'.
+ $SourcePath = Join-Path $TestModule "ExpTest.cs"
+ $SourcePath = (Copy-Item $SourcePath TestDrive:\ -PassThru).FullName
+ Add-Type -Path $SourcePath -OutputType Library -OutputAssembly $AssemblyPath
+ }
+ $moduleInfo = Import-Module $TestModule -PassThru
+ }
+ }
+
+ AfterAll {
+ if ($skipTest) {
+ $global:PSDefaultParameterValues = $originalDefaultParameterValues
+ } else {
+ Remove-Module -ModuleInfo $moduleInfo -Force -ErrorAction SilentlyContinue
+ }
+ }
+
+ It "No experimental feature is enabled" {
+ $EnabledExperimentalFeatures.Count | Should -Be 0
+ }
+
+ It "Replace existing command - version one should be shown" -TestCases @(
+ @{ Name = "Invoke-AzureFunction"; CommandType = "Function" }
+ @{ Name = "Invoke-AzureFunctionCSharp"; CommandType = "Cmdlet" }
+ ) {
+ param($Name, $CommandType)
+ $command = Get-Command $Name
+ $command.CommandType | Should -Be $CommandType
+ $command.Source | Should -BeExactly $moduleInfo.Name
+ & $Name -Token "Token" -Command "Command" | Should -BeExactly "Invoke-AzureFunction Version ONE"
+
+ if ($CommandType -eq "Function") {
+ $expectedErrorId = "CommandNotFoundException,Microsoft.PowerShell.Commands.GetCommandCommand"
+ { Get-Command "Invoke-AzureFunctionV2" -ErrorAction Stop } | Should -Throw -ErrorId $expectedErrorId
+ { & $moduleInfo { Get-Command "Invoke-AzureFunctionV2" -ErrorAction Stop } } | Should -Throw -ErrorId $expectedErrorId
+ }
+ }
+
+ It "Experimental parameter set - '' should NOT have '-SwitchOne' and '-SwitchTwo'" -TestCases @(
+ @{ Name = "Get-GreetingMessage"; CommandType = "Function" }
+ @{ Name = "Get-GreetingMessageCSharp"; CommandType = "Cmdlet" }
+ ) {
+ param($Name, $CommandType)
+ $command = Get-Command $Name
+ $command.CommandType | Should -Be $CommandType
+ ## Common parameters + '-Name'
+ $command.Parameters.Count | Should -Be ($CommonParameterCount + 1)
+ & $Name -Name Joe | Should -BeExactly "Hello World Joe."
+ }
+
+ It "Experimental parameter set - '' should NOT have 'WebSocket' parameter set" -TestCases @(
+ @{ Name = "Invoke-MyCommand"; CommandType = "Function" }
+ @{ Name = "Invoke-MyCommandCSharp"; CommandType = "Cmdlet" }
+ ) {
+ param($Name, $CommandType)
+ $command = Get-Command $Name
+ $command.CommandType | Should -Be $CommandType
+
+ ## Common parameters + '-UserName', '-ComputerName', '-ConfigurationName', '-VMName', '-Port', '-ThrottleLimit' and '-Command'
+ $command.Parameters.Count | Should -Be ($CommonParameterCount + 7)
+ $command.ParameterSets.Count | Should -Be 2
+
+ $command.Parameters["UserName"].ParameterSets.Count | Should -Be 1
+ $command.Parameters["UserName"].ParameterSets.ContainsKey("ComputerSet") | Should -Be $true
+
+ $command.Parameters["ComputerName"].ParameterSets.Count | Should -Be 1
+ $command.Parameters["ComputerName"].ParameterSets.ContainsKey("ComputerSet") | Should -Be $true
+
+ $command.Parameters["ConfigurationName"].ParameterSets.Count | Should -Be 1
+ $command.Parameters["ConfigurationName"].ParameterSets.ContainsKey("ComputerSet") | Should -Be $true
+
+ $command.Parameters["VMName"].ParameterSets.Count | Should -Be 1
+ $command.Parameters["VMName"].ParameterSets.ContainsKey("VMSet") | Should -Be $true
+
+ $command.Parameters["Port"].ParameterSets.Count | Should -Be 1
+ $command.Parameters["Port"].ParameterSets.ContainsKey("VMSet") | Should -Be $true
+
+ $command.Parameters["ThrottleLimit"].ParameterSets.Count | Should -Be 1
+ $command.Parameters["ThrottleLimit"].ParameterSets.ContainsKey("__AllParameterSets") | Should -Be $true
+
+ $command.Parameters["Command"].ParameterSets.Count | Should -Be 1
+ $command.Parameters["Command"].ParameterSets.ContainsKey("__AllParameterSets") | Should -Be $true
+
+ ## Common parameters + '-UserName', '-ComputerName', '-ConfigurationName', '-ThrottleLimit' and '-Command'
+ $command.ParameterSets[0].Name | Should -BeExactly "ComputerSet"
+ $command.ParameterSets[0].Parameters.Count | Should -Be ($CommonParameterCount + 5)
+
+ ## Common parameters + '-VMName', '-Port', '-ThrottleLimit' and '-Command'
+ $command.ParameterSets[1].Name | Should -BeExactly "VMSet"
+ $command.ParameterSets[1].Parameters.Count | Should -Be ($CommonParameterCount + 4)
+
+ & $Name -UserName "user" -ComputerName "localhost" -ConfigurationName "config" | Should -BeExactly "Invoke-MyCommand with ComputerSet"
+ & $Name -VMName "VM" -Port "80" | Should -BeExactly "Invoke-MyCommand with VMSet"
+ }
+
+ It "Experimental parameter set - '' should have '-SessionName' only" -TestCases @(
+ @{ Name = "Test-MyRemoting"; CommandType = "Function" }
+ @{ Name = "Test-MyRemotingCSharp"; CommandType = "Cmdlet" }
+ ) {
+ param($Name, $CommandType)
+ $command = Get-Command $Name
+ $command.CommandType | Should -Be $CommandType
+ ## Common parameters + '-SessionName'
+ $command.Parameters.Count | Should -Be ($CommonParameterCount + 1)
+ $command.Parameters["SessionName"].ParameterType.FullName | Should -BeExactly "System.String"
+ $command.Parameters.ContainsKey("ComputerName") | Should -Be $false
+ }
+
+ It "Use 'Experimental' attribute directly on parameters - ''" -TestCases @(
+ @{ Name = "Save-MyFile"; CommandType = "Function" }
+ @{ Name = "Save-MyFileCSharp"; CommandType = "Cmdlet" }
+ ) {
+ param($Name, $CommandType)
+ $command = Get-Command $Name
+ $command.CommandType | Should -Be $CommandType
+ ## Common parameters + '-ByUrl', '-ByRadio', '-FileName', '-Configuration'
+ $command.Parameters.Count | Should -Be ($CommonParameterCount + 4)
+ $command.ParameterSets.Count | Should -Be 2
+
+ $command.Parameters["ByUrl"].ParameterSets.Count | Should -Be 1
+ $command.Parameters["ByUrl"].ParameterSets.ContainsKey("UrlSet") | Should -Be $true
+
+ $command.Parameters["ByRadio"].ParameterSets.Count | Should -Be 1
+ $command.Parameters["ByRadio"].ParameterSets.ContainsKey("RadioSet") | Should -Be $true
+
+ $command.Parameters["Configuration"].ParameterSets.Count | Should -Be 2
+ $command.Parameters["Configuration"].ParameterSets.ContainsKey("UrlSet") | Should -Be $true
+ $command.Parameters["Configuration"].ParameterSets.ContainsKey("RadioSet") | Should -Be $true
+
+ $command.Parameters["FileName"].ParameterSets.Count | Should -Be 1
+ $command.Parameters["FileName"].ParameterSets.ContainsKey("__AllParameterSets") | Should -Be $true
+
+ $command.Parameters.ContainsKey("Destination") | Should -Be $false
+ }
+
+ It "Dynamic parameters - -" -TestCases @(
+ @{ Name = "Test-MyDynamicParamOne"; CommandType = "Function" }
+ @{ Name = "Test-MyDynamicParamOneCSharp"; CommandType = "Cmdlet" }
+ @{ Name = "Test-MyDynamicParamTwo"; CommandType = "Function" }
+ @{ Name = "Test-MyDynamicParamTwoCSharp"; CommandType = "Cmdlet" }
+ ) {
+ param($Name, $CommandType)
+ $command = Get-Command $Name
+ $command.CommandType | Should -Be $CommandType
+ ## Common parameters + '-Name' (dynamic parameters are not triggered)
+ $command.Parameters.Count | Should -Be ($CommonParameterCount + 1)
+ $command.Parameters["Name"] | Should -Not -BeNullOrEmpty
+
+ $command = Get-Command $Name -ArgumentList "Joe"
+ ## Common parameters + '-Name' and '-ConfigName' (dynamic parameters are triggered)
+ $command.Parameters.Count | Should -Be ($CommonParameterCount + 2)
+ $command.Parameters["ConfigName"].Attributes.Count | Should -Be 2
+ $command.Parameters["ConfigName"].Attributes[0] | Should -BeOfType [parameter]
+ $command.Parameters["ConfigName"].Attributes[1] | Should -BeOfType [ValidateNotNullOrEmpty]
+
+ $command.Parameters.ContainsKey("ConfigFile") | Should -Be $false
+ }
+}
+
+Describe "Experimental Feature Basic Tests - Feature-Enabled" -Tag "CI" {
+
+ BeforeAll {
+ $skipTest = -not $EnabledExperimentalFeatures.Contains('ExpTest.FeatureOne')
+
+ if ($skipTest) {
+ Write-Verbose "Test Suite Skipped. The test suite requires the experimental feature 'ExpTest.FeatureOne' to be enabled." -Verbose
+ $originalDefaultParameterValues = $PSDefaultParameterValues.Clone()
+ $PSDefaultParameterValues["it:skip"] = $true
+ } else {
+ ## Common parameters are defined in the type 'CommonParameters' as public properties.
+ $CommonParameterCount = [System.Management.Automation.Internal.CommonParameters].GetProperties().Length
+ $TestModule = Join-Path $PSScriptRoot "assets" "ExpTest"
+ $AssemblyPath = Join-Path $TestModule "ExpTest.dll"
+ if (-not (Test-Path $AssemblyPath)) {
+ $SourcePath = Join-Path $TestModule "ExpTest.cs"
+ $SourcePath = (Copy-Item $SourcePath TestDrive:\ -PassThru).FullName
+ Add-Type -Path $SourcePath -OutputType Library -OutputAssembly $AssemblyPath
+ }
+ $moduleInfo = Import-Module $TestModule -PassThru
+ }
+ }
+
+ AfterAll {
+ if ($skipTest) {
+ $global:PSDefaultParameterValues = $originalDefaultParameterValues
+ } else {
+ Remove-Module -ModuleInfo $moduleInfo -Force -ErrorAction SilentlyContinue
+ }
+ }
+
+ It "Experimental feature 'ExpTest.FeatureOne' should be enabled" {
+ $EnabledExperimentalFeatures.Count | Should -Be 1
+ $EnabledExperimentalFeatures -contains "ExpTest.FeatureOne" | Should -Be $true
+ }
+
+ It "Replace existing command - version two should be shown" -TestCases @(
+ @{ Name = "Invoke-AzureFunction"; CommandType = "Alias" }
+ @{ Name = "Invoke-AzureFunctionCSharp"; CommandType = "Cmdlet" }
+ ) {
+ param($Name, $CommandType)
+ $command = Get-Command $Name
+ $command.CommandType | Should -Be $CommandType
+ $command.Source | Should -BeExactly $moduleInfo.Name
+ & $Name -Token "Token" -Command "Command" | Should -BeExactly "Invoke-AzureFunction Version TWO"
+
+ if ($CommandType -eq "Alias") {
+ $command.Definition | Should -Be "Invoke-AzureFunctionV2"
+ $expectedErrorId = "CommandNotFoundException,Microsoft.PowerShell.Commands.GetCommandCommand"
+ { Get-Command "Invoke-AzureFunction" -CommandType Function -ErrorAction Stop } | Should -Throw -ErrorId $expectedErrorId
+ { & $moduleInfo { Get-Command "Invoke-AzureFunction" -CommandType Function -ErrorAction Stop } } | Should -Throw -ErrorId $expectedErrorId
+ }
+ }
+
+ It "Experimental parameter set - '' should have '-SwitchOne' and '-SwitchTwo'" -TestCases @(
+ @{ Name = "Get-GreetingMessage"; CommandType = "Function" }
+ @{ Name = "Get-GreetingMessageCSharp"; CommandType = "Cmdlet" }
+ ) {
+ param($Name, $CommandType)
+ $command = Get-Command $Name
+ $command.CommandType | Should -Be $CommandType
+ ## Common parameters + '-Name' + '-SwitchOne' + '-SwitchTwo'
+ $command.Parameters.Count | Should -Be ($CommonParameterCount + 3)
+ $command.ParameterSets.Count | Should -Be 3
+
+ & $Name -Name Joe | Should -BeExactly "Hello World Joe."
+ & $Name -Name Joe -SwitchOne | Should -BeExactly "Hello World Joe.-SwitchOne is on."
+ & $Name -Name Joe -SwitchTwo | Should -BeExactly "Hello World Joe.-SwitchTwo is on."
+ }
+
+ It "Experimental parameter set - '' should have 'WebSocket' parameter set" -TestCases @(
+ @{ Name = "Invoke-MyCommand"; CommandType = "Function" }
+ @{ Name = "Invoke-MyCommandCSharp"; CommandType = "Cmdlet" }
+ ) {
+ param($Name, $CommandType)
+ $command = Get-Command $Name
+ $command.CommandType | Should -Be $CommandType
+
+ ## Common parameters + '-UserName', '-ComputerName', '-ConfigurationName', '-VMName', '-Port',
+ ## '-Token', '-WebSocketUrl', '-ThrottleLimit' and '-Command'
+ $command.Parameters.Count | Should -Be ($CommonParameterCount + 9)
+ $command.ParameterSets.Count | Should -Be 3
+
+ $command.Parameters["UserName"].ParameterSets.Count | Should -Be 1
+ $command.Parameters["UserName"].ParameterSets.ContainsKey("ComputerSet") | Should -Be $true
+
+ $command.Parameters["ComputerName"].ParameterSets.Count | Should -Be 1
+ $command.Parameters["ComputerName"].ParameterSets.ContainsKey("ComputerSet") | Should -Be $true
+
+ $command.Parameters["VMName"].ParameterSets.Count | Should -Be 1
+ $command.Parameters["VMName"].ParameterSets.ContainsKey("VMSet") | Should -Be $true
+
+ $command.Parameters["Token"].ParameterSets.Count | Should -Be 1
+ $command.Parameters["Token"].ParameterSets.ContainsKey("WebSocketSet") | Should -Be $true
+
+ $command.Parameters["WebSocketUrl"].ParameterSets.Count | Should -Be 1
+ $command.Parameters["WebSocketUrl"].ParameterSets.ContainsKey("WebSocketSet") | Should -Be $true
+
+ $command.Parameters["ConfigurationName"].ParameterSets.Count | Should -Be 2
+ $command.Parameters["ConfigurationName"].ParameterSets.ContainsKey("ComputerSet") | Should -Be $true
+ $command.Parameters["ConfigurationName"].ParameterSets.ContainsKey("WebSocketSet") | Should -Be $true
+
+ $command.Parameters["Port"].ParameterSets.Count | Should -Be 2
+ $command.Parameters["Port"].ParameterSets.ContainsKey("VMSet") | Should -Be $true
+ $command.Parameters["Port"].ParameterSets.ContainsKey("WebSocketSet") | Should -Be $true
+
+ $command.Parameters["ThrottleLimit"].ParameterSets.Count | Should -Be 1
+ $command.Parameters["ThrottleLimit"].ParameterSets.ContainsKey("__AllParameterSets") | Should -Be $true
+
+ $command.Parameters["Command"].ParameterSets.Count | Should -Be 1
+ $command.Parameters["Command"].ParameterSets.ContainsKey("__AllParameterSets") | Should -Be $true
+
+ ## Common parameters + '-UserName', '-ComputerName', '-ConfigurationName', '-ThrottleLimit' and '-Command'
+ $command.ParameterSets[0].Name | Should -BeExactly "ComputerSet"
+ $command.ParameterSets[0].Parameters.Count | Should -Be ($CommonParameterCount + 5)
+
+ ## Common parameters + '-VMName', '-Port', '-ThrottleLimit' and '-Command'
+ $command.ParameterSets[1].Name | Should -BeExactly "VMSet"
+ $command.ParameterSets[1].Parameters.Count | Should -Be ($CommonParameterCount + 4)
+
+ ## Common parameters + '-Token', '-WebSocketUrl', '-ConfigurationName', '-Port', '-ThrottleLimit', '-Command'
+ $command.ParameterSets[2].Name | Should -BeExactly "WebSocketSet"
+ $command.ParameterSets[2].Parameters.Count | Should -Be ($CommonParameterCount + 6)
+
+ & $Name -UserName "user" -ComputerName "localhost" | Should -BeExactly "Invoke-MyCommand with ComputerSet"
+ & $Name -UserName "user" -ComputerName "localhost" -ConfigurationName "config" | Should -BeExactly "Invoke-MyCommand with ComputerSet"
+
+ & $Name -VMName "VM" | Should -BeExactly "Invoke-MyCommand with VMSet"
+ & $Name -VMName "VM" -Port "80" | Should -BeExactly "Invoke-MyCommand with VMSet"
+
+ & $Name -Token "token" -WebSocketUrl 'url' | Should -BeExactly "Invoke-MyCommand with WebSocketSet"
+ & $Name -Token "token" -WebSocketUrl 'url' -ConfigurationName 'config' -Port 80 | Should -BeExactly "Invoke-MyCommand with WebSocketSet"
+ }
+
+ It "Experimental parameter set - '' should have '-ComputerName' only" -TestCases @(
+ @{ Name = "Test-MyRemoting"; CommandType = "Function" }
+ @{ Name = "Test-MyRemotingCSharp"; CommandType = "Cmdlet" }
+ ) {
+ param($Name, $CommandType)
+ $command = Get-Command $Name
+ $command.CommandType | Should -Be $CommandType
+ ## Common parameters + '-ComputerName'
+ $command.Parameters.Count | Should -Be ($CommonParameterCount + 1)
+ $command.Parameters["ComputerName"].ParameterType.FullName | Should -BeExactly "System.String"
+ $command.Parameters.ContainsKey("SessionName") | Should -Be $false
+ }
+
+ It "Use 'Experimental' attribute directly on parameters - ''" -TestCases @(
+ @{ Name = "Save-MyFile"; CommandType = "Function" }
+ @{ Name = "Save-MyFileCSharp"; CommandType = "Cmdlet" }
+ ) {
+ param($Name, $CommandType)
+ $command = Get-Command $Name
+ $command.CommandType | Should -Be $CommandType
+ ## Common parameters + '-ByUrl', '-ByRadio', '-FileName', '-Destination'
+ $command.Parameters.Count | Should -Be ($CommonParameterCount + 4)
+ $command.ParameterSets.Count | Should -Be 2
+
+ $command.Parameters["ByUrl"].ParameterSets.Count | Should -Be 1
+ $command.Parameters["ByUrl"].ParameterSets.ContainsKey("UrlSet") | Should -Be $true
+
+ $command.Parameters["ByRadio"].ParameterSets.Count | Should -Be 1
+ $command.Parameters["ByRadio"].ParameterSets.ContainsKey("RadioSet") | Should -Be $true
+
+ $command.Parameters["Destination"].ParameterSets.Count | Should -Be 1
+ $command.Parameters["Destination"].ParameterSets.ContainsKey("__AllParameterSets") | Should -Be $true
+
+ $command.Parameters["FileName"].ParameterSets.Count | Should -Be 1
+ $command.Parameters["FileName"].ParameterSets.ContainsKey("__AllParameterSets") | Should -Be $true
+
+ $command.Parameters.ContainsKey("Configuration") | Should -Be $false
+ }
+
+ It "Dynamic parameters - -" -TestCases @(
+ @{ Name = "Test-MyDynamicParamOne"; CommandType = "Function" }
+ @{ Name = "Test-MyDynamicParamOneCSharp"; CommandType = "Cmdlet" }
+ @{ Name = "Test-MyDynamicParamTwo"; CommandType = "Function" }
+ @{ Name = "Test-MyDynamicParamTwoCSharp"; CommandType = "Cmdlet" }
+ ) {
+ param($Name, $CommandType)
+
+ $command = Get-Command $Name
+ $command.CommandType | Should -Be $CommandType
+ ## Common parameters + '-Name' (dynamic parameters are not triggered)
+ $command.Parameters.Count | Should -Be ($CommonParameterCount + 1)
+ $command.Parameters["Name"] | Should -Not -BeNullOrEmpty
+
+ $command = Get-Command $Name -ArgumentList "Joe"
+ ## Common parameters + '-Name' and '-ConfigFile' (dynamic parameters are triggered)
+ $command.Parameters.Count | Should -Be ($CommonParameterCount + 2)
+ $command.Parameters["ConfigFile"].Attributes.Count | Should -Be 2
+ $command.Parameters["ConfigFile"].Attributes[0] | Should -BeOfType [parameter]
+ $command.Parameters["ConfigFile"].Attributes[1] | Should -BeOfType [ValidateNotNullOrEmpty]
+
+ $command.Parameters.ContainsKey("ConfigName") | Should -Be $false
+ }
+}
+
+Describe "Expected errors" -Tag "CI" {
+ It "'[Experimental()]' should fail to construct the attribute" {
+ { [Experimental()]param() } | Should -Throw -ErrorId "MethodCountCouldNotFindBest"
+ }
+
+ It "Argument validation for constructors of 'ExperimentalAttribute' - " -TestCases @(
+ @{ TestName = "Name is empty string"; FeatureName = ""; FeatureAction = "None"; ErrorId = "PSArgumentNullException" }
+ @{ TestName = "Name is null"; FeatureName = [NullString]::Value; FeatureAction = "None"; ErrorId = "PSArgumentNullException" }
+ @{ TestName = "Action is None"; FeatureName = "feature"; FeatureAction = "None"; ErrorId = "PSArgumentException" }
+ @{ TestName = "Action is Show"; FeatureName = "feature"; FeatureAction = "Show"; ErrorId = $null }
+ @{ TestName = "Action is Hide"; FeatureName = "feature"; FeatureAction = "Hide"; ErrorId = $null }
+ ) {
+ param($FeatureName, $FeatureAction, $ErrorId)
+
+ if ($ErrorId -ne $null) {
+ { [Experimental]::new($FeatureName, $FeatureAction) } | Should -Throw -ErrorId $ErrorId
+ } else {
+ { [Experimental]::new($FeatureName, $FeatureAction) } | Should -Not -Throw
+ }
+ }
+
+ It "Argument validation for constructors of 'ParameterAttribute' - " -TestCases @(
+ @{ TestName = "Name is empty string"; FeatureName = ""; FeatureAction = "None"; ErrorId = "PSArgumentNullException" }
+ @{ TestName = "Name is null"; FeatureName = [NullString]::Value; FeatureAction = "None"; ErrorId = "PSArgumentNullException" }
+ @{ TestName = "Action is None"; FeatureName = "feature"; FeatureAction = "None"; ErrorId = "PSArgumentException" }
+ @{ TestName = "Action is Show"; FeatureName = "feature"; FeatureAction = "Show"; ErrorId = $null }
+ @{ TestName = "Action is Hide"; FeatureName = "feature"; FeatureAction = "Hide"; ErrorId = $null }
+ ) {
+ param($FeatureName, $FeatureAction, $ErrorId)
+
+ if ($ErrorId -ne $null) {
+ { [Parameter]::new($FeatureName, $FeatureAction) } | Should -Throw -ErrorId $ErrorId
+ } else {
+ { [Parameter]::new($FeatureName, $FeatureAction) } | Should -Not -Throw
+ }
+ }
+
+ It "Feature name check" {
+ $psd1Content = @'
+@{
+ModuleVersion = '0.0.1'
+CompatiblePSEditions = @('Core')
+GUID = 'ce31259c-1804-4016-bc29-083bd2599e19'
+PrivateData = @{
+ PSData = @{
+ ExperimentalFeatures = @(
+ @{ Name = '.Feature1'; Description = "Test feature number 1." }
+ @{ Name = 'Feature2.'; Description = "Test feature number 2." }
+ @{ Name = 'Feature3'; Description = "Test feature number 3." }
+ @{ Name = 'Module.Feature4'; Description = "Test feature number 4." }
+ @{ Name = 'InvalidFeatureName.Feature5'; Description = "Test feature number 5." }
+ )
+ }
+}
+}
+'@
+ $moduleFile = Join-Path $TestDrive InvalidFeatureName.psd1
+ Set-Content -Path $moduleFile -Value $psd1Content -Encoding Ascii
+
+ Import-Module $moduleFile -ErrorVariable featureNameError -ErrorAction SilentlyContinue
+ $featureNameError | Should -Not -BeNullOrEmpty
+ $featureNameError[0].FullyQualifiedErrorId | Should -Be "Modules_InvalidExperimentalFeatureName,Microsoft.PowerShell.Commands.ImportModuleCommand"
+ $featureNameError[0].Exception.Message.Contains(".Feature1") | Should -Be $true
+ $featureNameError[0].Exception.Message.Contains("Feature2.") | Should -Be $true
+ $featureNameError[0].Exception.Message.Contains("Feature3") | Should -Be $true
+ $featureNameError[0].Exception.Message.Contains("Module.Feature4") | Should -Be $true
+ $featureNameError[0].Exception.Message.Contains("InvalidFeatureName.Feature5") | Should -Be $false
+ }
+}
diff --git a/test/powershell/engine/ExperimentalFeature/Get-ExperimentalFeature.Tests.ps1 b/test/powershell/engine/ExperimentalFeature/Get-ExperimentalFeature.Tests.ps1
new file mode 100644
index 00000000000..4681b0660ec
--- /dev/null
+++ b/test/powershell/engine/ExperimentalFeature/Get-ExperimentalFeature.Tests.ps1
@@ -0,0 +1,117 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+Describe "Get-ExperimentalFeature basic tests - Feature-Disabled" -tags "CI" {
+
+ BeforeAll {
+ $skipTest = $EnabledExperimentalFeatures.Contains('ExpTest.FeatureOne')
+
+ if ($skipTest) {
+ Write-Verbose "Test Suite Skipped. The test suite requires the experimental feature 'ExpTest.FeatureOne' to be disabled." -Verbose
+ $originalDefaultParameterValues = $PSDefaultParameterValues.Clone()
+ $PSDefaultParameterValues["it:skip"] = $true
+ } else {
+ Remove-Module -Name ExpTest -Force -ErrorAction SilentlyContinue
+ $testModulePath = Join-Path -Path $PSScriptRoot -ChildPath "assets"
+ $testModuleManifestPath = Join-Path -Path $testModulePath "ExpTest" "ExpTest.psd1"
+ $originalModulePath = $env:PSModulePath
+ $env:PSModulePath = $testModulePath
+ }
+ }
+
+ AfterAll {
+ if ($skipTest) {
+ $global:PSDefaultParameterValues = $originalDefaultParameterValues
+ } else {
+ $env:PSModulePath = $originalModulePath
+ }
+ }
+
+ It "'Get-ExperimentalFeature' should only return enabled features" {
+ $EnabledExperimentalFeatures.Count | Should -Be 0
+ Get-ExperimentalFeature | Should -BeNullOrEmpty
+ }
+
+ It "'Get-ExperimentalFeature -ListAvailable' should return all available features from module path" {
+ $features = Get-ExperimentalFeature "ExpTest*" -ListAvailable
+ $features | Should -Not -BeNullOrEmpty
+ $features[0].Name | Should -BeExactly "ExpTest.FeatureOne"
+ $features[0].Enabled | Should -Be $false
+ $features[0].Source | Should -BeExactly $testModuleManifestPath
+
+ $features[1].Name | Should -BeExactly "ExpTest.FeatureTwo"
+ $features[1].Enabled | Should -Be $false
+ $features[1].Source | Should -BeExactly $testModuleManifestPath
+ }
+
+ It "'Get-ExperimentalFeature -ListAvailable' pipeline input" {
+ $features = "ExpTest.FeatureOne", "ExpTest.FeatureTwo" | Get-ExperimentalFeature -ListAvailable
+ $features | Should -Not -BeNullOrEmpty
+ $features[0].Name | Should -BeExactly "ExpTest.FeatureOne"
+ $features[0].Enabled | Should -Be $false
+ $features[0].Source | Should -BeExactly $testModuleManifestPath
+
+ $features[1].Name | Should -BeExactly "ExpTest.FeatureTwo"
+ $features[1].Enabled | Should -Be $false
+ $features[1].Source | Should -BeExactly $testModuleManifestPath
+ }
+}
+
+Describe "Get-ExperimentalFeature basic tests - Feature-Enabled" -tags "CI" {
+
+ BeforeAll {
+ $skipTest = -not $EnabledExperimentalFeatures.Contains('ExpTest.FeatureOne')
+
+ if ($skipTest) {
+ Write-Verbose "Test Suite Skipped. The test suite requires the experimental feature 'ExpTest.FeatureOne' to be enabled." -Verbose
+ $originalDefaultParameterValues = $PSDefaultParameterValues.Clone()
+ $PSDefaultParameterValues["it:skip"] = $true
+ } else {
+ Remove-Module -Name ExpTest -Force -ErrorAction SilentlyContinue
+ $testModulePath = Join-Path -Path $PSScriptRoot -ChildPath "assets"
+ $testModuleManifestPath = Join-Path -Path $testModulePath "ExpTest" "ExpTest.psd1"
+ $originalModulePath = $env:PSModulePath
+ $env:PSModulePath = $testModulePath
+ }
+ }
+
+ AfterAll {
+ if ($skipTest) {
+ $global:PSDefaultParameterValues = $originalDefaultParameterValues
+ } else {
+ $env:PSModulePath = $originalModulePath
+ }
+ }
+
+ It "'Get-ExperimentalFeature' should return enabled features 'ExpTest.FeatureOne'" {
+ $EnabledExperimentalFeatures.Count | Should -Be 1
+ $feature = Get-ExperimentalFeature "ExpTest.FeatureOne"
+ $feature | Should -Not -BeNullOrEmpty
+ $feature.Enabled | Should -Be $true
+ $feature.Source | Should -BeExactly $testModuleManifestPath
+ }
+
+ It "'Get-ExperimentalFeature -ListAvailable' should return all available features from module path" {
+ $features = Get-ExperimentalFeature "ExpTest*" -ListAvailable
+ $features | Should -Not -BeNullOrEmpty
+ $features[0].Name | Should -BeExactly "ExpTest.FeatureOne"
+ $features[0].Enabled | Should -Be $true
+ $features[0].Source | Should -BeExactly $testModuleManifestPath
+
+ $features[1].Name | Should -BeExactly "ExpTest.FeatureTwo"
+ $features[1].Enabled | Should -Be $false
+ $features[1].Source | Should -BeExactly $testModuleManifestPath
+ }
+
+ It "'Get-ExperimentalFeature -ListAvailable' pipeline input" {
+ $features = "ExpTest.FeatureOne", "ExpTest.FeatureTwo" | Get-ExperimentalFeature -ListAvailable
+ $features | Should -Not -BeNullOrEmpty
+ $features[0].Name | Should -BeExactly "ExpTest.FeatureOne"
+ $features[0].Enabled | Should -Be $true
+ $features[0].Source | Should -BeExactly $testModuleManifestPath
+
+ $features[1].Name | Should -BeExactly "ExpTest.FeatureTwo"
+ $features[1].Enabled | Should -Be $false
+ $features[1].Source | Should -BeExactly $testModuleManifestPath
+ }
+}
diff --git a/test/powershell/engine/ExperimentalFeature/assets/ExpTest/ExpTest.cs b/test/powershell/engine/ExperimentalFeature/assets/ExpTest/ExpTest.cs
new file mode 100644
index 00000000000..a73b649f48c
--- /dev/null
+++ b/test/powershell/engine/ExperimentalFeature/assets/ExpTest/ExpTest.cs
@@ -0,0 +1,216 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Management.Automation;
+
+namespace ExperimentalFeatureTest
+{
+ #region "Replace existing cmdlet"
+
+ [Experimental("ExpTest.FeatureOne", ExperimentAction.Hide)]
+ [Cmdlet("Invoke", "AzureFunctionCSharp")]
+ public class InvokeAzureFunctionCommand : PSCmdlet
+ {
+ [Parameter]
+ public string Token { get; set; }
+
+ [Parameter]
+ public string Command { get; set; }
+
+ protected override void EndProcessing()
+ {
+ WriteObject("Invoke-AzureFunction Version ONE");
+ }
+ }
+
+ [Experimental("ExpTest.FeatureOne", ExperimentAction.Show)]
+ [Cmdlet("Invoke", "AzureFunctionCSharp")]
+ public class InvokeAzureFunctionCommandV2 : PSCmdlet
+ {
+ [Parameter(Mandatory = true)]
+ public string Token { get; set; }
+
+ [Parameter(Mandatory = true)]
+ public string Command { get; set; }
+
+ protected override void EndProcessing()
+ {
+ WriteObject("Invoke-AzureFunction Version TWO");
+ }
+ }
+
+ #endregion
+
+ #region "Make parameter set experimental"
+
+ [Cmdlet("Get", "GreetingMessageCSharp", DefaultParameterSetName = "Default")]
+ public class GetGreetingMessageCommand : PSCmdlet
+ {
+ [Parameter(Mandatory = true)]
+ public string Name { get; set; }
+
+ [Parameter("ExpTest.FeatureOne", ExperimentAction.Show, ParameterSetName = "SwitchOneSet")]
+ public SwitchParameter SwitchOne { get; set; }
+
+ [Parameter("ExpTest.FeatureOne", ExperimentAction.Show, ParameterSetName = "SwitchTwoSet")]
+ public SwitchParameter SwitchTwo { get; set; }
+
+ protected override void EndProcessing()
+ {
+ string message = $"Hello World {Name}.";
+ if (ExperimentalFeature.IsEnabled("ExpTest.FeatureOne"))
+ {
+ if (SwitchOne.IsPresent) { message += "-SwitchOne is on."; }
+ if (SwitchTwo.IsPresent) { message += "-SwitchTwo is on."; }
+ }
+ WriteObject(message);
+ }
+ }
+
+ [Cmdlet("Invoke", "MyCommandCSharp")]
+ public class InvokeMyCommandCommand : PSCmdlet
+ {
+ [Parameter(Mandatory = true, ParameterSetName = "ComputerSet")]
+ public string UserName { get; set; }
+
+ [Parameter(Mandatory = true, ParameterSetName = "ComputerSet")]
+ public string ComputerName { get; set; }
+
+ [Parameter(Mandatory = true, ParameterSetName = "VMSet")]
+ public string VMName { get; set; }
+
+ // Enable web socket only if the feature is turned on.
+ [Parameter("ExpTest.FeatureOne", ExperimentAction.Show, Mandatory = true, ParameterSetName = "WebSocketSet")]
+ public string Token { get; set; }
+
+ [Parameter("ExpTest.FeatureOne", ExperimentAction.Show, Mandatory = true, ParameterSetName = "WebSocketSet")]
+ public string WebSocketUrl { get; set; }
+
+ // Add -ConfigurationName to parameter set "WebSocketSet" only if the feature is turned on.
+ [Parameter(ParameterSetName = "ComputerSet")]
+ [Parameter("ExpTest.FeatureOne", ExperimentAction.Show, ParameterSetName = "WebSocketSet")]
+ public string ConfigurationName { get; set; }
+
+ // Add -Port to parameter set "WebSocketSet" only if the feature is turned on.
+ [Parameter(ParameterSetName = "VMSet")]
+ [Parameter("ExpTest.FeatureOne", ExperimentAction.Show, ParameterSetName = "WebSocketSet")]
+ public int Port { get; set; }
+
+ [Parameter]
+ public int ThrottleLimit { get; set; }
+
+ [Parameter]
+ public string Command { get; set; }
+
+ protected override void EndProcessing()
+ {
+ switch (this.ParameterSetName)
+ {
+ case "ComputerSet": WriteObject("Invoke-MyCommand with ComputerSet"); break;
+ case "VMSet": WriteObject("Invoke-MyCommand with VMSet"); break;
+ case "WebSocketSet": WriteObject("Invoke-MyCommand with WebSocketSet"); break;
+ default: break;
+ }
+ }
+ }
+
+ [Cmdlet("Test", "MyRemotingCSharp")]
+ public class TestMyRemotingCommand : PSCmdlet
+ {
+ // Replace one parameter with another one when the feature is turned on.
+ [Parameter("ExpTest.FeatureOne", ExperimentAction.Hide)]
+ public string SessionName { get; set; }
+
+ [Parameter("ExpTest.FeatureOne", ExperimentAction.Show)]
+ public string ComputerName { get; set; }
+
+ protected override void EndProcessing() { }
+ }
+
+ #endregion
+
+ #region "Use 'Experimental' attribute on parameters"
+
+ [Cmdlet("Save", "MyFileCSharp")]
+ public class SaveMyFileCommand : PSCmdlet
+ {
+ [Parameter(ParameterSetName = "UrlSet")]
+ public SwitchParameter ByUrl { get; set; }
+
+ [Parameter(ParameterSetName = "RadioSet")]
+ public SwitchParameter ByRadio { get; set; }
+
+ [Parameter]
+ public string FileName { get; set; }
+
+ [Experimental("ExpTest.FeatureOne", ExperimentAction.Show)]
+ [Parameter]
+ public string Destination { get; set; }
+
+ [Experimental("ExpTest.FeatureOne", ExperimentAction.Hide)]
+ [Parameter(ParameterSetName = "UrlSet")]
+ [Parameter(ParameterSetName = "RadioSet")]
+ public string Configuration { get; set; }
+
+ protected override void EndProcessing() { }
+ }
+
+ #endregion
+
+ #region "Dynamic parameters"
+
+ public class DynamicParamOne
+ {
+ [Parameter("ExpTest.FeatureOne", ExperimentAction.Show)]
+ [ValidateNotNullOrEmpty]
+ public string ConfigFile { get; set; }
+
+ [Parameter("ExpTest.FeatureOne", ExperimentAction.Hide)]
+ [ValidateNotNullOrEmpty]
+ public string ConfigName { get; set; }
+ }
+
+ [Cmdlet("Test", "MyDynamicParamOneCSharp")]
+ public class TestMyDynamicParamOneCommand : PSCmdlet, IDynamicParameters
+ {
+ [Parameter(Position = 0)]
+ public string Name { get; set; }
+
+ public object GetDynamicParameters()
+ {
+ return Name == "Joe" ? new DynamicParamOne() : null;
+ }
+
+ protected override void EndProcessing() { }
+ }
+
+ public class DynamicParamTwo
+ {
+ [Experimental("ExpTest.FeatureOne", ExperimentAction.Show)]
+ [Parameter()]
+ [ValidateNotNullOrEmpty]
+ public string ConfigFile { get; set; }
+
+ [Experimental("ExpTest.FeatureOne", ExperimentAction.Hide)]
+ [Parameter()]
+ [ValidateNotNullOrEmpty]
+ public string ConfigName { get; set; }
+ }
+
+ [Cmdlet("Test", "MyDynamicParamTwoCSharp")]
+ public class TestMyDynamicParamTwoCommand : PSCmdlet, IDynamicParameters
+ {
+ [Parameter(Position = 0)]
+ public string Name { get; set; }
+
+ public object GetDynamicParameters()
+ {
+ return Name == "Joe" ? new DynamicParamTwo() : null;
+ }
+
+ protected override void EndProcessing() { }
+ }
+
+ #endregion
+}
diff --git a/test/powershell/engine/ExperimentalFeature/assets/ExpTest/ExpTest.psd1 b/test/powershell/engine/ExperimentalFeature/assets/ExpTest/ExpTest.psd1
new file mode 100644
index 00000000000..ad613c7126e
--- /dev/null
+++ b/test/powershell/engine/ExperimentalFeature/assets/ExpTest/ExpTest.psd1
@@ -0,0 +1,43 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+# Module manifest for module 'ExpTest'
+
+@{
+
+# Version number of this module.
+ModuleVersion = '0.0.1'
+
+# Supported PSEditions
+CompatiblePSEditions = @('Core')
+
+# ID used to uniquely identify this module
+GUID = '109f75d1-38c1-46b3-8995-e80661ce822d'
+
+# Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export.
+FunctionsToExport = if ($EnabledExperimentalFeatures -contains "ExpTest.FeatureOne") {
+ 'Invoke-AzureFunctionV2', 'Get-GreetingMessage', 'Invoke-MyCommand', 'Test-MyRemoting', 'Save-MyFile', 'Test-MyDynamicParamOne', 'Test-MyDynamicParamTwo'
+} else {
+ 'Invoke-AzureFunction', 'Get-GreetingMessage', 'Invoke-MyCommand', 'Test-MyRemoting', 'Save-MyFile', 'Test-MyDynamicParamOne', 'Test-MyDynamicParamTwo'
+}
+
+# Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export.
+CmdletsToExport = 'Invoke-AzureFunctionCSharp', 'Get-GreetingMessageCSharp', 'Invoke-MyCommandCSharp', 'Test-MyRemotingCSharp', 'Save-MyFileCSharp', 'Test-MyDynamicParamOneCSharp', 'Test-MyDynamicParamTwoCSharp'
+
+# Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export.
+AliasesToExport = @(if ($EnabledExperimentalFeatures -contains "ExpTest.FeatureOne") { 'Invoke-AzureFunction' })
+
+# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess
+NestedModules = @('ExpTest.psm1', 'ExpTest.dll')
+
+# Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell.
+PrivateData = @{
+ PSData = @{
+ ExperimentalFeatures = @(
+ @{ Name = 'ExpTest.FeatureOne'; Description = "Test feature number one." }
+ @{ Name = 'ExpTest.FeatureTwo'; Description = "Test feature number two." }
+ )
+ }
+}
+
+}
diff --git a/test/powershell/engine/ExperimentalFeature/assets/ExpTest/ExpTest.psm1 b/test/powershell/engine/ExperimentalFeature/assets/ExpTest/ExpTest.psm1
new file mode 100644
index 00000000000..27c9864d824
--- /dev/null
+++ b/test/powershell/engine/ExperimentalFeature/assets/ExpTest/ExpTest.psm1
@@ -0,0 +1,206 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+using namespace System.Management.Automation
+
+#region "Replace existing function"
+
+function Invoke-AzureFunction
+{
+ [Experimental("ExpTest.FeatureOne", [ExperimentAction]::Hide)]
+ param(
+ [string] $Token,
+ [string] $Command
+ )
+
+ "Invoke-AzureFunction Version ONE"
+}
+
+function Invoke-AzureFunctionV2
+{
+ [Experimental("ExpTest.FeatureOne", [ExperimentAction]::Show)]
+ [Alias("Invoke-AzureFunction")]
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory)]
+ [string] $Token,
+
+ [Parameter(Mandatory)]
+ [string] $Command
+ )
+
+ "Invoke-AzureFunction Version TWO"
+}
+
+#endregion
+
+#region "Make parameter set experimental"
+
+function Get-GreetingMessage
+{
+ [CmdletBinding(DefaultParameterSetName = "Default")]
+ param(
+ [Parameter(Mandatory)]
+ [string] $Name,
+
+ ## If only one parameter attribute is declared for a parameter, then the parameter is
+ ## hidden when the parameter attribute needs to be hide.
+ [Parameter("ExpTest.FeatureOne", [ExperimentAction]::Show, ParameterSetName = "SwitchOneSet")]
+ [switch] $SwitchOne,
+
+ [Parameter("ExpTest.FeatureOne", [ExperimentAction]::Show, ParameterSetName = "SwitchTwoSet")]
+ [switch] $SwitchTwo
+ )
+
+ $message = "Hello World $Name."
+
+ if ([ExperimentalFeature]::IsEnabled("ExpTest.FeatureOne"))
+ {
+ if ($SwitchOne) { $message += "-SwitchOne is on." }
+ if ($SwitchTwo) { $message += "-SwitchTwo is on." }
+ }
+
+ Write-Output $message
+}
+
+function Invoke-MyCommand
+{
+ param(
+ [Parameter(Mandatory, ParameterSetName = "ComputerSet")]
+ [string] $UserName,
+ [Parameter(Mandatory, ParameterSetName = "ComputerSet")]
+ [string] $ComputerName,
+
+ [Parameter(Mandatory, ParameterSetName = "VMSet")]
+ [string] $VMName,
+
+ ## Enable web socket only if the feature is turned on.
+ [Parameter("ExpTest.FeatureOne", [ExperimentAction]::Show, Mandatory, ParameterSetName = "WebSocketSet")]
+ [string] $Token,
+ [Parameter("ExpTest.FeatureOne", [ExperimentAction]::Show, Mandatory, ParameterSetName = "WebSocketSet")]
+ [string] $WebSocketUrl,
+
+ ## Add -ConfigurationName to parameter set "WebSocketSet" only if the feature is turned on.
+ [Parameter(ParameterSetName = "ComputerSet")]
+ [Parameter("ExpTest.FeatureOne", [ExperimentAction]::Show, ParameterSetName = "WebSocketSet")]
+ [string] $ConfigurationName,
+
+ ## Add -Port to parameter set "WebSocketSet" only if the feature is turned on.
+ [Parameter(ParameterSetName = "VMSet")]
+ [Parameter("ExpTest.FeatureOne", [ExperimentAction]::Show, ParameterSetName = "WebSocketSet")]
+ [int] $Port,
+
+ [int] $ThrottleLimit,
+ [string] $Command
+ )
+
+ switch ($PSCmdlet.ParameterSetName)
+ {
+ "ComputerSet" { "Invoke-MyCommand with ComputerSet" }
+ "VMSet" { "Invoke-MyCommand with VMSet" }
+ "WebSocketSet" { "Invoke-MyCommand with WebSocketSet" }
+ }
+}
+
+function Test-MyRemoting
+{
+ param(
+ ## Replace one parameter with another one when the feature is turned on.
+ [Parameter("ExpTest.FeatureOne", [ExperimentAction]::Hide)]
+ [string] $SessionName,
+
+ [Parameter("ExpTest.FeatureOne", [ExperimentAction]::Show)]
+ [string] $ComputerName
+ )
+}
+
+#endregion
+
+#region "Use 'Experimental' attribute on parameters"
+
+function Save-MyFile
+{
+ param(
+ [Parameter(ParameterSetName = "UrlSet")]
+ [switch] $ByUrl,
+
+ [Parameter(ParameterSetName = "RadioSet")]
+ [switch] $ByRadio,
+
+ [string] $FileName,
+
+ [Experimental("ExpTest.FeatureOne", [ExperimentAction]::Show)]
+ [string] $Destination,
+
+ [Experimental("ExpTest.FeatureOne", [ExperimentAction]::Hide)]
+ [Parameter(ParameterSetName = "UrlSet")]
+ [Parameter(ParameterSetName = "RadioSet")]
+ [string] $Configuration
+ )
+}
+
+#endregion
+
+#region "Dynamic parameters"
+
+function Test-MyDynamicParamOne
+{
+ [CmdletBinding()]
+ param(
+ [string] $Name
+ )
+
+ ## Use the parameter attribute to hide or show a dynamic parameter.
+ DynamicParam {
+ if ($Name -eq "Joe") {
+ $runtimeParams = [RuntimeDefinedParameterDictionary]::new()
+
+ $configFileAttributes = [System.Collections.ObjectModel.Collection[Attribute]]::new()
+ $configFileAttributes.Add([Parameter]::new("ExpTest.FeatureOne", [ExperimentAction]::Show))
+ $configFileAttributes.Add([ValidateNotNullOrEmpty]::new())
+ $configFileParam = [RuntimeDefinedParameter]::new("ConfigFile", [string], $configFileAttributes)
+
+ $configNameAttributes = [System.Collections.ObjectModel.Collection[Attribute]]::new()
+ $configNameAttributes.Add([Parameter]::new("ExpTest.FeatureOne", [ExperimentAction]::Hide))
+ $configNameAttributes.Add([ValidateNotNullOrEmpty]::new())
+ $ConfigNameParam = [RuntimeDefinedParameter]::new("ConfigName", [string], $configNameAttributes)
+
+ $runtimeParams.Add("ConfigFile", $configFileParam)
+ $runtimeParams.Add("ConfigName", $ConfigNameParam)
+ return $runtimeParams
+ }
+ }
+}
+
+function Test-MyDynamicParamTwo
+{
+ [CmdletBinding()]
+ param(
+ [string] $Name
+ )
+
+ ## Use the experimental attribute to hide or show a dynamic parameter.
+ DynamicParam {
+ if ($Name -eq "Joe") {
+ $runtimeParams = [RuntimeDefinedParameterDictionary]::new()
+
+ $configFileAttributes = [System.Collections.ObjectModel.Collection[Attribute]]::new()
+ $configFileAttributes.Add([Experimental]::new("ExpTest.FeatureOne", [ExperimentAction]::Show))
+ $configFileAttributes.Add([Parameter]::new())
+ $configFileAttributes.Add([ValidateNotNullOrEmpty]::new())
+ $configFileParam = [RuntimeDefinedParameter]::new("ConfigFile", [string], $configFileAttributes)
+
+ $configNameAttributes = [System.Collections.ObjectModel.Collection[Attribute]]::new()
+ $configNameAttributes.Add([Experimental]::new("ExpTest.FeatureOne", [ExperimentAction]::Hide))
+ $configNameAttributes.Add([Parameter]::new())
+ $configNameAttributes.Add([ValidateNotNullOrEmpty]::new())
+ $ConfigNameParam = [RuntimeDefinedParameter]::new("ConfigName", [string], $configNameAttributes)
+
+ $runtimeParams.Add("ConfigFile", $configFileParam)
+ $runtimeParams.Add("ConfigName", $ConfigNameParam)
+ return $runtimeParams
+ }
+ }
+}
+
+#endregion
diff --git a/test/powershell/engine/Help/HelpSystem.Tests.ps1 b/test/powershell/engine/Help/HelpSystem.Tests.ps1
index dbb42f02d09..7b22e1e84e5 100644
--- a/test/powershell/engine/Help/HelpSystem.Tests.ps1
+++ b/test/powershell/engine/Help/HelpSystem.Tests.ps1
@@ -10,7 +10,8 @@ $script:cmdletsToSkip = @(
"New-PSRoleCapabilityFile",
"Get-PSSessionCapability",
"Disable-PSRemoting", # Content not available: Issue # https://github.com/PowerShell/PowerShell-Docs/issues/1790
- "Enable-PSRemoting"
+ "Enable-PSRemoting",
+ "Get-ExperimentalFeature"
)
function UpdateHelpFromLocalContentPath {
diff --git a/tools/ResxGen/ResxGen.ps1 b/tools/ResxGen/ResxGen.ps1
index ab67f16e7fa..af43c496e79 100755
--- a/tools/ResxGen/ResxGen.ps1
+++ b/tools/ResxGen/ResxGen.ps1
@@ -1,5 +1,6 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
+
<#
.SYNOPSIS
Generates a resx file and code file from an ETW manifest
@@ -23,7 +24,7 @@
The path to the directory to use to create the C# code file.
.EXAMPLE
- .\ResxGen.ps1 -Manifest ./PowerShell-Core-Instrumentation.man -ResxPath ../../src/System.Management.Automation\resources -CodePath ../../src/System.Management.Automation/CoreCLR
+ .\tools\ResxGen\ResxGen.ps1 -Manifest .\src\PowerShell.Core.Instrumentation\PowerShell.Core.Instrumentation.man -ResxPath .\src\System.Management.Automation\resources -CodePath .\src\System.Management.Automation\CoreCLR
#>
[CmdletBinding()]
param
@@ -48,7 +49,7 @@ param
)
-Import-Module .\ResxGen.psm1 -Force
+Import-Module $PSScriptRoot\ResxGen.psm1 -Force
try
{
ConvertTo-Resx -Manifest $Manifest -Name $Name -ResxPath $ResxPath -CodePath $CodePath -Namespace $Namespace