Мне достаточно часто задают вопросы связанные не столько с самим PowerShell, сколько с применением в нем регулярных выражений. Это и понятно — регулярные выражения (или если сокращенно “регэкспы” (regexp, regular expressions)) обладают огромной мощью, и способны сильно упростить жизнь системного администратора или программиста. Однако в мире системного администрирования Windows они мало известны и непопулярны — в cmd.exe практически единственная возможность их применения это утилита findstr.exe, которая обладает очень маленьким функционалом и использует жутко урезанный диалект регулярных выражений.
В VBScript функционал регулярных выражений тоже хорошо запрятан, и практически не используется. А вот в PowerShell, авторы языка позаботились о том чтобы регулярные выражения были легко доступны, удобны в использовании и максимально функциональны. Тем более что с последним пунктом всё оказалось достаточно просто — PowerShell использует реализацию регулярных выражений .NET, а она является одной из самых функциональных и производительных, и даже способна потягаться даже с признанным лидером в этой области — perl’ом
Итак, перейдем к делу. Что же такое регулярные выражения? Я не помню правильных и сухих определений из умных книжек, да и незачем, кому интересно — прочитает их сам Регулярные выражения это специальный мини-язык служащий для разбора (parsing) текстовых данных. С его помощью можно разделять строки на компоненты, выбирать нужные части строк для дальнейшей обработки, производить замены, и всё это с огромной гибкостью и точностью.
Впрочем… знакомство с регулярными выражениями лучше начать не с них, а с более простой технологии которая служит подобным целям, с которой знакомы все Windows администраторы – с подстановочных символов. Наверняка вы не раз выполняли команду dir, и указывали ей в качестве аргумента маску файла, например *.exe. В данном случае звёздочка означает “любое количество любых символов”. Аналогично можно использовать и знак вопроса, он будет означать “один любой символ”, то есть dir ??.exe
выведет все файлы с расширением .exe и именем из двух символов. В PowerShell’овской реализации подстановочных символов можно применять и еще одну конструкцию — группы символов. Так например [a-f]
будет означать “один любой символ от a
до f
(a,b,c,d,e,f) ”, а [smw]
любую из трех букв (s
, m
или w
). Таким образом команда get-childitem [smw]??.exe
выведет файлы с расширением .exe
, у которых имя состоит из трех букв, и первая буква либо s
, либо m
, либо w
. Неплохо, неправда ли? Так вот, по сравнению с возможностями регулярных выражения — это ерунда Но начнём с малого
Для начала изучения мы будем использовать оператор PowerShell -match
, который позволяет сравнивать текст слева от него, с регулярным выражением справа. В случае если текст подпадает под регулярное выражение, оператор выдаёт True
, иначе — False
.
1 2 |
PS C:\> "PowerShell" -match "Power" True |
Вы наверное обратили внимание, что при сравнении с регулярным выражением ищется лишь вхождение строки, полное совпадение текста необязательно (разумеется это можно изменить, но об этом позже). То есть достаточно чтобы регулярное выражение встречалось в тексте.
1 2 3 4 |
PS C:\> "Shell" -match "Power" False PS C:\> "PowerShell" -match "rsh" True |
Еще одна тонкость: оператор -match
по умолчанию не чувствителен к регистру символов (как и другие текстовые операторы в PowerShell), если же вам нужна чувствительность к регистру, используйте -cmatch
:
1 2 |
PS C:\> "PowerShell" -cmatch "rsh" False |
В регулярных выражениях можно использовать и группы символов:
1 2 3 4 5 6 7 |
PS C:\> Get-Process | where {$_.name -match "sy[ns]"} Handles NPM(K) PM(K) WS(K) VM(M) CPU(s) Id ProcessName ------- ------ ----- ----- ----- ------ -- ----------- 165 11 2524 8140 79 0,30 5228 mobsync 114 10 3436 3028 83 50,14 3404 SynTPEnh 149 11 2356 492 93 0,06 1592 SynTPStart 810 0 116 380 6 4 System |
И диапазоны в этих группах:
1 2 3 |
PS C:\> "яблоко","апельсин","груша","абрикос" -match "а[а-п]" апельсин абрикос |
Кстати тут я в левой части оператора -match
поместил массив строк, и он соответственно вывел лишь те строки, которые подошли под регулярное выражение.
Разумеется перечисления символов можно комбинировать, например группа [агдэ-я]
будет означать “А или Г или Д или любой символ от Э до Я включительно”. Но гораздо интереснее использовать диапазоны для определения целых классов символов. Например [а-я]
будет означать любую букву русского алфавита, а [a-z]
английского. Аналогично можно поступать с цифрами — следующая команда выведет все процессы в именах которых встречаются цифры:
1 2 3 4 5 6 |
PS C:\> Get-Process | where {$_.name -match "[0-9]"} Handles NPM(K) PM(K) WS(K) VM(M) CPU(s) Id ProcessName ------- ------ ----- ----- ----- ------ -- ----------- 93 10 1788 2336 70 1,25 548 FlashUtil10c 158 12 6500 1024 96 0,14 3336 smax4pnp 30 6 764 160 41 0,02 3920 TabTip32 |
Так как эта группа используется достаточно часто, для неё была выделена специальная последовательность — \d
(от слова digit). По смыслу она полностью идентична [0-9]
, но гораздо короче
1 2 3 4 5 6 |
PS C:\> Get-Process | where {$_.name -match "\d"} Handles NPM(K) PM(K) WS(K) VM(M) CPU(s) Id ProcessName ------- ------ ----- ----- ----- ------ -- ----------- 93 10 1788 2336 70 1,25 548 FlashUtil10c 158 12 6500 1024 96 0,14 3336 smax4pnp 30 6 764 160 41 0,02 3920 TabTip32 |
Так же последовательность была выделена для группы “любые буквы любого алфавита, любые цифры, или символ подчеркивания” эта группа обозначается как \w
(от word) она примерно эквивалентна конструкции [a-zа-я_0-9]
(в \w
еще входят символы других алфавитов которые используются для написания слов).
Еще вам наверняка встретится другая популярная группа: \s
— “пробел, или другой пробельный символ” (например символ табуляции). Сокращение от слова space. В большинстве случаев вы можете обозначать пробел просто как пробел но эта конструкция добавляет читабельности регулярному выражению.
Не менее популярной группой можно назвать символ .
(точка). Точка в регулярных выражениях аналогична по смыслу знаку вопроса в подстановочных символах, то есть обозначает один любой символ.
Все вышеперечисленные конструкции можно использовать как отдельно, так и в составе групп, например [\s\d]
будет соответствовать любой цифре или пробелу. Если вы хотите указать внутри группы символ -
(тире/минус) то надо либо экранировать его символом \
(обратный слеш), либо поставить его в начале группы, чтобы он не был случайно истолкован как диапазон:
1 2 3 |
PS C:\> "?????","Word","123","-" -match "[-\d]" 123 - |
Отрицательные группы и якоря
Вы уже знаете как указать регулярному выражению какие символы и/или их последовательности должны быть в строке для совпадения. А что если вам нужно указать не те символы которые должны присутствовать, а те которых, наоборот, не должно быть? То есть если вам нужно вывести лишь согласные буквы, вы можете конечно их перечислить их все, а можете использовать и отрицательную группу с гласными, например:
1 2 3 4 5 6 7 |
PS C:\> "a","b","c","d","e","f","g","h" -match "[^aoueyi]" b c d f g h |
«Крышка» в качестве первого символа группы символов означает именно отрицание. То есть на месте группы может присутствовать любой символ кроме перечисленных в ней. Для того чтобы включить отрицание в символьных группах (\d
, \w
, \s
), не обязательно заключать их в квадратные скобки, достаточно… перевести их в верхний регистр Например \D
будет означать «что угодно, кроме цифр», а \S
«всё кроме пробелов»
1 2 3 4 5 6 7 8 |
PS C:\> "a","b","1","c","45" -match "\D" a b c PS C:\> "a","-","*","c","&" -match "\W" - * & |
Уже гораздо могущественнее обычных символов подстановки, не так ли? А ведь мы только начали изучать основы! Символьные группы позволяют нам указать лишь содержимое одной позиции, один символ находящийся в неопределенном месте строки. А что если нам надо например выбрать все слова которые начинаются с буквы w
? если просто поместить эту букву в регулярное выражение, то оно совпадёт для всех строк где w
вообще встречается, и не важно — в начале, в середине или в конце строки. В таких случаях на помощь приходят «якоря». Они позволяют производить сравнение начиная с определенной позиции в строке. ^<code> (крышка) является якорем начала строки, а <code>$
(знак доллара) — обозначает конец строки. Не запутайтесь — ^
как символ отрицания используется лишь в начале группы символов, а вне группы — этот символ является уже якорем Да, да, авторам регулярных выражений явно не хватало специальных символов, и они по возможности, использовали их более чем в одном месте (о втором значении $
поговорим позже) Впрочем лучше посмотреть на примере:
1 2 3 4 5 6 7 8 9 10 |
PS C:\> Get-Process | where {$_.name -match "^w"} Handles NPM(K) PM(K) WS(K) VM(M) CPU(s) Id ProcessName ------- ------ ----- ----- ----- ------ -- ----------- 80 10 1460 156 47 0,11 452 wininit 114 9 2732 1428 55 0,56 3508 winlogon 162 11 3660 1652 44 0,14 3620 wisptis 225 20 5076 4308 95 31,33 3800 wisptis 469 28 9572 11904 101 3,23 1844 wlcrasvc 706 54 52452 43008 632 9,64 1072 wmdc 105 10 2308 1428 76 0,08 4056 wuauclt |
Эта команда вывела процессы у которых сразу после начала имени (^
) следует символ w
. Иначе говоря имя начинается на w<code>. Давайте для усложнения примера, и для упрощения понимания, добавим сюда "крышку" в значении отрицательной группы:
1 2 3 4 5 6 7 |
PS C:\> Get-Process | where {$_.name -match "^w[^l-z]"} Handles NPM(K) PM(K) WS(K) VM(M) CPU(s) Id ProcessName ------- ------ ----- ----- ----- ------ -- ----------- 80 10 1460 156 47 0,11 452 wininit 114 9 2732 1428 55 0,56 3508 winlogon 162 11 3660 1652 44 0,14 3620 wisptis 225 20 5076 4308 95 31,50 3800 wisptis |
Теперь команда вывела нам процессы у которых имя начинается с символа w
, а следующий символ является чем угодно, только не из диапазона l-z
.
Обратите внимание, примеры уже начинают походить на модзибакэ, а вы их уже можете понимать
Ну и для закрепления, опробуем второй якорь — конец строки:
1 2 3 4 |
PS C:\> "Яблоки","Груши","Дыня","Енот","Апельсины","Персик" -match "[ыи]$" Яблоки Груши Апельсины |
Это выражение вывело нам все слова в которых последняя буква И
или Ы
.
Если вы можете точно описать содержимое всей строки, то вы можете использовать и оба якоря одновременно:
1 2 3 4 |
PS C:\> "abc","adc","aef","bca","aeb","abec","abce" -match "^a.[cb]$" abc adc aeb |
Это регулярное выражение выводит все строки которые начинаются с буквы А, за которой следует один любой символ (точка), затем символ C или B и затем конец строки.
Квантификаторы
Что ж, мы уже немало узнали о символьных группах и якорях. Это неплохое начало, но обычно регулярные выражения гораздо сложнее, и записывать их по одному символу было бы тяжеловато. Что если вам нужно отобрать строки состоящие из четырех символов, каждый из которых может быть буквой от A до F или цифрой? Регулярное выражение могло бы выглядеть примерно так:
1 2 3 4 |
PS C:\> "af12","1FE0","1fz1","B009","C1212" -match "^[a-f\d][a-f\d][a-f\d][a-f\d]$" af12 1FE0 B009 |
Не слишком то лаконично, не правда ли? К счастью всю эту конструкцию можно значительно сократить. Для этого в регулярных выражениях существует специальная концепция — «количественные модификаторы» (квантификаторы). Эти модификаторы приписываются к любой группе справа, и оговаривают количество вхождений этой группы. Например количественный модификатор {4}
означает 4 вхождения. Посмотрим на нашем примере:
1 2 3 4 |
PS C:\> "af12","1FE0","1fz1","B009","C1212" -match "^[a-f\d]{4}$" af12 1FE0 B009 |
Данное регулярное выражение полностью эквивалентно предыдущему — “4 раза по [a-f\d]”. Но этот количественный модификатор не обязательно жестко оговаривает количество повторений. Можно например задать количество как «от 4 до 6». Делается это указанием внутри фигурных скобок двух чисел через запятую — минимума и максимума:
1 2 3 4 5 6 7 |
PS C:\> "af12","1FE0","1fA999","B009","C1212","A00062","FF00FF9" -match "^[a-f\d]{4,6}$" af12 1FE0 1fA999 B009 C1212 A00062 |
Если вам безразлично максимальное количество вхождений, например вы хотите указать «3 вхождения или больше», то максимум можно просто опустить (оставив запятую на месте), например «строка состоящая из 3х или более цифр»:
1 2 3 4 |
PS C:\> "1","12","123","1234","12345" -match "^\d{3,}$" 123 1234 12345 |
Минимальное значение опустить не получится, впрочем можно просто указать единицу:
1 2 3 4 |
PS C:\> "1","12","123","1234","12345" -match "^\d{1,3}$" 1 12 123 |
Как и в случае с символьными группами, для особенно популярных значений количественных модификаторов, есть короткие псевдонимы:
+
(плюс), эквивалентен {1,}
то есть, «одно или больше вхождений»
*
(звездочка), то же самое что и {0,}
или если на русском языке — «любое количество вхождений, в том числе и 0»
?
(вопросительный знак), равен {0,1}
— «либо одно вхождение, либо полное отсутствие вхождений».
Помните, в регулярных выражениях, количественные модификаторы сами по себе использоваться не могут. Для них обязателен символ или символьная группа, которые и будут определять их смысл. Вот несколько примеров:
.+
Один или более любых символов. Аналог ?*
в простых подстановках (как в cmd.exe). Следующее выражение выбирает процессы у которых имя «начинается с буквы S, затем следует 1 или более любых символов, затем снова буква S и сразу после неё конец строки». Иначе говоря «имена которые начинаются и заканчиваются на S»:
1 2 3 4 5 |
PS C:\> Get-Process | where {$_.name -match "^s.+s$"} Handles NPM(K) PM(K) WS(K) VM(M) CPU(s) Id ProcessName ------- ------ ----- ----- ----- ------ -- ----------- 257 14 6540 5220 53 5,97 508 services 30 2 424 128 5 0,08 280 smss |
\S*
Любое количество символов не являющихся пробелами. Помните, подобное выражение может совпасть и с “”(с пустой строкой), ведь под любым количеством подразумевается и ноль, то есть 0 вхождений — тоже результат.
1 2 3 4 |
PS C:\> "abc", "cab", "a c","ac","abdec" -match "a\S*c" abc ac abdec |
Заметьте, строка «ac» тоже совпала, хотя между буквами A и C вообще не было символов. Если заменить *
на +
то будет иначе:
1 2 3 |
PS C:\> "abc", "cab", "a c","ac","abdec" -match "a\S+c" abc abdec |
бобры?
(Это был не вопрос, а регулярное выражение ) Последовательность «бобр», после которой может идти символ «ы», а может и отсутствовать:
1 2 3 |
PS C:\> "бобр","бобры","бобрята" -match "^бобры?$" бобр бобры |
Группы захвата
Теперь, когда мы можем с помощью регулярных выражений описывать и проверять строки по достаточно сложным правилам, пора познакомится с другой не менее важной концепцией регулярных выражений — «группами захвата» (capture groups). Как следует из названия, группы можно использовать для … сюрприз … группировки К группам захвата, как и к символам и символьным группам можно применять количественные модификаторы. Например следующее выражение означает «Первая буква в строке — S, затем одна или больше групп состоящих из “знака -
(минус) и любого количества цифр за ним” до конца строки»:
1 2 3 4 5 6 |
PS C:\> "S-1-5-21-1964843605-2840444903-4043112481" -match "^S(-\d+)+$" True Или: PS C:\> "Ноут","Ноутбук","Лептоп" -match "Ноут(бук)?" Ноут Ноутбук |
Эти примеры показывают как можно использовать группы захвата для группировки, но это вовсе не главное их качество Гораздо важнее то, что часть строки подпавшая под подвыражение находящееся внутри такой группы, помещается в специальную переменную — $matches
. $Matches
это массив, и в нем может находится содержимое нескольких групп. Причем под индексом 0 туда помещается вся совпавшая строка, а лишь начиная с единицы идет содержимое групп захвата. Опять же, лучше посмотреть на примере:
1 2 3 4 5 6 7 8 |
PS C:\> "At 17:04 Firewall service was stopped." -match "(\d\d:\d\d) (\S+)" True PS C:\> $matches Name Value ---- ----- 2 Firewall 1 17:04 0 17:04 Firewall |
Под индексом 0 находится вся часть строки подпавшая под регулярное выражение, под 1 находится содержимое первых скобок, и под 2 соответственно содержимое вторых скобок. К содержимому $matches
можно обращаться как к элементам любого другого массива в PowerShell:
1 2 3 4 |
PS C:\> $matches[1] 17:04 PS C:\> $matches[2] Firewall |
Если в строке присутствует много групп захвата, то бывает полезно дать им имена, это сильно облегчает дальнейшую работу с полученными данными:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
PS C:\> "At 17:04 Firewall service was stopped." -match "(?<Время>\d\d:\d\d) (?<Служба>\S+)" True PS C:\> $matches Name Value ---- ----- Время 17:04 Служба Firewall 0 17:04 Firewall PS C:\> $matches.Время 17:04 PS C:\> $matches["Служба"] Firewall |
Регулярное выражение конечно усложнилось, но зато работать с результатами гораздо приятнее. Синтаксис именования следующий:
(?<Название Группы>подвыражение)
Не перепутайте порядок, сначала следует знак вопроса. Как вы помните, количественные модификаторы, в том числе ?
могут применяться только после группы, и следовательно в начале подвыражения — бессмысленны. Поэтому в группах, знак вопроса следующий сразу за открывающей скобкой, означает особый тип группы, в нашем примере — именованную.
Другой тип группы который часто используется – незахватывающая группа. Она может пригодиться в тех случаях когда вам не нужно захватывать содержимое группы, а надо применить её только для группировки. Например в вышеприведённом примере с SID, такая группа была бы более уместна:
1 2 3 4 5 6 |
PS C:\> "S-1-5-21-1964843605-2840444903-4043112481" -match "^S(?:-\d+)+$" True PS C:\> $matches Name Value ---- ----- 0 S-1-5-21-1964843605-2840444903-4043112481 |
Как вы могли заметить, синтаксис такой группы: (?:подвыражение)
. Группы можно и вкладывать одну в другую:
1 2 3 4 5 6 7 |
PS C:\> "MAC address is '00-19-D2-73-77-6F'." -match "is '([a-f\d]{2}(?:-[a-f\d]{2}){5})'" True PS C:\> $matches Name Value ---- ----- 1 00-19-D2-73-77-6F 0 is '00-19-D2-73-77-6F' |
Попробуйте разобрать данное выражение самостоятельно
Операторы -replace и -split
Мы уже научились не просто проверять строки на соответствие правилам, но и извлекать из них самое интересное содержимое. Но регулярные выражения позволяют не только анализировать строки, но и изменять их. Так что пора познакомиться с еще одним оператором PowerShell, который использует в своей работе регулярные выражения — -replace
. Слева от этого оператора указывается обрабатываемая строка, а справа массив состоящий из двух элементов: первый — регулярное выражение определяющее что заменяем, и второй элемент — строка на которую заменяем.
Давайте посмотрим пример:
1 2 |
PS C:\> "PowerShell" -replace "Power","Super" SuperShell |
Так как мы уже знаем некоторые возможности регулярных выражений, мы можем использовать оператор более интересным способом:
1 2 |
PS C:\> "PowerShell" -replace "S.+$", "GUI" PowerGUI |
Оператор -replace
замещает всю совпавшую часть выражения, то есть то что в $matches
находилось бы под индексом 0. Впрочем в -replace
можно использовать и группы захвата. Вместо помещения в $matches
, захваченные данные подставляются во второй элемент массива (то на что заменяем) с помощью символа доллара, и индекса группы, например $0
— всё совпавшее выражение, $1
— содержимое первой группы, $2
— второй, и так далее. Обратите внимание, не только в регулярных выражениях символ $
имеет дополнительные значения (якорь конца строки), но и в самом PowerShell. Так что если просто вставить $1
в строку окруженную двойными кавычками, то PowerShell попытается подставить туда значение переменной $1
, а так как такой скорее всего не существует, то он просто заменит эту последовательность на пустоту. Чтобы этого не произошло, а $1
был бы обработан оператором -replace
, следует использовать одинарные кавычки (внутри которых PowerShell не раскрывает переменные):
1 2 3 4 |
PS C:\> "Латинские буквы, например ABCDE, надо подчеркнуть." -replace "[a-z]+", '_$0_' Латинские буквы, например _ABCDE_, надо подчеркнуть. PS C:\> "Жирный <b>текст</b> выделяется <b>соответствующим</b> тегом." -replace "<([^<]+)>",'[$1]' Жирный [b]текст[/b] выделяется [b]соответствующим[/b] тегом. |
У оператора -replace
, как и у других операторов PowerShell для работы со строками, есть и версия отличающая верхний и нижний регистр символов:
1 2 |
PS C:\> "Все слова начинающиеся с Заглавных Букв надо выделить." -creplace "[А-Я]\S+",'*$0*' *Все* слова начинающиеся с *Заглавных* *Букв* надо выделить. |
Не все об этом знают, но второй элемент массива справа от -replace
необязательный. Если его опустить, то команда будет выполнять замену на ""
(пустоту). Именно поэтому и нет оператора -remove
Например уберём из текста всё кроме цифр:
1 2 |
PS C:\> "+7 911 123-45-67" -replace "\D" 79111234567 |
Ну и раз уж взялись за номера телефонов, отформатируем эту последовательность в нужном нам формате
1 2 |
PS C:\> "79111234567" -replace '^(.)(...)(...)(..)(..)$','+$1 ($2) $3-$4-$5' +7 (911) 123-45-67 |
Другой оператор, похожий на -replace
, это -split
. Он появился только во второй версии PowerShell. В отличии от -replace
, он разделяет строку на части и возвращает массив строк. Справа от него указывается регулярное выражение по которому он будет делить строку. В следующем примере это пробел или знак минуса:
1 2 3 4 5 6 7 8 9 10 11 |
PS C:\> "+7 911 123-45-67" -split "[- ]" +7 911 123 45 67 PS C:\> "Хакерам^не-нужны*пробелы" -split '\W' Хакерам не нужны пробелы |
Еще ему можно указать максимальное количество частей в результате:
1 2 3 4 |
PS C:\> "+7 911 123-45-67" -split "[- ]", 3 +7 911 123-45-67 |
То есть, после достижения указанного количества результатов, дальнейшее деление не происходит.
Жадность
По умолчанию, все количественные модификаторы в регулярных выражениях — жадные. То есть они пытаются захватить как можно больше символов (разумеется пока это позволяют условия). Взять к примеру .+
Это выражение означает 1 или более вхождений, но разве оно остановится на одном вхождении? Нет! Оно будет жрать захватывать символы пока у него будет эта возможность, то есть до ограничителя если он есть, а если его нет — то до конца (или начала) строки. Например:
1 2 |
PS C:\> "Очень вкусная булка." -replace "б\S+" Очень вкусная |
Заметьте, захавал всё, и даже точкой не подавился А что если мы допускаем любое количество повторений группы, но хотим ограничиться минимумом? Тогда нам поможет «нежадная» версия этого количественного модификатора:
1 2 |
PS C:\> "Очень вкусная булка." -replace "б\S+?" Очень вкусная лка. |
Да, просто добавив после этого количественного модификатора вопросительный знак, мы сразу заставили его ограничится минимумом — одним символом. Нежадные версии других количественных модификаторов получаются таким же образом: *? ?? {1,5}?
Разумеется нежадный модификатор может захватить и больше, но только если у него не останется другого выбора:
1 2 |
PS C:\> "Очень вкусная булка." -replace "б\S+?\." Очень вкусная |
В этом выражении оговаривается что \S+? должен захватить минимум, но до следующей точки.
Маскировка служебных символов в регулярных выражениях, в отличии от других строк PowerShell, делается с помощью символа
\
Например
\.
Точка \(
Открывающая скобка \\
Обратный слеш Это традиционно для регулярных выражений, и обеспечивает возможность использовать в PowerShell без изменения выражения из других источников, и наоборот.
И еще один пример полезности нежадных квантификаторов:
1 2 3 4 |
PS C:\> "Теги <123> надо удалить <456>." -replace '<.+>' Теги . PS C:\> "Теги <123> надо удалить <456>." -replace '<.+?>' Теги надо удалить . |
Впрочем… конкретно в этой ситуации можно поступить и иначе:
1 2 |
PS C:\> "Теги <123> надо удалить <456>." -replace '<[^>]+>' Теги надо удалить . |
В тех случаях где это возможно, лучше выбирать второй вариант, он несколько производительнее.
Разумеется подобные «нежадные» модификаторы можно использовать не только в -replace
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
PS C:\> "Число 123." -match '\d+' True PS C:\> $matches Name Value ---- ----- 0 123 PS C:\> "Число 123." -match '\d+?' True PS C:\> $matches Name Value ---- ----- 0 1 |
В случае если в выражении присутствует несколько количественных модификаторов, которые могут захватить одну и ту же часть строки, то приоритет будет у первого. Но первый всегда уступит следующему, если это необходимо для совпадения выражения:
1 2 3 4 5 6 |
PS C:\Windows\system32> if ('123456' -match '^(\d+)(\d+)$') {$matches} Name Value ---- ----- 2 6 1 12345 0 123456 |
Тут первый \d+
захватил максимум цифр, оставив второму лишь минимально необходимое для него – одну. Если же использовать нежадные версии, то первый квантификатор постарается захватить минимум, а уж всё остальное придётся захватывать второму:
1 2 3 4 5 6 |
PS C:\Windows\system32> if ('123456' -match '^(\d+?)(\d+?)$') {$matches} Name Value ---- ----- 2 23456 1 1 0 123456 |
Select-String
Пришло время познакомится с целым командлетом PowerShell, который использует регулярные выражения. Таким командлетом является Select-String
. Он используется для поиска строк совпадающих с регулярным выражением. Строки для отбора можно передать из массива строк, например:
1 2 3 4 5 6 7 8 9 |
PS C:\> $lines = Get-Content C:\Windows\setupact.log PS C:\> $lines | Select-String "error" dispci.dll: DispCIOpenDxgKrnlAndDisableNewReferences: D3DKMTOpenAdapterFromDeviceName failed with error 0xc000007a. [10/24/2009 20:47.16.192] WudfCoInstaller: Final status: error(0) The operation completed successfully. [10/26/2009 14:45.08.912] WudfCoInstaller: Final status: error(0) The operation completed successfully. [10/27/2009 18:24.13.032] WudfCoInstaller: Final status: error(0) The operation completed successfully. [10/27/2009 18:24.14.421] WudfCoInstaller: Final status: error(0) The operation completed successfully. [11/02/2009 11:32.22.880] WudfCoInstaller: Final status: error(0) The operation completed successfully. [11/13/2009 15:16.16.837] WudfCoInstaller: Final status: error(0) The operation completed successfully. |
Еще можно указывать файлы для проверки содержащихся в них строк, просто указав их путь, или маску (с помощью обычных подстановочных знаков). Так в следующем примере я делаю поиск строки error:<code> во всех файлах <code>*.log
в папке c:\Windows:
1 2 3 4 5 6 7 8 9 |
PS C:\> Select-String "error:" C:\Windows\*.log TSSysprep.log:7:sysprep.cpp(314)ERROR: ResetTSPublicPrivateKeys() FAILED: 2 WindowsUpdate.log:2663:2009-10-31 15:11:26:983 896 13fc PT WARNING: PTError: 0x80072ee2 WindowsUpdate.log:3926:2009-11-01 19:09:14:748 896 548 PT WARNING: PTError: 0x8024402c WindowsUpdate.log:3930:2009-11-01 19:09:14:749 896 548 PT WARNING: PTError: 0x8024402c WindowsUpdate.log:3941:2009-11-01 19:09:28:778 896 548 PT WARNING: PTError: 0x8024402c WindowsUpdate.log:3945:2009-11-01 19:09:28:778 896 548 PT WARNING: PTError: 0x8024402c WindowsUpdate.log:3956:2009-11-01 19:09:42:808 896 548 PT WARNING: PTError: 0x8024402c WindowsUpdate.log:3960:2009-11-01 19:09:42:808 896 548 PT WARNING: PTError: 0x8024402c |
Select-String
отличается от конструкции where {$_ -match "error:"}
тем что выводит не просто совпадения строк, а полноценные объекты содержащие дополнительную информацию. В данном случае были выведены не только совпавшие строки, но и файлы в которых они были найдены (TSSysprep.log и WindowsUpdate.log), и номера строк. Полный список доступных свойств можно посмотреть следующей командой:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
PS C:\> Select-String "error:" C:\Windows\*.log | >> Get-Member -MemberType property TypeName: Microsoft.PowerShell.Commands.MatchInfo Name MemberType Definition ---- ---------- ---------- Context Property Microsoft.PowerShell.Commands.MatchInfoContext Context {get;set;} Filename Property System.String Filename {get;} IgnoreCase Property System.Boolean IgnoreCase {get;set;} Line Property System.String Line {get;set;} LineNumber Property System.Int32 LineNumber {get;set;} Matches Property System.Text.RegularExpressions.Match[] Matches {get;set;} Path Property System.String Path {get;set;} Pattern Property System.String Pattern {get;set;} |
Давайте например выведем только имена файлов и номера совпавших строк:
1 2 3 4 5 6 7 8 9 10 11 12 |
PS C:\> Select-String "error:" C:\Windows\*.log | >> Format-Table Path, LineNumber -AutoSize Path LineNumber ---- ---------- C:\Windows\TSSysprep.log 7 C:\Windows\WindowsUpdate.log 2663 C:\Windows\WindowsUpdate.log 3926 C:\Windows\WindowsUpdate.log 3930 C:\Windows\WindowsUpdate.log 3941 C:\Windows\WindowsUpdate.log 3945 C:\Windows\WindowsUpdate.log 3956 C:\Windows\WindowsUpdate.log 3960 |
Если весь этот «объектный мусор» вам не нужен, вы можете получить только строки, следующей командой:
1 2 3 4 5 6 7 8 9 10 |
PS C:\> Select-String "error:" C:\Windows\*.log | >> Select-Object -ExpandProperty line sysprep.cpp(314)ERROR: ResetTSPublicPrivateKeys() FAILED: 2 2009-10-31 15:11:26:983 896 13fc PT WARNING: PTError: 0x80072ee2 2009-11-01 19:09:14:748 896 548 PT WARNING: PTError: 0x8024402c 2009-11-01 19:09:14:749 896 548 PT WARNING: PTError: 0x8024402c 2009-11-01 19:09:28:778 896 548 PT WARNING: PTError: 0x8024402c 2009-11-01 19:09:28:778 896 548 PT WARNING: PTError: 0x8024402c 2009-11-01 19:09:42:808 896 548 PT WARNING: PTError: 0x8024402c 2009-11-01 19:09:42:808 896 548 PT WARNING: PTError: 0x8024402c |
У Select-String
есть и несколько дополнительных возможностей. Так если вам не интересно знать какие строки совпали, а лишь необходимо выяснить были ли совпадения вообще, воспользуйтесь ключом -Quiet:
1 2 3 |
PS C:\> netsh advfirewall firewall show rule "Remote Desktop (TCP-In)" | >> select-string "Enabled:\s+Yes" -Quiet True |
Эта команда проверяет, содержится ли в выводе netsh
строка совпадающая с Enabled:\s+Yes
и если содержится, то выводит значение $True
. Разумеется тут тоже можно указывать напрямую имя файла или несколько с помощью подстановочных символов, тогда True
будет выдано в случае если хотя бы один из файлов содержит указанную строку.
Параметр -List
говорит Select-String
что нужно найти лишь по одному совпадению на каждый файл. Это может быть полезно если вам надо найти все файлы содержащие определенную строку:
1 2 3 4 |
PS C:\> Select-String "error:" C:\Windows\*.log -List | >> select -ExpandProperty path C:\Windows\TSSysprep.log C:\Windows\WindowsUpdate.log |
В PowerShell 2.0 у Select-String
появился еще один очень полезный ключ — -Context
. Он позволяет вывести не только совпавшую строку, но еще и указанное количество строк до неё и после неё. В следующем примере выводится 3 строки предшествующих совпадению и одна после него:
1 2 3 4 5 6 |
PS C:\> Select-String "error:" C:\Windows\TSSysprep.log -Context 3,1 Windows\TSSysprep.log:4:*******Version:Major=6, Minor=1, Build=7600, PlatForm=2, CSDVer=, Free Windows\TSSysprep.log:5: Windows\TSSysprep.log:6:sysprep.cpp(309)Entering RCMSysPrepRestore > Windows\TSSysprep.log:7:sysprep.cpp(314)ERROR: ResetTSPublicPrivateKeys() FAILED: 2 Windows\TSSysprep.log:8:sysprep.cpp(316)Leaving RCMSysPrepRestore |
Непосредственно совпавшая строка помечается с помощью символа >
в начале строки. Если указать в качестве аргумента не массив из двух элементов, а просто число, то будет выведено указанное количество строк с обоих сторон от совпадения:
1 2 3 4 5 6 7 |
PS C:\> netsh advfirewall firewall show rule "Remote Desktop (TCP-In)" | >> select-string "Enabled:" -Context 2 Rule Name: Remote Desktop (TCP-In) ---------------------------------------------------------------------- > Enabled: Yes Direction: In Profiles: Domain,Private,Public |
В Select-String тоже можно использовать группы захвата, хотя получить их содержимое несколько сложнее. Дело в том что тут не используется специальная переменная $Matches
, а вместо неё результаты совпадения, в виде объекта System.Text.RegularExpressions.Match
помещаются в свойство Matches
результирующего объекта. Подробнее устройство этого объекта мы рассмотрим позднее, когда будем изучать класс [Regex]
, а пока я просто покажу как же можно получить например значение первой группы захвата:
1 2 3 4 5 6 7 8 9 10 11 12 |
PS C:\> Select-String "error: (\S+)" C:\Windows\*.log | >> Format-table path,linenumber,{$_.Matches[0].groups[1].value} Path LineNumber $_.Matches[0].groups[1].value ---- ---------- ----------------------------- C:\Windows\TSSysprep.log 7 ResetTSPublicPrivateKeys() C:\Windows\WindowsUpdate.log 2663 0x80072ee2 C:\Windows\WindowsUpdate.log 3926 0x8024402c C:\Windows\WindowsUpdate.log 3930 0x8024402c C:\Windows\WindowsUpdate.log 3941 0x8024402c C:\Windows\WindowsUpdate.log 3945 0x8024402c C:\Windows\WindowsUpdate.log 3956 0x8024402c C:\Windows\WindowsUpdate.log 3960 0x8024402c |
Другие полезные параметры командлета на которые стоит обратить внимание, это -CaseSensetive
, -Encoding
и -NotMatch
. Их названия говорят сами за себя, поэтому не буду показывать примеры для каждого.
Switch
Давайте теперь рассмотрим конструкцию Switch
, которая тоже может задействовать регекспы, и в результате становится вдвое полезнее
Сначала хорошо бы вспомнить что такое вообще switch Наверняка вы уже знакомы, но всё же… В switch
вы указываете некоторую переменную, и несколько блоков кода с вариантами действий, в зависимости от значения этой переменной. Например:
1 2 3 4 5 6 7 |
switch ($n) { 1 {Write-Host "Единица"} 2 {Write-Host "Двойка"} 3 {Write-Host "Тройка"} default {Write-Host "Другое число"} } |
Разумеется на самом деле возможности несколько богаче. К примеру вместо переменной можно указать подвыражение PowerShell, а вместо конкретных вариантов чисел, задать другие скриптблоки:
1 2 3 4 5 |
switch (get-process | where {$_.path -like "c:\windows\*"}) { {$_.Handles -gt 300} {"у $($_.Name) слишком много handle'ов"} {$_.Handles -le 100} {"у $($_.Name) очень мало handle'ов"} } |
Но и этого команде PowerShell показалось мало, и они добавили несколько дополнительных возможностей. Нас впрочем интересует лишь одна из них, ключ -regex
. Вообще ничего сложного в нём нет — он добавляется после ключевого слова switch, и перед выражением с данными, и заставляет switch
интерпретировать варианты значений как регулярные выражения:
1 2 3 4 5 |
switch -regex (Get-Content C:\Windows\win.ini) { "^\[(.+)\]$" {"Секция '" + $matches[1] + "'"} "^([^=]+)=(.*)$" {"Ключ '" + $matches[1] + "' со значением '" + $matches[2] + "'"} } |
В вышеприведенном примере я указал в качестве источника значений содержимое файла win.ini, а в качестве вариантов — два регулярных выражения. При работе такой конструкции, каждая строчка проверяется на совпадения с каждым регулярным выражением, и в случае совпадения выполняется соответствующий блок кода.
Вот еще один пример, в нём я использую командлет foreach-object
для того чтобы передавать в switch
элементы для обработки по очереди. Это позволяет начать получать результаты не дожидаясь окончания выполнения команды.
1 2 3 4 5 6 7 8 9 10 11 |
C:\SysInternals\tcpvcon.exe -n | foreach {switch -regex ($_) { "\[(.+)\] (.+)" { if ($Obj) {$Obj} $Obj = New-Object PSObject -Property @{Protocol=$Matches[1]; Executable=$Matches[2]} } "(\S+):\s+(\S.*)$" { $Obj | Add-Member noteproperty -Name $Matches[1] -Value $Matches[2] } } } |
Иногда вы можете столкнуться с поведением switch
которое может показаться странным — для некоторых значений выполняется более одного блока кода:
1 2 3 4 5 6 7 |
PS C:\> switch -regex ("word") { >> "^w" {"$_ starts with letter 'w'"} >> "^\w+$" {"$_ is word"} >> } >> word starts with letter 'w' word is word |
Дело в том что даже если строчка уже совпала с первым выражением и код выполнился, после этого она будет сравниваться со следующим, и так далее. Чтобы этого не происходило, можно добавить в конце кода ключевое слово break
, тогда после выполнения этого блока кода, будет осуществлен выход из switch
:
1 2 3 4 5 6 |
PS C:\> switch -regex ("word") { >> "^w" {"$_ starts with letter 'w'"; break} >> "^\w+$" {"$_ starts with another letter"} >> } >> word starts with letter 'w' |
Как вы могли убедится, switch
очень хорошо подходит для разбора вывода консольных команд и преобразования их в объекты, а так же для разбора конфигурационных файлов. Аналогично его можно использовать и для работы с файлами текстовых логов, особенно в тех случаях когда записи не следуют единому правилу.
Regex
Ну и напоследок выглянем за пределы стандартных командлетов и операторов, и воспользуемся для работы с регулярными выражениями, так называемым, «сырым» .NET Это не так страшно как может показаться, зато очень полезно, и зачастую просто необходимо.
Работать мы будем с классом .NET, полное имя которого звучит как System.Text.RegularExpressions.Regex
, но к счастью в PowerShell для него есть адаптер-ярлык, позволяющий называть его просто [Regex]
. Давайте рассмотрим этот класс поближе. У него есть несколько интересных нам статических методов. Статические — значит для их вызова не требуется создавать объект класса, а можно вызывать напрямую. Посмотреть список таких методов можно командлетом Get-Member
с ключом -Static
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
PS C:\> [regex] | Get-Member -Static TypeName: System.Text.RegularExpressions.Regex Name MemberType Definition ---- ---------- ---------- CompileToAssembly Method static System.Void CompileToAssembly(System.Text.RegularExpressions.RegexCompilationInf... Equals Method static bool Equals(System.Object objA, System.Object objB) Escape Method static string Escape(string str) IsMatch Method static bool IsMatch(string input, string pattern), static bool IsMatch(string input, st... Match Method static System.Text.RegularExpressions.Match Match(string input, string pattern), static... Matches Method static System.Text.RegularExpressions.MatchCollection Matches(string input, string patt... ReferenceEquals Method static bool ReferenceEquals(System.Object objA, System.Object objB) Replace Method static string Replace(string input, string pattern, string replacement), static string ... Split Method static string[] Split(string input, string pattern), static string[] Split(string input... Unescape Method static string Unescape(string str) CacheSize Property static System.Int32 CacheSize {get;set;} |
На всякий случай, напомню основные особенности синтаксиса статических методов .NET:
- Во-первых, для вызова статического метода класса используется конструкция
[класс]::метод()
- Во-вторых, в отличии от параметров командлетов и функций, для передачи параметров методам .NET они помещаются в скобки сразу за именем метода, и разделяются запятой, например:
[класс]::метод(параметр1,параметр2)
- В отличии от «родных» конструкций PowerShell, все .NET сравнения по умолчанию чувствительны к регистру символов.
Теперь можно приступать Сначала рассмотрим метод IsMatch
. Он принимает два параметра: строку текста и регулярное выражение с которым сравнивается эта строка. В качестве результата он возвращает либо True
либо False
:
1 2 3 4 |
PS C:\> [regex]::IsMatch("123","^\d+$") True PS C:\> [regex]::IsMatch("123","^\d$") False |
Всё очень просто, даже проще -match
Далее возьмем Replace
, у него уже 3 параметра: строка, регулярное выражение, и текст для замены. Ну и возвращает он, как можно догадаться уже измененный текст.
1 2 |
PS C:\> [regex]::replace('321 12 34','\s','-') 321-12-34 |
В этом методе уже можно использовать группы захвата, и вставлять их значения в текст для замены используя переменные $1 $2 и т.д.
Ну и последний из простых методов, это Split
, он, как нетрудно догадаться принимает в качестве первого параметра текст, а в качестве второго регулярное выражение по которому он будет резать текст на части. Результат соответственно — массив строк:
1 2 3 4 5 |
PS C:\> [regex]::Split("123-45=67+89",'[-=+]') 123 45 67 89 |
Теперь перейдем к самому интересному — это метод Match. Как и метод IsMatch он принимает как параметры текст, и регулярное выражение. Но вот его результат — сильно отличается. Он возвращает не простой ответ True/False, а целый объект класса System.Text.RegularExpressions.Match, содержащий результаты сравнения.
1 2 3 4 5 6 7 8 |
PS C:\> $match = [regex]::Match("def5abc","(\d).+$") PS C:\> $match Groups : {5abc, 5} Success : True Captures : {5abc} Index : 3 Length : 4 Value : 5abc |
Разберём его возможности.
В свойстве Success
содержится булево значение, показывающее было ли сравнение успешным. Index
— показывает номер символа в тексте начиная с которого выражение совпало. Length
— длинна совпавшего текста. Value
– сам совпавший текст (если бы использовался -match, то это значение было бы в $matches[0]
).
В свойстве Groups
содержится коллекция объектов такого же класса Match
, но для каждой группы захвата (включая нулевую):
1 2 3 4 5 6 7 8 9 10 11 12 13 |
PS C:\> $match.Groups Groups : {5abc, 5} Success : True Captures : {5abc} Index : 3 Length : 4 Value : 5abc Success : True Captures : {5} Index : 3 Length : 1 Value : 5 |
Таким образом чтобы собрать вручную аналог переменной $matches
, нужно собрать все значения свойства value
у объектов содержащихся в Groups
, например так:
1 2 3 4 5 6 |
PS C:\> $myMatches = $match.Groups | select -ExpandProperty value PS C:\> $myMatches 5abc 5 PS C:\> $myMatches[1] 5 |
Еще у объекта Match
, есть метод NextMatch
, который не требует аргументов, и возвращает следующий объект Match
в данной строке, если такой есть. С его помощью можно реализовать пошаговый перебор всех совпадений, например:
1 2 3 4 5 6 7 8 9 |
$Line = '<b>abc</b> <b>bca</b> <i>123</i> <b>cab</b>' $Pattern = '<(.)>(.[^<]+)</.>' $Match = [regex]::match($Line,$Pattern) While ($Match.Success) { $line = "Found '" + $Match.Groups[2].value + "' in '" + $Match.Groups[1].value + "'." write-host $line $Match = $Match.NextMatch() } |
Впрочем обычно такую задачу можно решить еще проще, с помощью метода [Regex]::Matches<code>. Он сразу возвращает коллекцию объектов <code>Match
для всех совпадений в строке. Например получим все ссылки со страницы http://ya.ru
:
1 2 3 4 |
$client = New-Object system.net.webclient $text = $client.DownloadString("http://ya.ru") $AllMatches = [regex]::matches($text,'<a [^>]*href="(http://[^"]+)"') $AllMatches | Foreach-Object {$_.groups[1].value} |
Вот так вот просто
Все методы которые я перечислил выше, позволяют указывать еще дополнительный параметр — RegexOptions
. В нём можно задавать всевозможные опции для выполнения сравнения, такие как IgnoreCase
(игнорирование регистра символов) или Compiled
(выражение компилируется при первом сравнении, и последующие выполняются быстрее). Список опций можно посмотреть тут.
Еще маленькая тонкость, не обязательно использовать статические методы и постоянно указывать опции и регулярное выражение. Можно создать экземпляр класса Regex
, задать опции в нём, и использовать уже обычные, не статические методы:
1 2 3 4 5 6 7 8 9 10 11 |
PS C:\> $reg = [regex]'<a [^>]*href="(http://[^"]+)"' PS C:\> $reg.Matches($text) | %{$_.groups[1].value} http://help.yandex.ru/start/ http://mail.yandex.ru http://www.yandex.ru http://www.yandex.ru http://www.artlebedev.ru |
или так:
1 2 3 4 |
$reg = New-Object regex '<a [^>]*href="(http://[^"]+)"','Compiled,IgnoreCase' PS C:\> $reg.Options IgnoreCase, Compiled PS C:\> $reg.Replace($text,'<a href="localhost"') |
Как можно видеть, здесь указывается уже меньше параметров, так как регулярное выражение уже задано в объекте метод которого мы вызываем.
Ну и в качестве бонуса, еще два метода класса [Regex]
— Escape
и Unescape
. Как несложно догадаться они служат для маскировки текстовых строк для их безопасного использования в регулярных выражениях, и обратного преобразования.
1 2 |
PS C:\> [regex]::Escape('C:\Windows\explorer.exe') C:\\Windows\\explorer\.exe |
После такого преобразования текст можно спокойно использовать как компонент в регулярных выражениях, не опасаясь что какие то знаки из него будут интерпретированы как спецсимволы. Ну и Unescape
преобразовывает текст обратно:
1 2 |
PS C:\> [regex]::Unescape('C:\\Windows\\explorer\.exe') C:\Windows\explorer.exe |
На этом пока всё