Skip to content

Commit ba49eb2

Browse files
authored
Optimize IEnumerable variant of replace operator (#14221)
1 parent 971c428 commit ba49eb2

File tree

2 files changed

+70
-25
lines changed

2 files changed

+70
-25
lines changed

src/System.Management.Automation/engine/lang/parserutils.cs

Lines changed: 52 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -965,6 +965,7 @@ internal static object ReplaceOperator(ExecutionContext context, IScriptExtent e
965965
}
966966
}
967967

968+
var replacer = ReplaceOperatorImpl.Create(context, rr, substitute);
968969
IEnumerator list = LanguagePrimitives.GetEnumerator(lval);
969970
if (list == null)
970971
{
@@ -978,53 +979,63 @@ internal static object ReplaceOperator(ExecutionContext context, IScriptExtent e
978979
lvalString = lval?.ToString() ?? string.Empty;
979980
}
980981

981-
return ReplaceOperatorImpl(context, lvalString, rr, substitute);
982+
return replacer.Replace(lvalString);
982983
}
983984
else
984985
{
985986
List<object> resultList = new List<object>();
986987
while (ParserOps.MoveNext(context, errorPosition, list))
987988
{
988989
string lvalString = PSObject.ToStringParser(context, ParserOps.Current(errorPosition, list));
989-
resultList.Add(ReplaceOperatorImpl(context, lvalString, rr, substitute));
990+
resultList.Add(replacer.Replace(lvalString));
990991
}
991992

992993
return resultList.ToArray();
993994
}
994995
}
995996

996-
/// <summary>
997-
/// ReplaceOperator implementation.
998-
/// Abstracts away conversion of the optional substitute parameter to either a string or a MatchEvaluator delegate
999-
/// and finally returns the result of the final Regex.Replace operation.
1000-
/// </summary>
1001-
/// <param name="context">The execution context in which to evaluate the expression.</param>
1002-
/// <param name="input">The input string.</param>
1003-
/// <param name="regex">A Regex instance.</param>
1004-
/// <param name="substitute">The substitute value.</param>
1005-
/// <returns>The result of the regex.Replace operation.</returns>
1006-
private static object ReplaceOperatorImpl(ExecutionContext context, string input, Regex regex, object substitute)
997+
private struct ReplaceOperatorImpl
1007998
{
1008-
switch (substitute)
999+
public static ReplaceOperatorImpl Create(ExecutionContext context, Regex regex, object substitute)
1000+
{
1001+
return new ReplaceOperatorImpl(context, regex, substitute);
1002+
}
1003+
1004+
private readonly Regex _regex;
1005+
private readonly string _cachedReplacementString;
1006+
private readonly MatchEvaluator _cachedMatchEvaluator;
1007+
1008+
private ReplaceOperatorImpl(
1009+
ExecutionContext context,
1010+
Regex regex,
1011+
object substitute)
10091012
{
1010-
case string replacementString:
1011-
return regex.Replace(input, replacementString);
1013+
_regex = regex;
1014+
_cachedReplacementString = null;
1015+
_cachedMatchEvaluator = null;
1016+
1017+
switch (substitute)
1018+
{
1019+
case string replacement:
1020+
_cachedReplacementString = replacement;
1021+
break;
10121022

1013-
case ScriptBlock sb:
1014-
MatchEvaluator me = GetMatchEvaluator(context, sb);
1015-
return regex.Replace(input, me);
1023+
case ScriptBlock sb:
1024+
_cachedMatchEvaluator = GetMatchEvaluator(context, sb);
1025+
break;
10161026

1017-
case object val when LanguagePrimitives.TryConvertTo(val, out MatchEvaluator matchEvaluator):
1018-
return regex.Replace(input, matchEvaluator);
1027+
case object val when LanguagePrimitives.TryConvertTo(val, out _cachedMatchEvaluator):
1028+
break;
10191029

1020-
default:
1021-
string replacement = PSObject.ToStringParser(context, substitute);
1022-
return regex.Replace(input, replacement);
1030+
default:
1031+
_cachedReplacementString = PSObject.ToStringParser(context, substitute);
1032+
break;
1033+
}
10231034
}
10241035

10251036
// Local helper function to avoid creating an instance of the generated delegate helper class
10261037
// every time 'ReplaceOperatorImpl' is invoked.
1027-
static MatchEvaluator GetMatchEvaluator(ExecutionContext context, ScriptBlock sb)
1038+
private static MatchEvaluator GetMatchEvaluator(ExecutionContext context, ScriptBlock sb)
10281039
{
10291040
return match =>
10301041
{
@@ -1039,6 +1050,22 @@ static MatchEvaluator GetMatchEvaluator(ExecutionContext context, ScriptBlock sb
10391050
return PSObject.ToStringParser(context, result);
10401051
};
10411052
}
1053+
1054+
/// <summary>
1055+
/// ReplaceOperator implementation.
1056+
/// Abstracts away conversion of the optional substitute parameter to either a string or a MatchEvaluator delegate
1057+
/// and finally returns the result of the final Regex.Replace operation.
1058+
/// </summary>
1059+
public object Replace(string input)
1060+
{
1061+
if (_cachedReplacementString is not null)
1062+
{
1063+
return _regex.Replace(input, _cachedReplacementString);
1064+
}
1065+
1066+
Dbg.Assert(_cachedMatchEvaluator is not null, "_cachedMatchEvaluator should be not null when code reach here.");
1067+
return _regex.Replace(input, _cachedMatchEvaluator);
1068+
}
10421069
}
10431070

10441071
/// <summary>

test/powershell/Language/Operators/ReplaceOperator.Tests.ps1

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ Describe "Replace Operator" -Tags CI {
1111
$res | Should -BeExactly "image.jpg"
1212
}
1313

14+
It "Replace operator can convert an substitution object to string" {
15+
$substitution = Get-Process -Id $pid
16+
$res = "!3!" -replace "3",$substitution
17+
$res | Should -BeExactly "!System.Diagnostics.Process (pwsh)!"
18+
}
19+
1420
It "Replace operator can be case-insensitive and case-sensitive" {
1521
$res = "book" -replace "B","C"
1622
$res | Should -BeExactly "Cook"
@@ -29,6 +35,18 @@ Describe "Replace Operator" -Tags CI {
2935
$res = "PowerPoint" -replace "Point"
3036
$res | Should -BeExactly "Power"
3137
}
38+
39+
It "Replace operator can take an enumerable as first argument, a mandatory pattern, and an optional substitution" {
40+
$res = "PowerPoint1","PowerPoint2" -replace "Point","Shell"
41+
$res.Count | Should -Be 2
42+
$res[0] | Should -BeExactly "PowerShell1"
43+
$res[1] | Should -BeExactly "PowerShell2"
44+
45+
$res = "PowerPoint1","PowerPoint2" -replace "Point"
46+
$res.Count | Should -Be 2
47+
$res[0] | Should -BeExactly "Power1"
48+
$res[1] | Should -BeExactly "Power2"
49+
}
3250
}
3351

3452
Context "Replace operator substitutions" {

0 commit comments

Comments
 (0)