Invoke-SQL

августа 16, 2010 by xaegr

Всем привет!

thumbnailCA2CQQEOСпасибо всем кто не отписался от моего блога несмотря на отсутствие новых постов в последнее время. У меня для вас есть кое что вкусное Smile

Я думаю большинству читателей моего блога, так или иначе приходится сталкиваться и взаимодействовать с серверами MSSQL, ведь большинстов современных серверных продуктов используют их для хранения данных. И я думаю что вам, так же как и мне, часто хотелось автоматизировать взаимодействие с этими хранилищами данных с помощью PowerShell. И Microsoft даже пошла нам на встречу выпустив SQL Server 2008 с интеграцией PowerShell. Это позволило нам автоматизировать настройку серверов SQL, но возможностей работы с данными содержащимися в базах к сожалению не прибавило. То же относится к недавно выпущенному MSSQL 2008 R2. А ведь казалось бы, нам нужно совсем немного – хотя бы простой командлет который смог бы принимать на вход SQL запрос, и возвращать данные не в текстовом представлении, как это делают osql.exe или isql.exe, а в виде объектов, со свойствами – столбцами. Аналогичный командлет на самом деле уже написала компания SoftwareFX, но он поставляется в составе их платного продукта PowerGadgets, который хоть и хорош сам по себе, но покупать его только для того чтобы удобно выполнять SQL запросы из PowerShell – просто нецелесообразно. Поэтому я решил написать собственный командлет (advanced function), с таким функционалом, являющийся на самом деле обёрткой над .NET классами для работы с SQL.

Вот несколько примеров его использования:

PS C:\> Invoke-SQLCommand -database sms_lab 'select collectionname,collectionid from dbo.collections'

collectionname                                  collectionid
--------------                                  ------------
All Active Directory Security Groups                       6
All Desktops and Servers                                  19
All Systems                                                2
All Unknown Computers                                     24
All User Groups                                            4
All Users                                                  3
All Windows 2000 Professional Systems                     14
All Windows 2000 Server Systems                           15
All Windows Mobile Devices                                18
All Windows Mobile Pocket PC 2003 Devices                 20
All Windows Mobile Pocket PC 5.0 Devices                  21
All Windows Mobile Smartphone 2003 Devices                22
All Windows Mobile Smartphone 5.0 Devices                 23
All Windows Server 2003 Systems                           17
All Windows Server Systems                                13
All Windows Workstation or Professional Systems            5
All Windows XP Systems                                    16
Root Collection                                            1

PS C:\> $Reports = Invoke-SQLCommand -database sms_lab `
>> -command 'select reportid,name,category,sqlquery from dbo.report'
>>

PS C:\> $Reports | where {$_.category -eq 'Operating System'} |
>> Format-Table reportid, name, category -AutoSize
>>

reportid name                                                        category
-------- ----                                                        --------
      38 Computer operating system version history                   Operating System
      39 Services - Computers running Remote Access Server           Operating System
      40 Services - Computers running a specific service             Operating System
      41 Services - Services information for a specific computer     Operating System
      42 Windows Server computers                                    Operating System
     100 Computers with a specific operating system and service pack Operating System
     101 Computers with a specific operating system                  Operating System
     102 Count operating systems and service packs                   Operating System
     103 Count operating system versions                             Operating System

Кроме простых запросов можно выполнять и другие операции, например изменение или удаление записей. В таком случае в качестве результата возвращается количество измененных записей.

PS C:\> Invoke-SQLCommand -server sccm -database sms_lab `
>> -Command "update dbo.smspackages set description='Updated from PowerShell' where Name='Test Package'"
>>
1

Кроме того, так как эта команда позволяет изменять данные, я реализовал и поддержку ключей –whatif и –confirm.

Ну и наконец, исходный код, с кучей комментариев Smile

function Invoke-SQLCommand
{
    #Объявление о том что мы будем использовать возможности Advanced 
    #Functions, в частности несколько наборов параметров, и поддержку
    #ключей -WhatIf -Confirm -Verbose (ShouldProcess)
    [CmdletBinding(
    SupportsShouldProcess=$True,
    ConfirmImpact="Low",
    DefaultParameterSetName="Default")]            

    Param
    (
        #Команда SQL. Обязательный параметр присутствующий во всех наборах 
        #параметров, по умолчанию находится на первом месте, и принимает
        #значения из конвейера. Тип - массив строк.
        [Parameter(Mandatory=$True, Position=1, ValueFromPipeLine=$True)]
        [String[]]$Command,            

        #Имя сервера SQL. Присутствует только в наборе параметров Default.
        #По умолчанию равен имени текущего компьютера.
        [Parameter(Position=2, ParameterSetName="Default")]
        [String]$Server = $Env:computername,            

        #База данных. По умолчанию - "master".
        [Parameter(Position=3, ParameterSetName="Default")]
        [String]$Database = "master",            

        #Надо ли отключить интегрированную аутентикацию и использовать
        #родную SQL (менее безопасно). Тип - switch (наличие параметра 
        #означает $true.
        [Parameter(ParameterSetName="Default")]
        [switch]$SQLAuthentication,            

        #Учетные данные для подключения, тип - PSCredential.
        #Можно получить выполнив командлет Get-Credential
        [Parameter(ParameterSetName="Default")]
        [System.Management.Automation.PSCredential]$Credential,            

        #Строка для подключения сервера. Позволяет указать имя сервера,
        #базы, параметры аутентикации, и прочее, вручную. Принадлежит 
        #набору параметров ConnectionString.
        [Parameter(Mandatory=$True,
        Position=2,
        ParameterSetName="ConnectionString")]
        [String]$ConnectionString            

    )            

    #Блок BEGIN выполняется 1 раз, в начале выполнения командлета, до
    #начала обработки данных из конвейера. Обычно здесь происходит инициализация.
    BEGIN
    {
        #Создаем объект подключения к SQL
        $Connection = New-Object System.Data.SQLClient.SQLConnection            

        #Если использовался набор параметров Default, то формируем
        #строку подключения самостоятельно.
        if ($PSCmdlet.ParameterSetName -eq "Default")
        {
            $Trusted = if ($SQLAuthentication) {"False"} else {"True"}
            $String = "Server=$Server;Database=$Database;Trusted_Connection=$Trusted;"            

            #Если указан параметр $Credential, то вытаскиваем из него имя и пароль
            #и помещаем в строку подключения.
            if ($Credential)
            {
                #При необходимости удаляем \ в начале строки.
                $String += "User ID=$($Credential.UserName -replace '^\\');"
                $String += "Password=$($Credential.GetNetworkCredential().password);"
            }
            #Присваиваем строку объекту подключения.
            $Connection.ConnectionString = $String
        }
        else
        {
            #Присваиваем строку объекту подключения.
            $Connection.ConnectionString = $ConnectionString
        }
        #Выводим отладочную информацию.
        Write-Verbose ("ConnectionString is: " + $Connection.ConnectionString)
        #Открываем соединение. Если не закрыть его методом .Close() то оно
        #будет закрыто автоматически по таймауту (15 мин по умолчанию).
        $Connection.Open()
        #Если не получилось открыть соединение - вываливаем ошибку.
        if ($Connection.State -ne "Open")
        {
            Throw "Unable to open SQL connection"
        }
        #Создаем строку $Commands. Она будет содержать полный текст сценария SQL
        #если по конвейеру будет передано несколько команд.
        $Commands = ""
    }            

    #Блок PROCESS выполняется по разу для каждого объекта полученного по конвейеру
    #или для каждого элемента массива $Command если он был передан как аргумент.
    #Если как аргумент была передана одна строка, то блок выполняется 1 раз.
    #Обычно в этом блоке происходит непосредственная обработка и вывод данных, но 
    #в данном случае он нужен лишь для склейки строк из конвейера в одну.
    PROCESS
    {
        #Прицепляем к $Commands содержимое элемента $Command и перенос строки.
        $Commands += "$Command`n"
    }            

    #Блок END Выполняется 1 раз, в конце работы командлета, когда все элементы 
    #переданные по конвейеру уже были обработаны в блоке PROCESS.
    #В этом блоке принято производить удаление временных данных, завершение сессий
    #и т.п. Однако в данном командлете здесь выполняется непосредственно команда SQL,
    #а уже затем происходит завершение сессии SQL.
    END
    {
        #Конструкция Try, Catch, Finally нужна для обработки ошибок. Если внутри try
        #произойдет ошибка, то она будет перехвачена, и управление будет передано 
        #блоку Catch. Finally выполняется всегда, независимо от того произошла ошибка
        #или нет - соединение к SQL нам надо закрыть в любом случае.
        try
        {
            #Это условие единственное что необходимо для реализации стандартных ключей
            #-WhatIf, -Confirm и -Verbose. Как аргумент для ShouldProcess передается
            #текущий элемент (команда SQL). Это имя будет использоваться для вывода 
            #информации при использовании аргументов. Внутри блока If находится код 
            #который будет выполнен в том случае если не указан ключ -WhatIf. Если же
            #указан ключ -Confirm, то выполнение этого кода будет зависеть от ответа
            #пользователя.
            if ($PSCmdlet.ShouldProcess($Commands.Trim()))
            {
                #Создаем объект SQLCommand, и присваиваем ему текст команды и объект
                #соединения.
                $SqlCommand = New-Object System.Data.SQLClient.SQLCommand
                $SqlCommand.Connection = $Connection
                $SqlCommand.CommandText = $Commands
                #Выполняем команду, и получаем объект Reader, который будем использовать
                #для чтения результатов.
                $Reader = $SQLCommand.ExecuteReader()            

                #Если в результатах есть строки, то выводим их содержимое.
                If ($Reader.HasRows)
                {
                    #Инициализируем переменную для подсчета строк.
                    $RowsCount = 0
                    #Смотрим количество возвращенных столбцов.
                    $FieldCount = $Reader.FieldCount
                    #Пока есть необработанные данные, читаем их.
                    while ($Reader.Read())
                    {
                        #Увеличиваем счетчик строк.
                        $RowsCount ++
                        #Создаем "пустой" объект.
                        $Obj = New-Object psobject
                        #Для каждого столбца...
                        for ($i=0; $i -lt $FieldCount; $i++)
                        {
                            #Добавляем к объекту свойство с именем столбца, где 
                            #значение - значение столбца.
                            $Obj | Add-Member -MemberType NoteProperty `
                            -Name ($Reader.GetName($i)) -Value ($Reader.GetValue($i))
                        }
                        #Выводим получившийся объект.
                        Write-Output $Obj
                    }
                }            

                #Если были затронуты (созданы/изменены/удалены) записи, то выводим
                #их количество.
                If ($Reader.RecordsAffected -gt 0)
                {
                    Write-Output $Reader.RecordsAffected
                }
            }
        }
        Catch
        {
            #Если произошла ошибка, то достаем и выводим её текст, плюс помещаем
            #оригинальный объект исключения в поле Exception.
            Write-Error -Message ($_.exception.innerexception.message) `
            -Exception $_.Exception
        }
        Finally
        {
            #Закрываем соединение с SQL.
            $Connection.Close()
        }
    }
}            

 

Напоследок небольшое замечание. Помните что MSSQL был создан для быстрой обработки данных, а PowerShell – для быстрого написания сценариев и удобной интерактивной работы. Поэтому старайтесь максимум обработки оставить на стороне MSSQL – он справится гораздо быстрее.


Я снова читаю курс по Windows PowerShell, на этот раз 2.0!

июня 28, 2010 by xaegr

SNC00007

Итак, совершенно внезапно, я снова читаю курс по PowerShell. На этот раз курс будет о PowerShell 2.0 и так как на этот раз он пятидневный, то получилось поместить в него гораздо больше интересного. В частности будет и про автоматизацию Exchange 2010, SharePoint 2010, ISA/TMG 2010 и ConfigMgr 2007. С полной программой можно ознакомится здесь. Содержание курса уникальное, больше нигде не найдете :)

Уровень курса указан как 200, но это спорное определение. Тут как в конвейере PowerShell, на вход можно подавать совсем новичков, лишь слышавших о скриптинге, а на выходе получатся настоящие гуру PowerShell, которые легко будут понимать всё что я пишу в этом блоге и создавать свои скрипты не хуже :)

Курс я буду читать в учебном центре Advanced Training, расположенном недалеко от метро. Кроме того он еще отличается отличными современными компьютерами (Core i7 920, 8 ядер, 12гигов DDR3), так что проблем с тормозящими лабами не возникнет, несмотря на то что последние версии Exchange и SharePoint достаточно требовательные к ресурсам.

PS: Ну и бонусная скидка для читателей блога – 5% :) Кстати доступно еще множество других скидок, в том числе работают скидки из других учебных центров.

PPS: Фотка из Advanced Training, там еще классные курсы по Cisco можно послушать :)


Выкачивание драйверов с сайта HP

мая 9, 2010 by xaegr

usbVacuum Не так давно мне понадобилось скачать драйверы и утилиты для множества моделей компьютеров HP. Учитывая что обычно для каждой модели приводится несколько десятков драйверов, скачивать их вручную и раскладывать по папкам мне показалось слишком долго и нудно. Кроме того я не хотел устанавливать какие либо менеджеры закачек которые помогли бы просто выдрать все ссылки со страницы, да они и не смогли бы правильно разложить файлы по категориям… Поэтому я решил написать простенький сценарий, который бы разбирал html страницы с драйверами для модели, понимал бы к какой категории относится драйвер или утилита, скачивал бы их, и раскладывал бы в соответствующие папки (при необходимости создавая эти папки самостоятельно).

На вход сценарий принимает адрес страницы с которой необходимо скачать драйверы, например такой http://h20000.www2.hp.com/bizsupport/TechSupport/SoftwareIndex.jsp?lang=ru&cc=ru&prodNameId=462858&prodTypeId=321957&prodSeriesId=462857&swLang=33&taskId=135&swEnvOID=1093

Если вы приглядитесь к кнопкам “Загрузить” на этой странице, то вы увидите что в них содержится ftp ссылка на файл, так что для закачки нам не подойдет простой System.Net.WebClient или BitsTransfer а понадобится System.Net.FtpWebRequest. Он несколько сложнее в использовании, но я нашел хороший пример функции для скачивания файлов по FTP здесь, и включил его в тело сценария. Разумеется можно просто поместить его в свой профиль, но я посчитал что лучше сделать сценарий самодостаточным и независимым от окружения.

Кроме страницы для скачивания, сценарию можно указать еще параметры Destination (папка куда скачивать файлы, по умолчанию текущий каталог) и ключ Plain (если он указан, то файлы сваливаются в одну кучу, без раскладывания по папкам).

Ну и наконец Get-HPDrivers.ps1:

param ($Url, $Destination=$Pwd, [switch]$Plain)            

#Функция Get-FTPFile взята отсюда - http://powershell.com/cs/media/p/804.aspx
function Get-FTPFile ($Source,$Target,$UserName,$Password)
{              

 # Create a FTPWebRequest object to handle the connection to the ftp server  
 $ftprequest = [System.Net.FtpWebRequest]::create($Source)              

 # set the request's network credentials for an authenticated connection  
 $ftprequest.Credentials =
     New-Object System.Net.NetworkCredential($username,$password)              

 $ftprequest.Method = [System.Net.WebRequestMethods+Ftp]::DownloadFile
 $ftprequest.UseBinary = $true
 $ftprequest.KeepAlive = $false              

 # send the ftp request to the server  
 $ftpresponse = $ftprequest.GetResponse()              

 # get a download stream from the server response  
 $responsestream = $ftpresponse.GetResponseStream()              

 # create the target file on the local system and the download buffer  
 $targetfile = New-Object IO.FileStream ($Target,[IO.FileMode]::Create)
 [byte[]]$readbuffer = New-Object byte[] 1024              

 # loop through the download stream and send the data to the target file  
 do{
     $readlength = $responsestream.Read($readbuffer,0,1024)
     $targetfile.Write($readbuffer,0,$readlength)
 }
 while ($readlength -ne 0)              

 $targetfile.close()
}              

#Выводим на экран текущее выполняемое действие:
Write-Progress "Receiving metadata" "Downloading"
#Создаем объект WebClient для выкачивания текста страницы
$wc = new-object System.Net.WebClient
#Устанавливаем для него кодировку
$wc.Encoding = [System.Text.Encoding]::GetEncoding("UTF-8")
#Указываем что надо использовать учетные данные той учетной записи 
#под которой запущен сценарий
$wc.UseDefaultCredentials = $true
#Загружаем страницу в переменную $Page
$Page = $wc.DownloadString($url)            

#Обновляем статус
Write-Progress "Receiving metadata" "Parsing HTML"
$Meta = $(
    #С помощью конструкции switch разбираем текст страницы используя регулярные выражения
    #Так как разные элементы (категория, имя, ссылка) находятся на разных строках, я использовал
    #переменные $Category и $Name для сохранения их значений
 switch -regex ($page.split("`n"))
 {
        #Это категория, устанавливаем переменную $Category
  '<a name="\d+"></a><b>([^<]+)</b>' {$Category = $matches[1]}
        #Это имя пакета, помещаем его в $Name
  'SoftwareDescription.jsp[^"]+">([^<]+)' {$Name = $matches[1]}
        #Это уже ссылка
  'targetPage=([^&]+exe)' {
            #Размаскировываем ссылку и помещаем в переменную $Link
   $Link = [system.uri]::UnescapeDataString($matches[1])
            #Создаем новый объект со свойствами Name, Category и Link.
   New-Object PSObject -Property @{Name=$Name; Category=$Category; Link = $Link}
  }
 }
)
#Страница разобрана, полученные объекты помещены в массив $Meta            

#Получаем имя для файла со списком распарсенного, 
#он будет лежать для справки в корне целевой папки
$FileName = Join-Path $Destination "Index.csv"
#Отображаем состояние
Write-Progress "Exporting Metadata" $FileName
#Экспортируем $Meta в файл
$Meta | Export-Csv $FileName -Encoding UTF8 -NoTypeInformation -Delimiter ","            

#Для каждого объекта в массиве $Meta...
foreach ($Driver in $Meta)
{
    #Выводим статус и имя текущего драйвера/программы
 Write-Progress "Downloading files" $Driver.Name
    #Если не указан ключ $Plain то...
 if (!$Plain)
 {
        #Определяем папки для категории и файла, и при необходимости создаем их
  $CategoryFolder = Join-Path $Destination $Driver.Category
  if (-not (test-path $CategoryFolder)) {md $CategoryFolder | out-null}
  $DriverFolder = Join-Path $CategoryFolder $Driver.Name.replace("/","")
  if (-not (test-path $DriverFolder)) {md $DriverFolder | out-null}
        #Устанавливаем имя под которым будет сохранен файл в соответствующей папке
  $FileName = Join-Path $DriverFolder ($Driver.Link -replace '^.+/')
 }
 Else
 {
        #Устанавливаем имя под которым будет сохранен файл в корне папки
  $FileName = Join-Path $Destination ($Driver.Link -replace '^.+/')
 }
    #Если файл еще не существует...
 if (-not (test-path $FileName)) {
        #Вызываем функцию Get-FTPFile для его скачивания
  Get-FTPFile -Source $Driver.Link -Target $Filename -User "Anonymous" -Pass "anon@hp.com"
 } else {
        #Иначе выдаем предупреждение, и продолжаем работу
  Write-Warning "File $FileName already exists, skipping..."
 }
}

Так как моделей у меня было много, я не стал для каждой ссылки вызывать сценарий, а просто сохранил ссылки в текстовый файл urls.txt и запустил следующую команду:

Get-Content .\urls.txt | foreach { C:\Root\Get-HPDrivers.ps1 -Url $_ }

Ну и вот как то так оно выглядит в работе :)

hpdownload

Стоит выразить отдельную благодарность Hewlett-Packard за хорошо структурированную страницу, которую можно разобрать подобным образом. Не многие поставщики способны похвастаться таким.


Регулярные выражения – Regex

апреля 9, 2010 by xaegr

NET_small Ура, я всё таки выкроил время для завершающего поста в серии про регулярные выражения :) На этот раз мы выглянем за пределы стандартных командлетов и операторов, и воспользуемся так называемым, "сырым" .NET :) Это не так страшно как может показаться, зато очень полезно, и зачастую просто необходимо.

Работать мы будем с классом .NET, полное имя которого звучит как System.Text.RegularExpressions.Regex, но к счастью в PowerShell для него есть адаптер-ярлык, позволяющий называть его просто [Regex]. Давайте рассмотрим этот класс поближе. У него есть несколько интересных нам статических методов. Статические – значит для их вызова не требуется создавать объект класса, а можно вызывать напрямую. Посмотреть список таких методов можно командлетом Get-Member с ключом -Static:

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:

PS C:\> [regex]::IsMatch("123","^\d+$")
True
PS C:\> [regex]::IsMatch("123","^\d$")
False

Всё очень просто, даже проще -match :)

Далее возьмем Replace, у него уже 3 параметра: строка, регулярное выражение, и текст для замены. Ну и возвращает он, как можно догадаться уже измененный текст.

PS C:\> [regex]::replace('321 12 34','\s','-')
321-12-34

В этом методе уже можно использовать группы захвата, и вставлять их значения в текст для замены используя переменные $1 $2 и т.д.

Ну и последний из простых методов, это Split, он, как нетрудно догадаться принимает в качестве первого параметра текст, а в качестве второго регулярное выражение по которому он будет резать текст на части. Результат соответственно – массив строк:

PS C:\> [regex]::Split("123-45=67+89",'[-=+]')
123
45
67
89

Теперь перейдем к самому интересному – это метод Match. Как и метод IsMatch он принимает как параметры текст, и регулярное выражение. Но вот его результат – сильно отличается. Он возвращает не простой ответ True/False, а целый объект класса System.Text.RegularExpressions.Match, содержащий результаты сравнения.

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, но для каждой группы захвата (включая нулевую):

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, например так:

PS C:\> $myMatches = $match.Groups | select -ExpandProperty value
PS C:\> $myMatches
5abc
5
PS C:\> $myMatches[1]
5

Еще у объекта Match, есть метод NextMatch, который не требует аргументов, и возвращает следующий объект Match в данной строке, если такой есть. С его помощью можно реализовать пошаговый перебор всех совпадений, например:

$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. Он сразу возвращает коллекцию объектов Match для всех совпадений в строке. Например получим все ссылки со страницы http://ya.ru:

$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, задать опции в нём, и использовать уже обычные, не статические методы:

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

или так:

$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. Как несложно догадаться они служат для маскировки текстовых строк для их безопасного использования в регулярных выражениях, и обратного преобразования.

PS C:\> [regex]::Escape('C:\Windows\explorer.exe')
C:\\Windows\\explorer\.exe

После такого преобразования текст можно спокойно использовать как компонент в регулярных выражениях, не опасаясь что какие то знаки из него будут интерпритированы как спецсимволы. Ну и Unescape преобразовывает текст обратно:

PS C:\> [regex]::Unescape('C:\\Windows\\explorer\.exe')
C:\Windows\explorer.exe

На этом пока всё :)


Регулярные выражения – Switch

февраля 24, 2010 by xaegr

regexp-8 Ну чтож, пора новогодних каникул давно закончилась, и мне стало уже тяжело находить отмазки на вопросы о продолжении серии :) Так как времени с момента предыдущего поста прошло уже немало времени, да и для тех кто натолкнулся на этот пост случайно – вот ссылки на посты которые рекомендуется прочитать сначала: 1, 2, 3, 4, 5, 6, 7.

В последнем выпуске мы познакомились с Select-String, командлетом PowerShell который использует в своей работе регулярные выражения. Сегодня же мы рассмотрим конструкцию Switch, которая тоже может задействовать регекспы, и в результате становится вдвое полезнее :)

Сначала хорошо бы вспомнить что такое вообще switch :) Наверняка вы уже знакомы, но всё же… В switch вы указываете некоторую переменную, и несколько блоков кода с вариантами действий, в зависимости от значения этой переменной. Например:

switch ($n)
{
    1 {Write-Host "Единица"}
    2 {Write-Host "Двойка"}
    3 {Write-Host "Тройка"}
    default {Write-Host "Другое число"}
}

Разумеется на самом деле возможности несколько богаче. К примеру вместо переменной можно указать подвыражение PowerShell, а вместо конкретных вариантов чисел, задать другие скриптблоки:

switch (get-process | where {$_.path -like "c:\windows\*"})
{
    {$_.Handles -gt 300} {"у $($_.Name) слишком много handle'ов"}
    {$_.Handles -le 100} {"у $($_.Name) очень мало handle'ов"}
}

Но и этого команде PowerShell показалось мало, и они добавили несколько дополнительных возможностей. Нас впрочем интерисует лишь одна из них, ключ -regex. Вобщем ничего сложного в нём нет – он добавляется после ключевого слова switch, и перед выражением с данными, и заставляет switch интерпретировать варианты значений как регулярные выражения:

switch -regex (Get-Content C:\Windows\win.ini)
{
    "^\[(.+)\]$" {"Секция '" + $matches[1] + "'"}
    "^([^=]+)=(.*)$" {"Ключ '" + $matches[1] + "' со значением '" + $matches[2] + "'"}
}            

В вышеприведенном примере я указал в качестве источника значений содержимое файла win.ini, а в качестве вариантов – два регулярных выражения. При работе такой конструкции, каждая строчка проверяется на совпадения с каждым регулярным выражением, и в случае совпадения выполняется соответствующий блок кода.

Вот еще один пример, в нём я использую командлет foreach-object для того чтобы передавать в switch элементы для обработки по очереди. Это позволяет начать получать результаты не дожидаясь окончания выполнения команды.

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 которое может показаться странным – для некоторых значений выполняется более одного блока кода:

PS C:\> switch -regex ("word") {
>> "^w" {"$_ starts with letter 'w'"}
>> "^\w+$" {"$_ is word"}
>> }
>>
word starts with letter 'w'
word is word

Дело в том что даже если строчка уже совпала с первым выражением и код выполнился, после этого она будет сравниваться со следующим, и так далее. Чтобы этого не происходило, можно добавить в конце кода ключевое слово break, тогда после выполнения этого блока кода, будет осуществлен выход из switch:

PS C:\> switch -regex ("word") {
>> "^w" {"$_ starts with letter 'w'"; break}
>> "^\w+$" {"$_ starts with another letter"}
>> }
>>
word starts with letter 'w'

Как вы могли убедится, switch очень хорошо подходит для разбора вывода консольных команд и преобразования их в объекты, а так же для разбора конфигурационных файлов. Аналогично его можно использовать и для работы с файлами текстовых логов, особенно в тех случаях когда записи не следуют единому правилу.

Продолжение: Regex


Курс по PowerShell (6434A) в моём исполнении :)

января 26, 2010 by xaegr

teacher1 10-12 марта, в Москве, в учебном центре Инвента, я буду читать курс по Windows PowerShell. Это официальный Microsoft’овский курс – 6434A – Automating Windows Server 2008 Administration with Windows PowerShell. С его программой можно ознакомиться тут.

Хотя в названии и фигурирует Windows Server 2008 – большая часть информации касается PowerShell вообще, независимо от операционной системы, а Windows Server 2008 используется лишь для демонстрации функционала PowerShell.

Курс подойдет и для людей совсем не знакомых с PowerShell, и для уже обладающих некоторыми знаниями. Обучение начинается с установки PowerShell и рассмотрения базового функционала. Затем идёт работа с конвейерами, форматированием вывода, и конструкциями контроля выполнения, и т.д. Ближе к концу вы научитесь использовать возможности PowerShell для работы с WMI, ADSI, COM, и .NET на реальных примерах. В курсе много лабораторных работ, что очень важно для хорошего усвоения материала.

Внимание, бонус! Курс не ограничивается программой курса :) Если мы будем укладываться по времени – можно пытать меня всевозможными вопросами, я с удовольствием расскажу вам всё что знаю о PowerShell, лишь бы это было интересно и другим слушателям :)

Продолжительность курса – 3 дня, с 9 утра, до 17 вечера.

Стоимость – 13500 руб.

Записаться можно тут. И лучше сделать это пораньше, так как количество мест ограничено.

До встречи :)


SharePoint User Group

января 12, 2010 by xaegr

Маленькая такая “новость” :) 22 января я прочитаю доклад о PowerShell в московской юзергруппе SharePoint. Там будет краткое знакомство с PowerShell для разработчиков, покажу несколько примеров работы из PowerShell с SharePoint, ну и конечно можно будет попытать меня вопросами :)

 

Объявление о встрече на сайте юзергруппы.

Время проведения: 22 января, сбор в 18:30, начало первого доклада в 19:00.

Место проведения: Офис Microsoft:

Проезд до ст. метро «Крылатское»: 1-й вагон из центра, в переходе направо и на улице еще раз направо. Сразу у выхода (напротив д. 10 по Осеннему бульвару ) остановка маршруток с надписью «Krylatsky Hills».

Бесплатный рейсовый микроавтобус до бизнес-центра «Крылатские холмы» регулярно курсирует от метро. Дорога до бизнес-центра занимает порядка 5–7 минут. Автобусы следуют без остановки. Время работы автобуса — с 8:00 до 22:00.

Офис «Microsoft Россия» расположен в корп. 1 (самое левое здание).

Карта.

Регистрироваться на встречу можно здесь, участие бесплатное.


Интересные примеры на PowerShell

января 10, 2010 by xaegr

perl Недавно натолкнулся вот на этот пост. В нём приведено несколько интересных примеров того “как в Perl одна-две строчки кода могут сделать больше, чем десять строк в каком-нибудь другом языке программирования” ;) Мне показалось что будет интересно сравнить как эти примеры выглядели бы на PowerShell ;) И главное – это может быть полезно тем кто уже знает Perl но сейчас изучает PS.

Как известно PowerShell очень молодой язык, и разумеется он унаследовал множество элементов других языков, и следовательно местами схож с многими из них. Я часто слышу о коде PowerShell фразы типа “О, да они же украли PHP!”, “Это же C# с более простым синтаксисом”. Но по-моему больше всего PowerShell похож на Perl. Это и не удивительно – Perl был одним из любимых языков авторов PS, и это здорово – многим хорошим особенностям в PS мы обязанны именно Perl’у.

Hats off to superstar Larry Wall and Perl, very few people and technologies that have had the level of (positive :-) ) impact these 2 have had on the industry.  The world is a better place because that guy was born!

Jeffrey Snover


Это была отмазка ;) Теперь перейдем к коду :)

Сразу скажу – мне кажется это просто удачная подборка попалась, многие примеры удалось записать на PS сильно короче, и главное – понятнее (субъективно конечно :) ). В очень многих областях Perl легко даст фору PowerShell’у в плане компактности кода, может быть даже и некоторые из этих примеров на Perl можно записать гораздо более красиво, так что буду рад если знающие Perl люди оставят свои конструктивные комментарии :)

Я цитирую только описания, код на Perl можно посмотреть в оригинальном посте.

1. Проверить, существует ли элемент (первый аргумент функции, передается по значению) в массиве (второй аргумент функции, передается по ссылке).

#Встроенный оператор -contains
$array -contains $element            

2. Удалить из массива @arr элементы, которые есть в массиве @skip.

#Вопросительный знак - алиас для where
$arr | ? {$skip -notcontains $_}

Вместо пункта 3 я написал красивый (субъективно разумеется ;) ) фильтр:

filter Replace-Words
{
    foreach ($arg in $args) {
        $pair = $arg.split("=",2) #Разрезаем аргумент на 2 части по знаку 
        $_ = $_ -replace $pair[0],$pair[1] #Заменяем вхождения в строке
    }
    $_ #Выдаём результирующую строку
}

Использовать например так:

$text = Get-Content .\test1.txt
$text | Replace-Words плохое=хорошее яблоки=груши | Set-Content .\test1.txt

4. Вывести список имен файлов и каталогов в заданной директории, отсортированный по дате последнего доступа. Обычно глобы сортируют список по имени файлов и каталогов. Для сортировки по дате последнего изменения, заменить цифру 8 на 9.

В PS для сортировки по дате изменения надо заменить не 8 на 9, а LastAccessTime на LastWriteTime ;)

ls | sort lastAccessTime

5. Удалить повторяющиеся элементы в массиве.

$arr | select -Unique

6. Перемешать элементы массива

$arr | sort {Get-Random}

Командлет Get-Random появился только в PS 2.0, в 1.0 можно сделать так:

$r = New-Object random
$arr | sort {$r.next()}

7. Выбрать случайный элемент в массиве можно как минимум двумя способами. Можно перемешать элементы, как в предыдущем примере, и выбрать нулевой, а можно в одну строчку:

$arr | Get-Random

8. Аналог PHP функции urlencode.

[System.Uri]::EscapeUriString(http://проверка)

Регулярные выражения – Select-String

декабря 28, 2009 by xaegr

regexp-7 Итак, подведём итоги этого года :) В области регулярных выражений разумеется :) Мы успели познакомится с основами, отрицательными группами и якорями, квантификаторами, группами захвата, операторами –replace и –split, а так же с концепцией “жадности”. Пришло время познакомится с целым командлетом PowerShell, который использует регулярные выражения. Таким командлетом является Select-String. Он используется для поиска строк совпадающих с регулярным выражением. Строки для отбора можно передать из массива строк, например:

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: во всех файлах *.log в папке c:\Windows:

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), и номера строк. Полный список доступных свойств можно посмотреть следующей командой:

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;}

Давайте например выведем только имена файлов и номера совпавших строк:

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

Если весь этот "объектный мусор" вам не нужен, вы можете получить только строки, следующей командой:

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:

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 что нужно найти лишь по одному совпадению на каждый файл. Это может быть полезно если вам надо найти все файлы содержащие определенную строку:

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 строки предшествующих совпадению и одна после него:

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

Непосредственно совпавшая строка помечается с помощью символа > в начале строки. Если указать в качестве аргумента не массив из двух элементов, а просто число, то будет выведено указанное количество строк с обоих сторон от совпадения:

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], а пока я просто покажу как же можно получить например значение первой группы захвата:

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. Их названия говорят сами за себя, поэтому не буду показывать примеры для каждого.

Продолжение следует, но уже в следующем году :)

Пользуясь случаем хочу пожелать всем читателям моего блога успехов в следующем году. Чтобы скрипты экономили вам еще больше времени принося больше дохода, и доставляя всё больше удовольствия от работы. Ну и просто счастья и здоровья конечно :) До встречи в новом году!


Регулярные выражения – Жадность

декабря 24, 2009 by xaegr

regexp-6 Просто отличное название для очередной статьи о регулярных выражениях в блоге посвященном PowerShell :) Но оно действительно подходит лучше всего. Сегодня мы поговорим об одной важной концепции регулярных выражений. От чего зависит сколько символов будет захвачено количественным модификатором с варьирующейся длинной? Именно от жадности :) Если вы наткнулись на пост случайно, то сначала лучше ознакомьтесь с предыдущими постами серии – 1,2,3,4,5.

По умолчанию, все количественные модификаторы в регулярных выражениях – жадные. То есть они пытаются захватить как можно больше символов (разумеется пока это позволяют условия). Взять к примеру .+ Это выражение означает 1 или более вхождений, но разве оно остановится на одном вхождении? Нет! Оно будет жрать захватывать символы пока у него будет эта возможность, то есть до ограничителя если он есть, а если его нет – то до конца (или начала) строки. Например:

PS C:\> "Очень вкусная булка." -replace "б\S+"
Очень вкусная

Заметьте, захавал всё, и даже точкой не подавился :) А что если мы допускаем любое количество повторений группы, но хотим ограничиться минимумом? Тогда нам поможет "нежадная" версия этого количественного модификатора:

PS C:\> "Очень вкусная булка." -replace "б\S+?"
Очень вкусная лка.

Да, просто добавив после этого количественного модификатора вопросительный знак, мы сразу заставили его ограничится минимумом – одним символом. Нежадные версии других количественных модификаторов получаются таким же образом: *? ?? {1,5}?

Разумеется нежадный модификатор может захватить и больше, но только если у него не останется другого выбора:

PS C:\> "Очень вкусная булка." -replace "б\S+?\."
Очень вкусная

В этом выражении оговаривается что \S+? должен захватить минимум, но до следующей точки.

Маскировка служебных символов в регулярных выражениях, в отличии от других строк PowerShell, делается с помощью символа \ Например

\. Точка
\( Открывающая скобка
\\ Обратный слеш

Это традиционно для регулярных выражений, и обеспечивает возможность использовать в PowerShell без изменения выражения из других источников, и наоборот.

И еще один пример полезности нежадных квантификаторов:

PS C:\> "Теги <123> надо удалить <456>." -replace '<.+>'
Теги .
PS C:\> "Теги <123> надо удалить <456>." -replace '<.+?>'
Теги  надо удалить .

Впрочем… конкретно в этой ситуации можно поступить и иначе:

PS C:\> "Теги <123> надо удалить <456>." -replace '<[^>]+>'
Теги  надо удалить .

В тех случаях где это возможно, лучше выбирать второй вариант, он несколько производительнее.

Разумеется подобные "нежадные" модификаторы можно использовать не только в -replace:

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

В случае если в выражении присутствует несколько количественных модификаторов, которые могут захватить одну и ту же часть строки, то приоритет будет у первого. Но первый всегда уступит следующему, если это необходимо для совпадения выражения:

PS C:\Windows\system32> if ('123456' -match '^(\d+)(\d+)$') {$matches}

Name                           Value
----                           -----
2                              6
1                              12345
0                              123456

Тут первый \d+ захватил максимум цифр, оставив второму лишь минимально необходимое для него – одну. Если же использовать нежадные версии, то первый квантификатор постарается захватить минимум, а уж всё остальное придётся захватывать второму:

PS C:\Windows\system32> if ('123456' -match '^(\d+?)(\d+?)$') {$matches}

Name                           Value
----                           -----
2                              23456
1                              1
0                              123456

 

Продолжение: Select-String