Лог-файл на сорок тысяч строк, а нужны только адреса, с которых приходили ошибки. Папка с фотографиями, названия которых надо привести к единому виду. Выгрузка из учётной системы, где даты записаны то через точку, то через дефис. Обычный поиск подстроки с такими задачами не справляется: он умеет искать только то, что известно дословно, а здесь известна лишь форма искомого. Тут и пригождаются регулярные выражения: короткие шаблоны, которые описывают не конкретное слово, а правило, по которому оно устроено. В PowerShell они встроены глубоко и работают через несколько операторов и командлетов. Разобраться в них стоит один раз, потому что потом половина рутинной работы с текстом сводится к одной строке.
Где в PowerShell живут регулярные выражения и какие операторы и командлеты с ними работают
PowerShell использует движок регулярных выражений платформы .NET. Он понимает привычный синтаксис: классы символов, якоря, группы, квантификаторы, просмотр вперёд и назад. Работает с регулярными выражениями целый набор инструментов: операторы match, notmatch, replace и split, оператор switch с ключом Regex, командлет Select-String и класс [regex] с его статическими методами.
Простейшая проверка выглядит так:
'book' -match 'oo'
'123' -match '\d+'
'PowerShell' -match '^Power'
Все три строки вернут True. Оператор -match отвечает на вопрос "есть ли в строке совпадение с шаблоном".
Первый подводный камень поджидает сразу. Регулярные выражения в PowerShell по умолчанию не различают регистр. Выражение 'ABC' -match 'abc' тоже вернёт True. Для различения регистра есть парные операторы с буквой c в начале: -cmatch, -creplace, -csplit. Соответственно 'ABC' -cmatch 'abc' даст False. У Select-String для этого служит ключ -CaseSensitive.
Второй камень связан с путаницей двух похожих операторов. Оператор -like работает с подстановочными знаками, где звёздочка означает "любое количество любых символов". В регулярных выражениях звёздочка означает "ноль или больше повторов предыдущего элемента". Смешивать эти два мира нельзя: шаблон *.log для -match даст ошибку, потому что звёздочке не к чему применяться.
Третье правило касается кавычек. Шаблоны лучше писать в одинарных кавычках. В двойных PowerShell подставляет значения переменных, и знак доллара в конце шаблона может быть принят за начало имени переменной. Обратная косая черта в PowerShell обычным символом экранирования не является, поэтому писать её дважды, как в некоторых языках программирования, не нужно. Шаблон \d+ записывается ровно так.
Основы синтаксиса, классы символов, якоря, квантификаторы и жадность
Большинство шаблонов собирается из нескольких десятков элементов. Вот самые нужные: \d означает цифру, \w букву, цифру или знак подчёркивания, \s пробельный символ, точка любой символ, кроме перевода строки. Квадратные скобки задают набор: [abc] одна из трёх букв, [^abc] любой символ, кроме них, [0-9] диапазон. Якорь ^ привязывает совпадение к началу строки, $ к концу, \b к границе слова.
Количество повторов управляется квантификаторами. Звёздочка означает ноль и более раз, плюс один и более, вопросительный знак ноль или один раз, а фигурные скобки задают точные границы: \d{4} ровно четыре цифры, \d{1,3} от одной до трёх. Вертикальная черта выражает "или": cat|dog.
Из таких кирпичей строятся проверки. Дата в формате год, месяц, день:
'2026-10-01' -match '^\d{4}-\d{2}-\d{2}$'
А вот время в 24-часовом формате, где простого набора цифр уже недостаточно, потому что "25:00" не бывает:
'12:45' -match '^([01]\d|2[0-3]):[0-5]\d$'
'25:00' -match '^([01]\d|2[0-3]):[0-5]\d$'
Первая строка вернёт True, вторая False. Часы описаны двумя вариантами: либо начинаются с нуля или единицы, либо это двадцать с цифрой от нуля до трёх. Минуты не могут начинаться с цифры больше пяти.
Специальные знаки, которые нужно найти буквально (точку, плюс, скобки, знак доллара), экранируются обратной косой чертой. Если строка приходит извне, а вручную экранировать её долго, помогает метод класса [regex]:
[regex]::Escape('a.b*c')
Он вернёт a\.b\*c, готовый для использования в шаблоне.
Отдельного разговора заслуживает жадность. По умолчанию квантификаторы захватывают максимум возможного. Проверка на строке с двумя тегами показывает разницу:
$html = '<b>one</b><b>two</b>'
$html -match '<b>.+</b>' # $Matches[0] содержит всю строку целиком
$html -match '<b>.+?</b>' # $Matches[0] содержит только <b>one</b>
Знак вопроса после плюса переводит квантификатор в ленивый режим: он берёт минимум, при котором шаблон ещё срабатывает. Привычка добавлять его там, где нужна ближайшая закрывающая граница, спасает от самой частой ошибки новичков, когда шаблон "съедает" полстроки.
Группы захвата, именованные группы и автоматическая переменная $Matches
Круглые скобки в шаблоне не только группируют элементы, но и запоминают найденное. После успешной проверки оператором -match результаты лежат в автоматической переменной $Matches, это хеш-таблица. Элемент с номером ноль хранит совпадение целиком, остальные номера соответствуют группам слева направо:
if ('Иванов Пётр' -match '(\w+) (\w+)') {
$Matches[1] # Иванов
$Matches[2] # Пётр
}
Буква \w в .NET понимает кириллицу, так что русские слова находятся без дополнительных усилий.
Номера хороши для коротких шаблонов, а в длинных быстро запутывают. Тогда группам даются имена: внутри скобок в самом начале пишется ?<имя>. Типичная задача - разобрать строку журнала на части:
$line = '2026-10-01 14:35:07 ERROR Disk C: is full'
$pattern = '^(?<date>\S+) (?<time>\S+) (?<level>[A-Z]+) (?<text>.+)$'
if ($line -match $pattern) {
$Matches.level # ERROR
$Matches.text # Disk C: is full
}
Читается такой код почти как обычное описание: дата, время, уровень, текст. Именно так выражения становятся поддерживаемыми.
Нужно знать два ограничения переменной $Matches. Первое: оператор -match запоминает только первое совпадение в строке. Если в тексте пять чисел, а шаблон \d+, в $Matches окажется лишь первое. Второе: переменная заполняется, только когда слева стоит одна строка. Если слева массив, оператор ведёт себя как фильтр: возвращает те элементы массива, которые подошли под шаблон, а $Matches остаётся нетронутой. Для поиска всех совпадений в одном тексте служит метод класса:
[regex]::Matches('a1b22c333', '\d+') | ForEach-Object Value
Он выдаст три значения: 1, 22 и 333.
Поиск по файлам и логам командлетом Select-String, ключи AllMatches и CaseSensitive
Когда данные лежат в файлах, основным инструментом становится Select-String. Его шаблон по умолчанию тоже регулярное выражение, а результатом выступают объекты, у которых есть имя файла, номер строки, сама строка и коллекция Matches:
Select-String -Path 'C:\Logs\*.log' -Pattern 'ERROR|FATAL'
Ключ -AllMatches возвращает не только первое, но и все совпадения в каждой строке. Ключ -CaseSensitive включает различение регистра, ключ -NotMatch инвертирует поиск, а ключ -SimpleMatch отключает регулярные выражения и ищет текст буквально. Это пригождается, когда в искомой строке много специальных знаков. Ключ -Context показывает соседние строки: значение 2,3 выдаёт две строки до совпадения и три после, и лог читается почти как в редакторе.
Классический пример - достать из журнала все адреса и посчитать, какие встречаются чаще:
Select-String -Path .\access.log -Pattern '\b(?:\d{1,3}\.){3}\d{1,3}\b' -AllMatches |
ForEach-Object { $_.Matches.Value } |
Group-Object |
Sort-Object Count -Descending |
Select-Object -First 5 Count, Name
Шаблон устроен так: границы слова по краям, затем три раза повторяющаяся связка из одной-трёх цифр и точки, и в конце ещё одна группа цифр. Конструкция (?:...) - группа без запоминания, она нужна только для повторения. Дальше результаты собираются в список, группируются, сортируются по убыванию и обрезаются до пяти строк.
Но у шаблона есть слабость. Он принимает и "999.999.999.999", потому что проверяет лишь количество цифр. Для строгой версии каждый октет описывается через допустимые диапазоны:
$octet = '(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)'
$ip = "\b(?:$octet\.){3}$octet\b"
Первое выражение описывает числа от 0 до 255: либо 250-255, либо 200-249, либо 100-199, либо от нуля до девяноста девяти. Здесь шаблон собран из двух частей в двойных кавычках, потому что нужно подставить переменную. Это одно из немногих мест, где двойные кавычки уместны.
Для просмотра вложенных папок Select-String удобно соединять с Get-ChildItem: Get-ChildItem -Recurse -Filter *.log | Select-String -Pattern 'timeout'.
Замена текста оператором replace со ссылками на группы и вычисляемыми подстановками
Оператор -replace принимает два значения: шаблон и строку замены. В строке замены можно ссылаться на группы: по номеру через знак доллара или по имени через конструкцию с фигурными скобками. Из-за знака доллара строку замены нужно брать в одинарные кавычки, иначе PowerShell попытается подставить переменную с таким именем, а получит пустое место.
Перестановка частей даты:
'2026-10-01' -replace '(\d{4})-(\d{2})-(\d{2})', '$3.$2.$1'
Результат: 01.10.2026. Тот же шаблон с именами читается намного лучше:
'2026-10-01' -replace '(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})', '${day}.${month}.${year}'
Если нужно просто убрать лишнее, второй параметр опускается. Схлопывание подряд идущих пробелов в один:
'a b c' -replace '\s+', ' '
Результат: a b c. А если в замене нужен сам знак доллара, он записывается двойным: $$.
Интереснее случаи, где замена зависит от соседних символов. Маскирование всех цифр, кроме последних четырёх, решается просмотром вперёд:
'1234567890123456' -replace '\d(?=\d{4})', '*'
Шаблон означает "цифра, за которой следуют ещё не менее четырёх цифр". Первые двенадцать цифр удовлетворяют условию и превращаются в звёздочки, а последние четыре остаются. Итог: ************3456.
Если замену нужно вычислять, на помощь приходит метод класса [regex], который принимает блок кода вместо строки:
[regex]::Replace('a1b22', '\d+', { param($m) [int]$m.Value * 2 })
Каждое найденное число передаётся блоку, а то, что он вернёт, подставляется на место. Результат: a2b44.
При правке файлов действует простое правило: сначала копия. Содержимое читается целиком ключом -Raw, обрабатывается и записывается обратно:
Copy-Item .\report.txt .\report.bak
(Get-Content .\report.txt -Raw -Encoding UTF8) -replace 'colour', 'color' | Set-Content .\report.txt -Encoding UTF8
Кодировку лучше указывать явно, иначе кириллица при записи рискует превратиться в вопросительные знаки.
Продвинутые условия, просмотр вперёд и назад, обратные ссылки, модификаторы и защита от зависания
Просмотр вперёд и назад проверяет, что стоит рядом, но не включает это в совпадение. Позитивный просмотр вперёд записывается как (?=...), негативный как (?!...). Просмотр назад: (?<=...) и (?<!...). Чтобы достать число после слова "Цена: ", не прихватив само слово:
'Цена: 1500 руб, скидка: 200 руб' -match '(?<=Цена: )\d+'
$Matches[0] # 1500
Из просмотров вперёд собираются многоусловные проверки. Пароль не короче восьми символов, с хотя бы одной цифрой и хотя бы одной заглавной буквой:
'Secret123' -cmatch '^(?=.*\d)(?=.*[A-Z]).{8,}$'
Каждое условие в начале строки проверяется независимо, а потом .{8,} проверяет длину. Здесь важна буква c в названии оператора. С обычным -match строка "secret123" тоже вернёт True, потому что класс [A-Z] без учёта регистра сдружится со строчными буквами. Это та самая ловушка, о которой шла речь в начале.
Обратные ссылки позволяют шаблону ссылаться на уже найденное. Поиск повторяющихся подряд слов:
'это это пример' -match '\b(\w+)\s+\1\b'
$Matches[1] # это
Цифра один с обратной косой чертой означает "ещё раз то же самое, что нашла первая группа".
Встроенные модификаторы включаются в начале шаблона. Выражение (?i) отключает различение регистра внутри -cmatch, (?s) заставляет точку совпадать и с переводом строки, а (?m) заставляет якоря действовать на каждую строку, а не только на всю строку целиком. Последний особенно нужен при чтении файла ключом -Raw, когда весь текст приходит одной строкой. Удаление пробелов и табуляций в начале и в конце каждой строки:
(Get-Content .\notes.txt -Raw) -replace '(?m)^[ \t]+|[ \t]+$', ''
Класс [ \t] взят специально: символ \s включает перевод строки, и пустые строки были бы испорчены.
Последнее предупреждение касается зависаний. Шаблоны с вложенными повторами вроде (a+)+$ на длинной строке, не подходящей под шаблон, заставляют движок перебирать огромное число вариантов, и команда подвисает на минуты. Лечится это точностью: чем конкретнее описаны символы, тем меньше вариантов для перебора. Для критичных мест создаётся объект с ограничением времени:
$re = [regex]::new('(a+)+$', 'None', [TimeSpan]::FromSeconds(2))
Если проверка не уложится в две секунды, будет выброшено исключение вместо бесконечного ожидания.
Разбиение строк, переключатель Regex и повседневные сценарии с готовыми шаблонами
Оператор -split режет строку по шаблону, и разделитель может быть любой сложности. Выражение 'a1b22c333d' -split '\d+' вернёт буквы a, b, c и d. Если разделитель обернуть в скобки, он сохранится в результате: 'a1b2' -split '(\d)' вернёт элементы a, 1, b, 2 и пустую строку в конце. Если разбирать строки с запятыми внутри кавычек, тянуться к сложному шаблону не стоит: для таблиц в формате CSV есть командлет ConvertFrom-Csv, который справляется надёжнее любого выражения.
Оператор switch с ключом Regex сопоставляет значение с серией шаблонов, и в каждой ветке доступна переменная $Matches:
switch -Regex ($line) {
'^ERROR (?<code>\d+)' { "Ошибка с кодом $($Matches.code)" }
'^WARN' { 'Предупреждение' }
default { 'Остальное' }
}
Стоит помнить: если подходят несколько веток, сработают все, а не только первая. Чтобы остановиться после первого совпадения, в ветке пишется break.
Пакетное переименование файлов с помощью регулярного выражения укладывается в одну строку. Файлы вида IMG_20261001_153000.jpg получат название с аккуратно разделённой датой:
Get-ChildItem *.jpg | Rename-Item -NewName { $_.Name -replace '^IMG_(\d{4})(\d{2})(\d{2})_', '$1-$2-$3_' } -WhatIf
Ключ -WhatIf не выполняет переименование, а лишь показывает, что произошло бы. Когда результат устраивает, ключ убирается.
Простые проверки ввода тоже поддаются шаблонам, но у них есть предел. Выражение ^[^@\s]+@[^@\s]+\.[^@\s]+$ отсеет явный мусор вроде адреса без знака @, однако полностью проверить электронный адрес регулярным выражением невозможно. Для надёжности нужно отправить письмо, а шаблон годится только как первый фильтр.
Рабочий порядок отладки нового выражения обычно укладывается в шесть шагов:
- записать словами, что именно должно совпасть и что совпадать не должно;
- собрать пять-десять тестовых строк, включая заведомо неподходящие;
- начать с простого шаблона и проверять его оператором -match на каждой строке;
- усложнять шаблон по одному элементу, пересматривая результат после каждого шага;
- проверить регистр и при необходимости заменить -match на -cmatch;
- перед массовой заменой в файлах сделать копию и прогнать команду на тестовых данных.
Регулярное выражение - это маленький язык внутри большого, и учить его проще на живых строках, чем по справочнику. Двух-трёх рабочих шаблонов, записанных в отдельный файл, хватает, чтобы разгрузить большую часть рутины. Главное - не требовать от шаблона невозможного: там, где структура данных ясна и вложена, надёжнее разбирать её специальными командлетами, а регулярные выражения оставлять для текста, у которого формы нет, а только повторяющиеся приметы.