Support the pipeline chain operators && and || in PowerShell language (#9849)

This commit is contained in:
Robert Holt 2019-10-17 14:43:46 -07:00 committed by Dongbo Wang
parent 425bc36a6f
commit 2a518fcfe2
17 changed files with 1132 additions and 194 deletions

View file

@ -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) }

View file

@ -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;

View file

@ -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);

View file

@ -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; }
}
}

View file

@ -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>();

View file

@ -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);

View file

@ -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>

View file

@ -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;

View file

@ -5342,6 +5342,127 @@ namespace System.Management.Automation.Language
#endregion Visitors
}
/// <summary>
/// An AST representing a syntax element chainable with '&amp;&amp;' 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 &amp;&amp; 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);

View file

@ -505,7 +505,7 @@ namespace System.Management.Automation
}
internal static void InvokePipelineInBackground(
PipelineAst pipelineAst,
PipelineBaseAst pipelineAst,
FunctionContext funcContext)
{
PipelineProcessor pipelineProcessor = new PipelineProcessor();

View file

@ -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>

View file

@ -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'
}
}

View file

@ -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"
}
}

View file

@ -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')
}

View file

@ -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
}
}

View file

@ -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>

View file

@ -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" ]
}
}