diff --git a/build.psm1 b/build.psm1 index 5a9aa3f189..4d64391e2e 100644 --- a/build.psm1 +++ b/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) } diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs index 2034244aa9..828b8c52cf 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs @@ -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; diff --git a/src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs b/src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs index a2ca36f3c7..78a46a83e4 100644 --- a/src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs +++ b/src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs @@ -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(engineFeatures); diff --git a/src/System.Management.Automation/engine/parser/AstVisitor.cs b/src/System.Management.Automation/engine/parser/AstVisitor.cs index 2f8dfeff4c..a0089fc5d3 100644 --- a/src/System.Management.Automation/engine/parser/AstVisitor.cs +++ b/src/System.Management.Automation/engine/parser/AstVisitor.cs @@ -175,6 +175,9 @@ namespace System.Management.Automation.Language /// object VisitTernaryExpression(TernaryExpressionAst ternaryExpressionAst) => DefaultVisit(ternaryExpressionAst); + + /// + 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); } /// @@ -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); } } /// @@ -690,5 +697,7 @@ namespace System.Management.Automation.Language public virtual object VisitFunctionMember(FunctionMemberAst functionMemberAst) { return null; } /// public virtual object VisitTernaryExpression(TernaryExpressionAst ternaryExpressionAst) { return null; } + /// + public virtual object VisitPipelineChain(PipelineChainAst statementChainAst) { return null; } } } diff --git a/src/System.Management.Automation/engine/parser/Compiler.cs b/src/System.Management.Automation/engine/parser/Compiler.cs index 2998cdb406..fcae018bd8 100644 --- a/src/System.Management.Automation/engine/parser/Compiler.cs +++ b/src/System.Management.Automation/engine/parser/Compiler.cs @@ -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(); + + // 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(); + var dispatchTargets = new List(); + var tryBodyExprs = new List() + { + 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(); diff --git a/src/System.Management.Automation/engine/parser/Parser.cs b/src/System.Management.Automation/engine/parser/Parser.cs index 9c07cbca48..e798cf9542 100644 --- a/src/System.Management.Automation/engine/parser/Parser.cs +++ b/src/System.Management.Automation/engine/parser/Parser.cs @@ -35,6 +35,8 @@ namespace System.Management.Automation.Language /// 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 = , and have seen . + // 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 ' = ' 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); diff --git a/src/System.Management.Automation/engine/parser/PreOrderVisitor.cs b/src/System.Management.Automation/engine/parser/PreOrderVisitor.cs index 9f0ebf5c7d..ad95def8db 100644 --- a/src/System.Management.Automation/engine/parser/PreOrderVisitor.cs +++ b/src/System.Management.Automation/engine/parser/PreOrderVisitor.cs @@ -189,6 +189,9 @@ namespace System.Management.Automation.Language /// public virtual AstVisitAction VisitTernaryExpression(TernaryExpressionAst ternaryExpressionAst) { return AstVisitAction.Continue; } + + /// + public virtual AstVisitAction VisitPipelineChain(PipelineChainAst statementChain) { return AstVisitAction.Continue; } } /// diff --git a/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs b/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs index bcc504bbde..9380092a35 100644 --- a/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs +++ b/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs @@ -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(); + types.AddRange(InferTypes(pipelineChainAst.LhsPipelineChain)); + types.AddRange(InferTypes(pipelineChainAst.RhsPipeline)); + return GetArrayType(types); + } + private static CommandBaseAst GetPreviousPipelineCommand(CommandAst commandAst) { var pipe = (PipelineAst)commandAst.Parent; diff --git a/src/System.Management.Automation/engine/parser/ast.cs b/src/System.Management.Automation/engine/parser/ast.cs index 5c10509d38..fc42624df2 100644 --- a/src/System.Management.Automation/engine/parser/ast.cs +++ b/src/System.Management.Automation/engine/parser/ast.cs @@ -5342,6 +5342,127 @@ namespace System.Management.Automation.Language #endregion Visitors } + /// + /// An AST representing a syntax element chainable with '&&' or '||'. + /// + public abstract class ChainableAst : PipelineBaseAst + { + /// + /// Construct a new chainable AST with the given extent. + /// + /// The script extent of the AST. + protected ChainableAst(IScriptExtent extent) : base(extent) + { + } + } + + /// + /// A command-oriented flow-controlled pipeline chain. + /// E.g. npm build && npm test or Get-Content -Raw ./file.txt || "default". + /// + public class PipelineChainAst : ChainableAst + { + /// + /// Create a new statement chain AST from two statements and an operator. + /// + /// The extent of the chained statement. + /// The pipeline or pipeline chain to the left of the operator. + /// The pipeline to the right of the operator. + /// The operator used. + /// True when this chain has been invoked with the background operator, false otherwise. + 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); + } + + /// + /// The left hand pipeline in the chain. + /// + public ChainableAst LhsPipelineChain { get; } + + /// + /// The right hand pipeline in the chain. + /// + public PipelineAst RhsPipeline { get; } + + /// + /// The chaining operator used. + /// + public TokenKind Operator { get; } + + /// + /// Indicates whether this chain has been invoked with the background operator. + /// + public bool Background { get; } + + /// + /// Create a copy of this Ast. + /// + 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. gci -re . *.cs | select-string Foo or 65..90 | % { [char]$_ }. /// A pipeline must have at least 1 command. The first command may be an expression or a command invocation. /// - public class PipelineAst : PipelineBaseAst + public class PipelineAst : ChainableAst { /// /// Construct a pipeline from a collection of commands. @@ -5461,7 +5582,7 @@ namespace System.Management.Automation.Language /// /// Indicates that this pipeline should be run in the background. /// - public bool Background { get; private set; } + public bool Background { get; internal set; } /// /// If the pipeline represents a pure expression, the expression is returned, otherwise null is returned. @@ -5485,6 +5606,7 @@ namespace System.Management.Automation.Language /// /// Copy the PipelineAst instance. /// + /// A fresh copy of this PipelineAst instance. public override Ast Copy() { var newPipelineElements = CopyElements(this.PipelineElements); diff --git a/src/System.Management.Automation/engine/runtime/Operations/MiscOps.cs b/src/System.Management.Automation/engine/runtime/Operations/MiscOps.cs index db135416d4..830c7f7ad9 100644 --- a/src/System.Management.Automation/engine/runtime/Operations/MiscOps.cs +++ b/src/System.Management.Automation/engine/runtime/Operations/MiscOps.cs @@ -505,7 +505,7 @@ namespace System.Management.Automation } internal static void InvokePipelineInBackground( - PipelineAst pipelineAst, + PipelineBaseAst pipelineAst, FunctionContext funcContext) { PipelineProcessor pipelineProcessor = new PipelineProcessor(); diff --git a/src/System.Management.Automation/resources/ParserStrings.resx b/src/System.Management.Automation/resources/ParserStrings.resx index 7d68e49d66..35adff9e63 100644 --- a/src/System.Management.Automation/resources/ParserStrings.resx +++ b/src/System.Management.Automation/resources/ParserStrings.resx @@ -1503,4 +1503,10 @@ ModuleVersion : Version of module to import. If used, ModuleName must represent Missing ':' in the ternary expression. + + A pipeline chain operator must be followed by a pipeline. + + + Background operators can only be used at the end of a pipeline chain. + diff --git a/test/powershell/Language/Operators/PipelineChainOperator.Tests.ps1 b/test/powershell/Language/Operators/PipelineChainOperator.Tests.ps1 new file mode 100644 index 0000000000..d376161f3f --- /dev/null +++ b/test/powershell/Language/Operators/PipelineChainOperator.Tests.ps1 @@ -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 ' } + @{ Statement = 'testexe -returncode 0 && testexe -echoargs "A"'; Output = '0','Arg 0 is ' } + @{ 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 ', '-2') } + @{ Statement = 'testexe -echoargs "A" && testexe -returncode -2 || testexe -echoargs "B"'; Output = @('Arg 0 is ', '-2', 'Arg 0 is ') } + @{ 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 ') } + + # 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 ''" -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 ''" -TestCases $simpleTestCases { + param($Statement, $Output) + + Invoke-Expression -Command $Statement 2>$null | Should -Be $Output + } + + It "Sets the variable correctly with 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 '' 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 ''" -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 ''" -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' + } +} diff --git a/test/powershell/Modules/CimCmdlets/CimInstance.Tests.ps1 b/test/powershell/Modules/CimCmdlets/CimInstance.Tests.ps1 index 0442de2006..53fa209665 100644 --- a/test/powershell/Modules/CimCmdlets/CimInstance.Tests.ps1 +++ b/test/powershell/Modules/CimCmdlets/CimInstance.Tests.ps1 @@ -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" + } } diff --git a/test/powershell/Modules/CimCmdlets/CimSession.Tests.ps1 b/test/powershell/Modules/CimCmdlets/CimSession.Tests.ps1 index 8352dddb4e..deda339ed6 100644 --- a/test/powershell/Modules/CimCmdlets/CimSession.Tests.ps1 +++ b/test/powershell/Modules/CimCmdlets/CimSession.Tests.ps1 @@ -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') -} diff --git a/test/powershell/Modules/CimCmdlets/Get-CimClass.Tests.ps1 b/test/powershell/Modules/CimCmdlets/Get-CimClass.Tests.ps1 index 3b76065057..33407436d5 100644 --- a/test/powershell/Modules/CimCmdlets/Get-CimClass.Tests.ps1 +++ b/test/powershell/Modules/CimCmdlets/Get-CimClass.Tests.ps1 @@ -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 + } } diff --git a/test/tools/TestExe/TestExe.cs b/test/tools/TestExe/TestExe.cs index 40167a86d6..fd064e2527 100644 --- a/test/tools/TestExe/TestExe.cs +++ b/test/tools/TestExe/TestExe.cs @@ -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; } // diff --git a/test/tools/TestMetadata.json b/test/tools/TestMetadata.json index 605057a873..76725c6680 100644 --- a/test/tools/TestMetadata.json +++ b/test/tools/TestMetadata.json @@ -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" ] } }