PowerShell: выбор строки, предшествующей совпадению - проблема Select-String -Context при использовании входной строковой переменной
Мне нужно вернуть строку, предшествующую совпадению, в многострочной строковой переменной.
Кажется, что при использовании строковой переменной для ввода Select-String считает всю строку соответствующей. Таким образом, свойства контекста находятся "за пределами" любого конца строки и являются нулевыми.
Рассмотрим приведенный ниже пример:
$teststring = @"
line1
line2
line3
line4
line5
"@
Write-Host "Line Count:" ($teststring | Measure-Object -Line).Lines #verify PowerShell does regard input as a multi-line string (it does)
Select-String -Pattern "line3" -InputObject $teststring -AllMatches -Context 1,0 | % {
$_.Matches.Value #this prints the exact match
$_.Context #output shows all context properties to be empty
$_.Context.PreContext[0] #this would ideally output first line before the match
$_.Context.PreContext[0] -eq $null #but instead is null
}
Я что-то здесь неправильно понимаю?
Каков наилучший способ вернуть "line2" при совпадении с "line3"?
Спасибо!
Редактировать: Дополнительные требования, которые я не упомянул, чтобы заявить: Необходимо предоставить строку выше ВСЕ совпадающие строки для строки неопределенной длины. Например, при поиске ниже строки "line3" мне нужно вернуть "line2" и "line5".
line1
line2
line3
line4
line5
line3
line6
2 ответа
Select-String
работает с массивами ввода, поэтому вместо одной многострочной строки необходимо предоставить массив строк для -Context
а также -AllMatches
работать по назначению:
$teststring = @"
line1
line2
line3
line4
line5
line3
line6
"@
$teststring -split '\r?\n' | Select-String -Pattern "line3" -AllMatches -Context 1,0 | % {
"line before: " + $_.Context.PreContext[0]
"matched part: " + $_.Matches.Value # Prints the what the pattern matched
}
Это дает:
line before: line2
matched part: line3
line before: line5
matched part: line3
$teststring -split '\r?\n'
разбивает многострочную строку на массив строк:- Примечание. Какие последовательности разрыва строки использует ваш документ (только для LF и CRLF), зависит от файла прилагаемого скрипта; регулярное выражение
\r?\n
обрабатывает любой стиль.
- Примечание. Какие последовательности разрыва строки использует ваш документ (только для LF и CRLF), зависит от файла прилагаемого скрипта; регулярное выражение
Обратите внимание, что крайне важно использовать конвейер для обеспечения
Select-String
вход; если вы использовали-InputObject
массив будет приведен обратно к одной строке.
Select-String
это удобно, но медленно.
Специально для одной строки, уже находящейся в памяти, решение с использованием.NET Framework [Regex]::Matches()
метод будет работать намного лучше, хотя и сложнее.
Обратите внимание, что PowerShell собственный -match
а также -replace
операторы построены на одном и том же классе.NET, но не раскрывают всю его функциональность; -match
- который сообщает о захвате групп в автоматическом $Matches
переменная - здесь не вариант, потому что она возвращает только 1 совпадение.
Следующее, по сути, тот же подход, что и в ответном ответе Мьолинора, но с некоторыми исправленными проблемами [1].
# Note: The sample string is defined so that it contains LF-only (\n)
# line breaks, merely to simplify the regex below for illustration.
# If your script file use LF-only line breaks, the
# `-replace '\r?\n', "`n" call isn't needed.
$teststring = @"
line1
line2
line3
line4
line5
line3
line6
"@ -replace '\r?\n', "`n"
[Regex]::Matches($teststring, '(?:^|(.*)\n).*(line3)') | ForEach-Object {
"line before: " + $_.Groups[1].Value
"matched part: " + $_.Groups[2].Value
}
Regex
(?:^|(.*)\n).*(line3)
использует 2 группы захвата ((...)
) чтобы захватить (совпадающую часть) строку для сопоставления и строку перед ((?:...)
является вспомогательной группой без захвата, которая необходима для приоритета):(?:^|(.*)\n)
соответствует либо самому началу строки (^
) или же (|
) любая - возможно, пустая - последовательность символов, не являющихся символом новой строки (.*
) с последующим переводом строки (\n
); это гарантирует, что соответствующая строка также будет найдена, когда нет предшествующей строки (т. е. совпадающая строка является первой).(line3)
группа, определяющая линию для сопоставления; ему предшествует.*
чтобы соответствовать поведению в вопросе, где шаблонline3
найден даже это только часть строки.- Если вы хотите, чтобы совпадали только полные строки, используйте следующее регулярное выражение:
(?:^|(.*)\n)(line3)(?:\n|$)
- Если вы хотите, чтобы совпадали только полные строки, используйте следующее регулярное выражение:
[Regex]::Matches()
находит все совпадения и возвращает их как коллекциюSystem.Text.RegularExpressions.Match
объекты, которыеForEach-Object
Затем можно использовать вызов командлета для извлечения совпадений группы захвата ($_.Groups[<n>].Value
).
[1] На момент написания статьи:
- Нет необходимости совпадать дважды - ограждающие if ($teststring -match $pattern) { ... }
не нужно
- Встроенный вариант (?m)
не нужно, потому что .
не соответствует переводу строки по умолчанию.
- (.+?)
захватывает только непустые строки (и ?
не жадный квантификатор, не нужен).
- Если линия интереса - первая строка - то есть, если нет никакой линии прежде, это не будет соответствовать.
Вы можете использовать многострочное регулярное выражение, с -match
оператор:
$teststring = @"
line1
line2
line3
line4
line5
line3
line6
"@
$pattern =
@'
(?m)
(.+?)
line3
'@
if ($teststring -match $pattern)
{ [Regex]::Matches($teststring,$pattern) |
foreach {$_.groups[1].value} }