Эффективное программирование в Windows PowerShell Разбираться в Windows PowerShell и получать от него больше
Скачать 0.88 Mb.
|
BIND arg [1] to param [Id] SKIPPED Parameter [Id] PIPELINE INPUT ValueFromPipeline WITH COERCION BIND arg [1] to parameter [Id] COERCE arg type [System.Management.Automation.PSObject] to [System.Int64[]] ENCODING arg into collection Binding collection parameter Id: argument type [PSObject], parameter type System.Int64[]], collection type Array, element type [System.Int64], coerceElementType Creating array with element type [System.Int64] and 1 elements Argument type PSObject is not IList, treating this as scalar COERCE arg type [System.Management.Automation.PSObject] to [System.Int64] CONVERT arg type to param type using LanguagePrimitives.ConvertTo CONVERT SUCCESSFUL using LanguagePrimitives.ConvertTo: [1] Adding scalar element of type Int64 to array position 0 Executing VALIDATION metadata: [System.Management.Automation.ValidateRangeAttribute] BIND arg [System.Int64[]] to param [Id] SUCCESSFUL MANDATORY PARAMETER CHECK on cmdlet [Get-History] CALLING ProcessRecord CALLING EndProcessing Заметьте, что при первой попытке PowerShell пытается конвертировать строку в массив Int64, но ему это не удаётся. Затем он пытается обработать исходные данные как PSObject. Он передаёт этот PSObject функции вспомогательного класса LanguagePrimitives.ConvertTo(), которая успешно преобразовывает '1' в массив Int64, содержащий элемент 1. Когда параметр помечен и как ByValue и как ByPropertyName одновременно, PowerShell пытается связать его с аргументами в следующем порядке: 1. Привязать по значению, без преобразования типов 2. Привязать по имени свойства без преобразования типов 3. Привязать по значению, с преобразованием типов 4. Привязать по имени свойства без преобразования типов Кроме того, еще используется дополнительная логика при нахождении лучшего совпадения среди нескольких наборов параметров. Последнее замечание, касающееся параметров. Файлы справки PowerShell создаются не полностью автоматически, и, как результат - они не всегда корректны. Например, посмотрите справку к Get-Content, и попробуйте найти упоминание параметра –Wait. Вы не найдете его. Однако метаданные командлета всегда содержат полную и точную информацию, например: PS> Get-Command Get-Content -Syntax Get-Content [-Path] [-Include ] [-Verbose] [-Debug] [-ErrorAction [-OutBuffer Get-Content [-LiteralPath] [-Include ] [-Verbose] 46 [-Debug] [-ErrorAction [-OutBuffer Надеюсь, эта глава добавила вам немного понимания о том, как работают параметры привязывающиеся по значению (ByValue), и не только. В итоге, практически, вам не нужно много знать о привязке параметров, потому что в большинстве случаев она работает интуитивно. Просто старайтесь обращать внимание на те параметры, которые привязываются по имени (ByPropertyName). Их привязка в конвейере не всегда является очевидной. 47 Часть 10: Регулярные выражения – один из мощнейших инструментов PowerShell Windows PowerShell основан на .NET Framework. То есть, он построен с использованием .NET Framework и предоставляет возможности .NET Framework пользователю. Одна из наиболее удобных возможностей в .NET Framework - класс Regex в пространстве имён System.Text.RegularExpressions. Это очень мощный механизм регулярных выражений. PowerShell использует его в ряде сценариев с помощью: • оператора -match • оператора -notmatch • параметра -Pattern командлета Select-String Конечно, чтобы использовать эти операторы и Select-String максимально эффективно, необходимо хорошо понимать применение регулярных выражений. Справочная система PowerShell содержит раздел с именем "about_Regular_Expression", который вы можете вызвать так: PS> help about_reg* Данный раздел представляет собой лишь краткое справочное руководство по различным метасимволам, с его помощью вы не научитесь создавать мощные регулярные выражения. Чтобы узнать, как получить максимальную отдачу от регулярных выражений, и, следовательно, PowerShell, я очень рекомендую прочитать книгу Jeffrey Friedl Mastering Regular Expressions (Джеффри Фридл, Регулярные выражения). В механизме поддержки регулярных выражений PowerShell существует один недостаток, который необходимо знать. Большинство других скриптовых языков поддерживают синтаксис, позволяющий обнаружить все вхождения шаблона в строке. Например, в Perl я могу сделать так: $_ = "paul xjohny xgeorgey xringoy stu pete brian"; # PERL script ($first, $second, $third) = /x(.+?)y/g; К сожалению, командлет Select-String не поддерживает эту возможность в версии 1.0. Тем не менее, вы можете обойти это ограничение, используя класс System.Text.RegularExpressions.Regex напрямую. Нет необходимости указывать имя класса полностью, PowerShell имеет для него псевдоним [regex]. Например: PS> $str = "paul xjohny xgeorgey xringoy stu pete brian" PS> $first,$second,$third = ([regex]'x(.+?)y').Matches($str) | >>Foreach {$_.Groups[1].Value} PS> $first john PS> $second george PS> $third ringo Одна из вещей, за которой необходимо тщательно следить - написание регулярного выражения для поиска во всей строке. Например, вы решили использовать Get-Content для получения содержимого файла и применения к нему регулярного выражения. В этом случае необходимо помнить, что Get- Content считывает файл частями - строка за строкой. Для применения регулярного выражения ко всему содержимому файла, необходимо будет преобразовать это содержимое в одну строку. Я могу сделать это в PowerShell 1.0, например, так: 48 PS> $regex = [regex]'(? PS> Get-Content foo.c | Join-String -Newline | Foreach {$regex.Matches($_)} | >> Foreach {$_.Groups["CMultilineComment"].Value} >> Здесь я использовал командлет Join-String, разработанный PowerShell Community Extensions, который принимает отдельные строки, выводимые Get-Content, и создаёт из них одну строку. Также в примере использован именованный захват CMultilineComment. Этот пример демонстрирует, что при отсутствии возможностей в самом PowerShell, использование среды .NET может стать замечательным "запасным выходом". Дополнение для PowerShell 2.0 В PowerShell 2.0 появились новые возможности, помогающие выполнять поиск в вышеописанном случае. Во-первых, добавлен оператор объединения, с помощью которого можно соединять многострочный текст в одну строку. Во-вторых, в Select-String появились новые параметры, такие как -Context, -NotMatch и -AllMatches. Параметр AllMatches как раз подходит для решения предыдущей задачи. Вот как можно выполнить поиск комментариев в PowerShell 2.0: $pattern = '(? PS> (get-content .\foo.c) -join "`n" | Select-String $pattern -all | >>Foreach {$_.Matches} | Foreach {$_.Value} Использование регулярных выражений - очень мощный инструмент PowerShell. Научитесь его использовать, и он откроет вам много возможностей поиска и обработки текстовых данных. 49 Часть 11: Сравнение массивов В PowerShell используется несколько полезных операторов вроде -contains, которые проверяют, содержит ли массив заданный элемент. Но насколько я могу сказать, в PowerShell отсутствует простой способ проверить, является ли содержимое двух массивов идентичным. Такая потребность возникает довольно часто, и меня слегка удивило отсутствие такой возможности. Я столкнулся с такой необходимостью при ответе на вопрос в группе новостей microsoft.public.windows.powershell. Его автор спрашивал о поиске файлов с кодировкой UTF-8 при помощи проверки метки порядка байтов (англ. Byte Order Mark, BOM - способ определения формата представления Юникода в текстовом файле; для обозначения формата UTF-8 в заголовке файла используется последовательность EF BB BF - прим. переводчика). Одним из простых способов решения такой задачи может быть таким: PS> $preamble = [System.Text.Encoding]::UTF8.GetPreamble() PS> $preamble | foreach {"0x{0:X2}" -f $_} 0xEF 0xBB 0xBF PS> $fileHeader = Get-Content Utf8File.txt -Enc byte -Total 3 PS> $fileheader | foreach {"0x{0:X2}" -f $_} 0xEF 0xBB 0xBF Визуально, конечно, легко проверить, совпадают значения или нет. Но, к сожалению, визуальная проверка не работает в сценариях. Можно также проверять каждый отдельный элемент, что годится для трех элементов массива, но когда вы столкнётесь, скажем, с 10 элементами, этот подход начинает выглядеть утомительным. Вы думаете, что мы могли бы просто сравнить эти два массива непосредственно примерно так: PS> $preamble -eq $fileHeader | Get-TypeName WARNING: Get-TypeName did not receive any input. The input may be an empty collection. You can either prepend the collection expression with the comma operator e.g. ",$collection | gtn" or you can pass the variable or expression to Get-TypeName as an argument e.g. "gtn $collection". PS> $preamble -eq 0xbb 187 ЗАМЕЧАНИЕ: Get-TypeName - это функция, написанная сообществом PowerShell Community Extensions. Сравнение массивов с помощью оператора -eq на самом деле не сравнивает содержимое этих двух массивов. Как показывает код в первой строке, эта конструкция ничего не возвращает. Когда слева от оператора -eq расположен массив, PowerShell возвращает элементы массива, значение которых совпадает со значением, заданным справа от оператора. Последние две строки в примере выше это поясняют - при сравнении с '0xbb' возвращен элемент со значением 187. Похоже, нам придётся создать свой собственный механизм сравнения массивов. Вот один из способов: 50 function AreArraysEqual($a1, $a2) { if ($a1 -isnot [array] -or $a2 -isnot [array]) { throw "Both inputs must be an array" } if ($a1.Rank -ne $a2.Rank) { return $false } if ([System.Object]::ReferenceEquals($a1, $a2)) { return $true } for ($r = 0; $r -lt $a1.Rank; $r++) { if ($a1.GetLength($r) -ne $a2.GetLength($r)) { return $false } } $enum1 = $a1.GetEnumerator() $enum2 = $a2.GetEnumerator() while ($enum1.MoveNext() -and $enum2.MoveNext()) { if ($enum1.Current -ne $enum2.Current) { return $false } } return $true } Он работает так, как и ожидалось: PS> AreArraysEqual $preamble $fileHeader True Однако существует способ сделать сравнение и штатными средствами PowerShell, но он не совсем очевиден. По крайней мере, для меня. PS> @(Compare-Object $preamble $fileHeader -sync 0).Length -eq 0 True Compare-Object сравнит массивы, и если различий не будет, на выход ничего передано не будет. Если мы "обернём" вывод Compare-Object в массив с помощью оператора @(), то получим массив, содержащий 0 или более элементов, в зависимости от результата. Таким образом, если длина этого массива будет равна 0, это будет означать, что сравниваемые массивы равны. Compare-Object сравнивает два объекта с точки зрения одинакового набора элементов. Обычно при этом не учитывается последовательность расположения элементов. Посмотрите: PS> $a1 = 1,1,2 PS> $a2 = 1,2,1 PS> @(Compare-Object $a1 $a2).length -eq 0 True 51 Скажем так, это не совсем то, когда мы говорим о сравнении на эквивалентность. К счастью, можно использовать параметр SyncWindow, установив ему значение 0. Это заставит Compare-Object сравнивать объекты, соблюдая последовательность расположения элементов в них. Давайте замерим производительность этих двух методов: PS> $a1 = 1..10000 PS> $a2 = 1..10000 PS> (Measure-Command { AreArraysEqual $a1 $a2 }).TotalSeconds 1.236252 PS> (Measure-Command { @(Compare-Object $a1 $a2 -sync 0).Length -eq 0 }).TotalSeconds 0.3259954 Как видим, Compare-Object "уверенно побеждает" мою функцию AreArraysEqual, что, впрочем, и не удивительно*. * - за исключением случая, когда происходит сравнение объектов, ссылающихся на один и тот же массив. В этом случае функция оказывается на два порядка быстрее. Видимо, Compare-Object не использует проверку System.Object.ReferenceEquals. Следует признать, что это частный случай сценария. Всё-таки, Compare-Object использует скомпилированный код, а функция - интерпретируется. Если вам необходим быстрый способ сравнить массивы, просто запомните, что массивы - это тоже объекты, а для сравнения объектов лучше всего использовать Compare-Object. 52 Часть 12: Старайтесь использовать Set-PSDebug -Strict в своих сценариях Windows PowerShell, как и многие другие динамические языки, позволяет использовать переменные без объявления типа и без присваивания им начальных значений. Это удобно для интерактивного использования, и вы можете делать что-то наподобие этого: PS> Get-ChildItem | Foreach -Process {$sum += $_.Name.Length} -End {$sum} Здесь переменная $sum не объявлена, но мы добавляем к ней значение и присваиваем его. PowerShell просто берёт значение $null и преобразует его в 0 в этом случае. Попробуйте сами ввести: PS> $xyzzy -eq $null True Это не означает, что ранее эта переменная была объявлена. Конечно, мы можем убедится в том, что она не определена, обратившись к диску переменных: PS> Test-Path Variable:\xyzzy False Так почему мы должны стараться использовать Set-PSDebug -Strict в сценариях? Дело в том, что однажды вы можете ошибиться, допустив, например, досадную опечатку в коде. На обнаружение такой ошибки и её устранение вам придётся тратить время. Возможно, вы захотите избегать повторения таких ошибок в будущем. Возьмём для примера такой скрипт: $suceeded = test-path C:\ProjectX\Src\BuiltComponents\Release\app.exe if ($succeeded) { ... } else { ... } Этот скрипт содержит ошибку, о которой вам не сообщит PowerShell. Он с радостью покажет build failed даже в случае существования файла app.exe. Потому что в названии переменной, которой присваивается результат проверки пути файла, допущена небольшая опечатка. Здесь, в маленьком фрагменте, вероятно, обнаружить опечатку не столь сложно, но если речь будет идти о сценарии в несколько сотен строк? Вы можете предотвратить проблемы такого рода, поместив команду Set-PSDebug -Strict в начале файла сценария, сразу после инструкции param() (если она имеется). Например, этот же сценарий в виде Foo.ps1: 53 Set-PSDebug -Strict $suceeded = test-path C:\ProjectX\Src\BuiltComponents\Release\app.exe if ($succeeded) { "yeah" } else { "doh" } PS C:\Temp> .\foo.ps1 The variable $succeeded cannot be retrieved because it has not been set yet. At C:\Temp\foo.ps1:6 char:14 + if ($Succeded) <<<< { Что бы произошло если бы мы не написали Set-PSDebug -Strict? Сценарий будет всегда выводить "doh". В некоторых случаях избежать подобные ошибки помогает инициализация переменных. Возможно, название этой части звучит и слишком "перестраховочно", и вы можете не использовать Set-PSDebug -Strict при написании сценариев. Как всегда - решать вам. Примечание для PowerShell 2.0 В PowerShell 2.0, следует использовать новый командлет Set-StrictMode: param(...) Set-StrictMode –version Latest Set-StrictMode проверяет больше, чем использование только инициализированных переменных. Он также осуществляет проверку ссылок на несуществующие свойства, вызовы функций и методов .NET и неименованные переменные типа ${}. 54 Часть 13: Комментирование строк в файле сценария Windows PowerShell 1.0 не позволяет комментировать несколько строк. В версии 2.0 этот недостаток устранён, но я расскажу об этом в конце этого раздела. Если вы используете исключительно PowerShell 2.0, я бы всё равно рекомендовал ознакомиться с этим разделом, потому что в нём описаны некоторые особенности использования многострочных текстовых переменных (here-string). Многострочные комментарии полезны, когда необходимо закомментировать сразу несколько строк в сценарии. Существует и другой допустимый приём - использование многострочных текстовых переменных. Они позволяет ввести несколько строк кода, предотвращая их немедленную интерпретацию. Однако то, как будет расценивать содержимое строки интерпретатор PowerShell, зависит от типа используемой строки. Например, в двойных кавычках переменные будут заменены их значениями, а выражения - выполняться. Пример строки с двойными кавычками, в которой выполнено выражение : PS> @" >> $(get-process) >> "@ >> System.Diagnostics.Process (audiodg) System.Diagnostics.Process (csrss) … А в строке с одинарными кавычками такого не произойдёт: PS> @' >> $(get-process) >> '@ >> $(get-process) Используйте многострочный текст с одинарными кавычками для комментирования строк сценария, так как в них никакие выражения не выполняются. Необходимо лишь помнить, что строка - это выражение, и если больше ничего не делать, её содержимое будет выведено в консоль. Обычно бывает нежелательно отображать закомментированные строки на экране. Чтобы избежать вывода этой информации, можно привести строку к типу [void] или перенаправить её в $null: [void]@' "Получаем информацию о процессах" get-process | select Name, Id "Останавливаем процессы с именем vd*" stop-process -name vd* '@ Это позволяет эффективно комментировать строки скрипта. У этого метода существует несколько особенностей. После начальных символов последовательности @' не должно быть пробелов. Если после этой последовательности будет стоять хотя бы один пробел, вы получите вот такую загадочную ошибку: Unrecognized token in source text. At C:\Temp\foo.ps1:1 char:1 + @ <<<< ' 55 Далее, закрывающие символы '@ должны начинаться с начала строки, иначе вы получите ещё одну загадку: Encountered end of line while processing a string token. At C:\Temp\foo.ps1:1 char:3 + @' <<<< Ещё одна особенность - в PowerShell 1.0 нельзя вложить одну автономную строку в другую строку того же типа. Это означает, что вы не можете использовать строку с одинарными кавычками для окружения комментарием части сценария, если внутри этой части уже содержится строка с одинарными кавычками. Дополнение для PowerShell 2.0 В PowerShell 2.0 введена корректная поддержка многострочных комментариев. Используются они так: <# Это многострочный комментарий в PowerShell 2.0 #> И наконец-то, многострочный текст в PowerShell 2.0 может быть вложен друг в друга: @" $(Get-Process | Foreach { @" `r`n "@ }) "@ 56 |