diff --git a/src/System.Management.Automation/engine/lang/parserutils.cs b/src/System.Management.Automation/engine/lang/parserutils.cs index 3be7adb86bf..300d39a5afc 100644 --- a/src/System.Management.Automation/engine/lang/parserutils.cs +++ b/src/System.Management.Automation/engine/lang/parserutils.cs @@ -881,8 +881,8 @@ private static object AsChar(object obj) /// The result of the operator internal static object ReplaceOperator(ExecutionContext context, IScriptExtent errorPosition, object lval, object rval, bool ignoreCase) { - string replacement = ""; object pattern = ""; + object substitute = ""; rval = PSObject.Base(rval); IList rList = rval as IList; @@ -900,7 +900,7 @@ internal static object ReplaceOperator(ExecutionContext context, IScriptExtent e pattern = rList[0]; if (rList.Count > 1) { - replacement = PSObject.ToStringParser(context, rList[1]); + substitute = rList[1]; } } } @@ -935,8 +935,7 @@ internal static object ReplaceOperator(ExecutionContext context, IScriptExtent e { string lvalString = lval?.ToString() ?? String.Empty; - // Find a single match in the string. - return rr.Replace(lvalString, replacement); + return ReplaceOperatorImpl(context, lvalString, rr, substitute); } else { @@ -944,14 +943,50 @@ internal static object ReplaceOperator(ExecutionContext context, IScriptExtent e while (ParserOps.MoveNext(context, errorPosition, list)) { string lvalString = PSObject.ToStringParser(context, ParserOps.Current(errorPosition, list)); - - resultList.Add(rr.Replace(lvalString, replacement)); + resultList.Add(ReplaceOperatorImpl(context, lvalString, rr, substitute)); } return resultList.ToArray(); } } + /// + /// ReplaceOperator implementation. + /// Abstracts away conversion of the optional substitute parameter to either a string or a MatchEvaluator delegate + /// and finally returns the result of the final Regex.Replace operation. + /// + /// The execution context in which to evaluate the expression + /// The input string + /// A Regex instance. + /// The substitute value + /// The result of the regex.Replace operation + private static object ReplaceOperatorImpl(ExecutionContext context, string input, Regex regex, object substitute) + { + switch (substitute) + { + case ScriptBlock sb: + MatchEvaluator me = match => { + var result = sb.DoInvokeReturnAsIs( + useLocalScope: false, /* Use current scope to be consistent with 'ForEach/Where-Object {}' and 'collection.ForEach{}/Where{}' */ + errorHandlingBehavior: ScriptBlock.ErrorHandlingBehavior.WriteToCurrentErrorPipe, + dollarUnder: match, + input: AutomationNull.Value, + scriptThis: AutomationNull.Value, + args: Utils.EmptyArray()); + + return PSObject.ToStringParser(context, result);; + }; + return regex.Replace(input, me); + + case object val when LanguagePrimitives.TryConvertTo(val, out MatchEvaluator matchEvaluator): + return regex.Replace(input, matchEvaluator); + + default: + string replacement = PSObject.ToStringParser(context, substitute); + return regex.Replace(input, replacement); + } + } + /// /// Implementation of the PowerShell type operators... /// @@ -1906,4 +1941,3 @@ internal static void Trace(ExecutionContext context, int level, string messageId } #endregion ScriptTrace } - diff --git a/test/powershell/Language/Operators/ReplaceOperator.Tests.ps1 b/test/powershell/Language/Operators/ReplaceOperator.Tests.ps1 new file mode 100644 index 00000000000..be428d9dbce --- /dev/null +++ b/test/powershell/Language/Operators/ReplaceOperator.Tests.ps1 @@ -0,0 +1,67 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +Describe "Replace Operator" -Tags CI { + Context "Replace operator" { + It "Replace operator can replace string values using regular expressions" { + $res = "Get-Process" -replace "Get", "Stop" + $res | Should BeExactly "Stop-Process" + + $res = "image.gif" -replace "\.gif$",".jpg" + $res | Should BeExactly "image.jpg" + } + + It "Replace operator can be case-insensitive and case-sensitive" { + $res = "book" -replace "B","C" + $res | Should BeExactly "Cook" + + $res = "book" -ireplace "B","C" + $res | Should BeExactly "Cook" + + $res = "book" -creplace "B","C" + $res | Should BeExactly "book" + } + + It "Replace operator can take 2 arguments, a mandatory pattern, and an optional substitution" { + $res = "PowerPoint" -replace "Point","Shell" + $res | Should BeExactly "PowerShell" + + $res = "PowerPoint" -replace "Point" + $res | Should BeExactly "Power" + } + } + + Context "Replace operator substitutions" { + It "Replace operator supports numbered substitution groups using ```$n" { + $res = "domain.example" -replace ".*\.(\w+)$","Tld of '`$0' is - '`$1'" + $res | Should BeExactly "Tld of 'domain.example' is - 'example'" + } + + It "Replace operator supports named substitution groups using ```${name}" { + $res = "domain.example" -replace ".*\.(?\w+)$","`${tld}" + $res | Should BeExactly "example" + } + + It "Replace operator can take a ScriptBlock in place of a substitution string" { + $res = "ID ABC123" -replace "\b[A-C]+", {return "X" * $_[0].Value.Length} + $res | Should BeExactly "ID XXX123" + } + + It "Replace operator can take a MatchEvaluator in place of a substitution string" { + $matchEvaluator = {return "X" * $args[0].Value.Length} -as [System.Text.RegularExpressions.MatchEvaluator] + $res = "ID ABC123" -replace "\b[A-C]+", $matchEvaluator + $res | Should BeExactly "ID XXX123" + } + + It "Replace operator can take a static PSMethod in place of a substitution string" { + class R { + static [string] Replace([System.Text.RegularExpressions.Match]$Match) { + return "X" * $Match.Value.Length + } + } + $substitutionMethod = [R]::Replace + $res = "ID 0000123" -replace "\b0+", $substitutionMethod + $res | Should BeExactly "ID XXXX123" + } + } +}