diff --git a/src/System.Management.Automation/engine/lang/interface/PSToken.cs b/src/System.Management.Automation/engine/lang/interface/PSToken.cs
index 306a64a0b61..f5eb79be678 100644
--- a/src/System.Management.Automation/engine/lang/interface/PSToken.cs
+++ b/src/System.Management.Automation/engine/lang/interface/PSToken.cs
@@ -303,6 +303,8 @@ public static PSTokenType GetPSTokenType(Token token)
/* Hidden */ PSTokenType.Keyword,
/* Base */ PSTokenType.Keyword,
/* Default */ PSTokenType.Keyword,
+ /* Clean */ PSTokenType.Keyword,
+ /* AmpersandExclaim */ PSTokenType.Operator,
#endregion Flags for keywords
diff --git a/src/System.Management.Automation/engine/parser/Parser.cs b/src/System.Management.Automation/engine/parser/Parser.cs
index 1592d2e7e7d..fbc16b9ee76 100644
--- a/src/System.Management.Automation/engine/parser/Parser.cs
+++ b/src/System.Management.Automation/engine/parser/Parser.cs
@@ -5346,6 +5346,7 @@ private StatementAst FunctionDeclarationRule(Token functionToken)
case TokenKind.AndAnd:
case TokenKind.OrOr:
case TokenKind.Ampersand:
+ case TokenKind.AmpersandExclaim:
case TokenKind.Variable:
case TokenKind.SplattedVariable:
case TokenKind.HereStringExpandable:
@@ -5849,6 +5850,7 @@ private PipelineBaseAst PipelineChainRule()
Token currentChainOperatorToken = null;
Token nextToken = null;
bool background = false;
+ bool backgroundThreadJob = false;
while (true)
{
// Look for the next pipeline in the chain,
@@ -5938,6 +5940,24 @@ private PipelineBaseAst PipelineChainRule()
background = true;
goto default;
+ // ThreadJob background operator
+ case TokenKind.AmpersandExclaim:
+ SkipToken();
+ nextToken = PeekToken();
+
+ switch (nextToken.Kind)
+ {
+ case TokenKind.AndAnd:
+ case TokenKind.OrOr:
+ SkipToken();
+ ReportError(nextToken.Extent, nameof(ParserStrings.BackgroundOperatorInPipelineChain), ParserStrings.BackgroundOperatorInPipelineChain);
+ return new ErrorStatementAst(ExtentOf(currentPipelineChain ?? nextPipeline, nextToken.Extent));
+ }
+
+ background = true;
+ backgroundThreadJob = true;
+ goto default;
+
// No more chain operators -- return
default:
// If we haven't seen a chain yet, pass through the pipeline
@@ -5951,15 +5971,18 @@ private PipelineBaseAst PipelineChainRule()
// Set background on the pipeline AST
nextPipeline.Background = true;
+ nextPipeline.BackgroundThreadJob = backgroundThreadJob;
return nextPipeline;
}
- return new PipelineChainAst(
+ var chainAst = new PipelineChainAst(
ExtentOf(currentPipelineChain.Extent, nextPipeline.Extent),
currentPipelineChain,
nextPipeline,
currentChainOperatorToken.Kind,
background);
+ chainAst.BackgroundThreadJob = backgroundThreadJob;
+ return chainAst;
}
// Assemble the new chain statement AST
@@ -6008,6 +6031,7 @@ private PipelineBaseAst PipelineRule(
Token nextToken = null;
bool scanning = true;
bool background = false;
+ bool backgroundThreadJob = false;
ExpressionAst expr = startExpression;
while (scanning)
{
@@ -6125,6 +6149,20 @@ private PipelineBaseAst PipelineRule(
background = true;
break;
+ case TokenKind.AmpersandExclaim:
+ if (!allowBackground)
+ {
+ // Handled by invoking rule
+ scanning = false;
+ continue;
+ }
+
+ SkipToken();
+ scanning = false;
+ background = true;
+ backgroundThreadJob = true;
+ break;
+
case TokenKind.Pipe:
SkipToken();
SkipNewlines();
@@ -6156,7 +6194,12 @@ private PipelineBaseAst PipelineRule(
return null;
}
- return new PipelineAst(ExtentOf(startExtent, pipelineElements[pipelineElements.Count - 1]), pipelineElements, background);
+ var pipeline = new PipelineAst(ExtentOf(startExtent, pipelineElements[pipelineElements.Count - 1]), pipelineElements, background);
+ if (backgroundThreadJob)
+ {
+ pipeline.BackgroundThreadJob = true;
+ }
+ return pipeline;
}
private RedirectionAst RedirectionRule(RedirectionToken redirectionToken, RedirectionAst[] redirections, ref IScriptExtent extent)
@@ -6316,6 +6359,7 @@ private ExpressionAst GetCommandArgument(CommandArgumentContext context, Token t
case TokenKind.AndAnd:
case TokenKind.OrOr:
case TokenKind.Ampersand:
+ case TokenKind.AmpersandExclaim:
case TokenKind.MinusMinus:
case TokenKind.Comma:
UngetToken(token);
@@ -6520,6 +6564,7 @@ internal Ast CommandRule(bool forDynamicKeyword)
case TokenKind.AndAnd:
case TokenKind.OrOr:
case TokenKind.Ampersand:
+ case TokenKind.AmpersandExclaim:
UngetToken(token);
scanning = false;
continue;
diff --git a/src/System.Management.Automation/engine/parser/ast.cs b/src/System.Management.Automation/engine/parser/ast.cs
index 0325cf94aeb..d37dd394326 100644
--- a/src/System.Management.Automation/engine/parser/ast.cs
+++ b/src/System.Management.Automation/engine/parser/ast.cs
@@ -5610,7 +5610,9 @@ public PipelineChainAst(
///
public override Ast Copy()
{
- return new PipelineChainAst(Extent, CopyElement(LhsPipelineChain), CopyElement(RhsPipeline), Operator, Background);
+ var copy = new PipelineChainAst(Extent, CopyElement(LhsPipelineChain), CopyElement(RhsPipeline), Operator, Background);
+ copy.BackgroundThreadJob = this.BackgroundThreadJob;
+ return copy;
}
internal override object Accept(ICustomAstVisitor visitor)
@@ -5675,6 +5677,11 @@ public virtual ExpressionAst GetPureExpression()
{
return null;
}
+
+ ///
+ /// Indicates that this pipeline should be run in the background as a ThreadJob.
+ ///
+ public bool BackgroundThreadJob { get; internal set; }
}
///
@@ -5793,7 +5800,9 @@ public override ExpressionAst GetPureExpression()
public override Ast Copy()
{
var newPipelineElements = CopyElements(this.PipelineElements);
- return new PipelineAst(this.Extent, newPipelineElements, this.Background);
+ var copy = new PipelineAst(this.Extent, newPipelineElements, this.Background);
+ copy.BackgroundThreadJob = this.BackgroundThreadJob;
+ return copy;
}
#region Visitors
diff --git a/src/System.Management.Automation/engine/parser/token.cs b/src/System.Management.Automation/engine/parser/token.cs
index 3cee7580ff9..e578af1cc5e 100644
--- a/src/System.Management.Automation/engine/parser/token.cs
+++ b/src/System.Management.Automation/engine/parser/token.cs
@@ -591,6 +591,9 @@ public enum TokenKind
/// The 'clean' keyword.
Clean = 170,
+ /// The ThreadJob background operator '&!'.
+ AmpersandExclaim = 171,
+
#endregion Keywords
}
@@ -952,6 +955,7 @@ public static class TokenTraits
/* Base */ TokenFlags.Keyword,
/* Default */ TokenFlags.Keyword,
/* Clean */ TokenFlags.Keyword | TokenFlags.ScriptBlockBlockName,
+ /* AmpersandExclaim */ TokenFlags.SpecialOperator | TokenFlags.ParseModeInvariant,
#endregion Flags for keywords
};
@@ -1152,6 +1156,7 @@ public static class TokenTraits
/* Base */ "base",
/* Default */ "default",
/* Clean */ "clean",
+ /* AmpersandExclaim */ "&!",
#endregion Text for keywords
};
@@ -1160,10 +1165,10 @@ public static class TokenTraits
static TokenTraits()
{
Diagnostics.Assert(
- s_staticTokenFlags.Length == ((int)TokenKind.Clean + 1),
+ s_staticTokenFlags.Length == ((int)TokenKind.AmpersandExclaim + 1),
"Table size out of sync with enum - _staticTokenFlags");
Diagnostics.Assert(
- s_tokenText.Length == ((int)TokenKind.Clean + 1),
+ s_tokenText.Length == ((int)TokenKind.AmpersandExclaim + 1),
"Table size out of sync with enum - _tokenText");
// Some random assertions to make sure the enum and the traits are in sync
Diagnostics.Assert(GetTraits(TokenKind.Begin) == (TokenFlags.Keyword | TokenFlags.ScriptBlockBlockName),
diff --git a/src/System.Management.Automation/engine/parser/tokenizer.cs b/src/System.Management.Automation/engine/parser/tokenizer.cs
index e2aed94cc98..4804d574fd0 100644
--- a/src/System.Management.Automation/engine/parser/tokenizer.cs
+++ b/src/System.Management.Automation/engine/parser/tokenizer.cs
@@ -4975,12 +4975,19 @@ internal Token NextToken()
return ScanNumber(c);
case '&':
- if (PeekChar() == '&')
+ c1 = PeekChar();
+ if (c1 == '&')
{
SkipChar();
return NewToken(TokenKind.AndAnd);
}
+ if (c1 == '!')
+ {
+ SkipChar();
+ return NewToken(TokenKind.AmpersandExclaim);
+ }
+
return NewToken(TokenKind.Ampersand);
case '|':
diff --git a/src/System.Management.Automation/engine/runtime/Operations/MiscOps.cs b/src/System.Management.Automation/engine/runtime/Operations/MiscOps.cs
index d584666ab62..ed38d77d42d 100644
--- a/src/System.Management.Automation/engine/runtime/Operations/MiscOps.cs
+++ b/src/System.Management.Automation/engine/runtime/Operations/MiscOps.cs
@@ -550,51 +550,144 @@ internal static void InvokePipelineInBackground(
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(static x => x is VariableExpressionAst, true);
+ ScriptBlock sb;
+
+ // Check if the pipeline is already a script block expression (e.g., {1+1} &!)
+ // In this case, we should use the script block directly instead of wrapping it
+ // Note: PipelineElements is only available on PipelineAst, not PipelineBaseAst
+ var scriptBlockExpr = pipelineAst is PipelineAst pipeline &&
+ pipeline.PipelineElements.Count == 1 &&
+ pipeline.PipelineElements[0] is CommandExpressionAst cmdExpr &&
+ cmdExpr.Expression is ScriptBlockExpressionAst sbExpr
+ ? sbExpr
+ : null;
+
+ if (scriptBlockExpr != null)
+ {
+ // The pipeline is already a script block - extract the body without outer braces.
+ // ScriptBlockExpressionAst.Extent.Text includes the outer '{ }', so we strip them
+ // to get just the body. Using ScriptBlock.Create("{ content }") would create a
+ // script that returns a ScriptBlock object rather than executing the body.
+ var sbExprText = scriptBlockExpr.Extent.Text;
+ Diagnostics.Assert(
+ sbExprText.Length >= 2 && sbExprText[0] == '{' && sbExprText[sbExprText.Length - 1] == '}',
+ "ScriptBlockExpressionAst extent should always start with '{' and end with '}'");
+ var scriptblockBodyString = sbExprText.Substring(1, sbExprText.Length - 2);
+ var pipelineOffset = scriptBlockExpr.Extent.StartOffset + 1;
+ var variables = scriptBlockExpr.FindAll(static x => x is VariableExpressionAst, true);
+
+ // Minimize allocations by initializing the stringbuilder to the size of the source string + space for ${using:} * 2
+ System.Text.StringBuilder updatedScriptblock = new System.Text.StringBuilder(scriptblockBodyString.Length + 18);
+ int position = 0;
+
+ // Prefix variables in the scriptblock with $using:
+ foreach (var v in variables)
+ {
+ var variableName = ((VariableExpressionAst)v).VariablePath.UserPath;
- // Minimize allocations by initializing the stringbuilder to the size of the source string + space for ${using:} * 2
- System.Text.StringBuilder updatedScriptblock = new System.Text.StringBuilder(scriptblockBodyString.Length + 18);
- int position = 0;
+ // Skip variables that don't exist
+ if (funcContext._executionContext.EngineSessionState.GetVariable(variableName) == null)
+ {
+ continue;
+ }
- // Prefix variables in the scriptblock with $using:
- foreach (var v in variables)
+ // Skip PowerShell magic variables
+ if (!Regex.Match(
+ variableName,
+ "^(global:){0,1}(PID|PSVersionTable|PSEdition|PSHOME|HOST|TRUE|FALSE|NULL)$",
+ RegexOptions.IgnoreCase | RegexOptions.CultureInvariant).Success)
+ {
+ updatedScriptblock.Append(scriptblockBodyString.AsSpan(position, v.Extent.StartOffset - pipelineOffset - position));
+ updatedScriptblock.Append("${using:");
+ updatedScriptblock.Append(CodeGeneration.EscapeVariableName(variableName));
+ updatedScriptblock.Append('}');
+ position = v.Extent.EndOffset - pipelineOffset;
+ }
+ }
+
+ updatedScriptblock.Append(scriptblockBodyString.AsSpan(position));
+ sb = ScriptBlock.Create(updatedScriptblock.ToString());
+ }
+ else
{
- var variableName = ((VariableExpressionAst)v).VariablePath.UserPath;
+ // The pipeline is a regular command - wrap it in a script block
+ var scriptblockBodyString = pipelineAst.Extent.Text;
+ var pipelineOffset = pipelineAst.Extent.StartOffset;
+ var variables = pipelineAst.FindAll(static x => x is VariableExpressionAst, true);
+
+ // Minimize allocations by initializing the stringbuilder to the size of the source string + space for ${using:} * 2
+ System.Text.StringBuilder updatedScriptblock = new System.Text.StringBuilder(scriptblockBodyString.Length + 18);
+ int position = 0;
- // Skip variables that don't exist
- if (funcContext._executionContext.EngineSessionState.GetVariable(variableName) == null)
+ // Prefix variables in the scriptblock with $using:
+ foreach (var v in variables)
{
- continue;
+ var variableName = ((VariableExpressionAst)v).VariablePath.UserPath;
+
+ // Skip variables that don't exist
+ if (funcContext._executionContext.EngineSessionState.GetVariable(variableName) == null)
+ {
+ continue;
+ }
+
+ // Skip PowerShell magic variables
+ if (!Regex.Match(
+ variableName,
+ "^(global:){0,1}(PID|PSVersionTable|PSEdition|PSHOME|HOST|TRUE|FALSE|NULL)$",
+ RegexOptions.IgnoreCase | RegexOptions.CultureInvariant).Success)
+ {
+ updatedScriptblock.Append(scriptblockBodyString.AsSpan(position, v.Extent.StartOffset - pipelineOffset - position));
+ updatedScriptblock.Append("${using:");
+ updatedScriptblock.Append(CodeGeneration.EscapeVariableName(variableName));
+ updatedScriptblock.Append('}');
+ position = v.Extent.EndOffset - pipelineOffset;
+ }
}
- // Skip PowerShell magic variables
- if (!Regex.Match(
- variableName,
- "^(global:){0,1}(PID|PSVersionTable|PSEdition|PSHOME|HOST|TRUE|FALSE|NULL)$",
- RegexOptions.IgnoreCase | RegexOptions.CultureInvariant).Success)
+ updatedScriptblock.Append(scriptblockBodyString.AsSpan(position));
+ sb = ScriptBlock.Create(updatedScriptblock.ToString());
+ }
+
+ // Use Start-ThreadJob if BackgroundThreadJob is set, otherwise use Start-Job
+ CmdletInfo commandInfo;
+ bool usingThreadJob = false;
+ if (pipelineAst.BackgroundThreadJob)
+ {
+ // Use CommandTypes.Cmdlet only to avoid resolving a user-defined function that
+ // shadows the real Start-ThreadJob cmdlet, which would cause &! to silently fall
+ // back to Start-Job even when the ThreadJob module is installed.
+ var threadJobCmdlet = context.SessionState.InvokeCommand.GetCommand("Start-ThreadJob", CommandTypes.Cmdlet) as CmdletInfo;
+ if (threadJobCmdlet != null)
{
- updatedScriptblock.Append(scriptblockBodyString.AsSpan(position, v.Extent.StartOffset - pipelineOffset - position));
- updatedScriptblock.Append("${using:");
- updatedScriptblock.Append(CodeGeneration.EscapeVariableName(variableName));
- updatedScriptblock.Append('}');
- position = v.Extent.EndOffset - pipelineOffset;
+ commandInfo = threadJobCmdlet;
+ usingThreadJob = true;
+ }
+ else
+ {
+ // Fall back to Start-Job if Start-ThreadJob cmdlet is not available
+ commandInfo = new CmdletInfo("Start-Job", typeof(StartJobCommand));
}
}
-
- updatedScriptblock.Append(scriptblockBodyString.AsSpan(position));
- var sb = ScriptBlock.Create(updatedScriptblock.ToString());
- var commandInfo = new CmdletInfo("Start-Job", typeof(StartJobCommand));
+ else
+ {
+ commandInfo = new CmdletInfo("Start-Job", typeof(StartJobCommand));
+ }
+
commandProcessor = context.CommandDiscovery.LookupCommandProcessor(commandInfo, CommandOrigin.Internal, false, context.EngineSessionState);
- var workingDirectoryParameter = CommandParameterInternal.CreateParameterWithArgument(
- parameterAst: pipelineAst,
- parameterName: "WorkingDirectory",
- parameterText: null,
- argumentAst: pipelineAst,
- value: context.SessionState.Path.CurrentLocation.Path,
- spaceAfterParameter: false);
+ // Only add WorkingDirectory parameter for Start-Job, not for Start-ThreadJob
+ // Start-ThreadJob doesn't support the WorkingDirectory parameter
+ if (!usingThreadJob)
+ {
+ var workingDirectoryParameter = CommandParameterInternal.CreateParameterWithArgument(
+ parameterAst: pipelineAst,
+ parameterName: "WorkingDirectory",
+ parameterText: null,
+ argumentAst: pipelineAst,
+ value: context.SessionState.Path.CurrentLocation.Path,
+ spaceAfterParameter: false);
+ commandProcessor.AddParameter(workingDirectoryParameter);
+ }
var scriptBlockParameter = CommandParameterInternal.CreateParameterWithArgument(
parameterAst: pipelineAst,
@@ -604,7 +697,6 @@ internal static void InvokePipelineInBackground(
value: sb,
spaceAfterParameter: false);
- commandProcessor.AddParameter(workingDirectoryParameter);
commandProcessor.AddParameter(scriptBlockParameter);
pipelineProcessor.Add(commandProcessor);
pipelineProcessor.LinkPipelineSuccessOutput(outputPipe ?? new Pipe(new List