Лог-файл на сорок тысяч строк, а нужны только адреса, с которых приходили ошибки. Папка с фотографиями, названия которых надо привести к единому виду. Выгрузка из учётной системы, где даты записаны то через точку, то через дефис. Обычный поиск подстроки с такими задачами не справляется: он умеет искать только то, что известно дословно, а здесь известна лишь форма искомого. Тут и пригождаются регулярные выражения: короткие шаблоны, которые описывают не конкретное слово, а правило, по которому оно устроено. В 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]+$ отсеет явный мусор вроде адреса без знака @, однако полностью проверить электронный адрес регулярным выражением невозможно. Для надёжности нужно отправить письмо, а шаблон годится только как первый фильтр.

Рабочий порядок отладки нового выражения обычно укладывается в шесть шагов:

  1. записать словами, что именно должно совпасть и что совпадать не должно;
  2. собрать пять-десять тестовых строк, включая заведомо неподходящие;
  3. начать с простого шаблона и проверять его оператором -match на каждой строке;
  4. усложнять шаблон по одному элементу, пересматривая результат после каждого шага;
  5. проверить регистр и при необходимости заменить -match на -cmatch;
  6. перед массовой заменой в файлах сделать копию и прогнать команду на тестовых данных.

Регулярное выражение - это маленький язык внутри большого, и учить его проще на живых строках, чем по справочнику. Двух-трёх рабочих шаблонов, записанных в отдельный файл, хватает, чтобы разгрузить большую часть рутины. Главное - не требовать от шаблона невозможного: там, где структура данных ясна и вложена, надёжнее разбирать её специальными командлетами, а регулярные выражения оставлять для текста, у которого формы нет, а только повторяющиеся приметы.