Support negative numbers in -split
operator (#8960)
This commit is contained in:
parent
0e89b0f91f
commit
f222a686c5
2 changed files with 215 additions and 63 deletions
|
@ -584,7 +584,7 @@ namespace System.Management.Automation
|
||||||
return SplitOperatorImpl(context, errorPosition, lval, rval, SplitImplOptions.None, ignoreCase);
|
return SplitOperatorImpl(context, errorPosition, lval, rval, SplitImplOptions.None, ignoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static object SplitOperatorImpl(ExecutionContext context, IScriptExtent errorPosition, object lval, object rval, SplitImplOptions implOptions, bool ignoreCase)
|
private static IReadOnlyList<string> SplitOperatorImpl(ExecutionContext context, IScriptExtent errorPosition, object lval, object rval, SplitImplOptions implOptions, bool ignoreCase)
|
||||||
{
|
{
|
||||||
IEnumerable<string> content = enumerateContent(context, errorPosition, implOptions, lval);
|
IEnumerable<string> content = enumerateContent(context, errorPosition, implOptions, lval);
|
||||||
|
|
||||||
|
@ -634,84 +634,166 @@ namespace System.Management.Automation
|
||||||
options |= SplitOptions.IgnoreCase;
|
options |= SplitOptions.IgnoreCase;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (predicate != null)
|
if (predicate == null)
|
||||||
|
{
|
||||||
|
return SplitWithPattern(context, errorPosition, content, separatorPattern, limit, options);
|
||||||
|
}
|
||||||
|
else if (limit >= 0)
|
||||||
{
|
{
|
||||||
return SplitWithPredicate(context, errorPosition, content, predicate, limit);
|
return SplitWithPredicate(context, errorPosition, content, predicate, limit);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
return SplitWithPattern(context, errorPosition, content, separatorPattern, limit, options);
|
return NegativeSplitWithPredicate(context, errorPosition, content, predicate, limit);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static object SplitWithPredicate(ExecutionContext context, IScriptExtent errorPosition, IEnumerable<string> content, ScriptBlock predicate, int limit)
|
private static IReadOnlyList<string> NegativeSplitWithPredicate(ExecutionContext context, IScriptExtent errorPosition, IEnumerable<string> content, ScriptBlock predicate, int limit)
|
||||||
{
|
{
|
||||||
List<string> results = new List<string>();
|
var results = new List<string>();
|
||||||
|
|
||||||
|
if (limit == -1)
|
||||||
|
{
|
||||||
|
// If the user just wants 1 string
|
||||||
|
// then just return the content
|
||||||
|
return new List<string>(content);
|
||||||
|
}
|
||||||
|
|
||||||
foreach (string item in content)
|
foreach (string item in content)
|
||||||
{
|
{
|
||||||
List<string> split = new List<string>();
|
var split = new List<string>();
|
||||||
|
|
||||||
if (limit == 1)
|
// Used to traverse through the item
|
||||||
{
|
int cursor = item.Length - 1;
|
||||||
// Don't bother with looking for any delimiters,
|
|
||||||
// just return the original string.
|
|
||||||
results.Add(item);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
StringBuilder buf = new StringBuilder();
|
int subStringLength = 0;
|
||||||
for (int strIndex = 0; strIndex < item.Length; strIndex++)
|
|
||||||
{
|
for (int charCount = 0; charCount < item.Length; charCount++) {
|
||||||
object isDelimChar = predicate.DoInvokeReturnAsIs(
|
// Evaluate the predicate using the character at cursor.
|
||||||
|
object predicateResult = predicate.DoInvokeReturnAsIs(
|
||||||
useLocalScope: true,
|
useLocalScope: true,
|
||||||
errorHandlingBehavior: ScriptBlock.ErrorHandlingBehavior.WriteToExternalErrorPipe,
|
errorHandlingBehavior: ScriptBlock.ErrorHandlingBehavior.WriteToExternalErrorPipe,
|
||||||
dollarUnder: CharToString(item[strIndex]),
|
dollarUnder: CharToString(item[cursor]),
|
||||||
input: AutomationNull.Value,
|
input: AutomationNull.Value,
|
||||||
scriptThis: AutomationNull.Value,
|
scriptThis: AutomationNull.Value,
|
||||||
args: new object[] { item, strIndex });
|
args: new object[] { item, cursor });
|
||||||
if (LanguagePrimitives.IsTrue(isDelimChar))
|
|
||||||
|
if (!LanguagePrimitives.IsTrue(predicateResult))
|
||||||
{
|
{
|
||||||
split.Add(buf.ToString());
|
subStringLength++;
|
||||||
buf = new StringBuilder();
|
cursor -= 1;
|
||||||
|
continue;
|
||||||
if (limit > 0 && split.Count >= (limit - 1))
|
|
||||||
{
|
|
||||||
// We're one item below the limit. If
|
|
||||||
// we have any string left, go ahead
|
|
||||||
// and add it as the last item, otherwise
|
|
||||||
// add an empty string if there was
|
|
||||||
// a delimiter at the end.
|
|
||||||
if ((strIndex + 1) < item.Length)
|
|
||||||
{
|
|
||||||
split.Add(item.Substring(strIndex + 1));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
split.Add(string.Empty);
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If this delimiter is at the end of the string,
|
|
||||||
// add an empty string to denote the item "after"
|
|
||||||
// it.
|
|
||||||
if (strIndex == (item.Length - 1))
|
|
||||||
{
|
|
||||||
split.Add(string.Empty);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else
|
|
||||||
|
split.Add(item.Substring(cursor + 1, subStringLength));
|
||||||
|
|
||||||
|
subStringLength = 0;
|
||||||
|
|
||||||
|
cursor -= 1;
|
||||||
|
|
||||||
|
if (System.Math.Abs(limit) == (split.Count + 1))
|
||||||
{
|
{
|
||||||
buf.Append(item[strIndex]);
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add any remainder, if we're under the limit.
|
if (cursor == -1)
|
||||||
if (buf.Length > 0 &&
|
|
||||||
(limit <= 0 || split.Count < limit))
|
|
||||||
{
|
{
|
||||||
split.Add(buf.ToString());
|
// Used when the limit is negative
|
||||||
|
// and the cursor was allowed to go
|
||||||
|
// all the way to the start of the
|
||||||
|
// string.
|
||||||
|
split.Add(item.Substring(0, subStringLength));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Used to get the rest of the string
|
||||||
|
// when using a negative limit and
|
||||||
|
// the cursor doesn't reach the end
|
||||||
|
// of the string.
|
||||||
|
split.Add(item.Substring(0, cursor + 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
split.Reverse();
|
||||||
|
|
||||||
|
results.AddRange(split);
|
||||||
|
}
|
||||||
|
|
||||||
|
return results.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IReadOnlyList<string> SplitWithPredicate(ExecutionContext context, IScriptExtent errorPosition, IEnumerable<string> content, ScriptBlock predicate, int limit)
|
||||||
|
{
|
||||||
|
var results = new List<string>();
|
||||||
|
|
||||||
|
if (limit == 1)
|
||||||
|
{
|
||||||
|
// If the user just wants 1 string
|
||||||
|
// then just return the content
|
||||||
|
return new List<string>(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (string item in content)
|
||||||
|
{
|
||||||
|
var split = new List<string>();
|
||||||
|
|
||||||
|
// Used to traverse through the item
|
||||||
|
int cursor = 0;
|
||||||
|
|
||||||
|
// This is used to calculate how much to split from item.
|
||||||
|
int subStringLength = 0;
|
||||||
|
|
||||||
|
for (int charCount = 0; charCount < item.Length; charCount++)
|
||||||
|
{
|
||||||
|
// Evaluate the predicate using the character at cursor.
|
||||||
|
object predicateResult = predicate.DoInvokeReturnAsIs(
|
||||||
|
useLocalScope: true,
|
||||||
|
errorHandlingBehavior: ScriptBlock.ErrorHandlingBehavior.WriteToExternalErrorPipe,
|
||||||
|
dollarUnder: CharToString(item[cursor]),
|
||||||
|
input: AutomationNull.Value,
|
||||||
|
scriptThis: AutomationNull.Value,
|
||||||
|
args: new object[] { item, cursor });
|
||||||
|
|
||||||
|
// If the current character is not a delimiter
|
||||||
|
// then it must be included into a substring.
|
||||||
|
if (!LanguagePrimitives.IsTrue(predicateResult))
|
||||||
|
{
|
||||||
|
subStringLength++;
|
||||||
|
|
||||||
|
cursor += 1;
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Else, if the character is a delimiter
|
||||||
|
// then add a substring to the split list.
|
||||||
|
split.Add(item.Substring(cursor - subStringLength, subStringLength));
|
||||||
|
|
||||||
|
subStringLength = 0;
|
||||||
|
|
||||||
|
cursor += 1;
|
||||||
|
|
||||||
|
if (limit == (split.Count + 1))
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cursor == item.Length)
|
||||||
|
{
|
||||||
|
// Used to get the rest of the string
|
||||||
|
// when the limit is not negative and
|
||||||
|
// the cursor is allowed to make it to
|
||||||
|
// the end of the string.
|
||||||
|
split.Add(item.Substring(cursor - subStringLength, subStringLength));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Used to get the rest of the string
|
||||||
|
// when the limit is not negative and
|
||||||
|
// the cursor is not at the end of the
|
||||||
|
// string.
|
||||||
|
split.Add(item.Substring(cursor, item.Length - cursor));
|
||||||
}
|
}
|
||||||
|
|
||||||
results.AddRange(split);
|
results.AddRange(split);
|
||||||
|
@ -720,7 +802,7 @@ namespace System.Management.Automation
|
||||||
return results.ToArray();
|
return results.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static object SplitWithPattern(ExecutionContext context, IScriptExtent errorPosition, IEnumerable<string> content, string separatorPattern, int limit, SplitOptions options)
|
private static IReadOnlyList<string> SplitWithPattern(ExecutionContext context, IScriptExtent errorPosition, IEnumerable<string> content, string separatorPattern, int limit, SplitOptions options)
|
||||||
{
|
{
|
||||||
// Default to Regex matching if no match specified.
|
// Default to Regex matching if no match specified.
|
||||||
if ((options & SplitOptions.SimpleMatch) == 0 &&
|
if ((options & SplitOptions.SimpleMatch) == 0 &&
|
||||||
|
@ -743,20 +825,24 @@ namespace System.Management.Automation
|
||||||
separatorPattern = Regex.Escape(separatorPattern);
|
separatorPattern = Regex.Escape(separatorPattern);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (limit < 0)
|
RegexOptions regexOptions = parseRegexOptions(options);
|
||||||
|
|
||||||
|
int calculatedLimit = limit;
|
||||||
|
|
||||||
|
// If the limit is negative then set Regex to read from right to left
|
||||||
|
if (calculatedLimit < 0)
|
||||||
{
|
{
|
||||||
// Regex only allows 0 to signify "no limit", whereas
|
regexOptions |= RegexOptions.RightToLeft;
|
||||||
// we allow any integer <= 0.
|
calculatedLimit *= -1;
|
||||||
limit = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
RegexOptions regexOptions = parseRegexOptions(options);
|
|
||||||
Regex regex = NewRegex(separatorPattern, regexOptions);
|
Regex regex = NewRegex(separatorPattern, regexOptions);
|
||||||
|
|
||||||
List<string> results = new List<string>();
|
var results = new List<string>();
|
||||||
|
|
||||||
foreach (string item in content)
|
foreach (string item in content)
|
||||||
{
|
{
|
||||||
string[] split = regex.Split(item, limit, 0);
|
string[] split = regex.Split(item, calculatedLimit);
|
||||||
results.AddRange(split);
|
results.AddRange(split);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -33,12 +33,78 @@ Describe "Split Operator" -Tags CI {
|
||||||
$res[2] | Should -Be "c"
|
$res[2] | Should -Be "c"
|
||||||
$res[3] | Should -Be "d"
|
$res[3] | Should -Be "d"
|
||||||
|
|
||||||
|
$res = "a b c d" -split " ", -2
|
||||||
|
$res.count | Should -Be 2
|
||||||
|
$res[0] | Should -Be "a b c"
|
||||||
|
$res[1] | Should -Be "d"
|
||||||
|
|
||||||
$res = "a b c d" -split " ", -1
|
$res = "a b c d" -split " ", -1
|
||||||
|
$res.count | Should -Be 1
|
||||||
|
$res[0] | Should -Be "a b c d"
|
||||||
|
}
|
||||||
|
|
||||||
|
It "Binary split operator can work with different delimeter than split string" {
|
||||||
|
$res = "a b c d" -split " ",8
|
||||||
$res.count | Should -Be 4
|
$res.count | Should -Be 4
|
||||||
$res[0] | Should -Be "a"
|
$res[0] | Should -Be "a"
|
||||||
$res[1] | Should -Be "b"
|
$res[1] | Should -Be "b"
|
||||||
$res[2] | Should -Be "c"
|
$res[2] | Should -Be "c"
|
||||||
$res[3] | Should -Be "d"
|
$res[3] | Should -Be "d"
|
||||||
|
|
||||||
|
$res = "a b c d" -split " ",-8
|
||||||
|
$res.count | Should -Be 4
|
||||||
|
$res[0] | Should -Be "a"
|
||||||
|
$res[1] | Should -Be "b"
|
||||||
|
$res[2] | Should -Be "c"
|
||||||
|
$res[3] | Should -Be "d"
|
||||||
|
|
||||||
|
$res = " " -split " ",-2
|
||||||
|
$res.count | Should -Be 2
|
||||||
|
$res[0] | Should -Be ""
|
||||||
|
$res[1] | Should -Be ""
|
||||||
|
}
|
||||||
|
|
||||||
|
It "Binary split operator with predicate can work with negative numbers" {
|
||||||
|
$res = "a b c d" -split {$_ -like ' '},-2
|
||||||
|
$res.count | Should -Be 2
|
||||||
|
$res[0] | Should -Be "a b c"
|
||||||
|
$res[1] | Should -Be "d"
|
||||||
|
|
||||||
|
$res = "a b c d" -split {$_ -like ' '},-4
|
||||||
|
$res.count | Should -Be 4
|
||||||
|
$res[0] | Should -Be "a"
|
||||||
|
$res[1] | Should -Be "b"
|
||||||
|
$res[2] | Should -Be "c"
|
||||||
|
$res[3] | Should -Be "d"
|
||||||
|
|
||||||
|
$res = "a b c d" -split {$_ -like ' '},-8
|
||||||
|
$res.count | Should -Be 4
|
||||||
|
$res[0] | Should -Be "a"
|
||||||
|
$res[1] | Should -Be "b"
|
||||||
|
$res[2] | Should -Be "c"
|
||||||
|
$res[3] | Should -Be "d"
|
||||||
|
|
||||||
|
$res = " " -split {$_ -like ' '},-4
|
||||||
|
$res.count | Should -Be 2
|
||||||
|
$res[0] | Should -Be ""
|
||||||
|
$res[1] | Should -Be ""
|
||||||
|
|
||||||
|
$res = "folder/path/to/file" -split {$_ -like '/'}, -2
|
||||||
|
$res.count | Should -Be 2
|
||||||
|
$res[0] | Should -Be "folder/path/to"
|
||||||
|
$res[1] | Should -Be "file"
|
||||||
|
}
|
||||||
|
|
||||||
|
It "Binary split operator can work with regex expression" {
|
||||||
|
$res = "a2b3c4d" -split '\d+',2
|
||||||
|
$res.count | Should -Be 2
|
||||||
|
$res[0] | Should -Be "a"
|
||||||
|
$res[1] | Should -Be "b3c4d"
|
||||||
|
|
||||||
|
$res = "a2b3c4d" -split '\d+',-2
|
||||||
|
$res.count | Should -Be 2
|
||||||
|
$res[0] | Should -Be "a2b3c"
|
||||||
|
$res[1] | Should -Be "d"
|
||||||
}
|
}
|
||||||
|
|
||||||
It "Binary split operator can works with freeform delimiter" {
|
It "Binary split operator can works with freeform delimiter" {
|
||||||
|
|
Loading…
Reference in a new issue