diff --git a/src/System.Management.Automation/engine/parser/Compiler.cs b/src/System.Management.Automation/engine/parser/Compiler.cs index d24520080d9..bb7fc7dc087 100644 --- a/src/System.Management.Automation/engine/parser/Compiler.cs +++ b/src/System.Management.Automation/engine/parser/Compiler.cs @@ -318,6 +318,8 @@ internal static class CachedReflectionInfo typeof(PipelineOps).GetMethod(nameof(PipelineOps.FlushPipe), staticFlags); internal static readonly MethodInfo PipelineOps_InvokePipeline = typeof(PipelineOps).GetMethod(nameof(PipelineOps.InvokePipeline), staticFlags); + internal static readonly MethodInfo PipelineOps_InvokePipelineInBackground = + typeof(PipelineOps).GetMethod(nameof(PipelineOps.InvokePipelineInBackground), staticFlags); internal static readonly MethodInfo PipelineOps_Nop = typeof(PipelineOps).GetMethod(nameof(PipelineOps.Nop), staticFlags); internal static readonly MethodInfo PipelineOps_PipelineResult = @@ -1942,7 +1944,8 @@ private Expression CaptureStatementResultsHelper( } var pipelineAst = stmt as PipelineAst; - if (pipelineAst != null) + // If it's a pipeline that isn't being backgrounded, try to optimize expression + if (pipelineAst != null && ! pipelineAst.Background) { var expr = pipelineAst.GetPureExpression(); if (expr != null) { return Compile(expr); } @@ -2983,104 +2986,115 @@ public object VisitPipeline(PipelineAst pipelineAst) exprs.Add(UpdatePosition(pipelineAst)); } - var pipeElements = pipelineAst.PipelineElements; - var firstCommandExpr = (pipeElements[0] as CommandExpressionAst); - if (firstCommandExpr != null && pipeElements.Count == 1) + if (pipelineAst.Background) { - if (firstCommandExpr.Redirections.Count > 0) - { - exprs.Add(GetRedirectedExpression(firstCommandExpr, captureForInput: false)); - } - else - { - exprs.Add(Compile(firstCommandExpr)); - } + Expression invokeBackgroundPipe = Expression.Call( + CachedReflectionInfo.PipelineOps_InvokePipelineInBackground, + Expression.Constant(pipelineAst), + _functionContext); + exprs.Add(invokeBackgroundPipe); } else { - Expression input; - int i, commandsInPipe; - - if (firstCommandExpr != null) + var pipeElements = pipelineAst.PipelineElements; + var firstCommandExpr = (pipeElements[0] as CommandExpressionAst); + + if (firstCommandExpr != null && pipeElements.Count == 1) { if (firstCommandExpr.Redirections.Count > 0) { - input = GetRedirectedExpression(firstCommandExpr, captureForInput: true); + exprs.Add(GetRedirectedExpression(firstCommandExpr, captureForInput: false)); } else { - input = GetRangeEnumerator(firstCommandExpr.Expression) ?? - Compile(firstCommandExpr.Expression); + exprs.Add(Compile(firstCommandExpr)); } - i = 1; - commandsInPipe = pipeElements.Count - 1; } else { - // Compiled code normally never sees AutomationNull. We use that value - // here so that we can tell the difference b/w $null and no input when - // starting the pipeline, in other words, PipelineOps.InvokePipe will - // not pass this value to the pipe. - - input = ExpressionCache.AutomationNullConstant; - i = 0; - commandsInPipe = pipeElements.Count; - } - Expression[] pipelineExprs = new Expression[commandsInPipe]; - CommandBaseAst[] pipeElementAsts = new CommandBaseAst[commandsInPipe]; - var commandRedirections = new object[commandsInPipe]; - - for (int j = 0; i < pipeElements.Count; ++i, ++j) - { - var pipeElement = pipeElements[i]; - pipelineExprs[j] = Compile(pipeElement); - - commandRedirections[j] = GetCommandRedirections(pipeElement); - pipeElementAsts[j] = pipeElement; - } - - // The redirections are passed as a CommandRedirection[][] - one dimension for each command in the pipe, - // one dimension because each command may have multiple redirections. Here we create the array for - // each command in the pipe, either a compile time constant or created at runtime if necessary. - Expression redirectionExpr; - if (commandRedirections.Any(r => r is Expression)) - { - // If any command redirections are non-constant, commandRedirections will have a Linq.Expression in it, - // in which case we must create the array at runtime - redirectionExpr = - Expression.NewArrayInit(typeof(CommandRedirection[]), - commandRedirections.Select(r => (r as Expression) ?? Expression.Constant(r, typeof(CommandRedirection[])))); - } - else if (commandRedirections.Any(r => r != null)) - { - // There were redirections, but all were compile time constant, so build the array at compile time. - redirectionExpr = - Expression.Constant(commandRedirections.Map(r => r as CommandRedirection[])); - } - else - { - // No redirections. - redirectionExpr = ExpressionCache.NullCommandRedirections; - } - - if (firstCommandExpr != null) - { - var inputTemp = Expression.Variable(input.Type); - temps.Add(inputTemp); - exprs.Add(Expression.Assign(inputTemp, input)); - input = inputTemp; + Expression input; + int i, commandsInPipe; + + if (firstCommandExpr != null) + { + if (firstCommandExpr.Redirections.Count > 0) + { + input = GetRedirectedExpression(firstCommandExpr, captureForInput: true); + } + else + { + input = GetRangeEnumerator(firstCommandExpr.Expression) ?? + Compile(firstCommandExpr.Expression); + } + i = 1; + commandsInPipe = pipeElements.Count - 1; + } + else + { + // Compiled code normally never sees AutomationNull. We use that value + // here so that we can tell the difference b/w $null and no input when + // starting the pipeline, in other words, PipelineOps.InvokePipe will + // not pass this value to the pipe. + + input = ExpressionCache.AutomationNullConstant; + i = 0; + commandsInPipe = pipeElements.Count; + } + Expression[] pipelineExprs = new Expression[commandsInPipe]; + CommandBaseAst[] pipeElementAsts = new CommandBaseAst[commandsInPipe]; + var commandRedirections = new object[commandsInPipe]; + + for (int j = 0; i < pipeElements.Count; ++i, ++j) + { + var pipeElement = pipeElements[i]; + pipelineExprs[j] = Compile(pipeElement); + + commandRedirections[j] = GetCommandRedirections(pipeElement); + pipeElementAsts[j] = pipeElement; + } + + // The redirections are passed as a CommandRedirection[][] - one dimension for each command in the pipe, + // one dimension because each command may have multiple redirections. Here we create the array for + // each command in the pipe, either a compile time constant or created at runtime if necessary. + Expression redirectionExpr; + if (commandRedirections.Any(r => r is Expression)) + { + // If any command redirections are non-constant, commandRedirections will have a Linq.Expression in it, + // in which case we must create the array at runtime + redirectionExpr = + Expression.NewArrayInit(typeof(CommandRedirection[]), + commandRedirections.Select(r => (r as Expression) ?? Expression.Constant(r, typeof(CommandRedirection[])))); + } + else if (commandRedirections.Any(r => r != null)) + { + // There were redirections, but all were compile time constant, so build the array at compile time. + redirectionExpr = + Expression.Constant(commandRedirections.Map(r => r as CommandRedirection[])); + } + else + { + // No redirections. + redirectionExpr = ExpressionCache.NullCommandRedirections; + } + + if (firstCommandExpr != null) + { + var inputTemp = Expression.Variable(input.Type); + temps.Add(inputTemp); + exprs.Add(Expression.Assign(inputTemp, input)); + input = inputTemp; + } + + Expression invokePipe = Expression.Call( + CachedReflectionInfo.PipelineOps_InvokePipeline, + input.Cast(typeof(object)), + firstCommandExpr != null ? ExpressionCache.FalseConstant : ExpressionCache.TrueConstant, + Expression.NewArrayInit(typeof(CommandParameterInternal[]), pipelineExprs), + Expression.Constant(pipeElementAsts), + redirectionExpr, + _functionContext); + exprs.Add(invokePipe); } - - Expression invokePipe = Expression.Call( - CachedReflectionInfo.PipelineOps_InvokePipeline, - input.Cast(typeof(object)), - firstCommandExpr != null ? ExpressionCache.FalseConstant : ExpressionCache.TrueConstant, - Expression.NewArrayInit(typeof(CommandParameterInternal[]), pipelineExprs), - Expression.Constant(pipeElementAsts), - redirectionExpr, - _functionContext); - - exprs.Add(invokePipe); } return Expression.Block(temps, exprs); diff --git a/src/System.Management.Automation/engine/parser/Parser.cs b/src/System.Management.Automation/engine/parser/Parser.cs index 72c49aee994..98d6533d201 100644 --- a/src/System.Management.Automation/engine/parser/Parser.cs +++ b/src/System.Management.Automation/engine/parser/Parser.cs @@ -2451,7 +2451,7 @@ private StatementAst SwitchStatementRule(LabelToken labelToken, Token switchToke { endErrorStatement = fileNameExpr.Extent; condition = new PipelineAst(fileNameExpr.Extent, - new CommandExpressionAst(fileNameExpr.Extent, fileNameExpr, null)); + new CommandExpressionAst(fileNameExpr.Extent, fileNameExpr, null), background: false); if (!specifiedFlags.ContainsKey("file")) { @@ -5182,6 +5182,7 @@ private PipelineBaseAst PipelineRule() Token pipeToken = null; bool scanning = true; + bool background = false; while (scanning) { CommandBaseAst commandAst; @@ -5293,6 +5294,11 @@ private PipelineBaseAst PipelineRule() case TokenKind.EndOfInput: scanning = false; continue; + case TokenKind.Ampersand: + SkipToken(); + scanning = false; + background = true; + break; case TokenKind.Pipe: SkipToken(); SkipNewlines(); @@ -5328,7 +5334,7 @@ private PipelineBaseAst PipelineRule() return null; } - return new PipelineAst(ExtentOf(startExtent, pipelineElements[pipelineElements.Count - 1]), pipelineElements); + return new PipelineAst(ExtentOf(startExtent, pipelineElements[pipelineElements.Count - 1]), pipelineElements, background); } private RedirectionAst RedirectionRule(RedirectionToken redirectionToken, RedirectionAst[] redirections, ref IScriptExtent extent) @@ -5672,16 +5678,11 @@ internal Ast CommandRule(bool forDynamicKeyword) case TokenKind.Semi: case TokenKind.AndAnd: case TokenKind.OrOr: + case TokenKind.Ampersand: UngetToken(token); scanning = false; continue; - case TokenKind.Ampersand: - // ErrorRecovery: just ignore the token. - endExtent = token.Extent; - ReportError(token.Extent, () => ParserStrings.AmpersandNotAllowed); - break; - case TokenKind.MinusMinus: endExtent = token.Extent; // Add the first -- as a parameter, which is then ignored when constructing the command processor unless it's a native diff --git a/src/System.Management.Automation/engine/parser/ast.cs b/src/System.Management.Automation/engine/parser/ast.cs index f6a187bbb61..3e418fe5256 100644 --- a/src/System.Management.Automation/engine/parser/ast.cs +++ b/src/System.Management.Automation/engine/parser/ast.cs @@ -5472,13 +5472,14 @@ public class PipelineAst : PipelineBaseAst /// /// The extent of the pipeline. /// The collection of commands representing the pipeline. + /// Indicates that this pipeline should be run in the background /// /// If is null. /// /// /// If is null or is an empty collection. /// - public PipelineAst(IScriptExtent extent, IEnumerable pipelineElements) + public PipelineAst(IScriptExtent extent, IEnumerable pipelineElements, bool background) : base(extent) { if (pipelineElements == null || !pipelineElements.Any()) @@ -5486,19 +5487,37 @@ public PipelineAst(IScriptExtent extent, IEnumerable pipelineEle throw PSTraceSource.NewArgumentException("pipelineElements"); } + this.Background = background; this.PipelineElements = new ReadOnlyCollection(pipelineElements.ToArray()); SetParents(PipelineElements); } + /// + /// Construct a pipeline from a collection of commands. + /// + /// The extent of the pipeline. + /// The collection of commands representing the pipeline. + /// + /// If is null. + /// + /// + /// If is null or is an empty collection. + /// + public PipelineAst(IScriptExtent extent, IEnumerable pipelineElements) :this (extent, pipelineElements, background: false) + { + + } + /// /// Construct a pipeline from a single command. /// /// The extent of the pipeline (which should be the extent of the command). /// The command for the pipeline. + /// Indicates that this pipeline should be run in the background /// /// If or is null. /// - public PipelineAst(IScriptExtent extent, CommandBaseAst commandAst) + public PipelineAst(IScriptExtent extent, CommandBaseAst commandAst, bool background) : base(extent) { if (commandAst == null) @@ -5506,15 +5525,34 @@ public PipelineAst(IScriptExtent extent, CommandBaseAst commandAst) throw PSTraceSource.NewArgumentNullException("commandAst"); } + this.Background = background; this.PipelineElements = new ReadOnlyCollection(new CommandBaseAst[] { commandAst }); SetParent(commandAst); } + /// + /// Construct a pipeline from a single command. + /// + /// The extent of the pipeline (which should be the extent of the command). + /// The command for the pipeline. + /// + /// If or is null. + /// + public PipelineAst(IScriptExtent extent, CommandBaseAst commandAst) :this (extent, commandAst, background: false) + { + + } + /// /// A non-null, non-empty collection of commands that represent the pipeline. /// public ReadOnlyCollection PipelineElements { get; private set; } + /// + /// Indicates that this pipeline should be run in the background. + /// + public bool Background { get; private set; } + /// /// If the pipeline represents a pure expression, the expression is returned, otherwise null is returned. /// @@ -5540,7 +5578,7 @@ public override ExpressionAst GetPureExpression() public override Ast Copy() { var newPipelineElements = CopyElements(this.PipelineElements); - return new PipelineAst(this.Extent, newPipelineElements); + return new PipelineAst(this.Extent, newPipelineElements, this.Background); } internal override IEnumerable GetInferredType(CompletionContext context) @@ -6314,9 +6352,10 @@ public AssignmentStatementAst(IScriptExtent extent, ExpressionAst left, TokenKin throw PSTraceSource.NewArgumentException("operator"); } - // If the assignment is just an expression - remove the pipeline wrapping the expression. + // If the assignment is just an expression and the expression is not backgrounded then + // remove the pipeline wrapping the expression. var pipelineAst = right as PipelineAst; - if (pipelineAst != null) + if (pipelineAst != null && ! pipelineAst.Background) { if (pipelineAst.PipelineElements.Count == 1) { @@ -6662,7 +6701,7 @@ internal PipelineAst GenerateSetItemPipelineAst() var cmdAst = new CommandAst(this.Extent, cea, TokenKind.Unknown, null); - var pipeLineAst = new PipelineAst(this.Extent, cmdAst); + var pipeLineAst = new PipelineAst(this.Extent, cmdAst, background: false); var funcStatements = ConfigurationExtraParameterStatements.Select(statement => (StatementAst)statement.Copy()).ToList(); funcStatements.Add(pipeLineAst); var statmentBlockAst = new StatementBlockAst(this.Extent, funcStatements, null); @@ -6702,7 +6741,7 @@ internal PipelineAst GenerateSetItemPipelineAst() var setItemCmdlet = new CommandAst(this.Extent, setItemCmdElements, TokenKind.Unknown, null); #endregion - var returnPipelineAst = new PipelineAst(this.Extent, setItemCmdlet); + var returnPipelineAst = new PipelineAst(this.Extent, setItemCmdlet, background: false); SetParent(returnPipelineAst); @@ -7247,7 +7286,7 @@ internal PipelineAst GenerateCommandCallPipelineAst() // var cmdAst = new CommandAst(FunctionName.Extent, cea, TokenKind.Unknown, null); cmdAst.DefiningKeyword = Keyword; - _commandCallPipelineAst = new PipelineAst(FunctionName.Extent, cmdAst); + _commandCallPipelineAst = new PipelineAst(FunctionName.Extent, cmdAst, background: false); return _commandCallPipelineAst; } diff --git a/src/System.Management.Automation/engine/runtime/Operations/MiscOps.cs b/src/System.Management.Automation/engine/runtime/Operations/MiscOps.cs index 02e7519174b..9ee81b6e390 100644 --- a/src/System.Management.Automation/engine/runtime/Operations/MiscOps.cs +++ b/src/System.Management.Automation/engine/runtime/Operations/MiscOps.cs @@ -403,13 +403,14 @@ internal static void InvokePipeline(object input, CommandProcessorBase commandProcessor = null; CommandRedirection[] commandRedirection = null; + for (int i = 0; i < pipeElements.Length; i++) { commandRedirection = commandRedirections != null ? commandRedirections[i] : null; commandProcessor = AddCommand(pipelineProcessor, pipeElements[i], pipeElementAsts[i], commandRedirection, context); } - + var cmdletInfo = commandProcessor?.CommandInfo as CmdletInfo; if (cmdletInfo?.ImplementingType == typeof(OutNullCommand)) { @@ -419,7 +420,7 @@ internal static void InvokePipeline(object input, // Out-Null is the only command, bail without running anything return; } - + // Out-Null is the last command, rewrite command before Out-Null to a null pipe, but // only if it didn't redirect anything, e.g. `Get-Stuff > o.txt | Out-Null` var nextToLastCommand = pipelineProcessor.Commands[commandsCount - 2]; @@ -465,6 +466,83 @@ internal static void InvokePipeline(object input, } } + internal static void InvokePipelineInBackground( + PipelineAst pipelineAst, + FunctionContext funcContext) + { + PipelineProcessor pipelineProcessor = new PipelineProcessor(); + ExecutionContext context = funcContext._executionContext; + Pipe outputPipe = funcContext._outputPipe; + + try + { + if (context.Events != null) + { + context.Events.ProcessPendingActions(); + } + + CommandProcessorBase commandProcessor = null; + + // For background jobs rewrite the pipeline as a Start-Job command + var scriptblockBodyString = pipelineAst.Extent.Text; + var pipelineOffset = pipelineAst.Extent.StartOffset; + var variables = pipelineAst.FindAll(x => x is VariableExpressionAst, true); + // Used to make sure that the job runs in the current directory + const string cmdPrefix = @"Microsoft.PowerShell.Management\Set-Location -LiteralPath $using:pwd ; "; + // Minimize allocations by initializing the stringbuilder to the size of the source string + prefix + space for ${using:} * 2 + System.Text.StringBuilder updatedScriptblock = new System.Text.StringBuilder(cmdPrefix.Length + scriptblockBodyString.Length + 18); + updatedScriptblock.Append(cmdPrefix); + int position = 0; + // Prefix variables in the scriptblock with $using: + foreach (var v in variables) + { + var vName = ((VariableExpressionAst) v).VariablePath.UserPath; + // Skip variables that don't exist + if (funcContext._executionContext.EngineSessionState.GetVariable(vName) == null) + continue; + // Skip PowerShell magic variables + if (Regex.Match(vName, + "^(global:){0,1}(PID|PSVersionTable|PSEdition|PSHOME|HOST|TRUE|FALSE|NULL)$", + RegexOptions.IgnoreCase|RegexOptions.CultureInvariant).Success == false + ) + { + updatedScriptblock.Append(scriptblockBodyString.Substring(position, v.Extent.StartOffset - pipelineOffset - position)); + updatedScriptblock.Append("${using:"); + updatedScriptblock.Append(CodeGeneration.EscapeVariableName(vName)); + updatedScriptblock.Append('}'); + position = v.Extent.EndOffset - pipelineOffset; + } + } + updatedScriptblock.Append(scriptblockBodyString.Substring(position)); + var sb = ScriptBlock.Create(updatedScriptblock.ToString()); + var commandInfo = new CmdletInfo("Start-Job", typeof(StartJobCommand)); + commandProcessor = context.CommandDiscovery.LookupCommandProcessor( + commandInfo, CommandOrigin.Internal, false, context.EngineSessionState); + var parameter = CommandParameterInternal.CreateParameterWithArgument( + pipelineAst.Extent, "ScriptBlock", null, + pipelineAst.Extent, sb, + false); + commandProcessor.AddParameter(parameter); + pipelineProcessor.Add(commandProcessor); + pipelineProcessor.LinkPipelineSuccessOutput(outputPipe ?? new Pipe(new List())); + + context.PushPipelineProcessor(pipelineProcessor); + try + { + pipelineProcessor.SynchronousExecuteEnumerate(AutomationNull.Value); + } + finally + { + context.PopPipelineProcessor(false); + } + } + finally + { + context.QuestionMarkVariableValue = !pipelineProcessor.ExecutionFailed; + pipelineProcessor.Dispose(); + } + } + private static void AddNoopCommandProcessor(PipelineProcessor pipelineProcessor, ExecutionContext context) { var commandInfo = new CmdletInfo("Out-Null", typeof(OutNullCommand)); @@ -716,7 +794,7 @@ internal static void CheckForInterrupts(ExecutionContext context) internal static void Nop() { } } - #region Redirections +#region Redirections internal abstract class CommandRedirection { @@ -1105,7 +1183,7 @@ private void Dispose(bool disposing) } } - #endregion Redirections +#endregion Redirections internal static class FunctionOps { diff --git a/src/System.Management.Automation/resources/ParserStrings.resx b/src/System.Management.Automation/resources/ParserStrings.resx index 5d7e1c1aa68..2cb1226f57c 100644 --- a/src/System.Management.Automation/resources/ParserStrings.resx +++ b/src/System.Management.Automation/resources/ParserStrings.resx @@ -446,9 +446,6 @@ Possible matches are The token '{0}' is not a valid statement separator in this version. - - The ampersand (&) character is not allowed. The & operator is reserved for future use; wrap an ampersand in double quotation marks ("&") to pass it as part of a string. - The 'from' keyword is not supported in this version of the language. @@ -1447,4 +1444,4 @@ ModuleVersion : Version of module to import. If used, ModuleName must represent Conflict in using PsDscRunAsCredential for Resource {0} because it already specifies PsDscRunAsCredential value. We can only use one PsDscRunAsCredential for the composite resource. - \ No newline at end of file + diff --git a/test/powershell/Language/Parser/Parsing.Tests.ps1 b/test/powershell/Language/Parser/Parsing.Tests.ps1 index cd3f389e797..25810148086 100644 --- a/test/powershell/Language/Parser/Parsing.Tests.ps1 +++ b/test/powershell/Language/Parser/Parsing.Tests.ps1 @@ -278,7 +278,6 @@ Describe 'Pipes parsing' -Tags "CI" { ShouldBeParseError 'gps|' EmptyPipeElement 4 ShouldBeParseError '1|1' ExpressionsMustBeFirstInPipeline 2 ShouldBeParseError '$a=' ExpectedValueExpression 3 - ShouldBeParseError '1 &' UnexpectedToken,MissingExpression 2,2 } Describe 'commands parsing' -Tags "CI" { diff --git a/test/powershell/Modules/Microsoft.PowerShell.Core/Job.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Core/Job.Tests.ps1 index 5c33da9f6a8..0a6b861a474 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Core/Job.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Core/Job.Tests.ps1 @@ -166,3 +166,69 @@ Describe "Debug-job test" -tag "Feature" { $result.Command | Should be "" } } + +Describe "Ampersand background test" -tag "CI","Slow" { + Context "Simple background job" { + AfterEach { + Get-Job | Remove-Job -Force + } + It "Background with & produces a job object" { + $j = Write-Output Hi & + $j | Should BeOfType System.Management.Automation.Job + } + } + Context "Variable tests" { + AfterEach { + Get-Job | Remove-Job -Force + } + It "doesn't cause error when variable is missing" { + Remove-Item variable:name -ErrorAction Ignore + $j = write-output "Hi $name" & + Receive-Job $j -Wait | Should BeExactly "Hi " + } + It "Copies variables to the child process" { + $n1 = "Bob" + $n2 = "Mary" + ${n 3} = "Bill" + $j = Write-Output "Hi $n1! Hi ${n2}! Hi ${n 3}!" & + Receive-Job $j -Wait | Should BeExactly "Hi Bob! Hi Mary! Hi Bill!" + } + It 'Make sure that $PID from the parent process does not overwrite $PID in the child process' { + $j = Write-Output $pid & + $cpid = Receive-Job $j -Wait + $pid | Should Not BeExactly $cpid + } + It 'Make sure that $global:PID from the parent process does not overwrite $global:PID in the child process' { + $j = Write-Output $global:pid & + $cpid = Receive-Job -Wait $j + $pid | Should Not BeExactly $cpid + } + It "starts in the current directory" { + $j = Get-Location | Foreach-Object -MemberName Path & + Receive-Job -Wait $j | Should Be ($pwd.Path) + } + It "Test that output redirection is done in the background job" { + $j = Write-Output hello > $TESTDRIVE/hello.txt & + Receive-Job -Wait $j | Should Be $null + Get-Content $TESTDRIVE/hello.txt | Should BeExactly "hello" + } + It "Test that error redirection is done in the background job" { + $j = Write-Error MyError 2> $TESTDRIVE/myerror.txt & + Receive-Job -Wait $j | Should Be $null + Get-Content -Raw $TESTDRIVE/myerror.txt | Should Match "MyError" + } + } + Context "Backgrounding expressions" { + AfterEach { + Get-Job | Remove-Job -Force + } + It "handles backgrounding expressions" { + $j = 2+3 & + Receive-Job $j -Wait | Should Be 5 + } + It "handles backgrounding mixed expressions" { + $j = 1..10 | ForEach-Object -Begin {$s=0} -Process {$s += $_} -End {$s} & + Receive-Job -Wait $j | Should Be 55 + } + } +}