Support the pipeline chain operators &&
and ||
in PowerShell language (#9849)
This commit is contained in:
parent
425bc36a6f
commit
2a518fcfe2
11
build.psm1
11
build.psm1
|
@ -220,6 +220,10 @@ function Start-PSBuild {
|
|||
[switch]$NoPSModuleRestore,
|
||||
[switch]$CI,
|
||||
|
||||
# Skips the step where the pwsh that's been built is used to create a configuration
|
||||
# Useful when changing parsing/compilation, since bugs there can mean we can't get past this step
|
||||
[switch]$SkipExperimentalFeatureGeneration,
|
||||
|
||||
# this switch will re-build only System.Management.Automation.dll
|
||||
# it's useful for development, to do a quick changes in the engine
|
||||
[switch]$SMAOnly,
|
||||
|
@ -499,8 +503,13 @@ Fix steps:
|
|||
$config = @{ "Microsoft.PowerShell:ExecutionPolicy" = "RemoteSigned" }
|
||||
}
|
||||
|
||||
# When building preview, we want the configuration to enable all experiemental features by default
|
||||
# ARM is cross compiled, so we can't run pwsh to enumerate Experimental Features
|
||||
if ((Test-IsPreview $psVersion) -and -not $Runtime.Contains("arm") -and -not ($Runtime -like 'fxdependent*')) {
|
||||
if (-not $SkipExperimentalFeatureGeneration -and
|
||||
(Test-IsPreview $psVersion) -and
|
||||
-not $Runtime.Contains("arm") -and
|
||||
-not ($Runtime -like 'fxdependent*')) {
|
||||
|
||||
$json = & $publishPath\pwsh -noprofile -command {
|
||||
$expFeatures = [System.Collections.Generic.List[string]]::new()
|
||||
Get-ExperimentalFeature | ForEach-Object { $expFeatures.Add($_.Name) }
|
||||
|
|
|
@ -6966,16 +6966,15 @@ namespace System.Management.Automation
|
|||
|
||||
public object VisitUsingStatement(UsingStatementAst usingStatementAst) { return false; }
|
||||
|
||||
public object VisitDynamicKeywordStatement(DynamicKeywordStatementAst dynamicKeywordStatementAst) { return false; }
|
||||
|
||||
public object VisitPipelineChain(PipelineChainAst pipelineChainAst) { return false; }
|
||||
|
||||
public object VisitConfigurationDefinition(ConfigurationDefinitionAst configurationDefinitionAst)
|
||||
{
|
||||
return configurationDefinitionAst.Body.Accept(this);
|
||||
}
|
||||
|
||||
public object VisitDynamicKeywordStatement(DynamicKeywordStatementAst dynamicKeywordStatementAst)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public object VisitStatementBlock(StatementBlockAst statementBlockAst)
|
||||
{
|
||||
if (statementBlockAst.Traps != null) return false;
|
||||
|
|
|
@ -123,7 +123,10 @@ namespace System.Management.Automation
|
|||
description: "Print notification message when new releases are available"),
|
||||
new ExperimentalFeature(
|
||||
name: "PSCoalescingOperators",
|
||||
description: "Support the null coalescing operator and null coalescing assignment operator in PowerShell language")
|
||||
description: "Support the null coalescing operator and null coalescing assignment operator in PowerShell language"),
|
||||
new ExperimentalFeature(
|
||||
name: "PSBashCommandOperators",
|
||||
description: "Allow use of && and || as operators between pipeline invocations"),
|
||||
};
|
||||
EngineExperimentalFeatures = new ReadOnlyCollection<ExperimentalFeature>(engineFeatures);
|
||||
|
||||
|
|
|
@ -175,6 +175,9 @@ namespace System.Management.Automation.Language
|
|||
|
||||
/// <summary/>
|
||||
object VisitTernaryExpression(TernaryExpressionAst ternaryExpressionAst) => DefaultVisit(ternaryExpressionAst);
|
||||
|
||||
/// <summary/>
|
||||
object VisitPipelineChain(PipelineChainAst statementChainAst) => DefaultVisit(statementChainAst);
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
|
@ -318,6 +321,8 @@ namespace System.Management.Automation.Language
|
|||
public override AstVisitAction VisitDynamicKeywordStatement(DynamicKeywordStatementAst ast) { return CheckParent(ast); }
|
||||
|
||||
public override AstVisitAction VisitTernaryExpression(TernaryExpressionAst ast) => CheckParent(ast);
|
||||
|
||||
public override AstVisitAction VisitPipelineChain(PipelineChainAst ast) => CheckParent(ast);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -552,6 +557,8 @@ namespace System.Management.Automation.Language
|
|||
public override AstVisitAction VisitDynamicKeywordStatement(DynamicKeywordStatementAst ast) { return Check(ast); }
|
||||
|
||||
public override AstVisitAction VisitTernaryExpression(TernaryExpressionAst ast) { return Check(ast); }
|
||||
|
||||
public override AstVisitAction VisitPipelineChain(PipelineChainAst ast) { return Check(ast); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -690,5 +697,7 @@ namespace System.Management.Automation.Language
|
|||
public virtual object VisitFunctionMember(FunctionMemberAst functionMemberAst) { return null; }
|
||||
/// <summary/>
|
||||
public virtual object VisitTernaryExpression(TernaryExpressionAst ternaryExpressionAst) { return null; }
|
||||
/// <summary/>
|
||||
public virtual object VisitPipelineChain(PipelineChainAst statementChainAst) { return null; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -509,6 +509,7 @@ namespace System.Management.Automation.Language
|
|||
internal static readonly Expression InvariantCulture = Expression.Constant(CultureInfo.InvariantCulture);
|
||||
internal static readonly Expression OrdinalIgnoreCaseComparer = Expression.Constant(StringComparer.OrdinalIgnoreCase, typeof(StringComparer));
|
||||
internal static readonly Expression CatchAllType = Expression.Constant(typeof(ExceptionHandlingOps.CatchAll), typeof(Type));
|
||||
// Empty expression is used at the end of blocks to give them the void expression result
|
||||
internal static readonly Expression Empty = Expression.Empty();
|
||||
internal static Expression GetExecutionContextFromTLS =
|
||||
Expression.Call(CachedReflectionInfo.LocalPipeline_GetExecutionContextFromTLS);
|
||||
|
@ -648,6 +649,8 @@ namespace System.Management.Automation.Language
|
|||
internal static readonly ParameterExpression _executionContextParameter;
|
||||
internal static readonly ParameterExpression _functionContext;
|
||||
internal static readonly ParameterExpression _returnPipe;
|
||||
private static readonly Expression s_notDollarQuestion;
|
||||
private static readonly Expression s_getDollarQuestion;
|
||||
private static readonly Expression s_setDollarQuestionToTrue;
|
||||
private static readonly Expression s_callCheckForInterrupts;
|
||||
private static readonly Expression s_getCurrentPipe;
|
||||
|
@ -667,8 +670,12 @@ namespace System.Management.Automation.Language
|
|||
_functionContext = Expression.Parameter(typeof(FunctionContext), "funcContext");
|
||||
_executionContextParameter = Expression.Variable(typeof(ExecutionContext), "context");
|
||||
|
||||
s_getDollarQuestion = Expression.Property(_executionContextParameter, CachedReflectionInfo.ExecutionContext_QuestionMarkVariableValue);
|
||||
|
||||
s_notDollarQuestion = Expression.Not(s_getDollarQuestion);
|
||||
|
||||
s_setDollarQuestionToTrue = Expression.Assign(
|
||||
Expression.Property(_executionContextParameter, CachedReflectionInfo.ExecutionContext_QuestionMarkVariableValue),
|
||||
s_getDollarQuestion,
|
||||
ExpressionCache.TrueConstant);
|
||||
|
||||
s_callCheckForInterrupts = Expression.Call(CachedReflectionInfo.PipelineOps_CheckForInterrupts,
|
||||
|
@ -2903,8 +2910,9 @@ namespace System.Management.Automation.Language
|
|||
Compiler._functionContext, exception);
|
||||
var catchAll = Expression.Catch(
|
||||
exception,
|
||||
Expression.Block(callCheckActionPreference,
|
||||
Expression.Goto(dispatchNextStatementTarget)));
|
||||
Expression.Block(
|
||||
callCheckActionPreference,
|
||||
Expression.Goto(dispatchNextStatementTarget)));
|
||||
|
||||
var expr = Expression.TryCatch(Expression.Block(tryBodyExprs),
|
||||
new CatchBlock[] { s_catchFlowControl, catchAll });
|
||||
|
@ -3115,6 +3123,160 @@ namespace System.Management.Automation.Language
|
|||
return Expression.Block(exprs);
|
||||
}
|
||||
|
||||
public object VisitPipelineChain(PipelineChainAst pipelineChainAst)
|
||||
{
|
||||
// If the statement chain is backgrounded,
|
||||
// we defer that to the background operation call
|
||||
if (pipelineChainAst.Background)
|
||||
{
|
||||
return Expression.Call(
|
||||
CachedReflectionInfo.PipelineOps_InvokePipelineInBackground,
|
||||
Expression.Constant(pipelineChainAst),
|
||||
_functionContext);
|
||||
}
|
||||
|
||||
// We want to generate code like:
|
||||
//
|
||||
// dispatchIndex = 0;
|
||||
// DispatchNextStatementTarget:
|
||||
// try {
|
||||
// switch (dispatchIndex) {
|
||||
// case 0: goto L0;
|
||||
// case 1: goto L1;
|
||||
// case 2: goto L2;
|
||||
// }
|
||||
// L0: dispatchIndex = 1;
|
||||
// pipeline1;
|
||||
// L1: dispatchIndex = 2;
|
||||
// if ($?) pipeline2;
|
||||
// L2: dispatchIndex = 3;
|
||||
// if ($?) pipeline3;
|
||||
// ...
|
||||
// } catch (FlowControlException) {
|
||||
// throw;
|
||||
// } catch (Exception e) {
|
||||
// ExceptionHandlingOps.CheckActionPreference(functionContext, e);
|
||||
// goto DispatchNextStatementTarget;
|
||||
// }
|
||||
// LN:
|
||||
//
|
||||
// Note that we deliberately do not push trap handlers
|
||||
// so that those can be handled by the enclosing statement block instead.
|
||||
|
||||
var exprs = new List<Expression>();
|
||||
|
||||
// A pipeline chain is left-hand-side deep,
|
||||
// so to compile from left to right, we need to start from the leaf
|
||||
// and roll back up to the top, being the right-most element in the chain
|
||||
PipelineChainAst currentChain = pipelineChainAst;
|
||||
while (currentChain.LhsPipelineChain is PipelineChainAst lhsPipelineChain)
|
||||
{
|
||||
currentChain = lhsPipelineChain;
|
||||
}
|
||||
|
||||
// int chainIndex = 0;
|
||||
ParameterExpression dispatchIndex = Expression.Variable(typeof(int), nameof(dispatchIndex));
|
||||
var temps = new ParameterExpression[] { dispatchIndex };
|
||||
exprs.Add(Expression.Assign(dispatchIndex, ExpressionCache.Constant(0)));
|
||||
|
||||
// DispatchNextTargetStatement:
|
||||
LabelTarget dispatchNextStatementTargetLabel = Expression.Label();
|
||||
exprs.Add(Expression.Label(dispatchNextStatementTargetLabel));
|
||||
|
||||
// try statement body
|
||||
var switchCases = new List<SwitchCase>();
|
||||
var dispatchTargets = new List<LabelTarget>();
|
||||
var tryBodyExprs = new List<Expression>()
|
||||
{
|
||||
null, // Add a slot for the inital switch/case that we'll come back to
|
||||
};
|
||||
|
||||
// L0: dispatchIndex = 1; pipeline1
|
||||
LabelTarget label0 = Expression.Label();
|
||||
dispatchTargets.Add(label0);
|
||||
switchCases.Add(
|
||||
Expression.SwitchCase(
|
||||
Expression.Goto(label0),
|
||||
ExpressionCache.Constant(0)));
|
||||
tryBodyExprs.Add(Expression.Label(label0));
|
||||
tryBodyExprs.Add(Expression.Assign(dispatchIndex, ExpressionCache.Constant(1)));
|
||||
tryBodyExprs.Add(Compile(currentChain.LhsPipelineChain));
|
||||
|
||||
// Remainder of try statement body
|
||||
// L1: dispatchIndex = 2; if ($?) pipeline2;
|
||||
// ...
|
||||
int chainIndex = 1;
|
||||
do
|
||||
{
|
||||
// Record label and switch case for later use
|
||||
LabelTarget currentLabel = Expression.Label();
|
||||
dispatchTargets.Add(currentLabel);
|
||||
switchCases.Add(
|
||||
Expression.SwitchCase(
|
||||
Expression.Goto(currentLabel),
|
||||
ExpressionCache.Constant(chainIndex)));
|
||||
|
||||
// Add label and dispatchIndex for current pipeline
|
||||
tryBodyExprs.Add(Expression.Label(currentLabel));
|
||||
tryBodyExprs.Add(
|
||||
Expression.Assign(
|
||||
dispatchIndex,
|
||||
ExpressionCache.Constant(chainIndex + 1)));
|
||||
|
||||
// Increment chain index for next iteration
|
||||
chainIndex++;
|
||||
|
||||
Diagnostics.Assert(
|
||||
currentChain.Operator == TokenKind.AndAnd || currentChain.Operator == TokenKind.OrOr,
|
||||
"Chain operators must be either && or ||");
|
||||
|
||||
Expression dollarQuestionCheck = currentChain.Operator == TokenKind.AndAnd
|
||||
? s_getDollarQuestion
|
||||
: s_notDollarQuestion;
|
||||
|
||||
tryBodyExprs.Add(Expression.IfThen(dollarQuestionCheck, Compile(currentChain.RhsPipeline)));
|
||||
|
||||
currentChain = currentChain.Parent as PipelineChainAst;
|
||||
}
|
||||
while (currentChain != null);
|
||||
|
||||
// Add empty expression to make the block value void
|
||||
tryBodyExprs.Add(ExpressionCache.Empty);
|
||||
|
||||
// Create the final label that follows the entire try/catch
|
||||
LabelTarget afterLabel = Expression.Label();
|
||||
switchCases.Add(
|
||||
Expression.SwitchCase(
|
||||
Expression.Goto(afterLabel),
|
||||
ExpressionCache.Constant(chainIndex)));
|
||||
|
||||
// Now set the switch/case that belongs at the top
|
||||
tryBodyExprs[0] = Expression.Switch(dispatchIndex, switchCases.ToArray());
|
||||
|
||||
// Create the catch block for flow control and action preference
|
||||
ParameterExpression exception = Expression.Variable(typeof(Exception), nameof(exception));
|
||||
MethodCallExpression callCheckActionPreference = Expression.Call(
|
||||
CachedReflectionInfo.ExceptionHandlingOps_CheckActionPreference,
|
||||
Compiler._functionContext,
|
||||
exception);
|
||||
CatchBlock catchAll = Expression.Catch(
|
||||
exception,
|
||||
Expression.Block(
|
||||
callCheckActionPreference,
|
||||
Expression.Goto(dispatchNextStatementTargetLabel)));
|
||||
|
||||
TryExpression expr = Expression.TryCatch(
|
||||
Expression.Block(tryBodyExprs),
|
||||
new CatchBlock[] { s_catchFlowControl, catchAll });
|
||||
|
||||
exprs.Add(expr);
|
||||
exprs.Add(Expression.Label(afterLabel));
|
||||
|
||||
BlockExpression fullyExpandedBlock = Expression.Block(typeof(void), temps, exprs);
|
||||
|
||||
return fullyExpandedBlock;
|
||||
}
|
||||
|
||||
public object VisitPipeline(PipelineAst pipelineAst)
|
||||
{
|
||||
var temps = new List<ParameterExpression>();
|
||||
|
|
|
@ -35,6 +35,8 @@ namespace System.Management.Automation.Language
|
|||
/// </summary>
|
||||
public sealed class Parser
|
||||
{
|
||||
private static readonly bool s_pipelineChainsEnabled = ExperimentalFeature.IsEnabled("PSPipelineChainOperators");
|
||||
|
||||
private readonly Tokenizer _tokenizer;
|
||||
internal Token _ungotToken;
|
||||
private bool _disableCommaOperator;
|
||||
|
@ -1195,7 +1197,8 @@ namespace System.Management.Automation.Language
|
|||
// ErrorRecovery: ?
|
||||
|
||||
IScriptExtent errorPosition = After(token);
|
||||
ReportIncompleteInput(errorPosition,
|
||||
ReportIncompleteInput(
|
||||
errorPosition,
|
||||
nameof(ParserStrings.MissingExpressionInNamedArgument),
|
||||
ParserStrings.MissingExpressionInNamedArgument);
|
||||
expr = new ErrorExpressionAst(errorPosition);
|
||||
|
@ -1244,7 +1247,8 @@ namespace System.Management.Automation.Language
|
|||
// ErrorRecovery: Pretend we saw the argument and keep going.
|
||||
|
||||
IScriptExtent errorExtent = After(commaToken);
|
||||
ReportIncompleteInput(errorExtent,
|
||||
ReportIncompleteInput(
|
||||
errorExtent,
|
||||
nameof(ParserStrings.MissingExpressionAfterToken),
|
||||
ParserStrings.MissingExpressionAfterToken,
|
||||
commaToken.Kind.Text());
|
||||
|
@ -1921,7 +1925,7 @@ namespace System.Management.Automation.Language
|
|||
// G trap-statement
|
||||
// G try-statement
|
||||
// G data-statement
|
||||
// G pipeline statement-terminator
|
||||
// G pipeline-chain statement-terminator
|
||||
// G
|
||||
// G labeled-statement:
|
||||
// G switch-statement
|
||||
|
@ -2064,7 +2068,7 @@ namespace System.Management.Automation.Language
|
|||
if (attributes != null)
|
||||
{
|
||||
Resync(restorePoint);
|
||||
statement = PipelineRule();
|
||||
statement = PipelineChainRule();
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -2115,7 +2119,7 @@ namespace System.Management.Automation.Language
|
|||
UngetToken(token);
|
||||
}
|
||||
|
||||
statement = PipelineRule();
|
||||
statement = PipelineChainRule();
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -2226,9 +2230,9 @@ namespace System.Management.Automation.Language
|
|||
private ReturnStatementAst ReturnStatementRule(Token token)
|
||||
{
|
||||
// G flow-control-statement:
|
||||
// G 'return' pipeline:opt
|
||||
// G 'return' pipeline-chain:opt
|
||||
|
||||
PipelineBaseAst pipeline = PipelineRule();
|
||||
PipelineBaseAst pipeline = PipelineChainRule();
|
||||
IScriptExtent extent = (pipeline != null)
|
||||
? ExtentOf(token, pipeline)
|
||||
: token.Extent;
|
||||
|
@ -2238,9 +2242,9 @@ namespace System.Management.Automation.Language
|
|||
private ExitStatementAst ExitStatementRule(Token token)
|
||||
{
|
||||
// G flow-control-statement:
|
||||
// G 'exit' pipeline:opt
|
||||
// G 'exit' pipeline-chain:opt
|
||||
|
||||
PipelineBaseAst pipeline = PipelineRule();
|
||||
PipelineBaseAst pipeline = PipelineChainRule();
|
||||
IScriptExtent extent = (pipeline != null)
|
||||
? ExtentOf(token, pipeline)
|
||||
: token.Extent;
|
||||
|
@ -2252,10 +2256,11 @@ namespace System.Management.Automation.Language
|
|||
// G flow-control-statement:
|
||||
// G 'throw' pipeline:opt
|
||||
|
||||
PipelineBaseAst pipeline = PipelineRule();
|
||||
PipelineBaseAst pipeline = PipelineChainRule();
|
||||
IScriptExtent extent = (pipeline != null)
|
||||
? ExtentOf(token, pipeline)
|
||||
: token.Extent;
|
||||
|
||||
return new ThrowStatementAst(extent, pipeline);
|
||||
}
|
||||
|
||||
|
@ -2270,6 +2275,7 @@ namespace System.Management.Automation.Language
|
|||
// G for-statement
|
||||
// G while-statement
|
||||
// G do-statement
|
||||
// G pipeline-chain
|
||||
|
||||
StatementAst statement;
|
||||
Token token = NextToken();
|
||||
|
@ -2293,7 +2299,7 @@ namespace System.Management.Automation.Language
|
|||
default:
|
||||
// We can only unget 1 token, but have 2 to unget, so resync on the label.
|
||||
Resync(label);
|
||||
statement = PipelineRule();
|
||||
statement = PipelineChainRule();
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -2362,12 +2368,12 @@ namespace System.Management.Automation.Language
|
|||
private StatementAst IfStatementRule(Token ifToken)
|
||||
{
|
||||
// G if-statement:
|
||||
// G 'if' new-lines:opt '(' pipeline-statement ')' statement-block elseif-clauses:opt else-clause:opt
|
||||
// G 'if' new-lines:opt '(' pipeline-chain ')' statement-block elseif-clauses:opt else-clause:opt
|
||||
// G elseif-clauses:
|
||||
// G elseif-clause
|
||||
// G elseif-clauses elseif-clause
|
||||
// G elseif-clause:
|
||||
// G 'elseif' new-lines:opt '(' pipeline-statement ')' statement-block
|
||||
// G 'elseif' new-lines:opt '(' pipeline-chain ')' statement-block
|
||||
// G else-clause:
|
||||
// G 'else' statement-block
|
||||
|
||||
|
@ -2394,14 +2400,15 @@ namespace System.Management.Automation.Language
|
|||
}
|
||||
|
||||
SkipNewlines();
|
||||
PipelineBaseAst condition = PipelineRule();
|
||||
PipelineBaseAst condition = PipelineChainRule();
|
||||
if (condition == null)
|
||||
{
|
||||
// ErrorRecovery: assume pipeline just hasn't been entered yet, continue hoping
|
||||
// to find a close paren and statement block.
|
||||
|
||||
IScriptExtent errorPosition = After(lParen);
|
||||
ReportIncompleteInput(errorPosition,
|
||||
ReportIncompleteInput(
|
||||
errorPosition,
|
||||
nameof(ParserStrings.IfStatementMissingCondition),
|
||||
ParserStrings.IfStatementMissingCondition,
|
||||
keyword.Text);
|
||||
|
@ -2667,7 +2674,7 @@ namespace System.Management.Automation.Language
|
|||
|
||||
needErrorCondition = true; // need to add condition ast to the error statement if the parsing fails
|
||||
SkipNewlines();
|
||||
condition = PipelineRule();
|
||||
condition = PipelineChainRule();
|
||||
if (condition == null)
|
||||
{
|
||||
// ErrorRecovery: pretend we saw the condition and keep parsing.
|
||||
|
@ -3382,7 +3389,7 @@ namespace System.Management.Automation.Language
|
|||
else
|
||||
{
|
||||
SkipNewlines();
|
||||
pipeline = PipelineRule();
|
||||
pipeline = PipelineChainRule();
|
||||
if (pipeline == null)
|
||||
{
|
||||
// ErrorRecovery: assume the rest of the statement is missing.
|
||||
|
@ -3450,11 +3457,11 @@ namespace System.Management.Automation.Language
|
|||
// G new-lines:opt for-initializer:opt
|
||||
// G new-lines:opt ')' statement-block
|
||||
// G for-initializer:
|
||||
// G pipeline
|
||||
// G pipeline-chain
|
||||
// G for-condition:
|
||||
// G pipeline
|
||||
// G pipeline-chain
|
||||
// G for-iterator:
|
||||
// G pipeline
|
||||
// G pipeline-chain
|
||||
|
||||
IScriptExtent endErrorStatement = null;
|
||||
SkipNewlines();
|
||||
|
@ -3473,7 +3480,7 @@ namespace System.Management.Automation.Language
|
|||
}
|
||||
|
||||
SkipNewlines();
|
||||
PipelineBaseAst initializer = PipelineRule();
|
||||
PipelineBaseAst initializer = PipelineChainRule();
|
||||
if (initializer != null)
|
||||
{
|
||||
endErrorStatement = initializer.Extent;
|
||||
|
@ -3485,7 +3492,7 @@ namespace System.Management.Automation.Language
|
|||
}
|
||||
|
||||
SkipNewlines();
|
||||
PipelineBaseAst condition = PipelineRule();
|
||||
PipelineBaseAst condition = PipelineChainRule();
|
||||
if (condition != null)
|
||||
{
|
||||
endErrorStatement = condition.Extent;
|
||||
|
@ -3497,7 +3504,7 @@ namespace System.Management.Automation.Language
|
|||
}
|
||||
|
||||
SkipNewlines();
|
||||
PipelineBaseAst iterator = PipelineRule();
|
||||
PipelineBaseAst iterator = PipelineChainRule();
|
||||
if (iterator != null)
|
||||
{
|
||||
endErrorStatement = iterator.Extent;
|
||||
|
@ -3549,6 +3556,9 @@ namespace System.Management.Automation.Language
|
|||
{
|
||||
// G while-statement:
|
||||
// G 'while ' new-lines:opt '(' new-lines:opt while-condition new-lines:opt ')' statement-block
|
||||
// G
|
||||
// G while-condition:
|
||||
// G pipeline-chain
|
||||
|
||||
SkipNewlines();
|
||||
|
||||
|
@ -3567,7 +3577,7 @@ namespace System.Management.Automation.Language
|
|||
}
|
||||
|
||||
SkipNewlines();
|
||||
PipelineBaseAst condition = PipelineRule();
|
||||
PipelineBaseAst condition = PipelineChainRule();
|
||||
PipelineBaseAst errorCondition = null;
|
||||
if (condition == null)
|
||||
{
|
||||
|
@ -3575,7 +3585,8 @@ namespace System.Management.Automation.Language
|
|||
// to find a close paren and statement block.
|
||||
|
||||
IScriptExtent errorPosition = After(lParen);
|
||||
ReportIncompleteInput(errorPosition,
|
||||
ReportIncompleteInput(
|
||||
errorPosition,
|
||||
nameof(ParserStrings.MissingExpressionAfterKeyword),
|
||||
ParserStrings.MissingExpressionAfterKeyword,
|
||||
whileToken.Kind.Text());
|
||||
|
@ -4075,7 +4086,7 @@ namespace System.Management.Automation.Language
|
|||
else
|
||||
{
|
||||
SkipNewlines();
|
||||
condition = PipelineRule();
|
||||
condition = PipelineChainRule();
|
||||
if (condition == null)
|
||||
{
|
||||
// ErrorRecovery: try to get the matching close paren, then return an error statement.
|
||||
|
@ -5692,7 +5703,233 @@ namespace System.Management.Automation.Language
|
|||
|
||||
#region Pipelines
|
||||
|
||||
private PipelineBaseAst PipelineRule()
|
||||
private PipelineBaseAst PipelineChainRule()
|
||||
{
|
||||
// G pipeline-chain:
|
||||
// G pipeline
|
||||
// G pipeline-chain chain-operator pipeline
|
||||
// G
|
||||
// G chain-operator:
|
||||
// G '||'
|
||||
// G '&&'
|
||||
|
||||
// If this feature is not enabled,
|
||||
// just look for pipelines as before.
|
||||
if (!s_pipelineChainsEnabled)
|
||||
{
|
||||
return PipelineRule(allowBackground: true);
|
||||
}
|
||||
|
||||
RuntimeHelpers.EnsureSufficientExecutionStack();
|
||||
|
||||
// First look for assignment, since PipelineRule once handled that and this supercedes that.
|
||||
// We may end up with an expression here as a result,
|
||||
// in which case we hang on to it to pass it into the first pipeline rule call.
|
||||
|
||||
Token assignToken = null;
|
||||
ExpressionAst expr;
|
||||
|
||||
// Look for an expression,
|
||||
// which could either be a variable to assign to,
|
||||
// or the first expression in a pipeline: "Hi" | % { $_ }
|
||||
var oldTokenizerMode = _tokenizer.Mode;
|
||||
try
|
||||
{
|
||||
SetTokenizerMode(TokenizerMode.Expression);
|
||||
expr = ExpressionRule();
|
||||
if (expr != null)
|
||||
{
|
||||
// We peek here because we are in expression mode, otherwise =0 will get scanned
|
||||
// as a single token.
|
||||
var token = PeekToken();
|
||||
if (token.Kind.HasTrait(TokenFlags.AssignmentOperator))
|
||||
{
|
||||
SkipToken();
|
||||
assignToken = token;
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
SetTokenizerMode(oldTokenizerMode);
|
||||
}
|
||||
|
||||
// If we have an assign token, deal with assignment
|
||||
if (expr != null && assignToken != null)
|
||||
{
|
||||
SkipNewlines();
|
||||
StatementAst statement = StatementRule();
|
||||
|
||||
if (statement == null)
|
||||
{
|
||||
// ErrorRecovery: we are very likely at EOF because pretty much anything should result in some
|
||||
// pipeline, so just keep parsing.
|
||||
|
||||
IScriptExtent errorExtent = After(assignToken);
|
||||
ReportIncompleteInput(
|
||||
errorExtent,
|
||||
nameof(ParserStrings.ExpectedValueExpression),
|
||||
ParserStrings.ExpectedValueExpression,
|
||||
assignToken.Kind.Text());
|
||||
statement = new ErrorStatementAst(errorExtent);
|
||||
}
|
||||
|
||||
return new AssignmentStatementAst(
|
||||
ExtentOf(expr, statement),
|
||||
expr,
|
||||
assignToken.Kind,
|
||||
statement,
|
||||
assignToken.Extent);
|
||||
}
|
||||
|
||||
// Start scanning for a pipeline chain,
|
||||
// possibly starting with the expression from earlier
|
||||
ExpressionAst startExpression = expr;
|
||||
ChainableAst currentPipelineChain = null;
|
||||
Token currentChainOperatorToken = null;
|
||||
Token nextToken = null;
|
||||
bool background = false;
|
||||
while (true)
|
||||
{
|
||||
// Look for the next pipeline in the chain,
|
||||
// at this point we should already have parsed all assignments
|
||||
// in enclosing calls to PipelineChainRule
|
||||
PipelineAst nextPipeline;
|
||||
Token firstPipelineToken = null;
|
||||
if (startExpression != null)
|
||||
{
|
||||
nextPipeline = (PipelineAst)PipelineRule(startExpression);
|
||||
startExpression = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Remember the token for error reporting,
|
||||
// since erroneous results in the rules still consume tokens
|
||||
firstPipelineToken = PeekToken();
|
||||
nextPipeline = (PipelineAst)PipelineRule();
|
||||
}
|
||||
|
||||
if (nextPipeline == null)
|
||||
{
|
||||
if (currentChainOperatorToken == null)
|
||||
{
|
||||
// We haven't seen a chain token, so the caller is responsible
|
||||
// for expecting a pipeline and must manage this
|
||||
return null;
|
||||
}
|
||||
|
||||
// See if we are responsible for reporting the issue
|
||||
switch (firstPipelineToken.Kind)
|
||||
{
|
||||
case TokenKind.EndOfInput:
|
||||
// If we're at EOF, we should allow input to complete
|
||||
ReportIncompleteInput(
|
||||
After(currentChainOperatorToken),
|
||||
nameof(ParserStrings.EmptyPipelineChainElement),
|
||||
ParserStrings.EmptyPipelineChainElement,
|
||||
currentChainOperatorToken.Text);
|
||||
break;
|
||||
|
||||
case TokenKind.Dot:
|
||||
case TokenKind.Ampersand:
|
||||
// If something like 'command && &' or 'command && .' was provided,
|
||||
// CommandRule has already reported the error.
|
||||
break;
|
||||
|
||||
default:
|
||||
ReportError(
|
||||
ExtentOf(currentChainOperatorToken, firstPipelineToken),
|
||||
nameof(ParserStrings.EmptyPipelineChainElement),
|
||||
ParserStrings.EmptyPipelineChainElement,
|
||||
currentChainOperatorToken.Text);
|
||||
break;
|
||||
}
|
||||
|
||||
return new ErrorStatementAst(
|
||||
ExtentOf(currentPipelineChain, currentChainOperatorToken),
|
||||
currentChainOperatorToken,
|
||||
new[] { currentPipelineChain });
|
||||
}
|
||||
|
||||
// Look ahead for a chain operator
|
||||
nextToken = PeekToken();
|
||||
switch (nextToken.Kind)
|
||||
{
|
||||
case TokenKind.AndAnd:
|
||||
case TokenKind.OrOr:
|
||||
SkipToken();
|
||||
SkipNewlines();
|
||||
break;
|
||||
|
||||
// Background operators may also occur here
|
||||
case TokenKind.Ampersand:
|
||||
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;
|
||||
goto default;
|
||||
|
||||
// No more chain operators -- return
|
||||
default:
|
||||
// If we haven't seen a chain yet, pass through the pipeline
|
||||
// Simplifies the AST and prevents allocation
|
||||
if (currentPipelineChain == null)
|
||||
{
|
||||
if (!background)
|
||||
{
|
||||
return nextPipeline;
|
||||
}
|
||||
|
||||
// Set background on the pipeline AST
|
||||
nextPipeline.Background = true;
|
||||
return nextPipeline;
|
||||
}
|
||||
|
||||
return new PipelineChainAst(
|
||||
ExtentOf(currentPipelineChain.Extent, nextPipeline.Extent),
|
||||
currentPipelineChain,
|
||||
nextPipeline,
|
||||
currentChainOperatorToken.Kind,
|
||||
background);
|
||||
}
|
||||
|
||||
// Assemble the new chain statement AST
|
||||
currentPipelineChain = currentPipelineChain == null
|
||||
? (ChainableAst)nextPipeline
|
||||
: new PipelineChainAst(
|
||||
ExtentOf(currentPipelineChain.Extent, nextPipeline.Extent),
|
||||
currentPipelineChain,
|
||||
nextPipeline,
|
||||
currentChainOperatorToken.Kind);
|
||||
|
||||
// Remember the last operator to chain the coming pipeline
|
||||
currentChainOperatorToken = nextToken;
|
||||
|
||||
// Look ahead to report incomplete input if needed
|
||||
if (PeekToken().Kind == TokenKind.EndOfInput)
|
||||
{
|
||||
ReportIncompleteInput(
|
||||
After(nextToken),
|
||||
nameof(ParserStrings.EmptyPipelineChainElement),
|
||||
ParserStrings.EmptyPipelineChainElement);
|
||||
|
||||
return currentPipelineChain;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private PipelineBaseAst PipelineRule(
|
||||
ExpressionAst startExpression = null,
|
||||
bool allowBackground = false)
|
||||
{
|
||||
// G pipeline:
|
||||
// G assignment-expression
|
||||
|
@ -5711,32 +5948,40 @@ namespace System.Management.Automation.Language
|
|||
Token nextToken = null;
|
||||
bool scanning = true;
|
||||
bool background = false;
|
||||
ExpressionAst expr = startExpression;
|
||||
while (scanning)
|
||||
{
|
||||
CommandBaseAst commandAst;
|
||||
Token assignToken = null;
|
||||
ExpressionAst expr;
|
||||
|
||||
var oldTokenizerMode = _tokenizer.Mode;
|
||||
try
|
||||
if (expr == null)
|
||||
{
|
||||
SetTokenizerMode(TokenizerMode.Expression);
|
||||
expr = ExpressionRule();
|
||||
if (expr != null)
|
||||
// Look for an expression at the beginning of a pipeline
|
||||
var oldTokenizerMode = _tokenizer.Mode;
|
||||
try
|
||||
{
|
||||
// We peek here because we are in expression mode, otherwise =0 will get scanned
|
||||
// as a single token.
|
||||
var token = PeekToken();
|
||||
if (token.Kind.HasTrait(TokenFlags.AssignmentOperator))
|
||||
SetTokenizerMode(TokenizerMode.Expression);
|
||||
expr = ExpressionRule();
|
||||
|
||||
// When pipeline chains are not enabled, we must process assignment.
|
||||
// We are looking for <expr> = <statement>, and have seen <expr>.
|
||||
// Now looking for '='
|
||||
if (!s_pipelineChainsEnabled && expr != null)
|
||||
{
|
||||
SkipToken();
|
||||
assignToken = token;
|
||||
// We peek here because we are in expression mode,
|
||||
// otherwise =0 will get scanned as a single token.
|
||||
Token token = PeekToken();
|
||||
if (token.Kind.HasTrait(TokenFlags.AssignmentOperator))
|
||||
{
|
||||
SkipToken();
|
||||
assignToken = token;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
SetTokenizerMode(oldTokenizerMode);
|
||||
finally
|
||||
{
|
||||
SetTokenizerMode(oldTokenizerMode);
|
||||
}
|
||||
}
|
||||
|
||||
if (expr != null)
|
||||
|
@ -5745,31 +5990,40 @@ namespace System.Management.Automation.Language
|
|||
{
|
||||
// ErrorRecovery: this is a semantic error, so just keep parsing.
|
||||
|
||||
ReportError(expr.Extent,
|
||||
ReportError(
|
||||
expr.Extent,
|
||||
nameof(ParserStrings.ExpressionsMustBeFirstInPipeline),
|
||||
ParserStrings.ExpressionsMustBeFirstInPipeline);
|
||||
}
|
||||
|
||||
if (assignToken != null)
|
||||
// To process assignment when pipeline chains are not enabled,
|
||||
// we have seen '<expr> = ' and now expect a statement
|
||||
if (!s_pipelineChainsEnabled && assignToken != null)
|
||||
{
|
||||
SkipNewlines();
|
||||
StatementAst statement = StatementRule();
|
||||
|
||||
if (statement == null)
|
||||
{
|
||||
// ErrorRecovery: we are very likely at EOF because pretty much anything should result in some
|
||||
// pipeline, so just keep parsing.
|
||||
// ErrorRecovery:
|
||||
// We are likely at EOF, since almost anything else should result in a pipeline,
|
||||
// so just keep parsing
|
||||
|
||||
IScriptExtent errorExtent = After(assignToken);
|
||||
ReportIncompleteInput(errorExtent,
|
||||
ReportIncompleteInput(
|
||||
errorExtent,
|
||||
nameof(ParserStrings.ExpectedValueExpression),
|
||||
ParserStrings.ExpectedValueExpression,
|
||||
assignToken.Kind.Text());
|
||||
statement = new ErrorStatementAst(errorExtent);
|
||||
}
|
||||
|
||||
return new AssignmentStatementAst(ExtentOf(expr, statement),
|
||||
expr, assignToken.Kind, statement, assignToken.Extent);
|
||||
return new AssignmentStatementAst(
|
||||
ExtentOf(expr, statement),
|
||||
expr,
|
||||
assignToken.Kind,
|
||||
statement,
|
||||
assignToken.Extent);
|
||||
}
|
||||
|
||||
RedirectionAst[] redirections = null;
|
||||
|
@ -5791,8 +6045,10 @@ namespace System.Management.Automation.Language
|
|||
}
|
||||
|
||||
var exprExtent = lastRedirection != null ? ExtentOf(expr, lastRedirection) : expr.Extent;
|
||||
commandAst = new CommandExpressionAst(exprExtent, expr,
|
||||
redirections != null ? redirections.Where(r => r != null) : null);
|
||||
commandAst = new CommandExpressionAst(
|
||||
exprExtent,
|
||||
expr,
|
||||
redirections?.Where(r => r != null));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -5815,14 +6071,17 @@ namespace System.Management.Automation.Language
|
|||
// point before, but the pipe could be the first character), otherwise the empty element
|
||||
// is after the pipe character.
|
||||
IScriptExtent errorPosition = nextToken != null ? After(nextToken) : PeekToken().Extent;
|
||||
ReportIncompleteInput(errorPosition,
|
||||
ReportIncompleteInput(
|
||||
errorPosition,
|
||||
nameof(ParserStrings.EmptyPipeElement),
|
||||
ParserStrings.EmptyPipeElement);
|
||||
}
|
||||
|
||||
// Reset the expression for the next loop
|
||||
expr = null;
|
||||
nextToken = PeekToken();
|
||||
|
||||
// Skip newlines before pipe tokens to support (pipe)line continuance when pipe
|
||||
// Skip newlines before pipe tokens to support (pipe)line continuation when pipe
|
||||
// tokens start the next line of script
|
||||
if (nextToken.Kind == TokenKind.NewLine && _tokenizer.IsPipeContinuation(nextToken.Extent))
|
||||
{
|
||||
|
@ -5837,45 +6096,66 @@ namespace System.Management.Automation.Language
|
|||
case TokenKind.RParen:
|
||||
case TokenKind.RCurly:
|
||||
case TokenKind.EndOfInput:
|
||||
// Handled by invoking rule
|
||||
scanning = false;
|
||||
continue;
|
||||
|
||||
case TokenKind.AndAnd:
|
||||
case TokenKind.OrOr:
|
||||
if (s_pipelineChainsEnabled)
|
||||
{
|
||||
scanning = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Report that &&/|| are unsupported
|
||||
SkipToken();
|
||||
SkipNewlines();
|
||||
ReportError(
|
||||
nextToken.Extent,
|
||||
nameof(ParserStrings.InvalidEndOfLine),
|
||||
ParserStrings.InvalidEndOfLine,
|
||||
nextToken.Text);
|
||||
|
||||
if (PeekToken().Kind == TokenKind.EndOfInput)
|
||||
{
|
||||
scanning = false;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case TokenKind.Ampersand:
|
||||
// When pipeline chains are not enabled, pipelines always handle backgrounding
|
||||
if (s_pipelineChainsEnabled && !allowBackground)
|
||||
{
|
||||
// Handled by invoking rule
|
||||
scanning = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
SkipToken();
|
||||
scanning = false;
|
||||
background = true;
|
||||
break;
|
||||
|
||||
case TokenKind.Pipe:
|
||||
SkipToken();
|
||||
SkipNewlines();
|
||||
if (PeekToken().Kind == TokenKind.EndOfInput)
|
||||
{
|
||||
scanning = false;
|
||||
ReportIncompleteInput(After(nextToken),
|
||||
ReportIncompleteInput(
|
||||
After(nextToken),
|
||||
nameof(ParserStrings.EmptyPipeElement),
|
||||
ParserStrings.EmptyPipeElement);
|
||||
}
|
||||
|
||||
break;
|
||||
case TokenKind.AndAnd:
|
||||
case TokenKind.OrOr:
|
||||
// Parse in a manner similar to a pipe, but issue an error (for now, but should implement this for V3.)
|
||||
SkipToken();
|
||||
SkipNewlines();
|
||||
ReportError(nextToken.Extent,
|
||||
nameof(ParserStrings.InvalidEndOfLine),
|
||||
ParserStrings.InvalidEndOfLine,
|
||||
nextToken.Text);
|
||||
if (PeekToken().Kind == TokenKind.EndOfInput)
|
||||
{
|
||||
scanning = false;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
// ErrorRecovery: don't eat the token, assume it belongs to something else.
|
||||
|
||||
ReportError(nextToken.Extent,
|
||||
ReportError(
|
||||
nextToken.Extent,
|
||||
nameof(ParserStrings.UnexpectedToken),
|
||||
ParserStrings.UnexpectedToken,
|
||||
nextToken.Text);
|
||||
|
@ -6606,6 +6886,10 @@ namespace System.Management.Automation.Language
|
|||
|
||||
return expr;
|
||||
}
|
||||
else if (token.Kind == TokenKind.AndAnd || token.Kind == TokenKind.OrOr)
|
||||
{
|
||||
return expr;
|
||||
}
|
||||
|
||||
SkipToken();
|
||||
|
||||
|
@ -7317,7 +7601,7 @@ namespace System.Management.Automation.Language
|
|||
private ExpressionAst ParenthesizedExpressionRule(Token lParen)
|
||||
{
|
||||
// G parenthesized-expression:
|
||||
// G '(' new-lines:opt pipeline new-lines:opt ')'
|
||||
// G '(' new-lines:opt pipeline-chain new-lines:opt ')'
|
||||
Token rParen;
|
||||
PipelineBaseAst pipelineAst;
|
||||
|
||||
|
@ -7329,11 +7613,12 @@ namespace System.Management.Automation.Language
|
|||
_disableCommaOperator = false;
|
||||
|
||||
SkipNewlines();
|
||||
pipelineAst = PipelineRule();
|
||||
pipelineAst = PipelineChainRule();
|
||||
if (pipelineAst == null)
|
||||
{
|
||||
IScriptExtent errorPosition = After(lParen);
|
||||
ReportIncompleteInput(errorPosition,
|
||||
ReportIncompleteInput(
|
||||
errorPosition,
|
||||
nameof(ParserStrings.ExpectedExpression),
|
||||
ParserStrings.ExpectedExpression);
|
||||
pipelineAst = new ErrorStatementAst(errorPosition);
|
||||
|
@ -7617,7 +7902,8 @@ namespace System.Management.Automation.Language
|
|||
// the closing bracket, but build an expression that can't compile.
|
||||
|
||||
var errorExtent = After(lBracket);
|
||||
ReportIncompleteInput(errorExtent,
|
||||
ReportIncompleteInput(
|
||||
errorExtent,
|
||||
nameof(ParserStrings.MissingArrayIndexExpression),
|
||||
ParserStrings.MissingArrayIndexExpression);
|
||||
indexExpr = new ErrorExpressionAst(lBracket.Extent);
|
||||
|
|
|
@ -189,6 +189,9 @@ namespace System.Management.Automation.Language
|
|||
|
||||
/// <summary/>
|
||||
public virtual AstVisitAction VisitTernaryExpression(TernaryExpressionAst ternaryExpressionAst) { return AstVisitAction.Continue; }
|
||||
|
||||
/// <summary/>
|
||||
public virtual AstVisitAction VisitPipelineChain(PipelineChainAst statementChain) { return AstVisitAction.Continue; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -2331,6 +2331,14 @@ namespace System.Management.Automation
|
|||
return InferTypes(ternaryExpressionAst.IfTrue).Concat(InferTypes(ternaryExpressionAst.IfFalse));
|
||||
}
|
||||
|
||||
object ICustomAstVisitor2.VisitPipelineChain(PipelineChainAst pipelineChainAst)
|
||||
{
|
||||
var types = new List<PSTypeName>();
|
||||
types.AddRange(InferTypes(pipelineChainAst.LhsPipelineChain));
|
||||
types.AddRange(InferTypes(pipelineChainAst.RhsPipeline));
|
||||
return GetArrayType(types);
|
||||
}
|
||||
|
||||
private static CommandBaseAst GetPreviousPipelineCommand(CommandAst commandAst)
|
||||
{
|
||||
var pipe = (PipelineAst)commandAst.Parent;
|
||||
|
|
|
@ -5342,6 +5342,127 @@ namespace System.Management.Automation.Language
|
|||
#endregion Visitors
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An AST representing a syntax element chainable with '&&' or '||'.
|
||||
/// </summary>
|
||||
public abstract class ChainableAst : PipelineBaseAst
|
||||
{
|
||||
/// <summary>
|
||||
/// Construct a new chainable AST with the given extent.
|
||||
/// </summary>
|
||||
/// <param name="extent">The script extent of the AST.</param>
|
||||
protected ChainableAst(IScriptExtent extent) : base(extent)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A command-oriented flow-controlled pipeline chain.
|
||||
/// E.g. <c>npm build && npm test</c> or <c>Get-Content -Raw ./file.txt || "default"</c>.
|
||||
/// </summary>
|
||||
public class PipelineChainAst : ChainableAst
|
||||
{
|
||||
/// <summary>
|
||||
/// Create a new statement chain AST from two statements and an operator.
|
||||
/// </summary>
|
||||
/// <param name="extent">The extent of the chained statement.</param>
|
||||
/// <param name="lhsChain">The pipeline or pipeline chain to the left of the operator.</param>
|
||||
/// <param name="rhsPipeline">The pipeline to the right of the operator.</param>
|
||||
/// <param name="chainOperator">The operator used.</param>
|
||||
/// <param name="background">True when this chain has been invoked with the background operator, false otherwise.</param>
|
||||
public PipelineChainAst(
|
||||
IScriptExtent extent,
|
||||
ChainableAst lhsChain,
|
||||
PipelineAst rhsPipeline,
|
||||
TokenKind chainOperator,
|
||||
bool background = false)
|
||||
: base(extent)
|
||||
{
|
||||
if (lhsChain == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(lhsChain));
|
||||
}
|
||||
|
||||
if (rhsPipeline == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(rhsPipeline));
|
||||
}
|
||||
|
||||
if (chainOperator != TokenKind.AndAnd && chainOperator != TokenKind.OrOr)
|
||||
{
|
||||
throw new ArgumentException(nameof(chainOperator));
|
||||
}
|
||||
|
||||
LhsPipelineChain = lhsChain;
|
||||
RhsPipeline = rhsPipeline;
|
||||
Operator = chainOperator;
|
||||
Background = background;
|
||||
|
||||
SetParent(LhsPipelineChain);
|
||||
SetParent(RhsPipeline);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The left hand pipeline in the chain.
|
||||
/// </summary>
|
||||
public ChainableAst LhsPipelineChain { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The right hand pipeline in the chain.
|
||||
/// </summary>
|
||||
public PipelineAst RhsPipeline { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The chaining operator used.
|
||||
/// </summary>
|
||||
public TokenKind Operator { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether this chain has been invoked with the background operator.
|
||||
/// </summary>
|
||||
public bool Background { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a copy of this Ast.
|
||||
/// </summary>
|
||||
public override Ast Copy()
|
||||
{
|
||||
return new PipelineChainAst(Extent, CopyElement(LhsPipelineChain), CopyElement(RhsPipeline), Operator, Background);
|
||||
}
|
||||
|
||||
internal override object Accept(ICustomAstVisitor visitor)
|
||||
{
|
||||
return (visitor as ICustomAstVisitor2)?.VisitPipelineChain(this);
|
||||
}
|
||||
|
||||
internal override AstVisitAction InternalVisit(AstVisitor visitor)
|
||||
{
|
||||
AstVisitAction action = AstVisitAction.Continue;
|
||||
|
||||
// Can only visit new AST type if using AstVisitor2
|
||||
if (visitor is AstVisitor2 visitor2)
|
||||
{
|
||||
action = visitor2.VisitPipelineChain(this);
|
||||
if (action == AstVisitAction.SkipChildren)
|
||||
{
|
||||
return visitor.CheckForPostAction(this, AstVisitAction.Continue);
|
||||
}
|
||||
}
|
||||
|
||||
if (action == AstVisitAction.Continue)
|
||||
{
|
||||
action = LhsPipelineChain.InternalVisit(visitor);
|
||||
}
|
||||
|
||||
if (action == AstVisitAction.Continue)
|
||||
{
|
||||
action = RhsPipeline.InternalVisit(visitor);
|
||||
}
|
||||
|
||||
return visitor.CheckForPostAction(this, action);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Flow Control Statements
|
||||
|
||||
#region Pipelines
|
||||
|
@ -5377,7 +5498,7 @@ namespace System.Management.Automation.Language
|
|||
/// The ast that represents a PowerShell pipeline, e.g. <c>gci -re . *.cs | select-string Foo</c> or <c> 65..90 | % { [char]$_ }</c>.
|
||||
/// A pipeline must have at least 1 command. The first command may be an expression or a command invocation.
|
||||
/// </summary>
|
||||
public class PipelineAst : PipelineBaseAst
|
||||
public class PipelineAst : ChainableAst
|
||||
{
|
||||
/// <summary>
|
||||
/// Construct a pipeline from a collection of commands.
|
||||
|
@ -5461,7 +5582,7 @@ namespace System.Management.Automation.Language
|
|||
/// <summary>
|
||||
/// Indicates that this pipeline should be run in the background.
|
||||
/// </summary>
|
||||
public bool Background { get; private set; }
|
||||
public bool Background { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// If the pipeline represents a pure expression, the expression is returned, otherwise null is returned.
|
||||
|
@ -5485,6 +5606,7 @@ namespace System.Management.Automation.Language
|
|||
/// <summary>
|
||||
/// Copy the PipelineAst instance.
|
||||
/// </summary>
|
||||
/// <returns>A fresh copy of this PipelineAst instance.</returns>
|
||||
public override Ast Copy()
|
||||
{
|
||||
var newPipelineElements = CopyElements(this.PipelineElements);
|
||||
|
|
|
@ -505,7 +505,7 @@ namespace System.Management.Automation
|
|||
}
|
||||
|
||||
internal static void InvokePipelineInBackground(
|
||||
PipelineAst pipelineAst,
|
||||
PipelineBaseAst pipelineAst,
|
||||
FunctionContext funcContext)
|
||||
{
|
||||
PipelineProcessor pipelineProcessor = new PipelineProcessor();
|
||||
|
|
|
@ -1503,4 +1503,10 @@ ModuleVersion : Version of module to import. If used, ModuleName must represent
|
|||
<data name="MissingColonInTernaryExpression" xml:space="preserve">
|
||||
<value>Missing ':' in the ternary expression.</value>
|
||||
</data>
|
||||
<data name="EmptyPipelineChainElement" xml:space="preserve">
|
||||
<value>A pipeline chain operator must be followed by a pipeline.</value>
|
||||
</data>
|
||||
<data name="BackgroundOperatorInPipelineChain" xml:space="preserve">
|
||||
<value>Background operators can only be used at the end of a pipeline chain.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
|
|
@ -0,0 +1,352 @@
|
|||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
# Licensed under the MIT License.
|
||||
|
||||
Describe "Experimental Feature: && and || operators - Feature-Enabled" -Tag CI {
|
||||
BeforeAll {
|
||||
$skipTest = -not $EnabledExperimentalFeatures.Contains('PSPipelineChainOperators')
|
||||
if ($skipTest)
|
||||
{
|
||||
Write-Verbose "Test Suite Skipped: These tests require the PSPipelineChainOperators experimental feature to be enabled" -Verbose
|
||||
$originalDefaultParameterValues = $PSDefaultParameterValues.Clone()
|
||||
$PSDefaultParameterValues["it:skip"] = $true
|
||||
return
|
||||
}
|
||||
|
||||
function Test-SuccessfulCommand
|
||||
{
|
||||
Write-Output "SUCCESS"
|
||||
}
|
||||
|
||||
filter Test-NonTerminatingError
|
||||
{
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(ValueFromPipeline)]
|
||||
[object[]]
|
||||
$Input
|
||||
)
|
||||
|
||||
if ($Input -ne 2)
|
||||
{
|
||||
return $Input
|
||||
}
|
||||
|
||||
$exception = [System.Exception]::new("NTERROR")
|
||||
$errorId = 'NTERROR'
|
||||
$errorCategory = [System.Management.Automation.ErrorCategory]::NotSpecified
|
||||
|
||||
$errorRecord = [System.Management.Automation.ErrorRecord]::new($exception, $errorId, $errorCategory, $null)
|
||||
$PSCmdlet.WriteError($errorRecord)
|
||||
}
|
||||
|
||||
$simpleTestCases = @(
|
||||
# Two native commands
|
||||
@{ Statement = 'testexe -returncode -1 && testexe -echoargs "A"'; Output = @('-1') }
|
||||
@{ Statement = '& "testexe" -returncode -1 && & "testexe" -echoargs "A"'; Output = @('-1') }
|
||||
@{ Statement = 'testexe -returncode -1 || testexe -echoargs "A"'; Output = '-1','Arg 0 is <A>' }
|
||||
@{ Statement = 'testexe -returncode 0 && testexe -echoargs "A"'; Output = '0','Arg 0 is <A>' }
|
||||
@{ Statement = 'testexe -returncode 0 || testexe -echoargs "A"'; Output = @('0') }
|
||||
@{ Statement = 'testexe -returncode 0 > $null && testexe -returncode 1'; Output = @('1') }
|
||||
@{ Statement = 'testexe -returncode 0 && testexe -echoargs "A" > $null'; Output = @('0') }
|
||||
|
||||
# Three native commands
|
||||
@{ Statement = 'testexe -returncode -1 && testexe -returncode -2 && testexe -echoargs "A"'; Output = @('-1') }
|
||||
@{ Statement = 'testexe -echoargs "A" && testexe -returncode -2 && testexe -echoargs "A"'; Output = @('Arg 0 is <A>', '-2') }
|
||||
@{ Statement = 'testexe -echoargs "A" && testexe -returncode -2 || testexe -echoargs "B"'; Output = @('Arg 0 is <A>', '-2', 'Arg 0 is <B>') }
|
||||
@{ Statement = 'testexe -returncode -1 || testexe -returncode -2 && testexe -echoargs "A"'; Output = @('-1', '-2') }
|
||||
@{ Statement = 'testexe -returncode -1 || testexe -returncode -2 || testexe -echoargs "B"'; Output = @('-1', '-2', 'Arg 0 is <B>') }
|
||||
|
||||
# Native command and succesful cmdlet
|
||||
@{ Statement = 'Test-SuccessfulCommand && testexe -returncode 0'; Output = @('SUCCESS', '0') }
|
||||
@{ Statement = 'testexe -returncode 0 && Test-SuccessfulCommand'; Output = @('0', 'SUCCESS') }
|
||||
@{ Statement = 'Test-SuccessfulCommand && testexe -returncode 1'; Output = @('SUCCESS', '1') }
|
||||
@{ Statement = 'testexe -returncode 1 && Test-SuccessfulCommand'; Output = @('1') }
|
||||
|
||||
# Native command and non-terminating unsuccessful cmdlet
|
||||
@{ Statement = '1,2 | Test-NonTerminatingError && testexe -returncode 0'; Output = @(1) }
|
||||
@{ Statement = 'testexe -returncode 0 && 1,2 | Test-NonTerminatingError'; Output = @('0', 1) }
|
||||
@{ Statement = '1,2 | Test-NonTerminatingError || testexe -returncode 0'; Output = @(1, '0') }
|
||||
@{ Statement = 'testexe -returncode 0 || 1, 2 | Test-NonTerminatingError'; Output = @('0') }
|
||||
|
||||
# Expression and native command
|
||||
@{ Statement = '"hi" && testexe -returncode 0'; Output = @('hi', '0') }
|
||||
@{ Statement = 'testexe -returncode 0 && "Hi"'; Output = @('0', 'Hi') }
|
||||
@{ Statement = '"hi" || testexe -returncode 0'; Output = @('hi') }
|
||||
@{ Statement = 'testexe -returncode 0 || "hi"'; Output = @('0') }
|
||||
@{ Statement = '"hi" && testexe -returncode 1'; Output = @('hi', '1') }
|
||||
@{ Statement = 'testexe -returncode 1 && "Hi"'; Output = @('1') }
|
||||
@{ Statement = 'testexe -returncode 1 || "hi"'; Output = @('1', 'hi') }
|
||||
|
||||
# Pipeline and native command
|
||||
@{ Statement = '1,2,3 | % { $_ + 1 } && testexe -returncode 0'; Output = @('2','3','4','0') }
|
||||
@{ Statement = 'testexe -returncode 0 && 1,2,3 | % { $_ + 1 }'; Output = @('0','2','3','4') }
|
||||
@{ Statement = 'testexe -returncode 1 && 1,2,3 | % { $_ + 1 }'; Output = @('1') }
|
||||
@{ Statement = 'testexe -returncode 1 || 1,2,3 | % { $_ + 1 }'; Output = @('1','2','3','4') }
|
||||
@{ Statement = 'testexe -returncode 1 | % { [int]$_ + 1 } && testexe -returncode 0'; Output = @('2') }
|
||||
@{ Statement = 'testexe -returncode 1 | % { [int]$_ + 1 } || testexe -returncode 0'; Output = @('2', '0') }
|
||||
@{ Statement = '0,1 | % { testexe -returncode $_ } && testexe -returncode 0'; Output = @('0','1','0') }
|
||||
@{ Statement = '1,2 | % { testexe -returncode $_ } && testexe -returncode 0'; Output = @('1','2','0') }
|
||||
|
||||
# Control flow statements
|
||||
@{ Statement = 'foreach ($v in 0,1,2) { testexe -returncode $v || $(break) }'; Output = @('0', '1') }
|
||||
@{ Statement = 'foreach ($v in 0,1,2) { testexe -returncode $v || $(continue); $v + 1 }'; Output = @('0', 1, '1', '2') }
|
||||
|
||||
# Use in conditionals
|
||||
@{ Statement = 'if ($false && $true) { "Hi" }'; Output = 'Hi' }
|
||||
@{ Statement = 'if ("Hello" && testexe -return 1) { $?; 1 } else { throw "else" }'; Output = $true,1 }
|
||||
@{ Statement = 'if ("Hello" && Write-Error "Bad" -ErrorAction Ignore) { $?; 1 } else { throw "else" }'; Output = $true,1 }
|
||||
@{ Statement = 'if (Write-Error "Bad" -ErrorAction Ignore && "Hello") { throw "if" } else { $?; 1 }'; Output = $true,1 }
|
||||
@{ Statement = 'if ($y = $x = "Hello" && testexe -returncode 1) { $?; $x; $y } else { throw "else" }'; Output = $true,'Hello',1,'Hello',1 }
|
||||
)
|
||||
|
||||
$variableTestCases = @(
|
||||
@{ Statement = '$x = testexe -returncode 0 && testexe -returncode 1'; Variables = @{ x = '0','1' } }
|
||||
@{ Statement = '$x = testexe -returncode 0 || testexe -returncode 1'; Variables = @{ x = '0' } }
|
||||
@{ Statement = '$x = testexe -returncode 1 || testexe -returncode 0'; Variables = @{ x = '1','0' } }
|
||||
@{ Statement = '$x = testexe -returncode 1 && testexe -returncode 0'; Variables = @{ x = '1' } }
|
||||
@{ Statement = '$x = @(1); $x += testexe -returncode 0 && testexe -returncode 1'; Variables = @{ x = 1,'0','1' } }
|
||||
@{ Statement = '$x = $y = testexe -returncode 0 && testexe -returncode 1'; Variables = @{ x = '0','1'; y = '0','1' } }
|
||||
@{ Statement = '$x, $y = $z = testexe -returncode 0 && testexe -returncode 1'; Variables = @{ x = '0'; y = '1'; z = '0','1' } }
|
||||
@{ Statement = '$x = @(1); $v = $w, $y = $x += $z = testexe -returncode 0 && testexe -returncode 1'; Variables = @{ v = '1',@('0','1'); w = '1'; y = '0','1'; x = '1','0','1'; z = '0','1' } }
|
||||
@{ Statement = '$x = 1 && @(2, 3)'; Variables = @{ x = 1,2,3 } }
|
||||
@{ Statement = '$x = 1 && ,@(2, 3)'; Variables = @{ x = 1,@(2,3) } }
|
||||
@{ Statement = '$x = 1 && 2,@(3, 4)'; Variables = @{ x = 1,2,@(3,4) } }
|
||||
)
|
||||
|
||||
$jobTestCases = @(
|
||||
@{ Statement = 'testexe -returncode 0 && testexe -returncode 1 &'; Output = @('0', '1') }
|
||||
@{ Statement = 'testexe -returncode 1 && testexe -returncode 0 &'; Output = @('1') }
|
||||
@{ Statement = '$x = testexe -returncode 0 && Write-Output "mice" &'; Output = '0','mice'; Variable = 'x' }
|
||||
)
|
||||
|
||||
$invalidSyntaxCases = @(
|
||||
@{ Statement = 'testexe -returncode 0 & && testexe -returncode 1'; ErrorID = 'BackgroundOperatorInPipelineChain' }
|
||||
@{ Statement = 'testexe -returncode 0 && '; ErrorID = 'EmptyPipelineChainElement'; IncompleteInput = $true }
|
||||
@{ Statement = 'testexe -returncode 0 && testexe -returncode 1 && &'; ErrorID = 'MissingExpression' }
|
||||
)
|
||||
}
|
||||
|
||||
AfterAll {
|
||||
if ($skipTest) {
|
||||
$PSDefaultParameterValues = $originalDefaultParameterValues
|
||||
}
|
||||
}
|
||||
|
||||
Context "Pipeline chain error semantics" {
|
||||
BeforeAll {
|
||||
$pwsh = [powershell]::Create()
|
||||
|
||||
$pwsh.AddScript(@'
|
||||
filter Test-NonTerminatingError
|
||||
{
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(ValueFromPipeline)]
|
||||
[object[]]
|
||||
$Input
|
||||
)
|
||||
|
||||
if ($Input -ne 2)
|
||||
{
|
||||
return $Input
|
||||
}
|
||||
|
||||
$exception = [System.Exception]::new("NTERROR")
|
||||
$errorId = "NTERROR"
|
||||
$errorCategory = [System.Management.Automation.ErrorCategory]::NotSpecified
|
||||
|
||||
$errorRecord = [System.Management.Automation.ErrorRecord]::new($exception, $errorId, $errorCategory, $null)
|
||||
$PSCmdlet.WriteError($errorRecord)
|
||||
}
|
||||
|
||||
filter Test-PipelineTerminatingError
|
||||
{
|
||||
[CmdletBinding()]
|
||||
param([Parameter(ValueFromPipeline)][int[]]$Input)
|
||||
|
||||
if ($Input -ne 4)
|
||||
{
|
||||
return $Input
|
||||
}
|
||||
|
||||
$exception = [System.Exception]::new("PIPELINE")
|
||||
$errorId = "PIPELINE"
|
||||
$errorCategory = [System.Management.Automation.ErrorCategory]::NotSpecified
|
||||
|
||||
$errorRecord = [System.Management.Automation.ErrorRecord]::new($exception, $errorId, $errorCategory, $null)
|
||||
|
||||
$PSCmdlet.ThrowTerminatingError($errorRecord)
|
||||
}
|
||||
|
||||
function Test-FullyTerminatingError
|
||||
{
|
||||
throw 'TERMINATE'
|
||||
}
|
||||
'@).Invoke()
|
||||
|
||||
$errorSemanticsCases = @(
|
||||
# Simple error semantics
|
||||
@{ Statement = '1,2,3 | Test-NonTerminatingError || Write-Output 4'; Output = @(1, 3, 4); NTErrors = @('NTError') }
|
||||
@{ Statement = '1,3,4,5 | Test-PipelineTerminatingError || Write-Output 2'; Output = @(1, 3, 2); NTErrors = @('PIPELINE') }
|
||||
@{ Statement = 'Test-FullyTerminatingError || Write-Output 2'; ThrownError = 'TERMINATE' }
|
||||
@{ Statement = '1,2,3 | Test-NonTerminatingError -ErrorAction Stop || Write-Output 4'; ThrownError = 'NTERROR,Test-NonTerminatingError' }
|
||||
|
||||
# Assignment error semantics
|
||||
@{ Statement = '$x = 1,2,3 | Test-NonTerminatingError || Write-Output 4; $x'; Output = @(1, 3, 4); NTErrors = @('NTError') }
|
||||
@{ Statement = '$x = 1,3,4,5 | Test-PipelineTerminatingError || Write-Output 2; $x'; Output = @(1, 3, 2); NTErrors = @('PIPELINE') }
|
||||
|
||||
# Try/catch semantics
|
||||
@{ Statement = 'try { Write-Output 2 && Test-FullyTerminatingError } catch {}'; Output = @(2) }
|
||||
@{ Statement = 'try { 1,3,4,5 | Test-PipelineTerminatingError || Write-Output 2 } catch {}'; Output = @(1, 3) }
|
||||
@{ Statement = 'try { 1,2,3 | Test-NonTerminatingError -ErrorAction Stop || Write-Output 4 } catch {}'; Output = @(1) }
|
||||
@{ Statement = 'try { 1,2,3 | Test-NonTerminatingError || Write-Output 4 } catch {}'; Output = @(1, 3, 4); NTErrors = @('NTError') }
|
||||
@{ Statement = 'try { $result = Write-Output 2 && Test-FullyTerminatingError } catch {}; $result'; Output = @() }
|
||||
@{ Statement = 'try { $result = 1,3,4,5 | Test-PipelineTerminatingError || Write-Output 2 } catch {}; $result'; Output = @() }
|
||||
@{ Statement = 'try { $result = 1,2,3 | Test-NonTerminatingError -ErrorAction Stop || Write-Output 4 } catch {}; $result'; Output = @() }
|
||||
@{ Statement = 'try { $result = 1,2,3 | Test-NonTerminatingError || Write-Output 4 } catch {}; $result'; Output = @(1, 3, 4); NTErrors = @('NTError') }
|
||||
@{ Statement = 'try { "Hi" && "Bye" } catch { "Nothing" }'; Output = @("Hi", "Bye") }
|
||||
@{ Statement = 'try { "Hi" && "Bye" } catch { "Nothing" } finally { "Final" }'; Output = @("Hi", "Bye", "Final") }
|
||||
@{ Statement = 'try { "Hi" && Test-FullyTerminatingError || "Bye" } catch { "Nothing" } finally { "Final" }'; Output = @("Hi", "Nothing", "Final") }
|
||||
|
||||
# Trap continue semantics
|
||||
@{ Statement = 'trap { continue }; Write-Output 2 && Test-FullyTerminatingError'; Output = @(2) }
|
||||
@{ Statement = 'trap { continue }; Test-FullyTerminatingError && Write-Output 2'; Output = @() }
|
||||
@{ Statement = 'trap { continue }; Test-FullyTerminatingError || Write-Output 2'; Output = @(2) }
|
||||
@{ Statement = 'trap { continue }; 1,3,4,5 | Test-PipelineTerminatingError || Write-Output 2'; Output = @(1,3,2) }
|
||||
@{ Statement = 'trap { continue }; 1,2,3 | Test-NonTerminatingError -ErrorAction Stop || Write-Output 4'; Output = @(1,4) }
|
||||
@{ Statement = 'trap { continue }; 1,2,3 | Test-NonTerminatingError || Write-Output 4'; Output = @(1,3,4); NTErrors = @('NTError') }
|
||||
@{ Statement = 'trap { continue }; $result = Write-Output 2 && Test-FullyTerminatingError'; Output = @() }
|
||||
@{ Statement = 'trap { continue }; $result = 1,3,4,5 | Test-PipelineTerminatingError || Write-Output 2; $result'; Output = @(1,3,2) }
|
||||
@{ Statement = 'trap { continue }; $result = 1,2,3 | Test-NonTerminatingError -ErrorAction Stop || Write-Output 4; $result'; Output = @(1,4) }
|
||||
@{ Statement = 'trap { continue }; $result = 1,2,3 | Test-NonTerminatingError || Write-Output 4; $result'; Output = @(1,3,4); NTErrors = @('NTError') }
|
||||
|
||||
# Trap break semantics
|
||||
@{ Statement = 'trap { break }; 1,3,4,5 | Test-PipelineTerminatingError || Write-Output 2'; ThrownError = 'PIPELINE,Test-PipelineTerminatingError' }
|
||||
@{ Statement = 'trap { break }; 1,2,3 | Test-NonTerminatingError -ErrorAction Stop || Write-Output 4'; ThrownError = 'NTERROR,Test-NonTerminatingError' }
|
||||
@{ Statement = 'trap { break }; 1,2,3 | Test-NonTerminatingError || Write-Output 4'; Output = @(1,3,4); NTErrors = @('NTError') }
|
||||
@{ Statement = 'trap { break }; $result = Write-Output 2 && Test-FullyTerminatingError'; ThrownError = 'TERMINATE' }
|
||||
@{ Statement = 'trap { break }; $result = 1,3,4,5 | Test-PipelineTerminatingError || Write-Output 2; $result'; ThrownError = 'PIPELINE,Test-PipelineTerminatingError' }
|
||||
@{ Statement = 'trap { break }; $result = 1,2,3 | Test-NonTerminatingError -ErrorAction Stop || Write-Output 4; $result'; ThrownError = 'NTERROR,Test-NonTerminatingError' }
|
||||
@{ Statement = 'trap { break }; $result = 1,2,3 | Test-NonTerminatingError || Write-Output 4; $result'; Output = @(1,3,4); NTErrors = @('NTError') }
|
||||
)
|
||||
}
|
||||
|
||||
AfterEach {
|
||||
$pwsh.Commands.Clear()
|
||||
$pwsh.AddScript('Remove-Variable -Name result').Invoke()
|
||||
$pwsh.Commands.Clear()
|
||||
$pwsh.Streams.ClearStreams()
|
||||
}
|
||||
|
||||
AfterAll {
|
||||
$pwsh.Dispose()
|
||||
}
|
||||
|
||||
It "Uses the correct error semantics with statement '<Statement>'" -TestCases $errorSemanticsCases {
|
||||
param([string]$Statement, $Output, [array]$NTErrors, [string]$ThrownError)
|
||||
|
||||
try
|
||||
{
|
||||
$result = $pwsh.AddScript($Statement).Invoke()
|
||||
}
|
||||
catch
|
||||
{
|
||||
$_.Exception.InnerException.ErrorRecord.FullyQualifiedErrorId | Should -BeExactly $ThrownError
|
||||
}
|
||||
|
||||
$result | Should -Be $Output
|
||||
$pwsh.Streams.Error | Should -Be $NTErrors
|
||||
}
|
||||
}
|
||||
|
||||
It "Gets the correct output with statement '<Statement>'" -TestCases $simpleTestCases {
|
||||
param($Statement, $Output)
|
||||
|
||||
Invoke-Expression -Command $Statement 2>$null | Should -Be $Output
|
||||
}
|
||||
|
||||
It "Sets the variable correctly with statement '<Statement>'" -TestCases $variableTestCases {
|
||||
param($Statement, $Variables)
|
||||
|
||||
Invoke-Expression -Command $Statement
|
||||
foreach ($variableName in $Variables.get_Keys())
|
||||
{
|
||||
(Get-Variable -Name $variableName -ErrorAction Ignore).Value | Should -Be $Variables[$variableName] -Because "variable is '`$$variableName'"
|
||||
}
|
||||
}
|
||||
|
||||
It "Runs the statement chain '<Statement>' as a job" -TestCases $jobTestCases {
|
||||
param($Statement, $Output, $Variable)
|
||||
|
||||
$resultJob = Invoke-Expression -Command $Statement
|
||||
|
||||
if ($Variable)
|
||||
{
|
||||
$resultJob = (Get-Variable $Variable).Value
|
||||
}
|
||||
|
||||
$resultJob | Wait-Job | Receive-Job | Should -Be $Output
|
||||
}
|
||||
|
||||
It "Rejects invalid syntax usage in '<Statement>'" -TestCases $invalidSyntaxCases {
|
||||
param([string]$Statement, [string]$ErrorID, [bool]$IncompleteInput)
|
||||
|
||||
$tokens = $errors = $null
|
||||
[System.Management.Automation.Language.Parser]::ParseInput($Statement, [ref]$tokens, [ref]$errors)
|
||||
|
||||
$errors.Count | Should -BeExactly 1
|
||||
$errors[0].ErrorId | Should -BeExactly $ErrorID
|
||||
$errors[0].IncompleteInput | Should -Be $IncompleteInput
|
||||
}
|
||||
|
||||
Context "File redirection with && and ||" {
|
||||
BeforeAll {
|
||||
$redirectionTestCases = @(
|
||||
@{ Statement = "testexe -returncode 0 > '$TestDrive/1.txt' && testexe -returncode 1 > '$TestDrive/2.txt'"; Files = @{ "$TestDrive/1.txt" = '0'; "$TestDrive/2.txt" = '1' } }
|
||||
@{ Statement = "testexe -returncode 1 > '$TestDrive/1.txt' && testexe -returncode 1 > '$TestDrive/2.txt'"; Files = @{ "$TestDrive/1.txt" = '1'; "$TestDrive/2.txt" = $null } }
|
||||
@{ Statement = "testexe -returncode 1 > '$TestDrive/1.txt' || testexe -returncode 1 > '$TestDrive/2.txt'"; Files = @{ "$TestDrive/1.txt" = '1'; "$TestDrive/2.txt" = '1' } }
|
||||
@{ Statement = "testexe -returncode 0 > '$TestDrive/1.txt' || testexe -returncode 1 > '$TestDrive/2.txt'"; Files = @{ "$TestDrive/1.txt" = '0'; "$TestDrive/2.txt" = $null } }
|
||||
@{ Statement = "(testexe -returncode 0 && testexe -returncode 1) > '$TestDrive/3.txt'"; Files = @{ "$TestDrive/3.txt" = "0$([System.Environment]::NewLine)1$([System.Environment]::NewLine)" } }
|
||||
@{ Statement = "(testexe -returncode 0 && testexe -returncode 1 > '$TestDrive/2.txt') > '$TestDrive/3.txt'"; Files = @{ "$TestDrive/2.txt" = '1'; "$TestDrive/3.txt" = '0' } }
|
||||
@{ Statement = "(testexe -returncode 0 > '$TestDrive/1.txt' && testexe -returncode 1 > '$TestDrive/2.txt') > '$TestDrive/3.txt'"; Files = @{ "$TestDrive/1.txt" = '0'; "$TestDrive/2.txt" = '1'; "$TestDrive/3.txt" = '' } }
|
||||
)
|
||||
}
|
||||
|
||||
BeforeEach {
|
||||
Remove-Item -Path $TestDrive/*
|
||||
}
|
||||
|
||||
It "Handles redirection correctly with statement '<Statement>'" -TestCases $redirectionTestCases {
|
||||
param($Statement, $Files)
|
||||
|
||||
Invoke-Expression -Command $Statement
|
||||
|
||||
foreach ($file in $Files.get_Keys())
|
||||
{
|
||||
$expectedValue = $Files[$file]
|
||||
|
||||
if ($null -eq $expectedValue)
|
||||
{
|
||||
$file | Should -Not -Exist
|
||||
continue
|
||||
}
|
||||
|
||||
# Special case for empty file
|
||||
if ($expectedValue -eq '')
|
||||
{
|
||||
(Get-Item $file).Length | Should -Be 0
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
$file | Should -FileContentMatchMultiline $expectedValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
It "Recognises invalid assignment" {
|
||||
{
|
||||
Invoke-Expression -Command '$x = $x, $y += $z = testexe -returncode 0 && testexe -returncode 1'
|
||||
} | Should -Throw -ErrorID 'InvalidLeftHandSide,Microsoft.PowerShell.Commands.InvokeExpressionCommand'
|
||||
}
|
||||
}
|
|
@ -1,44 +1,35 @@
|
|||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
# Licensed under the MIT License.
|
||||
try {
|
||||
if ( ! $IsWindows ) {
|
||||
$PSDefaultParameterValues['it:pending'] = $true
|
||||
|
||||
Describe "CimInstance cmdlet tests" -Tag @("CI") {
|
||||
BeforeAll {
|
||||
if ( ! $IsWindows ) { return }
|
||||
$instance = Get-CimInstance CIM_ComputerSystem
|
||||
}
|
||||
|
||||
Describe "CimInstance cmdlet tests" -Tag @("CI") {
|
||||
BeforeAll {
|
||||
if ( ! $IsWindows ) { return }
|
||||
$instance = Get-CimInstance CIM_ComputerSystem
|
||||
}
|
||||
It "CimClass property should not be null" -Pending:(-not $IsWindows) {
|
||||
# we can't use equals here as on windows cimclassname
|
||||
# is win32_computersystem, but that's not likely to be the
|
||||
# case on non-Windows systems
|
||||
$instance.CimClass.CimClassName | Should -Match _computersystem
|
||||
}
|
||||
|
||||
It "CimClass property should not be null" {
|
||||
# we can't use equals here as on windows cimclassname
|
||||
# is win32_computersystem, but that's not likely to be the
|
||||
# case on non-Windows systems
|
||||
$instance.CimClass.CimClassName | Should -Match _computersystem
|
||||
}
|
||||
|
||||
It "Property access should be case insensitive" {
|
||||
foreach($property in $instance.psobject.properties.name) {
|
||||
$pUpper = $property.ToUpper()
|
||||
$pLower = $property.ToLower()
|
||||
[string]$pLowerValue = $instance.$pLower -join ","
|
||||
[string]$pUpperValue = $instance.$pUpper -join ","
|
||||
$pLowerValue | Should -BeExactly $pUpperValue
|
||||
}
|
||||
}
|
||||
|
||||
It "GetCimSessionInstanceId method invocation should return data" {
|
||||
$instance.GetCimSessionInstanceId() | Should -BeOfType "Guid"
|
||||
}
|
||||
|
||||
It "should produce an error for a non-existing classname" {
|
||||
{ Get-CimInstance -ClassName thisnameshouldnotexist -ErrorAction Stop } |
|
||||
Should -Throw -ErrorId "HRESULT 0x80041010,Microsoft.Management.Infrastructure.CimCmdlets.GetCimInstanceCommand"
|
||||
It "Property access should be case insensitive" -Pending:(-not $IsWindows) {
|
||||
foreach($property in $instance.psobject.properties.name) {
|
||||
$pUpper = $property.ToUpper()
|
||||
$pLower = $property.ToLower()
|
||||
[string]$pLowerValue = $instance.$pLower -join ","
|
||||
[string]$pUpperValue = $instance.$pUpper -join ","
|
||||
$pLowerValue | Should -BeExactly $pUpperValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
finally {
|
||||
$PSDefaultParameterValues.Remove('it:pending')
|
||||
It "GetCimSessionInstanceId method invocation should return data" -Pending:(-not $IsWindows) {
|
||||
$instance.GetCimSessionInstanceId() | Should -BeOfType "Guid"
|
||||
}
|
||||
|
||||
It "should produce an error for a non-existing classname" -Pending:(-not $IsWindows) {
|
||||
{ Get-CimInstance -ClassName thisnameshouldnotexist -ErrorAction Stop } |
|
||||
Should -Throw -ErrorId "HRESULT 0x80041010,Microsoft.Management.Infrastructure.CimCmdlets.GetCimInstanceCommand"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,48 +1,39 @@
|
|||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
# Licensed under the MIT License.
|
||||
try {
|
||||
if ( ! $IsWindows ) {
|
||||
$PSDefaultParameterValues['it:pending'] = $true
|
||||
|
||||
Describe "New-CimSession" -Tag @("CI","RequireAdminOnWindows") {
|
||||
BeforeAll {
|
||||
$sessions = @()
|
||||
}
|
||||
|
||||
Describe "New-CimSession" -Tag @("CI","RequireAdminOnWindows") {
|
||||
BeforeAll {
|
||||
AfterEach {
|
||||
$sessions | Remove-CimSession -ErrorAction SilentlyContinue
|
||||
$sessions = @()
|
||||
}
|
||||
}
|
||||
|
||||
AfterEach {
|
||||
$sessions | Remove-CimSession -ErrorAction SilentlyContinue
|
||||
$sessions = @()
|
||||
}
|
||||
It "A cim session can be created" -Pending:(-not $IsWindows) {
|
||||
$sessionName = [guid]::NewGuid().Guid
|
||||
$session = New-CimSession -ComputerName . -Name $sessionName
|
||||
$sessions += $session
|
||||
$session.Name | Should -BeExactly $sessionName
|
||||
$session.InstanceId | Should -BeOfType "System.Guid"
|
||||
}
|
||||
|
||||
It "A cim session can be created" {
|
||||
$sessionName = [guid]::NewGuid().Guid
|
||||
$session = New-CimSession -ComputerName . -Name $sessionName
|
||||
$sessions += $session
|
||||
$session.Name | Should -BeExactly $sessionName
|
||||
$session.InstanceId | Should -BeOfType "System.Guid"
|
||||
}
|
||||
It "A Cim session can be retrieved" -Pending:(-not $IsWindows) {
|
||||
$sessionName = [guid]::NewGuid().Guid
|
||||
$session = New-CimSession -ComputerName . -Name $sessionName
|
||||
$sessions += $session
|
||||
(Get-CimSession -Name $sessionName).InstanceId | Should -Be $session.InstanceId
|
||||
(Get-CimSession -Id $session.Id).InstanceId | Should -Be $session.InstanceId
|
||||
(Get-CimSession -InstanceId $session.InstanceId).InstanceId | Should -Be $session.InstanceId
|
||||
}
|
||||
|
||||
It "A Cim session can be retrieved" {
|
||||
$sessionName = [guid]::NewGuid().Guid
|
||||
$session = New-CimSession -ComputerName . -Name $sessionName
|
||||
$sessions += $session
|
||||
(Get-CimSession -Name $sessionName).InstanceId | Should -Be $session.InstanceId
|
||||
(Get-CimSession -Id $session.Id).InstanceId | Should -Be $session.InstanceId
|
||||
(Get-CimSession -InstanceId $session.InstanceId).InstanceId | Should -Be $session.InstanceId
|
||||
}
|
||||
|
||||
It "A cim session can be removed" {
|
||||
$sessionName = [guid]::NewGuid().Guid
|
||||
$session = New-CimSession -ComputerName . -Name $sessionName
|
||||
$sessions += $session
|
||||
$session.Name | Should -BeExactly $sessionName
|
||||
$session | Remove-CimSession
|
||||
Get-CimSession $session.Id -ErrorAction SilentlyContinue | Should -BeNullOrEmpty
|
||||
}
|
||||
It "A cim session can be removed" -Pending:(-not $IsWindows) {
|
||||
$sessionName = [guid]::NewGuid().Guid
|
||||
$session = New-CimSession -ComputerName . -Name $sessionName
|
||||
$sessions += $session
|
||||
$session.Name | Should -BeExactly $sessionName
|
||||
$session | Remove-CimSession
|
||||
Get-CimSession $session.Id -ErrorAction SilentlyContinue | Should -BeNullOrEmpty
|
||||
}
|
||||
}
|
||||
|
||||
finally {
|
||||
$PSDefaultParameterValues.Remove('it:pending')
|
||||
}
|
||||
|
|
|
@ -1,40 +1,30 @@
|
|||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
# Licensed under the MIT License.
|
||||
try {
|
||||
# Get-CimClass works only on windows right now
|
||||
if ( ! $IsWindows ) {
|
||||
$PSDefaultParameterValues['it:pending'] = $true
|
||||
|
||||
Describe 'Get-CimClass' -Tags "CI" {
|
||||
|
||||
It 'can get CIM_Error CIM class' -Pending:(-not $IsWindows) {
|
||||
Get-CimClass -ClassName CIM_Error | Should -Not -BeNullOrEmpty
|
||||
}
|
||||
|
||||
Describe 'Get-CimClass' -Tags "CI" {
|
||||
|
||||
It 'can get CIM_Error CIM class' {
|
||||
Get-CimClass -ClassName CIM_Error | Should -Not -BeNullOrEmpty
|
||||
}
|
||||
|
||||
It 'can get class when namespace is specified' {
|
||||
Get-CimClass -ClassName CIM_OperatingSystem -Namespace root/cimv2 | Should -Not -BeNullOrEmpty
|
||||
}
|
||||
|
||||
It 'produces an error when a non-existent class is used' {
|
||||
{ Get-CimClass -ClassName thisclasstypedoesnotexist -ErrorAction stop } |
|
||||
Should -Throw -ErrorId "HRESULT 0x80041002,Microsoft.Management.Infrastructure.CimCmdlets.GetCimClassCommand"
|
||||
}
|
||||
|
||||
It 'produces an error when an improper namespace is used' {
|
||||
{ Get-CimClass -ClassName CIM_OperatingSystem -Namespace badnamespace -ErrorAction stop } |
|
||||
Should -Throw -ErrorId "HRESULT 0x8004100e,Microsoft.Management.Infrastructure.CimCmdlets.GetCimClassCommand"
|
||||
}
|
||||
It 'can get class when namespace is specified' -Pending:(-not $IsWindows) {
|
||||
Get-CimClass -ClassName CIM_OperatingSystem -Namespace root/cimv2 | Should -Not -BeNullOrEmpty
|
||||
}
|
||||
|
||||
# feature tests
|
||||
Describe 'Get-CimClass' -Tags @("Feature") {
|
||||
It 'can retrieve a class when a method is provided' {
|
||||
Get-CimClass -MethodName Reboot | Should -Not -BeNullOrEmpty
|
||||
}
|
||||
It 'produces an error when a non-existent class is used' -Pending:(-not $IsWindows) {
|
||||
{ Get-CimClass -ClassName thisclasstypedoesnotexist -ErrorAction stop } |
|
||||
Should -Throw -ErrorId "HRESULT 0x80041002,Microsoft.Management.Infrastructure.CimCmdlets.GetCimClassCommand"
|
||||
}
|
||||
|
||||
It 'produces an error when an improper namespace is used' -Pending:(-not $IsWindows) {
|
||||
{ Get-CimClass -ClassName CIM_OperatingSystem -Namespace badnamespace -ErrorAction stop } |
|
||||
Should -Throw -ErrorId "HRESULT 0x8004100e,Microsoft.Management.Infrastructure.CimCmdlets.GetCimClassCommand"
|
||||
}
|
||||
}
|
||||
|
||||
finally {
|
||||
$PSDefaultParameterValues.Remove('it:pending')
|
||||
# feature tests
|
||||
Describe 'Get-CimClass' -Tags @("Feature") {
|
||||
It 'can retrieve a class when a method is provided' -Pending:(-not $IsWindows) {
|
||||
Get-CimClass -MethodName Reboot | Should -Not -BeNullOrEmpty
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ namespace TestExe
|
|||
{
|
||||
class TestExe
|
||||
{
|
||||
static void Main(string[] args)
|
||||
static int Main(string[] args)
|
||||
{
|
||||
if (args.Length > 0)
|
||||
{
|
||||
|
@ -20,6 +20,10 @@ namespace TestExe
|
|||
case "-createchildprocess":
|
||||
CreateChildProcess(args);
|
||||
break;
|
||||
// Used to test functionality depending on $LASTEXITCODE, like &&/|| operators
|
||||
case "-returncode":
|
||||
Console.WriteLine(args[1]);
|
||||
return int.Parse(args[1]);
|
||||
default:
|
||||
Console.WriteLine("Unknown test {0}", args[0]);
|
||||
break;
|
||||
|
@ -29,6 +33,8 @@ namespace TestExe
|
|||
{
|
||||
Console.WriteLine("Test not specified");
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// <Summary>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"ExperimentalFeatures": {
|
||||
"ExpTest.FeatureOne": [ "test/powershell/engine/ExperimentalFeature/ExperimentalFeature.Basic.Tests.ps1" ],
|
||||
"PSForEachObjectParallel": [ "test/powershell/Modules/Microsoft.PowerShell.Utility/Foreach-Object-Parallel.Tests.ps1", "test/powershell/Modules/Microsoft.PowerShell.Security/ConstrainedLanguageRestriction.Tests.ps1" ]
|
||||
"PSForEachObjectParallel": [ "test/powershell/Modules/Microsoft.PowerShell.Utility/Foreach-Object-Parallel.Tests.ps1", "test/powershell/Modules/Microsoft.PowerShell.Security/ConstrainedLanguageRestriction.Tests.ps1" ],
|
||||
"PSPipelineChainOperators": [ "test/powershell/Language/Operators/PipelineChainOperator.Tests.ps1" ]
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue