From a01d835260aa44a5c0bbc9a39f938466dc19062f Mon Sep 17 00:00:00 2001 From: "Mathias R. Jessen" Date: Thu, 25 Jan 2018 20:59:32 +0100 Subject: [PATCH 1/2] Add lambda support to -replace operator Add support for replacement lambdas when using the -replace operator. Requires minimal changes to existing code by using the following overload: Regex.Replace(string input, MatchEvaluator evaluator) when a ScriptBlock is passed in as the replacement argument. --- .../engine/lang/parserutils.cs | 40 +++++++++--- .../Operators/ReplaceOperator.Tests.ps1 | 64 +++++++++++++++++++ 2 files changed, 94 insertions(+), 10 deletions(-) create mode 100644 test/powershell/Language/Operators/ReplaceOperator.Tests.ps1 diff --git a/src/System.Management.Automation/engine/lang/parserutils.cs b/src/System.Management.Automation/engine/lang/parserutils.cs index 3be7adb86bf..354c63b8831 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; @@ -895,13 +895,14 @@ internal static object ReplaceOperator(ExecutionContext context, IScriptExtent e "BadReplaceArgument", ParserStrings.BadReplaceArgument, ignoreCase ? "-ireplace" : "-replace", rList.Count); } + if(rList.Count > 1) + { + substitute = rList[1]; + } + if (rList.Count > 0) { pattern = rList[0]; - if (rList.Count > 1) - { - replacement = PSObject.ToStringParser(context, rList[1]); - } } } else @@ -935,8 +936,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 +944,35 @@ 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) + { + MatchEvaluator matchEvaluator = null; + if (!(substitute is string) && LanguagePrimitives.TryConvertTo(substitute, out matchEvaluator)) + { + return regex.Replace(input, matchEvaluator); + } + + string replacement = PSObject.ToStringParser(context, substitute); + return regex.Replace(input, replacement); + } + /// /// Implementation of the PowerShell type operators... /// @@ -1906,4 +1927,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..2cb807d86db --- /dev/null +++ b/test/powershell/Language/Operators/ReplaceOperator.Tests.ps1 @@ -0,0 +1,64 @@ +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" * $args[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" + } + } +} From 02107f61977d758568ce9d4f101d177b99d8d4d2 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Mon, 12 Mar 2018 17:53:09 -0700 Subject: [PATCH 2/2] Pass the 'Match' object to $_ for the substitute script block in '-replace' operation (#6029) --- .../engine/lang/parserutils.cs | 36 +++++++++++++------ .../Operators/ReplaceOperator.Tests.ps1 | 5 ++- 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/src/System.Management.Automation/engine/lang/parserutils.cs b/src/System.Management.Automation/engine/lang/parserutils.cs index 354c63b8831..300d39a5afc 100644 --- a/src/System.Management.Automation/engine/lang/parserutils.cs +++ b/src/System.Management.Automation/engine/lang/parserutils.cs @@ -895,14 +895,13 @@ internal static object ReplaceOperator(ExecutionContext context, IScriptExtent e "BadReplaceArgument", ParserStrings.BadReplaceArgument, ignoreCase ? "-ireplace" : "-replace", rList.Count); } - if(rList.Count > 1) - { - substitute = rList[1]; - } - if (rList.Count > 0) { pattern = rList[0]; + if (rList.Count > 1) + { + substitute = rList[1]; + } } } else @@ -963,14 +962,29 @@ internal static object ReplaceOperator(ExecutionContext context, IScriptExtent e /// The result of the regex.Replace operation private static object ReplaceOperatorImpl(ExecutionContext context, string input, Regex regex, object substitute) { - MatchEvaluator matchEvaluator = null; - if (!(substitute is string) && LanguagePrimitives.TryConvertTo(substitute, out matchEvaluator)) + switch (substitute) { - return regex.Replace(input, matchEvaluator); - } + 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); - string replacement = PSObject.ToStringParser(context, substitute); - return regex.Replace(input, replacement); + 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); + } } /// diff --git a/test/powershell/Language/Operators/ReplaceOperator.Tests.ps1 b/test/powershell/Language/Operators/ReplaceOperator.Tests.ps1 index 2cb807d86db..be428d9dbce 100644 --- a/test/powershell/Language/Operators/ReplaceOperator.Tests.ps1 +++ b/test/powershell/Language/Operators/ReplaceOperator.Tests.ps1 @@ -1,3 +1,6 @@ +# 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" { @@ -40,7 +43,7 @@ Describe "Replace Operator" -Tags CI { } It "Replace operator can take a ScriptBlock in place of a substitution string" { - $res = "ID ABC123" -replace "\b[A-C]+", {return "X" * $args[0].Value.Length} + $res = "ID ABC123" -replace "\b[A-C]+", {return "X" * $_[0].Value.Length} $res | Should BeExactly "ID XXX123" }