From 5f00798223cf8c2072f26b62eb764cd2a686b836 Mon Sep 17 00:00:00 2001 From: Justin Chung <124807742+jshigetomi@users.noreply.github.com> Date: Wed, 19 Nov 2025 16:10:28 -0600 Subject: [PATCH 1/6] Add experimental feature check and add PSContentPath to standard platform paths --- src/code/Utils.cs | 147 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 143 insertions(+), 4 deletions(-) diff --git a/src/code/Utils.cs b/src/code/Utils.cs index ae540f9e4..4570bbf84 100644 --- a/src/code/Utils.cs +++ b/src/code/Utils.cs @@ -1049,14 +1049,14 @@ public static List GetPathsFromEnvVarAndScope( { GetStandardPlatformPaths( psCmdlet, - out string myDocumentsPath, + out string psUserContentPath, out string programFilesPath); List resourcePaths = new List(); if (scope is null || scope.Value is ScopeType.CurrentUser) { - resourcePaths.Add(Path.Combine(myDocumentsPath, "Modules")); - resourcePaths.Add(Path.Combine(myDocumentsPath, "Scripts")); + resourcePaths.Add(Path.Combine(psUserContentPath, "Modules")); + resourcePaths.Add(Path.Combine(psUserContentPath, "Scripts")); } if (scope.Value is ScopeType.AllUsers) @@ -1156,6 +1156,112 @@ private static string GetHomeOrCreateTempHome() } private readonly static Version PSVersion6 = new Version(6, 0); + + /// + /// Checks if a PowerShell experimental feature is enabled by reading the PowerShell configuration file. + /// Returns false if the configuration file doesn't exist or if the feature is not enabled. + /// + private static bool IsExperimentalFeatureEnabled(PSCmdlet psCmdlet, string featureName) + { + try + { + // PowerShell configuration file location + string configPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "powershell", + "powershell.config.json" + ); + + if (!File.Exists(configPath)) + { + psCmdlet.WriteVerbose("PowerShell configuration file not found, experimental features not enabled"); + return false; + } + + string jsonContent = File.ReadAllText(configPath); + + // Parse JSON to check for experimental features + // Look for "ExperimentalFeatures": ["FeatureName"] in the config + if (jsonContent.Contains($"\"{featureName}\"")) + { + psCmdlet.WriteVerbose(string.Format("Experimental feature '{0}' found in configuration file", featureName)); + return true; + } + + psCmdlet.WriteVerbose(string.Format("Experimental feature '{0}' not found in configuration file", featureName)); + return false; + } + catch (Exception ex) + { + psCmdlet.WriteVerbose(string.Format("Error reading PowerShell configuration file: {0}", ex.Message)); + return false; + } + } + + /// + /// Gets the custom PSUserContentPath from environment variable or PowerShell configuration file. + /// Environment variable takes precedence over the configuration file setting. + /// Returns null if neither is set or configured. + /// + private static string GetPSUserContentPath(PSCmdlet psCmdlet) + { + try + { + // First check the environment variable (takes precedence) + string envPSUserContentPath = Environment.GetEnvironmentVariable("PSUserContentPath"); + if (!string.IsNullOrEmpty(envPSUserContentPath)) + { + psCmdlet.WriteVerbose(string.Format("Found PSUserContentPath from environment variable: {0}", envPSUserContentPath)); + return envPSUserContentPath; + } + + // If environment variable not set, check the configuration file + string configPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "powershell", + "powershell.config.json" + ); + + if (!File.Exists(configPath)) + { + psCmdlet.WriteVerbose("PowerShell configuration file not found"); + return null; + } + + string jsonContent = File.ReadAllText(configPath); + + // Simple JSON parsing to find PSUserContentPath + // Format: "PSUserContentPath": "C:\\CustomPath" + int userPathIndex = jsonContent.IndexOf("\"PSUserContentPath\"", StringComparison.OrdinalIgnoreCase); + if (userPathIndex >= 0) + { + int colonIndex = jsonContent.IndexOf(':', userPathIndex); + if (colonIndex >= 0) + { + int firstQuote = jsonContent.IndexOf('"', colonIndex + 1); + int secondQuote = jsonContent.IndexOf('"', firstQuote + 1); + + if (firstQuote >= 0 && secondQuote > firstQuote) + { + string customPath = jsonContent.Substring(firstQuote + 1, secondQuote - firstQuote - 1); + // Unescape JSON string (handle \\) + customPath = customPath.Replace("\\\\", "\\"); + psCmdlet.WriteVerbose(string.Format("Found PSUserContentPath in config file: {0}", customPath)); + return customPath; + } + } + } + + psCmdlet.WriteVerbose("PSUserContentPath not configured in PowerShell configuration file or environment variable"); + return null; + } + catch (Exception ex) + { + psCmdlet.WriteVerbose(string.Format("Error reading PSUserContentPath: {0}", ex.Message)); + return null; + } + } + private static void GetStandardPlatformPaths( PSCmdlet psCmdlet, out string localUserDir, @@ -1164,7 +1270,40 @@ private static void GetStandardPlatformPaths( if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { string powerShellType = (psCmdlet.Host.Version >= PSVersion6) ? "PowerShell" : "WindowsPowerShell"; - localUserDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), powerShellType); + + // Check if PSContentPath experimental feature is enabled + bool usePSContentPath = IsExperimentalFeatureEnabled(psCmdlet, "PSContentPath"); + + if (usePSContentPath) + { + psCmdlet.WriteVerbose("PSContentPath experimental feature is enabled"); + + // Check environment variable and config file for custom PSUserContentPath + string customPSUserContentPath = GetPSUserContentPath(psCmdlet); + + if (!string.IsNullOrEmpty(customPSUserContentPath) && Directory.Exists(customPSUserContentPath)) + { + // Use custom configured path + localUserDir = customPSUserContentPath; + psCmdlet.WriteVerbose($"Using custom PSUserContentPath: {localUserDir}"); + } + else + { + // Use default LocalApplicationData location when PSContentPath is enabled + localUserDir = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + powerShellType + ); + psCmdlet.WriteVerbose($"Using default PSContentPath location: {localUserDir}"); + } + } + else + { + // PSContentPath not enabled, use legacy Documents folder + localUserDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), powerShellType); + psCmdlet.WriteVerbose($"Using legacy Documents folder: {localUserDir}"); + } + allUsersDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), powerShellType); } else From 7d033768ea063f0e05d9fdaa6b137e8b54276a7a Mon Sep 17 00:00:00 2001 From: Justin Chung <124807742+jshigetomi@users.noreply.github.com> Date: Wed, 19 Nov 2025 17:07:26 -0600 Subject: [PATCH 2/6] Use Json.net to parse instead --- src/code/Utils.cs | 40 +++++++++++++++++----------------------- 1 file changed, 17 insertions(+), 23 deletions(-) diff --git a/src/code/Utils.cs b/src/code/Utils.cs index 4570bbf84..0c9993ce6 100644 --- a/src/code/Utils.cs +++ b/src/code/Utils.cs @@ -1179,13 +1179,20 @@ private static bool IsExperimentalFeatureEnabled(PSCmdlet psCmdlet, string featu } string jsonContent = File.ReadAllText(configPath); + var config = Newtonsoft.Json.Linq.JObject.Parse(jsonContent); - // Parse JSON to check for experimental features // Look for "ExperimentalFeatures": ["FeatureName"] in the config - if (jsonContent.Contains($"\"{featureName}\"")) + var experimentalFeatures = config["ExperimentalFeatures"] as Newtonsoft.Json.Linq.JArray; + if (experimentalFeatures != null) { - psCmdlet.WriteVerbose(string.Format("Experimental feature '{0}' found in configuration file", featureName)); - return true; + foreach (var feature in experimentalFeatures) + { + if (string.Equals(feature.ToString(), featureName, StringComparison.OrdinalIgnoreCase)) + { + psCmdlet.WriteVerbose(string.Format("Experimental feature '{0}' found in configuration file", featureName)); + return true; + } + } } psCmdlet.WriteVerbose(string.Format("Experimental feature '{0}' not found in configuration file", featureName)); @@ -1229,27 +1236,14 @@ private static string GetPSUserContentPath(PSCmdlet psCmdlet) } string jsonContent = File.ReadAllText(configPath); + var config = Newtonsoft.Json.Linq.JObject.Parse(jsonContent); - // Simple JSON parsing to find PSUserContentPath - // Format: "PSUserContentPath": "C:\\CustomPath" - int userPathIndex = jsonContent.IndexOf("\"PSUserContentPath\"", StringComparison.OrdinalIgnoreCase); - if (userPathIndex >= 0) + // Look for PSUserContentPath in the config + var psUserContentPath = config["PSUserContentPath"]?.ToString(); + if (!string.IsNullOrEmpty(psUserContentPath)) { - int colonIndex = jsonContent.IndexOf(':', userPathIndex); - if (colonIndex >= 0) - { - int firstQuote = jsonContent.IndexOf('"', colonIndex + 1); - int secondQuote = jsonContent.IndexOf('"', firstQuote + 1); - - if (firstQuote >= 0 && secondQuote > firstQuote) - { - string customPath = jsonContent.Substring(firstQuote + 1, secondQuote - firstQuote - 1); - // Unescape JSON string (handle \\) - customPath = customPath.Replace("\\\\", "\\"); - psCmdlet.WriteVerbose(string.Format("Found PSUserContentPath in config file: {0}", customPath)); - return customPath; - } - } + psCmdlet.WriteVerbose(string.Format("Found PSUserContentPath in config file: {0}", psUserContentPath)); + return psUserContentPath; } psCmdlet.WriteVerbose("PSUserContentPath not configured in PowerShell configuration file or environment variable"); From bbb71488df2a2016e821176249161db06a358aff Mon Sep 17 00:00:00 2001 From: Justin Chung <124807742+jshigetomi@users.noreply.github.com> Date: Wed, 19 Nov 2025 17:29:10 -0600 Subject: [PATCH 3/6] Add support in Linux, MacOS and refactor --- src/code/Utils.cs | 132 ++++++++++++++++++++++++++++++---------------- 1 file changed, 86 insertions(+), 46 deletions(-) diff --git a/src/code/Utils.cs b/src/code/Utils.cs index 0c9993ce6..14ce9a6c1 100644 --- a/src/code/Utils.cs +++ b/src/code/Utils.cs @@ -22,6 +22,7 @@ using Azure.Identity; using System.Text.RegularExpressions; using System.Threading; +using System.Text.Json; using System.Threading.Tasks; using System.Xml; @@ -1157,6 +1158,42 @@ private static string GetHomeOrCreateTempHome() private readonly static Version PSVersion6 = new Version(6, 0); + /// + /// Gets the user content directory path based on PSContentPath experimental feature settings. + /// Checks if PSContentPath is enabled and returns the appropriate path (custom, default, or legacy). + /// + private static string GetUserContentPath(PSCmdlet psCmdlet, string defaultPSContentPath, string legacyPath) + { + bool usePSContentPath = IsExperimentalFeatureEnabled(psCmdlet, "PSContentPath"); + + if (usePSContentPath) + { + psCmdlet.WriteVerbose("PSContentPath experimental feature is enabled"); + + // Check environment variable and config file for custom PSUserContentPath + string customPSUserContentPath = GetPSUserContentPath(psCmdlet); + + if (!string.IsNullOrEmpty(customPSUserContentPath) && Directory.Exists(customPSUserContentPath)) + { + // Use custom configured path + psCmdlet.WriteVerbose($"Using custom PSUserContentPath: {customPSUserContentPath}"); + return customPSUserContentPath; + } + else + { + // Use default PSContentPath location when feature is enabled + psCmdlet.WriteVerbose($"Using default PSContentPath location: {defaultPSContentPath}"); + return defaultPSContentPath; + } + } + else + { + // PSContentPath not enabled, use legacy location + psCmdlet.WriteVerbose($"Using legacy location: {legacyPath}"); + return legacyPath; + } + } + /// /// Checks if a PowerShell experimental feature is enabled by reading the PowerShell configuration file. /// Returns false if the configuration file doesn't exist or if the feature is not enabled. @@ -1179,18 +1216,19 @@ private static bool IsExperimentalFeatureEnabled(PSCmdlet psCmdlet, string featu } string jsonContent = File.ReadAllText(configPath); - var config = Newtonsoft.Json.Linq.JObject.Parse(jsonContent); - - // Look for "ExperimentalFeatures": ["FeatureName"] in the config - var experimentalFeatures = config["ExperimentalFeatures"] as Newtonsoft.Json.Linq.JArray; - if (experimentalFeatures != null) + using (var jsonDoc = JsonDocument.Parse(jsonContent)) { - foreach (var feature in experimentalFeatures) + // Look for "ExperimentalFeatures": ["FeatureName"] in the config + if (jsonDoc.RootElement.TryGetProperty("ExperimentalFeatures", out var experimentalFeatures) && + experimentalFeatures.ValueKind == JsonValueKind.Array) { - if (string.Equals(feature.ToString(), featureName, StringComparison.OrdinalIgnoreCase)) + foreach (var feature in experimentalFeatures.EnumerateArray()) { - psCmdlet.WriteVerbose(string.Format("Experimental feature '{0}' found in configuration file", featureName)); - return true; + if (string.Equals(feature.GetString(), featureName, StringComparison.OrdinalIgnoreCase)) + { + psCmdlet.WriteVerbose(string.Format("Experimental feature '{0}' found in configuration file", featureName)); + return true; + } } } } @@ -1236,14 +1274,18 @@ private static string GetPSUserContentPath(PSCmdlet psCmdlet) } string jsonContent = File.ReadAllText(configPath); - var config = Newtonsoft.Json.Linq.JObject.Parse(jsonContent); - - // Look for PSUserContentPath in the config - var psUserContentPath = config["PSUserContentPath"]?.ToString(); - if (!string.IsNullOrEmpty(psUserContentPath)) + using (var jsonDoc = JsonDocument.Parse(jsonContent)) { - psCmdlet.WriteVerbose(string.Format("Found PSUserContentPath in config file: {0}", psUserContentPath)); - return psUserContentPath; + // Look for PSUserContentPath in the config + if (jsonDoc.RootElement.TryGetProperty("PSUserContentPath", out var pathElement)) + { + string psUserContentPath = pathElement.GetString(); + if (!string.IsNullOrEmpty(psUserContentPath)) + { + psCmdlet.WriteVerbose(string.Format("Found PSUserContentPath in config file: {0}", psUserContentPath)); + return psUserContentPath; + } + } } psCmdlet.WriteVerbose("PSUserContentPath not configured in PowerShell configuration file or environment variable"); @@ -1265,37 +1307,25 @@ private static void GetStandardPlatformPaths( { string powerShellType = (psCmdlet.Host.Version >= PSVersion6) ? "PowerShell" : "WindowsPowerShell"; - // Check if PSContentPath experimental feature is enabled - bool usePSContentPath = IsExperimentalFeatureEnabled(psCmdlet, "PSContentPath"); - - if (usePSContentPath) + // Windows PowerShell doesn't support experimental features or PSContentPath + if (powerShellType == "WindowsPowerShell") { - psCmdlet.WriteVerbose("PSContentPath experimental feature is enabled"); - - // Check environment variable and config file for custom PSUserContentPath - string customPSUserContentPath = GetPSUserContentPath(psCmdlet); - - if (!string.IsNullOrEmpty(customPSUserContentPath) && Directory.Exists(customPSUserContentPath)) - { - // Use custom configured path - localUserDir = customPSUserContentPath; - psCmdlet.WriteVerbose($"Using custom PSUserContentPath: {localUserDir}"); - } - else - { - // Use default LocalApplicationData location when PSContentPath is enabled - localUserDir = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), - powerShellType - ); - psCmdlet.WriteVerbose($"Using default PSContentPath location: {localUserDir}"); - } + // Use legacy Documents folder for Windows PowerShell + localUserDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), powerShellType); + psCmdlet.WriteVerbose($"Using Windows PowerShell Documents folder: {localUserDir}"); } else { - // PSContentPath not enabled, use legacy Documents folder - localUserDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), powerShellType); - psCmdlet.WriteVerbose($"Using legacy Documents folder: {localUserDir}"); + string defaultPSContentPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + powerShellType + ); + string legacyPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), + powerShellType + ); + + localUserDir = GetUserContentPath(psCmdlet, defaultPSContentPath, legacyPath); } allUsersDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), powerShellType); @@ -1303,14 +1333,24 @@ private static void GetStandardPlatformPaths( else { // paths are the same for both Linux and macOS - localUserDir = Path.Combine(GetHomeOrCreateTempHome(), ".local", "share", "powershell"); - // Create the default data directory if it doesn't exist. + string xdgDataHome = Environment.GetEnvironmentVariable("XDG_DATA_HOME"); + if (string.IsNullOrEmpty(xdgDataHome)) + { + xdgDataHome = Path.Combine(GetHomeOrCreateTempHome(), ".local", "share"); + } + + string defaultPSContentPath = Path.Combine(xdgDataHome, "powershell"); + string legacyPath = Path.Combine(GetHomeOrCreateTempHome(), ".local", "share", "powershell"); + + localUserDir = GetUserContentPath(psCmdlet, defaultPSContentPath, legacyPath); + + // Create the default data directory if it doesn't exist if (!Directory.Exists(localUserDir)) { Directory.CreateDirectory(localUserDir); } - allUsersDir = System.IO.Path.Combine("/usr", "local", "share", "powershell"); + allUsersDir = Path.Combine("/usr", "local", "share", "powershell"); } } From d53dd080e74b8124996ec71164fd0015b45c8555 Mon Sep 17 00:00:00 2001 From: Justin Chung <124807742+jshigetomi@users.noreply.github.com> Date: Thu, 20 Nov 2025 12:47:36 -0600 Subject: [PATCH 4/6] Use PowerShell GetPSModulePath API instead --- src/code/Utils.cs | 180 ++++++++++++---------------------------------- 1 file changed, 46 insertions(+), 134 deletions(-) diff --git a/src/code/Utils.cs b/src/code/Utils.cs index 14ce9a6c1..5fb5ba724 100644 --- a/src/code/Utils.cs +++ b/src/code/Utils.cs @@ -1157,145 +1157,62 @@ private static string GetHomeOrCreateTempHome() } private readonly static Version PSVersion6 = new Version(6, 0); + private readonly static Version PSVersion7_7 = new Version(7, 7, 0); /// - /// Gets the user content directory path based on PSContentPath experimental feature settings. - /// Checks if PSContentPath is enabled and returns the appropriate path (custom, default, or legacy). + /// Gets the user content directory path using PowerShell's GetPSModulePath API. + /// Falls back to legacy path if the API is not available or PowerShell version is below 7.7.0. /// - private static string GetUserContentPath(PSCmdlet psCmdlet, string defaultPSContentPath, string legacyPath) + private static string GetUserContentPath(PSCmdlet psCmdlet, string legacyPath) { - bool usePSContentPath = IsExperimentalFeatureEnabled(psCmdlet, "PSContentPath"); - - if (usePSContentPath) - { - psCmdlet.WriteVerbose("PSContentPath experimental feature is enabled"); - - // Check environment variable and config file for custom PSUserContentPath - string customPSUserContentPath = GetPSUserContentPath(psCmdlet); - - if (!string.IsNullOrEmpty(customPSUserContentPath) && Directory.Exists(customPSUserContentPath)) - { - // Use custom configured path - psCmdlet.WriteVerbose($"Using custom PSUserContentPath: {customPSUserContentPath}"); - return customPSUserContentPath; - } - else - { - // Use default PSContentPath location when feature is enabled - psCmdlet.WriteVerbose($"Using default PSContentPath location: {defaultPSContentPath}"); - return defaultPSContentPath; - } - } - else - { - // PSContentPath not enabled, use legacy location - psCmdlet.WriteVerbose($"Using legacy location: {legacyPath}"); - return legacyPath; - } - } + // Get PowerShell engine version from $PSVersionTable.PSVersion + Version psVersion = psCmdlet.SessionState.PSVariable.GetValue("PSVersionTable") is Hashtable versionTable + && versionTable["PSVersion"] is Version version + ? version + : new Version(5, 1); - /// - /// Checks if a PowerShell experimental feature is enabled by reading the PowerShell configuration file. - /// Returns false if the configuration file doesn't exist or if the feature is not enabled. - /// - private static bool IsExperimentalFeatureEnabled(PSCmdlet psCmdlet, string featureName) - { - try + // Only use GetPSModulePath API if PowerShell version is 7.7.0 or greater (when PSContentPath feature is available) + if (psVersion >= PSVersion7_7) { - // PowerShell configuration file location - string configPath = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), - "powershell", - "powershell.config.json" - ); - - if (!File.Exists(configPath)) - { - psCmdlet.WriteVerbose("PowerShell configuration file not found, experimental features not enabled"); - return false; - } - - string jsonContent = File.ReadAllText(configPath); - using (var jsonDoc = JsonDocument.Parse(jsonContent)) + // Try to use PowerShell's GetPSModulePath API via reflection + // This API automatically respects PSContentPath settings + try { - // Look for "ExperimentalFeatures": ["FeatureName"] in the config - if (jsonDoc.RootElement.TryGetProperty("ExperimentalFeatures", out var experimentalFeatures) && - experimentalFeatures.ValueKind == JsonValueKind.Array) + var moduleIntrinsicsType = typeof(PSModuleInfo).Assembly.GetType("System.Management.Automation.ModuleIntrinsics"); + var scopeEnumType = typeof(PSModuleInfo).Assembly.GetType("System.Management.Automation.PSModulePathScope"); + + if (moduleIntrinsicsType != null && scopeEnumType != null) { - foreach (var feature in experimentalFeatures.EnumerateArray()) + var getPSModulePathMethod = moduleIntrinsicsType.GetMethod("GetPSModulePath", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static); + + if (getPSModulePathMethod != null) { - if (string.Equals(feature.GetString(), featureName, StringComparison.OrdinalIgnoreCase)) + // PSModulePathScope.User = 0 + object userScope = Enum.ToObject(scopeEnumType, 0); + string userModulePath = (string)getPSModulePathMethod.Invoke(null, new object[] { userScope }); + + if (!string.IsNullOrEmpty(userModulePath)) { - psCmdlet.WriteVerbose(string.Format("Experimental feature '{0}' found in configuration file", featureName)); - return true; + string userContentPath = Path.GetDirectoryName(userModulePath); + psCmdlet.WriteVerbose($"User content path from GetPSModulePath API: {userContentPath}"); + return userContentPath; } } } } - - psCmdlet.WriteVerbose(string.Format("Experimental feature '{0}' not found in configuration file", featureName)); - return false; - } - catch (Exception ex) - { - psCmdlet.WriteVerbose(string.Format("Error reading PowerShell configuration file: {0}", ex.Message)); - return false; - } - } - - /// - /// Gets the custom PSUserContentPath from environment variable or PowerShell configuration file. - /// Environment variable takes precedence over the configuration file setting. - /// Returns null if neither is set or configured. - /// - private static string GetPSUserContentPath(PSCmdlet psCmdlet) - { - try - { - // First check the environment variable (takes precedence) - string envPSUserContentPath = Environment.GetEnvironmentVariable("PSUserContentPath"); - if (!string.IsNullOrEmpty(envPSUserContentPath)) - { - psCmdlet.WriteVerbose(string.Format("Found PSUserContentPath from environment variable: {0}", envPSUserContentPath)); - return envPSUserContentPath; - } - - // If environment variable not set, check the configuration file - string configPath = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), - "powershell", - "powershell.config.json" - ); - - if (!File.Exists(configPath)) - { - psCmdlet.WriteVerbose("PowerShell configuration file not found"); - return null; - } - - string jsonContent = File.ReadAllText(configPath); - using (var jsonDoc = JsonDocument.Parse(jsonContent)) + catch (Exception ex) { - // Look for PSUserContentPath in the config - if (jsonDoc.RootElement.TryGetProperty("PSUserContentPath", out var pathElement)) - { - string psUserContentPath = pathElement.GetString(); - if (!string.IsNullOrEmpty(psUserContentPath)) - { - psCmdlet.WriteVerbose(string.Format("Found PSUserContentPath in config file: {0}", psUserContentPath)); - return psUserContentPath; - } - } + psCmdlet.WriteVerbose($"GetPSModulePath API not available: {ex.Message}"); } - - psCmdlet.WriteVerbose("PSUserContentPath not configured in PowerShell configuration file or environment variable"); - return null; } - catch (Exception ex) + else { - psCmdlet.WriteVerbose(string.Format("Error reading PSUserContentPath: {0}", ex.Message)); - return null; + psCmdlet.WriteVerbose($"PowerShell version {psVersion} is below 7.7.0, using legacy location"); } + + // Fallback to legacy location + psCmdlet.WriteVerbose($"Using legacy location: {legacyPath}"); + return legacyPath; } private static void GetStandardPlatformPaths( @@ -1305,7 +1222,13 @@ private static void GetStandardPlatformPaths( { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - string powerShellType = (psCmdlet.Host.Version >= PSVersion6) ? "PowerShell" : "WindowsPowerShell"; + // Get PowerShell engine version from $PSVersionTable.PSVersion + Version psVersion = psCmdlet.SessionState.PSVariable.GetValue("PSVersionTable") is Hashtable versionTable + && versionTable["PSVersion"] is Version version + ? version + : new Version(5, 1); // Default to Windows PowerShell version if unable to determine + + string powerShellType = (psVersion >= PSVersion6) ? "PowerShell" : "WindowsPowerShell"; // Windows PowerShell doesn't support experimental features or PSContentPath if (powerShellType == "WindowsPowerShell") @@ -1316,16 +1239,12 @@ private static void GetStandardPlatformPaths( } else { - string defaultPSContentPath = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), - powerShellType - ); string legacyPath = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), powerShellType ); - localUserDir = GetUserContentPath(psCmdlet, defaultPSContentPath, legacyPath); + localUserDir = GetUserContentPath(psCmdlet, legacyPath); } allUsersDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), powerShellType); @@ -1333,16 +1252,9 @@ private static void GetStandardPlatformPaths( else { // paths are the same for both Linux and macOS - string xdgDataHome = Environment.GetEnvironmentVariable("XDG_DATA_HOME"); - if (string.IsNullOrEmpty(xdgDataHome)) - { - xdgDataHome = Path.Combine(GetHomeOrCreateTempHome(), ".local", "share"); - } - - string defaultPSContentPath = Path.Combine(xdgDataHome, "powershell"); string legacyPath = Path.Combine(GetHomeOrCreateTempHome(), ".local", "share", "powershell"); - localUserDir = GetUserContentPath(psCmdlet, defaultPSContentPath, legacyPath); + localUserDir = GetUserContentPath(psCmdlet, legacyPath); // Create the default data directory if it doesn't exist if (!Directory.Exists(localUserDir)) From a454cb389e0dfcad46e3db7b6e420c38dda8bedb Mon Sep 17 00:00:00 2001 From: Justin Chung <124807742+jshigetomi@users.noreply.github.com> Date: Thu, 20 Nov 2025 15:05:50 -0600 Subject: [PATCH 5/6] Use runspace instead of reflection --- src/code/Utils.cs | 41 ++++++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/src/code/Utils.cs b/src/code/Utils.cs index 5fb5ba724..278d71c02 100644 --- a/src/code/Utils.cs +++ b/src/code/Utils.cs @@ -1160,8 +1160,8 @@ private static string GetHomeOrCreateTempHome() private readonly static Version PSVersion7_7 = new Version(7, 7, 0); /// - /// Gets the user content directory path using PowerShell's GetPSModulePath API. - /// Falls back to legacy path if the API is not available or PowerShell version is below 7.7.0. + /// Gets the user content directory path using PowerShell's Get-PSContentPath cmdlet. + /// Falls back to legacy path if the cmdlet is not available or PowerShell version is below 7.7.0. /// private static string GetUserContentPath(PSCmdlet psCmdlet, string legacyPath) { @@ -1171,38 +1171,41 @@ private static string GetUserContentPath(PSCmdlet psCmdlet, string legacyPath) ? version : new Version(5, 1); - // Only use GetPSModulePath API if PowerShell version is 7.7.0 or greater (when PSContentPath feature is available) + // Only use Get-PSContentPath cmdlet if PowerShell version is 7.7.0 or greater (when PSContentPath feature is available) if (psVersion >= PSVersion7_7) { - // Try to use PowerShell's GetPSModulePath API via reflection - // This API automatically respects PSContentPath settings + // Try to use PowerShell's Get-PSContentPath cmdlet + // This cmdlet automatically respects PSContentPath settings try { - var moduleIntrinsicsType = typeof(PSModuleInfo).Assembly.GetType("System.Management.Automation.ModuleIntrinsics"); - var scopeEnumType = typeof(PSModuleInfo).Assembly.GetType("System.Management.Automation.PSModulePathScope"); - - if (moduleIntrinsicsType != null && scopeEnumType != null) + using (System.Management.Automation.PowerShell pwsh = System.Management.Automation.PowerShell.Create()) { - var getPSModulePathMethod = moduleIntrinsicsType.GetMethod("GetPSModulePath", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static); + pwsh.AddCommand("Get-PSContentPath"); + var results = pwsh.Invoke(); - if (getPSModulePathMethod != null) + if (!pwsh.HadErrors && results != null && results.Count > 0) { - // PSModulePathScope.User = 0 - object userScope = Enum.ToObject(scopeEnumType, 0); - string userModulePath = (string)getPSModulePathMethod.Invoke(null, new object[] { userScope }); - - if (!string.IsNullOrEmpty(userModulePath)) + // Get-PSContentPath returns a PSObject, extract the path string + string userContentPath = results[0]?.ToString(); + if (!string.IsNullOrEmpty(userContentPath)) { - string userContentPath = Path.GetDirectoryName(userModulePath); - psCmdlet.WriteVerbose($"User content path from GetPSModulePath API: {userContentPath}"); + psCmdlet.WriteVerbose($"User content path from Get-PSContentPath: {userContentPath}"); return userContentPath; } } + + if (pwsh.HadErrors) + { + foreach (var error in pwsh.Streams.Error) + { + psCmdlet.WriteVerbose($"Get-PSContentPath error: {error}"); + } + } } } catch (Exception ex) { - psCmdlet.WriteVerbose($"GetPSModulePath API not available: {ex.Message}"); + psCmdlet.WriteVerbose($"Get-PSContentPath cmdlet not available: {ex.Message}"); } } else From 94a156386c7e00e636b265600a6d08110e5a5772 Mon Sep 17 00:00:00 2001 From: Justin Chung <124807742+jshigetomi@users.noreply.github.com> Date: Thu, 20 Nov 2025 20:44:14 -0600 Subject: [PATCH 6/6] Use current runspace and only get PSVersion once --- src/code/Utils.cs | 62 +++++++++++++++++++---------------------------- 1 file changed, 25 insertions(+), 37 deletions(-) diff --git a/src/code/Utils.cs b/src/code/Utils.cs index 278d71c02..d8b61757f 100644 --- a/src/code/Utils.cs +++ b/src/code/Utils.cs @@ -1157,49 +1157,32 @@ private static string GetHomeOrCreateTempHome() } private readonly static Version PSVersion6 = new Version(6, 0); - private readonly static Version PSVersion7_7 = new Version(7, 7, 0); + private readonly static Version PSVersion7_7 = new Version(7, 7); /// /// Gets the user content directory path using PowerShell's Get-PSContentPath cmdlet. /// Falls back to legacy path if the cmdlet is not available or PowerShell version is below 7.7.0. /// - private static string GetUserContentPath(PSCmdlet psCmdlet, string legacyPath) + private static string GetUserContentPath(PSCmdlet psCmdlet, Version psVersion, string legacyPath) { - // Get PowerShell engine version from $PSVersionTable.PSVersion - Version psVersion = psCmdlet.SessionState.PSVariable.GetValue("PSVersionTable") is Hashtable versionTable - && versionTable["PSVersion"] is Version version - ? version - : new Version(5, 1); // Only use Get-PSContentPath cmdlet if PowerShell version is 7.7.0 or greater (when PSContentPath feature is available) if (psVersion >= PSVersion7_7) { - // Try to use PowerShell's Get-PSContentPath cmdlet - // This cmdlet automatically respects PSContentPath settings + // Try to use PowerShell's Get-PSContentPath cmdlet in the current runspace + // This cmdlet is only available if experimental feature PSContentPath is enabled try { - using (System.Management.Automation.PowerShell pwsh = System.Management.Automation.PowerShell.Create()) + var results = psCmdlet.InvokeCommand.InvokeScript("Get-PSContentPath"); + + if (results != null && results.Count > 0) { - pwsh.AddCommand("Get-PSContentPath"); - var results = pwsh.Invoke(); - - if (!pwsh.HadErrors && results != null && results.Count > 0) + // Get-PSContentPath returns a PSObject, extract the path string + string userContentPath = results[0]?.ToString(); + if (!string.IsNullOrEmpty(userContentPath)) { - // Get-PSContentPath returns a PSObject, extract the path string - string userContentPath = results[0]?.ToString(); - if (!string.IsNullOrEmpty(userContentPath)) - { - psCmdlet.WriteVerbose($"User content path from Get-PSContentPath: {userContentPath}"); - return userContentPath; - } - } - - if (pwsh.HadErrors) - { - foreach (var error in pwsh.Streams.Error) - { - psCmdlet.WriteVerbose($"Get-PSContentPath error: {error}"); - } + psCmdlet.WriteVerbose($"User content path from Get-PSContentPath: {userContentPath}"); + return userContentPath; } } } @@ -1223,14 +1206,19 @@ private static void GetStandardPlatformPaths( out string localUserDir, out string allUsersDir) { + // Get PowerShell engine version from $PSVersionTable.PSVersion + Version psVersion = new Version(5, 1); + try + { + dynamic psVersionObj = (psCmdlet.SessionState.PSVariable.GetValue("PSVersionTable") as Hashtable)?["PSVersion"]; + if (psVersionObj != null) psVersion = new Version((int)psVersionObj.Major, (int)psVersionObj.Minor); + } + catch { + // Fallback if dynamic access fails + } + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - // Get PowerShell engine version from $PSVersionTable.PSVersion - Version psVersion = psCmdlet.SessionState.PSVariable.GetValue("PSVersionTable") is Hashtable versionTable - && versionTable["PSVersion"] is Version version - ? version - : new Version(5, 1); // Default to Windows PowerShell version if unable to determine - string powerShellType = (psVersion >= PSVersion6) ? "PowerShell" : "WindowsPowerShell"; // Windows PowerShell doesn't support experimental features or PSContentPath @@ -1247,7 +1235,7 @@ private static void GetStandardPlatformPaths( powerShellType ); - localUserDir = GetUserContentPath(psCmdlet, legacyPath); + localUserDir = GetUserContentPath(psCmdlet, psVersion, legacyPath); } allUsersDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), powerShellType); @@ -1257,7 +1245,7 @@ private static void GetStandardPlatformPaths( // paths are the same for both Linux and macOS string legacyPath = Path.Combine(GetHomeOrCreateTempHome(), ".local", "share", "powershell"); - localUserDir = GetUserContentPath(psCmdlet, legacyPath); + localUserDir = GetUserContentPath(psCmdlet, psVersion, legacyPath); // Create the default data directory if it doesn't exist if (!Directory.Exists(localUserDir))