diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/Var.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/Var.cs
index 85bdd2b02d..12cfb784c7 100644
--- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/Var.cs
+++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/Var.cs
@@ -693,6 +693,13 @@ namespace Microsoft.PowerShell.Commands
private bool _passThru;
+ ///
+ /// Gets whether we will append to the variable if it exists.
+ ///
+ [Parameter]
+ [Experimental(ExperimentalFeature.PSRedirectToVariable, ExperimentAction.Show)]
+ public SwitchParameter Append { get; set; }
+
private bool _nameIsFormalParameter;
private bool _valueIsFormalParameter;
#endregion parameters
@@ -711,6 +718,33 @@ namespace Microsoft.PowerShell.Commands
{
_valueIsFormalParameter = true;
}
+
+ if (Append)
+ {
+ // create the list here and add to it if it has a value
+ // but if they have more than one name, produce an error
+ if (Name.Length != 1)
+ {
+ ErrorRecord appendVariableError = new ErrorRecord(new InvalidOperationException(), "SetVariableAppend", ErrorCategory.InvalidOperation, Name);
+ appendVariableError.ErrorDetails = new ErrorDetails("SetVariableAppend");
+ appendVariableError.ErrorDetails.RecommendedAction = VariableCommandStrings.UseSingleVariable;
+ ThrowTerminatingError(appendVariableError);
+ }
+
+ _valueList = new List();
+ var currentValue = Context.SessionState.PSVariable.Get(Name[0]);
+ if (currentValue is not null)
+ {
+ if (currentValue.Value is IList ilist)
+ {
+ _valueList.AddRange(ilist);
+ }
+ else
+ {
+ _valueList.Add(currentValue.Value);
+ }
+ }
+ }
}
///
@@ -726,6 +760,16 @@ namespace Microsoft.PowerShell.Commands
{
if (_nameIsFormalParameter && _valueIsFormalParameter)
{
+ if (Append)
+ {
+ if (Value != AutomationNull.Value)
+ {
+ _valueList ??= new List();
+
+ _valueList.Add(Value);
+ }
+ }
+
return;
}
@@ -756,7 +800,14 @@ namespace Microsoft.PowerShell.Commands
{
if (_valueIsFormalParameter)
{
- SetVariable(Name, Value);
+ if (Append)
+ {
+ SetVariable(Name, _valueList);
+ }
+ else
+ {
+ SetVariable(Name, Value);
+ }
}
else
{
diff --git a/src/Microsoft.PowerShell.Commands.Utility/resources/VariableCommandStrings.resx b/src/Microsoft.PowerShell.Commands.Utility/resources/VariableCommandStrings.resx
index be5d3bbe47..d385adda03 100644
--- a/src/Microsoft.PowerShell.Commands.Utility/resources/VariableCommandStrings.resx
+++ b/src/Microsoft.PowerShell.Commands.Utility/resources/VariableCommandStrings.resx
@@ -123,6 +123,9 @@
Name: {0} Value: {1}
+
+ Use a single variable rather than a collection
+
New variable
diff --git a/src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs b/src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs
index 1ddc7adca2..176089d29c 100644
--- a/src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs
+++ b/src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs
@@ -25,6 +25,7 @@ namespace System.Management.Automation
internal const string PSFeedbackProvider = "PSFeedbackProvider";
internal const string PSCommandWithArgs = "PSCommandWithArgs";
internal const string PSNativeWindowsTildeExpansion = nameof(PSNativeWindowsTildeExpansion);
+ internal const string PSRedirectToVariable = "PSRedirectToVariable";
#endregion
@@ -127,8 +128,10 @@ namespace System.Management.Automation
description: "Enable `-CommandWithArgs` parameter for pwsh"),
new ExperimentalFeature(
name: PSNativeWindowsTildeExpansion,
- description: "On windows, expand unquoted tilde (`~`) with the user's current home folder."
- )
+ description: "On windows, expand unquoted tilde (`~`) with the user's current home folder."),
+ new ExperimentalFeature(
+ name: PSRedirectToVariable,
+ description: "Add support for redirecting to the variable drive"),
};
EngineExperimentalFeatures = new ReadOnlyCollection(engineFeatures);
diff --git a/src/System.Management.Automation/engine/runtime/Operations/MiscOps.cs b/src/System.Management.Automation/engine/runtime/Operations/MiscOps.cs
index 8452c27b98..ddc70fabd5 100644
--- a/src/System.Management.Automation/engine/runtime/Operations/MiscOps.cs
+++ b/src/System.Management.Automation/engine/runtime/Operations/MiscOps.cs
@@ -1090,10 +1090,23 @@ namespace System.Management.Automation
// dir > out
internal override void Bind(PipelineProcessor pipelineProcessor, CommandProcessorBase commandProcessor, ExecutionContext context)
{
+ // Check first to see if File is a variable path. If so, we'll not create the FileBytePipe
+ bool redirectToVariable = false;
+ if (ExperimentalFeature.IsEnabled(ExperimentalFeature.PSRedirectToVariable))
+ {
+ ProviderInfo p;
+ context.SessionState.Path.GetUnresolvedProviderPathFromPSPath(File, out p, out _);
+ if (p != null && p.NameEquals(context.ProviderNames.Variable))
+ {
+ redirectToVariable = true;
+ }
+ }
+
if (commandProcessor is NativeCommandProcessor nativeCommand
&& nativeCommand.CommandRuntime.ErrorMergeTo is not MshCommandRuntime.MergeDataStream.Output
&& FromStream is RedirectionStream.Output
- && !string.IsNullOrWhiteSpace(File))
+ && !string.IsNullOrWhiteSpace(File)
+ && !redirectToVariable)
{
nativeCommand.StdOutDestination = FileBytePipe.Create(File, Appending);
return;
@@ -1208,26 +1221,51 @@ namespace System.Management.Automation
return new Pipe { NullPipe = true };
}
- CommandProcessorBase commandProcessor = context.CreateCommand("out-file", false);
- Diagnostics.Assert(commandProcessor != null, "CreateCommand returned null");
+ // determine whether we're trying to set a variable by inspecting the file path
+ // if we can determine that it's a variable, we'll use Set-Variable rather than Out-File
+ ProviderInfo p;
+ PSDriveInfo d;
+ CommandProcessorBase commandProcessor;
+ var name = context.SessionState.Path.GetUnresolvedProviderPathFromPSPath(File, out p, out d);
- // Previously, we mandated Unicode encoding here
- // Now, We can take what ever has been set if PSDefaultParameterValues
- // Unicode is still the default, but now may be overridden
-
- var cpi = CommandParameterInternal.CreateParameterWithArgument(
- /*parameterAst*/null, "Filepath", "-Filepath:",
- /*argumentAst*/null, File,
- false);
- commandProcessor.AddParameter(cpi);
-
- if (this.Appending)
+ if (ExperimentalFeature.IsEnabled(ExperimentalFeature.PSRedirectToVariable) && p != null && p.NameEquals(context.ProviderNames.Variable))
{
- cpi = CommandParameterInternal.CreateParameterWithArgument(
- /*parameterAst*/null, "Append", "-Append:",
- /*argumentAst*/null, true,
+ commandProcessor = context.CreateCommand("Set-Variable", false);
+ Diagnostics.Assert(commandProcessor != null, "CreateCommand returned null");
+ var cpi = CommandParameterInternal.CreateParameterWithArgument(
+ /*parameterAst*/null, "Name", "-Name:",
+ /*argumentAst*/null, name,
false);
commandProcessor.AddParameter(cpi);
+
+ if (this.Appending)
+ {
+ commandProcessor.AddParameter(CommandParameterInternal.CreateParameter("Append", "-Append", null));
+ }
+ }
+ else
+ {
+ commandProcessor = context.CreateCommand("out-file", false);
+ Diagnostics.Assert(commandProcessor != null, "CreateCommand returned null");
+
+ // Previously, we mandated Unicode encoding here
+ // Now, We can take what ever has been set if PSDefaultParameterValues
+ // Unicode is still the default, but now may be overridden
+
+ var cpi = CommandParameterInternal.CreateParameterWithArgument(
+ /*parameterAst*/null, "Filepath", "-Filepath:",
+ /*argumentAst*/null, File,
+ false);
+ commandProcessor.AddParameter(cpi);
+
+ if (this.Appending)
+ {
+ cpi = CommandParameterInternal.CreateParameterWithArgument(
+ /*parameterAst*/null, "Append", "-Append:",
+ /*argumentAst*/null, true,
+ false);
+ commandProcessor.AddParameter(cpi);
+ }
}
PipelineProcessor = new PipelineProcessor();
@@ -1243,9 +1281,15 @@ namespace System.Management.Automation
// is more specific tp the redirection operation...
if (rte.ErrorRecord.Exception is System.ArgumentException)
{
- throw InterpreterError.NewInterpreterExceptionWithInnerException(null,
- typeof(RuntimeException), null, "RedirectionFailed", ParserStrings.RedirectionFailed,
- rte.ErrorRecord.Exception, File, rte.ErrorRecord.Exception.Message);
+ throw InterpreterError.NewInterpreterExceptionWithInnerException(
+ null,
+ typeof(RuntimeException),
+ null,
+ "RedirectionFailed",
+ ParserStrings.RedirectionFailed,
+ rte.ErrorRecord.Exception,
+ File,
+ rte.ErrorRecord.Exception.Message);
}
throw;
diff --git a/test/powershell/Language/Parser/RedirectionOperator.Tests.ps1 b/test/powershell/Language/Parser/RedirectionOperator.Tests.ps1
index 0c9ef0f0b9..d948881f1d 100644
--- a/test/powershell/Language/Parser/RedirectionOperator.Tests.ps1
+++ b/test/powershell/Language/Parser/RedirectionOperator.Tests.ps1
@@ -123,3 +123,125 @@ Describe "File redirection should have 'DoComplete' called on the underlying pip
$errorContent | Should -Match "CommandNotFoundException,Microsoft.PowerShell.Commands.GetCommandCommand"
}
}
+
+Describe "Redirection and Set-Variable -append tests" -tags CI {
+ Context "variable redirection should work" {
+ BeforeAll {
+ if ( $EnabledExperimentalFeatures -contains "PSRedirectToVariable" ) {
+ $skipTest = $false
+ }
+ else {
+ $skipTest = $true
+ }
+ $testCases = @{ Name = "Variable should be created"; scriptBlock = { 1..3>variable:a }; Validation = { ($a -join "") | Should -Be ((1..3) -join "") } },
+ @{ Name = "variable should be appended"; scriptBlock = {1..3>variable:a; 4..6>>variable:a}; Validation = { ($a -join "") | Should -Be ((1..6) -join "")}},
+ @{ Name = "variable should maintain type"; scriptBlock = {@{one=1}>variable:a};Validation = {$a | Should -BeOfType [hashtable]}},
+ @{
+ Name = "variable should maintain type for multiple objects"
+ scriptBlock = {@{one=1}>variable:a;@{two=2}>>variable:a;1>>variable:a;"string">>variable:a}
+ Validation = {
+ $a.count | Should -Be 4
+ ,$a | Should -BeOfType [array]
+ $a[0] | Should -BeOfType [hashtable]
+ $a[1] | Should -BeOfType [hashtable]
+ $a[2] | Should -BeOfType [int]
+ $a[3] | Should -BeOfType [string]
+ $a[0].one | Should -Be 1
+ $a[1].two | Should -Be 2
+ $a[2] | Should -Be 1
+ $a[3] | Should -Be "string"
+ }
+ },
+ @{ Name = "Error stream should be redirectable"
+ scriptBlock = { write-error bad 2>variable:a}
+ validation = {
+ $a |Should -BeOfType [System.Management.Automation.ErrorRecord]
+ $a.Exception.Message | Should -BeExactly "bad"
+ }
+ },
+ @{ Name = "Warning stream should be redirectable"
+ scriptBlock = { write-warning warn 3>variable:a}
+ validation = {
+ $a |Should -BeOfType [System.Management.Automation.WarningRecord]
+ $a.Message | Should -BeExactly "warn"
+ }
+ },
+ @{ Name = "Verbose stream should be redirectable"
+ scriptBlock = { write-verbose -verbose verb 4>variable:a}
+ validation = {
+ $a |Should -BeOfType [System.Management.Automation.VerboseRecord]
+ $a.Message | Should -BeExactly "verb"
+ }
+ },
+ @{ Name = "Debug stream should be redirectable"
+ scriptBlock = { write-debug -debug deb 5>variable:a}
+ validation = {
+ $a |Should -BeOfType [System.Management.Automation.DebugRecord]
+ $a.Message | Should -BeExactly "deb"
+ }
+ },
+ @{ Name = "Information stream should be redirectable"
+ scriptBlock = { write-information info 6>variable:a}
+ validation = {
+ $a |Should -BeOfType [System.Management.Automation.InformationRecord]
+ $a.MessageData | Should -BeExactly "info"
+ }
+ },
+ @{ Name = "Complex redirection should be supported"
+ scriptBlock = {
+ . {
+ write-error bad
+ write-information info
+ } *>variable:a
+ }
+ validation = {
+ $a.Count | Should -Be 2
+ $a[0] | Should -BeOfType [System.Management.Automation.ErrorRecord]
+ $a[0].Exception.Message | Should -Be "bad"
+ $a[1] |Should -BeOfType [System.Management.Automation.InformationRecord]
+ $a[1].MessageData | Should -BeExactly "info"
+ }
+ },
+ @{
+ Name = "multiple redirections should work"
+ scriptBlock = {
+ . {
+ write-error bad
+ write-information info
+ } 2>variable:e 6>variable:i
+ }
+ validation = {
+ $e | Should -BeOfType [System.Management.Automation.ErrorRecord]
+ $e.Exception.Message | Should -Be "bad"
+ $i | Should -BeOfType [System.Management.Automation.InformationRecord]
+ $i.MessageData | Should -BeExactly "info"
+ }
+ }
+
+
+ }
+ It "" -TestCases $testCases -skip:$skipTest {
+ param ( $scriptBlock, $validation )
+ . $scriptBlock
+ . $validation
+ }
+
+ It 'Redirection of a native application is correct' {
+ $expected = @('Arg 0 is ','Arg 1 is ')
+ testexe -echoargs hi bye > variable:observed
+ $observed | Should -Be $expected
+ }
+
+ It 'Redirection while in variable provider is correct' {
+ $expected = @('Arg 0 is ','Arg 1 is ')
+ try {
+ Push-Location variable:
+ testexe -echoargs hi bye > observed
+ }
+ finally {
+ Pop-Location
+ }
+ $observed | Should -Be $expected
+ }
+ }
+}
diff --git a/test/powershell/Modules/Microsoft.PowerShell.Security/ConstrainedLanguageRestriction.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Security/ConstrainedLanguageRestriction.Tests.ps1
index 398bc49271..6fb97a46ca 100644
--- a/test/powershell/Modules/Microsoft.PowerShell.Security/ConstrainedLanguageRestriction.Tests.ps1
+++ b/test/powershell/Modules/Microsoft.PowerShell.Security/ConstrainedLanguageRestriction.Tests.ps1
@@ -460,7 +460,7 @@ try
$rs.Open()
$pl = $rs.CreatePipeline('"Hello" > c:\temp\foo.txt')
- $e = { $pl.Invoke() } | Should -Throw -ErrorId "CmdletInvocationException"
+ $e = { $pl.Invoke() } | Should -Throw -ErrorId "DriveNotFoundException"
$rs.Dispose()
}
diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/Set-Variable.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/Set-Variable.Tests.ps1
index 0b6b9c59df..d11fd5cfbb 100644
--- a/test/powershell/Modules/Microsoft.PowerShell.Utility/Set-Variable.Tests.ps1
+++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/Set-Variable.Tests.ps1
@@ -241,4 +241,47 @@ Describe "Set-Variable" -Tags "CI" {
{ Set-Variable globalVar -Value 1 -Scope local -Force } | Should -Not -Throw
}
}
+
+ Context "Set-Variable -Append tests" {
+ BeforeAll {
+ if (! (Get-ExperimentalFeature PSRedirectToVariable).Enabled) {
+ $skipTest = $true
+ }
+
+ $testCases = @{ value = 2; Count = 2 },
+ @{ value = @(2,3,4); Count = 2},
+ @{ value = "abc",(Get-Process -Id $PID) ; count = 2}
+ }
+
+ It "Can append values to a variable" -testCases $testCases {
+ param ($value, $count)
+
+ if ($skipTest) {
+ Set-ItResult -skip -because "Experimental Feature PSRedirectToVariable not enabled"
+ return
+ }
+
+ $variableName = "testVar"
+ Set-Variable -Name $variableName -Value 1
+ Set-Variable -Name $variableName -Value $value -Append
+
+ $observedValues = Get-Variable $variableName -Value
+
+ $observedValues.Count | Should -Be $count
+ $observedValues[0] | Should -Be 1
+
+ $observedValues[1] | Should -Be $value
+ }
+
+ It "Can use set-variable via streaming and append values" {
+ if ($skipTest) {
+ Set-ItResult -skip -because "Experimental Feature PSRedirectToVariable not enabled"
+ return
+ }
+
+ $testVar = 1
+ 4..6 | Set-Variable -Name testVar -Append
+ $testVar | Should -Be @(1,4,5,6)
+ }
+ }
}