Join GitHub today
GitHub is home to over 36 million developers working together to host and review code, manage projects, and build software together.
Sign upAdd ScriptBlock substitution overload to -replace operator #6029
Conversation
IISResetMe
requested review from
BrucePay,
daxian-dbw and
lzybkr
as
code owners
Jan 25, 2018
powercode
reviewed
Jan 25, 2018
| @@ -815,6 +816,10 @@ internal static object ReplaceOperator(ExecutionContext context, IScriptExtent e | |||
| pattern = rList[0]; | |||
| if (rList.Count > 1) | |||
| { | |||
| if(rList[1] is ScriptBlock) | |||
This comment has been minimized.
This comment has been minimized.
powercode
Jan 25, 2018
•
Collaborator
Shouldn't this also support a MatchEvaluator delegate? Do that we can use a method from a PowerShell or .NET class.
That is, in addition to the scriptblock.
Edit:
I mean something that is convertible to a MatchEvaluator.
Should work with
class R {
static [string] Replace([Match] $match) {
return "X"
}
}
"abc" -replace 'b', [R]::Replace
This comment has been minimized.
This comment has been minimized.
IISResetMe
Jan 25, 2018
Author
Contributor
Good call!
Wondering whether it should accept/evaluate System.Delegate, System.Func<string> and System.Func<Match, string> as well...
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
IISResetMe
Jan 25, 2018
Author
Contributor
@powercode Passing a PSMethodInfo object would make sense as a use case, but I'm not sure how to safely construct a scriptblock or delegate that dispatches it.
Any ideas?
This comment has been minimized.
This comment has been minimized.
|
I'd think it would be better to check if you have something that can be converted to a MatchEvaluator. This works:
It would be nice if
worked too. |
powercode
reviewed
Jan 26, 2018
| @@ -816,7 +816,11 @@ internal static object ReplaceOperator(ExecutionContext context, IScriptExtent e | |||
| pattern = rList[0]; | |||
| if (rList.Count > 1) | |||
| { | |||
| if(rList[1] is ScriptBlock) | |||
| if (rList[1] is MatchEvaluator) | |||
This comment has been minimized.
This comment has been minimized.
powercode
Jan 26, 2018
Collaborator
Maybe have a fast path for the string replacement (likely the most common case), and then, if it isn't a string check for
scriptblock, MatchEvaluator, and PSMethod?
If it is a PSMethod, try to convert it to a MatchEvaluator.
This comment has been minimized.
This comment has been minimized.
IISResetMe
Jan 26, 2018
•
Author
Contributor
How about a single branch like:
if(!(rList[1]] is string))
Attempt conversion to MatchEvaluator
| $res = "ID 0000123" -replace "\b0+", $matchEvaluator | ||
| $res | Should Be "ID 123" | ||
| } | ||
| } |
This comment has been minimized.
This comment has been minimized.
powercode
Jan 26, 2018
Collaborator
I would like a test like
class R { static [string] ReplaceWithShell([Text.RegularExpressions.Match] $m) {return "Shell"}}
"PowerPoint" -replace "Point", [R]::ReplaceWithShell | Should be "PowerShell"
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
LaurentDardenne
commented
Jan 26, 2018
|
What happens if the delegate code raises an exception ? |
This comment has been minimized.
This comment has been minimized.
|
@LaurentDardenne If you pass a scriptblock with no explicit params, the |
This comment has been minimized.
This comment has been minimized.
LaurentDardenne
commented
Jan 26, 2018
|
Thank you. |
iSazonov
requested review from
lzybkr
and removed request for
lzybkr
Jan 26, 2018
iSazonov
reviewed
Jan 26, 2018
| @@ -0,0 +1,67 @@ | |||
| Describe "Split Operator" -Tags CI { | |||
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
IISResetMe
Jan 26, 2018
Author
Contributor
Thanks @iSazonov, fixed in commit aa54fe56a9efb2c0c49b551599ec193d67c47e36
powercode
approved these changes
Jan 26, 2018
powercode
reviewed
Jan 26, 2018
| if (!(rList[1] is string)) | ||
| { | ||
| useMatchEvaluator = LanguagePrimitives.TryConvertTo(rList[1], out matchEvaluator); | ||
| } | ||
| replacement = PSObject.ToStringParser(context, rList[1]); |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
IISResetMe
Jan 26, 2018
•
Author
Contributor
TryConvertTo() will do a simple cast and return early if the input object and out param are the same type, no need to complicate the ReplaceOperator() implementation for this (edge-)case
This comment has been minimized.
This comment has been minimized.
iSazonov
Jan 26, 2018
Collaborator
It is too tricky code. Please store rList[1]); in variable as string and below use it in a help method (see my next comment).
iSazonov
reviewed
Jan 26, 2018
| if (!(rList[1] is string)) | ||
| { | ||
| useMatchEvaluator = LanguagePrimitives.TryConvertTo(rList[1], out matchEvaluator); | ||
| } | ||
| replacement = PSObject.ToStringParser(context, rList[1]); |
This comment has been minimized.
This comment has been minimized.
iSazonov
Jan 26, 2018
Collaborator
It is too tricky code. Please store rList[1]); in variable as string and below use it in a help method (see my next comment).
| // If a MatchEvaluator has been assigned, we | ||
| // use the corresponding Regex.Replace() overload | ||
| return rr.Replace(lvalString, matchEvaluator); | ||
| } | ||
| return rr.Replace(lvalString, replacement); |
This comment has been minimized.
This comment has been minimized.
| $res = "Get-Process" -replace "Get", "Stop" | ||
| $res | Should Be "Stop-Process" | ||
|
|
||
| $res = "image.gif" -replace "\.gif$",".jpg" |
This comment has been minimized.
This comment has been minimized.
iSazonov
Jan 26, 2018
Collaborator
Why we need the test? Looks as dup line 4. Do it test another code path?
This comment has been minimized.
This comment has been minimized.
IISResetMe
Jan 26, 2018
Author
Contributor
First test is the first and simplest example from the about_Comparison_Operators help file, this one is the first example that shows it's actually a regex operator and not just static string replacement.
|
|
||
| It "Replace operator can take 2 arguments, a mandatory pattern, and an optional substitution" { | ||
| $res = "PowerPoint" -replace "Point","Shell" | ||
| $res | Should Be "PowerShell" |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
| $res | Should Be "domain.example" | ||
|
|
||
| $res = "domain.example" -replace ".*\.(\w+)$","`$1" | ||
| $res | Should Be "example" |
This comment has been minimized.
This comment has been minimized.
iSazonov
Jan 26, 2018
Collaborator
We could use one Should with $res = "domain.example" -replace ".*\.(\w+)$","$0 - $1"
This comment has been minimized.
This comment has been minimized.
| } | ||
|
|
||
| It "Replace operator can take a ScriptBlock in place of a substitution string" { | ||
| $res = "ID 0000123" -replace "\b0+", {return " " * $args[0].Value.Length} |
This comment has been minimized.
This comment has been minimized.
iSazonov
Jan 26, 2018
Collaborator
Spaces are not visible - it looks bad. Could you please use another char? Below too.
This comment has been minimized.
This comment has been minimized.
daxian-dbw
added
the
Review - Committee
label
Jan 27, 2018
iSazonov
reviewed
Jan 27, 2018
| } | ||
|
|
||
| return resultList.ToArray(); | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// ReplaceOperator implementation... |
This comment has been minimized.
This comment has been minimized.
iSazonov
Jan 27, 2018
Collaborator
Really we don't need public comment for private method but if we use it please put one final dot in the comment and comments below. Also it is useful to mention that the method is needed to detect a matchEvaluator or use string regex (please reword :) ).
| /// ReplaceOperator implementation... | ||
| /// </summary> | ||
| /// <param name="context">The execution context in which to evaluate the expression</param> | ||
| /// <param name="errorPosition">The position to use for error reporting.</param> |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
|
@IISResetMe Thanks for your contribution. Now we are waiting PowerShell Committee conclusion in days. |
This comment has been minimized.
This comment has been minimized.
|
@PowerShell/powershell-committee reviewed this and agree with taking this change, would prefer |
SteveL-MSFT
added
Committee-Reviewed
and removed
Review - Committee
labels
Jan 31, 2018
This comment has been minimized.
This comment has been minimized.
|
@SteveL-MSFT Cool! I agree that Only way I can think of is by parsing the user-supplied scriptblock, inspecting the If anyone has suggestions or comments on this approach (or better ideas) I'm all ears. |
This comment has been minimized.
This comment has been minimized.
|
@IISResetMe - you could create a closure as the delegate instead, something like this: MatchEvaluator GetEvaluator(ScriptBlock sb)
{
return m => sb.DoInvokeReturnAsIs(
useLocalScope: true, // or false, not sure
errorHandlingBehavior: ErrorHandlingBehavior.WriteToCurrentErrorPipe,
dollarUnder: m,
input: AutomationNull.Value,
scriptThis: AutomationNull.Value,
args: Utils.EmptyArray<object>());
} |
This comment has been minimized.
This comment has been minimized.
|
@lzybkr Interesting, I thought |
This comment has been minimized.
This comment has been minimized.
|
As expected, the following works as intended:
... but it doesn't look or feel very clean :-\ |
This comment has been minimized.
This comment has been minimized.
|
@lzybkr Had a closer look at your suggestion, makes sense, but when I try to compile it I get:
|
This comment has been minimized.
This comment has been minimized.
|
Oh, right, I guess call |
daxian-dbw
self-assigned this
Feb 1, 2018
lzybkr
referenced this pull request
Feb 16, 2018
Closed
Enhancement: support a script block as the replacement operand of the -replace operator to enable dynamic substitutions #6164
This comment has been minimized.
This comment has been minimized.
|
Added fix #6164 in description. |
This comment has been minimized.
This comment has been minimized.
|
@IISResetMe What is the PR status? |
This comment has been minimized.
This comment has been minimized.
|
@IISResetMe This PR is a good enhancement to have. But it's been a while since this PR was left unattended, so I updated the changes to address the My changes were:
@iSazonov @lzybkr @powercode Can you please take another look? |
iSazonov
reviewed
Mar 13, 2018
| $res = "ID 0000123" -replace "\b0+", $matchEvaluator | ||
| $res | Should Be "ID 123" | ||
| } | ||
| } |
This comment has been minimized.
This comment has been minimized.
| case ScriptBlock sb: | ||
| MatchEvaluator me = match => { | ||
| var result = sb.DoInvokeReturnAsIs( | ||
| useLocalScope: false, /* Use local scope to be consistent with 'ForEach/Where-Object {}' and 'collection.ForEach{}/Where{}' */ |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
| return regex.Replace(input, matchEvaluator); | ||
|
|
||
| default: | ||
| string replacement = PSObject.ToStringParser(context, substitute); |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
daxian-dbw
Mar 13, 2018
Member
It's used in the original code, to convert an object to a string:
if (rList.Count > 1)
{
replacement = PSObject.ToStringParser(context, rList[1]);
}
This comment has been minimized.
This comment has been minimized.
iSazonov
Mar 14, 2018
•
Collaborator
What PowerShell magic it give us? If it is useful I'd prefer to see related tests.
This comment has been minimized.
This comment has been minimized.
daxian-dbw
Mar 14, 2018
•
Member
It converts an object to a string in a powershell way and is used extensively in powershell. You can see the code yourself for more information.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
iSazonov
Mar 15, 2018
Collaborator
@daxian-dbw Thanks! I thought that using the method add some magic in the PR but no - it is only to get string.
| @@ -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} | |||
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
daxian-dbw
Mar 13, 2018
Member
$args won't work. When used in the form of something -replace xxx, { ... }, the "Match" object is passed to $_ of the script block, just like ForEach/Where-Object {...} and collection.ForEach{...}/Where{...}.
This comment has been minimized.
This comment has been minimized.
|
@daxian-dbw LGTM! |
This comment has been minimized.
This comment has been minimized.
|
Thanks @daxian-dbw! Couldn't get Looks good! |
This comment has been minimized.
This comment has been minimized.
|
@iSazonov Your comments have been addressed. Can you please take another look? |
lzybkr
approved these changes
Mar 13, 2018
daxian-dbw
added
the
Documentation Needed
label
Mar 13, 2018
daxian-dbw
dismissed
iSazonov’s
stale review
Mar 14, 2018
The typo 'local scope' in the comment has been corrected to 'current scope'. Feel free to leave other comments.
This comment has been minimized.
This comment has been minimized.
|
@iSazonov Given that your pending comment is not blocking (questions about |


IISResetMe commentedJan 25, 2018
•
edited by iSazonov
PR Summary
Fix #6164.
This PR adds rudimentary lambda support to the
-replaceoperator:The current
-replaceoperator implementation has the following syntax:This (functionally) maps directly onto:
Regex.Replace()already has an overload that supports aMatchEvaluatordelegate, which in turn allows us to pass a scriptblock instead of a string value as the substitution pattern.Since only wiring is required, we can accomplish this with:
Update:
To support arbitrary delegates (like existing
MatchEvaluators andPSMethods),