diff --git a/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs b/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs
index 8321cb8b810..840e8fb4d68 100644
--- a/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs
+++ b/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs
@@ -188,6 +188,7 @@ internal class CommandLineParameterParser
"file",
"executionpolicy",
"command",
+ "settingsfile",
"help"
};
@@ -709,6 +710,36 @@ private void ParseHelper(string[] args)
break;
}
}
+
+ else if (MatchSwitch(switchKey, "settingsfile", "settings") )
+ {
+ ++i;
+ if (i >= args.Length)
+ {
+ WriteCommandLineError(
+ CommandLineParameterParserStrings.MissingSettingsFileArgument);
+ break;
+ }
+ string configFile = null;
+ try
+ {
+ configFile = NormalizeFilePath(args[i]);
+ }
+ catch (Exception ex)
+ {
+ string error = string.Format(CultureInfo.CurrentCulture, CommandLineParameterParserStrings.InvalidSettingsFileArgument, args[i], ex.Message);
+ WriteCommandLineError(error);
+ break;
+ }
+
+ if (!System.IO.File.Exists(configFile))
+ {
+ string error = string.Format(CultureInfo.CurrentCulture, CommandLineParameterParserStrings.SettingsFileNotExists, configFile);
+ WriteCommandLineError(error);
+ break;
+ }
+ PowerShellConfig.Instance.SetSystemConfigFilePath(configFile);
+ }
#if STAMODE
// explicit setting of the ApartmentState Not supported on NanoServer
else if (MatchSwitch(switchKey, "sta", "s"))
@@ -849,6 +880,15 @@ private void ParseExecutionPolicy(string[] args, ref int i, ref string execution
executionPolicy = args[i];
}
+ private static string NormalizeFilePath(string path)
+ {
+ // Normalize slashes
+ path = path.Replace(StringLiterals.AlternatePathSeparator,
+ StringLiterals.DefaultPathSeparator);
+
+ return Path.GetFullPath(path);
+ }
+
private bool ParseFile(string[] args, ref int i, bool noexitSeen)
{
// Process file execution. We don't need to worry about checking -command
@@ -906,10 +946,7 @@ bool TryGetBoolValue(string arg, out bool boolValue)
string exceptionMessage = null;
try
{
- // Normalize slashes
- _file = args[i].Replace(StringLiterals.AlternatePathSeparator,
- StringLiterals.DefaultPathSeparator);
- _file = Path.GetFullPath(_file);
+ _file = NormalizeFilePath(args[i]);
}
catch (Exception e)
{
@@ -986,11 +1023,11 @@ bool TryGetBoolValue(string arg, out bool boolValue)
string argName = arg.Substring(0, offset);
if (TryGetBoolValue(argValue, out bool boolValue))
{
- _collectedArgs.Add(new CommandParameter(argName, boolValue));
+ _collectedArgs.Add(new CommandParameter(argName, boolValue));
}
else
{
- _collectedArgs.Add(new CommandParameter(argName, argValue));
+ _collectedArgs.Add(new CommandParameter(argName, argValue));
}
}
}
diff --git a/src/Microsoft.PowerShell.ConsoleHost/resources/CommandLineParameterParserStrings.resx b/src/Microsoft.PowerShell.ConsoleHost/resources/CommandLineParameterParserStrings.resx
index 93b299bc817..d600de01d91 100644
--- a/src/Microsoft.PowerShell.ConsoleHost/resources/CommandLineParameterParserStrings.resx
+++ b/src/Microsoft.PowerShell.ConsoleHost/resources/CommandLineParameterParserStrings.resx
@@ -186,6 +186,15 @@
Cannot process the command because -Configuration requires an argument that is a remote endpoint configuration name. Specify this argument and try again.
+
+ Cannot process the command because -SettingsFile requires a file path. Supply a path for the SettingsFile parameter and then try the command again.
+
+
+ Processing -SettingsFile '{0}' failed: {1}. Specify a valid path for the -SettingsFile parameter.
+
+
+ The argument '{0}' passed to the -SettingsFile does not exist. Provide the path to an existing json file as an argument to the -SettingsFile parameter.
+
Invalid argument '{0}', did you mean:
diff --git a/src/Microsoft.PowerShell.ConsoleHost/resources/ManagedEntranceStrings.resx b/src/Microsoft.PowerShell.ConsoleHost/resources/ManagedEntranceStrings.resx
index 8bef56e8c24..67c74c04317 100644
--- a/src/Microsoft.PowerShell.ConsoleHost/resources/ManagedEntranceStrings.resx
+++ b/src/Microsoft.PowerShell.ConsoleHost/resources/ManagedEntranceStrings.resx
@@ -239,6 +239,17 @@ All parameters are case-insensitive.
-WindowStyle | -w
Sets the window style to Normal, Minimized, Maximized or Hidden.
+
+-SettingsFile | -settings
+ Overrides the system-wide powershell.config.json settings file for the session.
+ By default, system-wide settings are read from the powershell.config.json
+ in the $PSHOME directory.
+
+ Note that these settings are not used by the endpoint specified
+ by the -ConfigurationName argument.
+
+ Example: pwsh -SettingsFile c:\myproject\powershell.config.json
+
diff --git a/src/System.Management.Automation/engine/PSConfiguration.cs b/src/System.Management.Automation/engine/PSConfiguration.cs
index ef01eba36c7..8c60d6735d8 100644
--- a/src/System.Management.Automation/engine/PSConfiguration.cs
+++ b/src/System.Management.Automation/engine/PSConfiguration.cs
@@ -27,8 +27,16 @@ internal sealed class PowerShellConfig
private static readonly PowerShellConfig s_instance = new PowerShellConfig();
internal static PowerShellConfig Instance => s_instance;
- private string psHomeConfigDirectory;
- private string appDataConfigDirectory;
+ // The json file containing system-wide configuration settings.
+ // When passed as a pwsh command-line option,
+ // overrides the system wide configuration file.
+ private string systemWideConfigFile;
+ private string systemWideConfigDirectory;
+
+ // The json file containing the per-user configuration settings.
+ private string perUserConfigFile;
+ private string perUserConfigDirectory;
+
private const string configFileName = "powershell.config.json";
///
@@ -40,14 +48,40 @@ internal sealed class PowerShellConfig
private PowerShellConfig()
{
- // Sets the system-wide configuration directory
- psHomeConfigDirectory = Utils.DefaultPowerShellAppBase;
+ // Sets the system-wide configuration file.
+ systemWideConfigDirectory = Utils.DefaultPowerShellAppBase;
+ systemWideConfigFile = Path.Combine(systemWideConfigDirectory, configFileName);
// Sets the per-user configuration directory
// Note: This directory may or may not exist depending upon the
// execution scenario. Writes will attempt to create the directory
// if it does not already exist.
- appDataConfigDirectory = Utils.GetUserConfigurationDirectory();
+ perUserConfigDirectory = Utils.GetUserConfigurationDirectory();
+ perUserConfigFile = Path.Combine(perUserConfigDirectory, configFileName);
+ }
+
+ private string GetConfigFilePath(ConfigScope scope)
+ {
+ return (scope == ConfigScope.CurrentUser) ? perUserConfigFile : systemWideConfigFile;
+ }
+
+ ///
+ /// Sets the system wide configuration file path.
+ ///
+ /// A fully qualified path to the system wide configuration file.
+ /// is a null reference or the associated file does not exist.
+ ///
+ /// This method is for use when processing the -SettingsFile configuration setting and should not be used for any other purpose.
+ ///
+ internal void SetSystemConfigFilePath(string value)
+ {
+ if (!string.IsNullOrEmpty(value) && !File.Exists(value))
+ {
+ throw new FileNotFoundException(value);
+ }
+ FileInfo info = new FileInfo(value);
+ systemWideConfigFile = info.FullName;
+ systemWideConfigDirectory = info.Directory.FullName;
}
///
@@ -59,10 +93,7 @@ private PowerShellConfig()
/// Value if found, null otherwise. The behavior matches ModuleIntrinsics.GetExpandedEnvironmentVariable().
internal string GetModulePath(ConfigScope scope)
{
- string scopeDirectory = scope == ConfigScope.SystemWide ? psHomeConfigDirectory : appDataConfigDirectory;
- string fileName = Path.Combine(scopeDirectory, configFileName);
-
- string modulePath = ReadValueFromFile(fileName, Constants.PSModulePathEnvVar);
+ string modulePath = ReadValueFromFile(scope, Constants.PSModulePathEnvVar);
if (!string.IsNullOrEmpty(modulePath))
{
modulePath = Environment.ExpandEnvironmentVariables(modulePath);
@@ -87,17 +118,9 @@ internal string GetModulePath(ConfigScope scope)
internal string GetExecutionPolicy(ConfigScope scope, string shellId)
{
string execPolicy = null;
- string scopeDirectory = psHomeConfigDirectory;
-
- // Defaults to system wide.
- if(ConfigScope.CurrentUser == scope)
- {
- scopeDirectory = appDataConfigDirectory;
- }
- string fileName = Path.Combine(scopeDirectory, configFileName);
string valueName = string.Concat(shellId, ":", "ExecutionPolicy");
- string rawExecPolicy = ReadValueFromFile(fileName, valueName);
+ string rawExecPolicy = ReadValueFromFile(scope, valueName);
if (!String.IsNullOrEmpty(rawExecPolicy))
{
@@ -108,23 +131,12 @@ internal string GetExecutionPolicy(ConfigScope scope, string shellId)
internal void RemoveExecutionPolicy(ConfigScope scope, string shellId)
{
- string scopeDirectory = psHomeConfigDirectory;
-
- // Defaults to system wide.
- if (ConfigScope.CurrentUser == scope)
- {
- scopeDirectory = appDataConfigDirectory;
- }
-
- string fileName = Path.Combine(scopeDirectory, configFileName);
string valueName = string.Concat(shellId, ":", "ExecutionPolicy");
- RemoveValueFromFile(fileName, valueName);
+ RemoveValueFromFile(scope, valueName);
}
internal void SetExecutionPolicy(ConfigScope scope, string shellId, string executionPolicy)
{
- string scopeDirectory = psHomeConfigDirectory;
-
// Defaults to system wide.
if (ConfigScope.CurrentUser == scope)
{
@@ -132,13 +144,10 @@ internal void SetExecutionPolicy(ConfigScope scope, string shellId, string execu
// host for display to the user.
// CreateDirectory will succeed if the directory already exists
// so there is no reason to check Directory.Exists().
- Directory.CreateDirectory(appDataConfigDirectory);
- scopeDirectory = appDataConfigDirectory;
+ Directory.CreateDirectory(perUserConfigDirectory);
}
-
- string fileName = Path.Combine(scopeDirectory, configFileName);
string valueName = string.Concat(shellId, ":", "ExecutionPolicy");
- WriteValueToFile(fileName, valueName, executionPolicy);
+ WriteValueToFile(scope, valueName, executionPolicy);
}
///
@@ -153,14 +162,12 @@ internal void SetExecutionPolicy(ConfigScope scope, string shellId, string execu
/// Whether console prompting should happen. If the value cannot be read it defaults to false.
internal bool GetConsolePrompting()
{
- string fileName = Path.Combine(psHomeConfigDirectory, configFileName);
- return ReadValueFromFile(fileName, "ConsolePrompting");
+ return ReadValueFromFile(ConfigScope.SystemWide, "ConsolePrompting");
}
internal void SetConsolePrompting(bool shouldPrompt)
{
- string fileName = Path.Combine(psHomeConfigDirectory, configFileName);
- WriteValueToFile(fileName, "ConsolePrompting", shouldPrompt);
+ WriteValueToFile(ConfigScope.SystemWide, "ConsolePrompting", shouldPrompt);
}
///
@@ -175,14 +182,12 @@ internal void SetConsolePrompting(bool shouldPrompt)
/// Boolean indicating whether Update-Help should prompt. If the value cannot be read, it defaults to false.
internal bool GetDisablePromptToUpdateHelp()
{
- string fileName = Path.Combine(psHomeConfigDirectory, configFileName);
- return ReadValueFromFile(fileName, "DisablePromptToUpdateHelp");
+ return ReadValueFromFile(ConfigScope.SystemWide, "DisablePromptToUpdateHelp");
}
internal void SetDisablePromptToUpdateHelp(bool prompt)
{
- string fileName = Path.Combine(psHomeConfigDirectory, configFileName);
- WriteValueToFile(fileName, "DisablePromptToUpdateHelp", prompt);
+ WriteValueToFile(ConfigScope.SystemWide, "DisablePromptToUpdateHelp", prompt);
}
///
@@ -190,9 +195,7 @@ internal void SetDisablePromptToUpdateHelp(bool prompt)
///
internal PowerShellPolicies GetPowerShellPolicies(ConfigScope scope)
{
- string scopeDirectory = (scope == ConfigScope.SystemWide) ? psHomeConfigDirectory : appDataConfigDirectory;
- string fileName = Path.Combine(scopeDirectory, configFileName);
- return ReadValueFromFile(fileName, nameof(PowerShellPolicies));
+ return ReadValueFromFile(scope, nameof(PowerShellPolicies));
}
#if UNIX
@@ -204,8 +207,7 @@ internal PowerShellPolicies GetPowerShellPolicies(ConfigScope scope)
///
internal string GetSysLogIdentity()
{
- string fileName = Path.Combine(psHomeConfigDirectory, configFileName);
- string identity = ReadValueFromFile(fileName, "LogIdentity");
+ string identity = ReadValueFromFile(ConfigScope.SystemWide, "LogIdentity");
if (string.IsNullOrEmpty(identity) ||
identity.Equals(LogDefaultValue, StringComparison.OrdinalIgnoreCase))
@@ -223,8 +225,7 @@ internal string GetSysLogIdentity()
///
internal PSLevel GetLogLevel()
{
- string fileName = Path.Combine(psHomeConfigDirectory, configFileName);
- string levelName = ReadValueFromFile(fileName, "LogLevel");
+ string levelName = ReadValueFromFile(ConfigScope.SystemWide, "LogLevel");
PSLevel level;
if (string.IsNullOrEmpty(levelName) ||
@@ -256,8 +257,7 @@ internal PSLevel GetLogLevel()
///
internal PSChannel GetLogChannels()
{
- string fileName = Path.Combine(psHomeConfigDirectory, configFileName);
- string values = ReadValueFromFile(fileName, "LogChannels");
+ string values = ReadValueFromFile(ConfigScope.SystemWide, "LogChannels");
PSChannel result = 0;
if (!string.IsNullOrEmpty(values))
@@ -299,8 +299,7 @@ internal PSChannel GetLogChannels()
///
internal PSKeyword GetLogKeywords()
{
- string fileName = Path.Combine(psHomeConfigDirectory, configFileName);
- string values = ReadValueFromFile(fileName, "LogKeywords");
+ string values = ReadValueFromFile(ConfigScope.SystemWide, "LogKeywords");
PSKeyword result = 0;
if (!string.IsNullOrEmpty(values))
@@ -331,10 +330,19 @@ internal PSKeyword GetLogKeywords()
return result;
}
#endif // UNIX
-
- private T ReadValueFromFile(string fileName, string key, T defaultValue = default(T),
+
+ ///
+ /// Read a value from the configuration file.
+ ///
+ /// The type of the value
+ /// The ConfigScope of the configuration file to update.
+ /// The string key of the value.
+ /// The default value to return if the key is not present.
+ ///
+ private T ReadValueFromFile(ConfigScope scope, string key, T defaultValue = default(T),
Func readImpl = null)
{
+ string fileName = GetConfigFilePath(scope);
if (!File.Exists(fileName)) { return defaultValue; }
// Open file for reading, but allow multiple readers
@@ -363,15 +371,16 @@ internal PSKeyword GetLogKeywords()
}
///
- /// TODO: Should this return success fail or throw?
+ /// Update a value in the configuration file.
///
- ///
- ///
- ///
- ///
+ /// The type of the value
+ /// The ConfigScope of the configuration file to update.
+ /// The string key of the value.
+ /// The value to set.
/// Whether the key-value pair should be added to or removed from the file
- private void UpdateValueInFile(string fileName, string key, T value, bool addValue)
+ private void UpdateValueInFile(ConfigScope scope, string key, T value, bool addValue)
{
+ string fileName = GetConfigFilePath(scope);
fileLock.EnterWriteLock();
try
{
@@ -469,27 +478,28 @@ private void UpdateValueInFile(string fileName, string key, T value, bool add
///
/// TODO: Should this return success, fail, or throw?
///
- ///
- ///
- ///
- ///
- private void WriteValueToFile(string fileName, string key, T value)
+ /// The type of value to write.
+ /// The ConfigScope of the file to update.
+ /// The string key of the value.
+ /// The value to write.
+ private void WriteValueToFile(ConfigScope scope, string key, T value)
{
- UpdateValueInFile(fileName, key, value, true);
+ UpdateValueInFile(scope, key, value, true);
}
///
/// TODO: Should this return success, fail, or throw?
///
- ///
- ///
- ///
- private void RemoveValueFromFile(string fileName, string key)
+ /// The type of value to remove.
+ /// The ConfigScope of the file to update.
+ /// The string key of the value.
+ private void RemoveValueFromFile(ConfigScope scope, string key)
{
+ string fileName = GetConfigFilePath(scope);
// Optimization: If the file doesn't exist, there is nothing to remove
if (File.Exists(fileName))
{
- UpdateValueInFile(fileName, key, default(T), false);
+ UpdateValueInFile(scope, key, default(T), false);
}
}
}
diff --git a/test/powershell/Host/ConsoleHost.Tests.ps1 b/test/powershell/Host/ConsoleHost.Tests.ps1
index f6b08ca4dbc..70bbc0006a8 100644
--- a/test/powershell/Host/ConsoleHost.Tests.ps1
+++ b/test/powershell/Host/ConsoleHost.Tests.ps1
@@ -249,6 +249,51 @@ Describe "ConsoleHost unit tests" -tags "Feature" {
}
}
+ Context "-SettingsFile Commandline switch" {
+
+ BeforeAll {
+ $CustomSettingsFile = Join-Path -Path $TestDrive -ChildPath 'Powershell.test.json'
+ $DefaultExecutionPolicy = 'RemoteSigned'
+ }
+ BeforeEach {
+ # reset the content of the settings file to a known state.
+ Set-Content -Path $CustomSettingsfile -Value "{`"Microsoft.PowerShell:ExecutionPolicy`":`"$DefaultExecutionPolicy`"}" -ErrorAction Stop
+ }
+
+ # NOTE: The -settingsFile command-line option only reads settings for the local machine. As a result, the tests that use Set/Get-ExecutionPolicy
+ # must use an explicit scope of LocalMachine to ensure the setting is written to the expected file.
+
+ It "Verifies PowerShell reads from the custom -settingsFile" {
+ $actualValue = & $powershell -NoProfile -SettingsFile $CustomSettingsFile -Command {(Get-ExecutionPolicy -Scope LocalMachine).ToString()}
+ $actualValue | Should Be $DefaultExecutionPolicy
+ }
+
+ It "Verifies PowerShell writes to the custom -settingsFile" {
+ $expectedValue = 'AllSigned'
+
+ # Update the execution policy; this should update the settings file.
+ & $powershell -NoProfile -SettingsFile $CustomSettingsFile -Command {Set-ExecutionPolicy -ExecutionPolicy AllSigned -Scope LocalMachine }
+
+ # ensure the setting was written to the settings file.
+ $content = (Get-Content -Path $CustomSettingsFile | ConvertFrom-Json)
+ $content.'Microsoft.PowerShell:ExecutionPolicy' | Should Be $expectedValue
+
+ # ensure the setting is applied on next run
+ $actualValue = & $powershell -NoProfile -SettingsFile $CustomSettingsFile -Command {(Get-ExecutionPolicy -Scope LocalMachine).ToString()}
+ $actualValue | Should Be $expectedValue
+ }
+
+ It "Verify PowerShell removes a setting from the custom -settingsFile" {
+ # Remove the LocalMachine execution policy; this should update the settings file.
+ & $powershell -NoProfile -SettingsFile $CustomSettingsFile -Command {Set-ExecutionPolicy -ExecutionPolicy Undefined -Scope LocalMachine }
+
+ # ensure the setting was removed from the settings file.
+ $content = (Get-Content -Path $CustomSettingsFile | ConvertFrom-Json)
+ $content.'Microsoft.PowerShell:ExecutionPolicy' | Should Be $null
+ }
+
+ }
+
Context "Pipe to/from powershell" {
$p = [PSCustomObject]@{X=10;Y=20}