diff --git a/src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs b/src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs index 25d9b6e2743..1ddc7adca24 100644 --- a/src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs +++ b/src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs @@ -24,6 +24,7 @@ public class ExperimentalFeature internal const string PSModuleAutoLoadSkipOfflineFilesFeatureName = "PSModuleAutoLoadSkipOfflineFiles"; internal const string PSFeedbackProvider = "PSFeedbackProvider"; internal const string PSCommandWithArgs = "PSCommandWithArgs"; + internal const string PSNativeWindowsTildeExpansion = nameof(PSNativeWindowsTildeExpansion); #endregion @@ -124,6 +125,10 @@ static ExperimentalFeature() new ExperimentalFeature( name: PSCommandWithArgs, description: "Enable `-CommandWithArgs` parameter for pwsh"), + new ExperimentalFeature( + name: PSNativeWindowsTildeExpansion, + description: "On windows, expand unquoted tilde (`~`) with the user's current home folder." + ) }; EngineExperimentalFeatures = new ReadOnlyCollection(engineFeatures); diff --git a/src/System.Management.Automation/engine/NativeCommandParameterBinder.cs b/src/System.Management.Automation/engine/NativeCommandParameterBinder.cs index 7149f0a2e40..0124b01332c 100644 --- a/src/System.Management.Automation/engine/NativeCommandParameterBinder.cs +++ b/src/System.Management.Automation/engine/NativeCommandParameterBinder.cs @@ -328,7 +328,7 @@ private void AppendOneNativeArgument(ExecutionContext context, CommandParameterI } /// - /// On Windows, just append . + /// On Windows, do tilde expansion, otherwise just append . /// On Unix, do globbing as appropriate, otherwise just append . /// /// The argument that possibly needs expansion. @@ -400,23 +400,20 @@ private void PossiblyGlobArg(string arg, CommandParameterInternal parameter, boo { // Even if there are no wildcards, we still need to possibly // expand ~ into the filesystem provider home directory path - ProviderInfo fileSystemProvider = Context.EngineSessionState.GetSingleProvider(FileSystemProvider.ProviderName); - string home = fileSystemProvider.Home; - if (string.Equals(arg, "~")) + if (ExpandTilde(arg, parameter)) { - _arguments.Append(home); - AddToArgumentList(parameter, home); argExpanded = true; } - else if (arg.StartsWith("~/", StringComparison.OrdinalIgnoreCase)) + } +#else + if (!usedQuotes && ExperimentalFeature.IsEnabled(ExperimentalFeature.PSNativeWindowsTildeExpansion)) + { + if (ExpandTilde(arg, parameter)) { - var replacementString = string.Concat(home, arg.AsSpan(1)); - _arguments.Append(replacementString); - AddToArgumentList(parameter, replacementString); argExpanded = true; } } -#endif // UNIX +#endif if (!argExpanded) { @@ -425,6 +422,33 @@ private void PossiblyGlobArg(string arg, CommandParameterInternal parameter, boo } } + /// + /// Replace tilde for unquoted arguments in the form ~ and ~/. For windows, ~\ is also expanded. + /// + /// The argument that possibly needs expansion. + /// The parameter associated with the operation. + /// True if tilde expansion occurred. + private bool ExpandTilde(string arg, CommandParameterInternal parameter) + { + var fileSystemProvider = Context.EngineSessionState.GetSingleProvider(FileSystemProvider.ProviderName); + var home = fileSystemProvider.Home; + if (string.Equals(arg, "~")) + { + _arguments.Append(home); + AddToArgumentList(parameter, home); + return true; + } + else if (arg.StartsWith("~/") || (OperatingSystem.IsWindows() && arg.StartsWith(@"~\"))) + { + var replacementString = string.Concat(home, arg.AsSpan(1)); + _arguments.Append(replacementString); + AddToArgumentList(parameter, replacementString); + return true; + } + + return false; + } + /// /// Check to see if the string contains spaces and therefore must be quoted. /// diff --git a/test/powershell/Language/Scripting/NativeExecution/NativeWindowsTildeExpansion.Tests.ps1 b/test/powershell/Language/Scripting/NativeExecution/NativeWindowsTildeExpansion.Tests.ps1 new file mode 100644 index 00000000000..04156099fa4 --- /dev/null +++ b/test/powershell/Language/Scripting/NativeExecution/NativeWindowsTildeExpansion.Tests.ps1 @@ -0,0 +1,32 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +Describe 'Native Windows tilde expansion tests' -tags "CI" { + BeforeAll { + $originalDefaultParams = $PSDefaultParameterValues.Clone() + $PSDefaultParameterValues["it:skip"] = -Not $IsWindows + $EnabledExperimentalFeatures.Contains('PSNativeWindowsTildeExpansion') | Should -BeTrue + } + + AfterAll { + $global:PSDefaultParameterValues = $originalDefaultParams + } + + # Test ~ expansion + It 'Tilde should be replaced by the filesystem provider home directory' { + cmd /c echo ~ | Should -BeExactly ($ExecutionContext.SessionState.Provider.Get("FileSystem").Home) + } + # Test ~ expansion with a path fragment (e.g. ~/foo) + It '~/foo should be replaced by the /foo' { + cmd /c echo ~/foo | Should -BeExactly "$($ExecutionContext.SessionState.Provider.Get("FileSystem").Home)/foo" + cmd /c echo ~\foo | Should -BeExactly "$($ExecutionContext.SessionState.Provider.Get("FileSystem").Home)\foo" + } + It '~ should not be replaced when quoted' { + cmd /c echo '~' | Should -BeExactly '~' + cmd /c echo "~" | Should -BeExactly '~' + cmd /c echo '~/foo' | Should -BeExactly '~/foo' + cmd /c echo "~/foo" | Should -BeExactly '~/foo' + cmd /c echo '~\foo' | Should -BeExactly '~\foo' + cmd /c echo "~\foo" | Should -BeExactly '~\foo' + } +} diff --git a/test/tools/TestMetadata.json b/test/tools/TestMetadata.json index 66dc8572d37..cd94ce83a79 100644 --- a/test/tools/TestMetadata.json +++ b/test/tools/TestMetadata.json @@ -2,6 +2,7 @@ "ExperimentalFeatures": { "ExpTest.FeatureOne": [ "test/powershell/engine/ExperimentalFeature/ExperimentalFeature.Basic.Tests.ps1" ], "PSCultureInvariantReplaceOperator": [ "test/powershell/Language/Operators/ReplaceOperator.Tests.ps1" ], - "Microsoft.PowerShell.Utility.PSManageBreakpointsInRunspace": [ "test/powershell/Modules/Microsoft.PowerShell.Utility/RunspaceBreakpointManagement.Tests.ps1" ] + "Microsoft.PowerShell.Utility.PSManageBreakpointsInRunspace": [ "test/powershell/Modules/Microsoft.PowerShell.Utility/RunspaceBreakpointManagement.Tests.ps1" ], + "PSNativeWindowsTildeExpansion": [ "test/powershell/Language/Scripting/NativeExecution/NativeWindowsTildeExpansion.Tests.ps1" ] } }