Эффективное программирование в Windows PowerShell Разбираться в Windows PowerShell и получать от него больше
Скачать 0.88 Mb.
|
16 Часть 4: Разнообразие вывода - скаляры, коллекции и пустые наборы - о, боже! В части 2: Понимание вывода объектов , мы уже рассматривали некоторые основные понятия вывода PowerShell. Однако, для эффективного использования PowerShell необходимо понимать еще кое-что. В этой части мы поговорим о разнообразии вывода. Когда вывод является скалярным (то есть имеет одно значение), а когда - коллекцией? А в некоторых случаях вывод может вообще отсутствовать, образуя пустое множество. Я использую термин "коллекция" в широком смысле, для различных типов, включая массивы. Скаляры Работа со скалярами проста. Все следующие примеры создают скалярные значения: PS> $num = 1 PS> $str = "Hi" PS> $flt = [Math]::Pi PS> $proc = (get-process)[0] PS> $date = Get-Date Тем не менее, вы можете иметь дело со скалярами в тот момент, когда думаете, что работаете с коллекцией. Например, когда вы отправляете коллекцию на конвейер, PowerShell автоматически "разбирает" ее на части, отправляя дальше по конвейеру каждый отдельный элемент коллекции один за другим. Вот пример: PS> filter Get-TypeName {$_.GetType().Fullname} PS> $array = "hi",1,[Math]::Pi,$false PS> $array | Get-TypeName System.String System.Int32 System.Double System.Boolean Фактически, конвейер не оперирует начальной коллекцией целиком. В подавляющем большинстве случаев разбор коллекции на элементы внутри конвейера - это то, что вам и требуется. В противном случае для принудительного расщепления коллекции вам пришлось бы писать такой код: PS> foreach ($item in $array) {$item} | Get-TypeName Заметим, что это потребовало включения дополнительного оператора foreach в конвейере. Поскольку конвейер, как правило, работает с элементами последовательности, а не с целой последовательностью, довольно разумно, что разбиение на элементы происходит автоматически. Тем не менее, случаются ситуации, когда разделение на элементы недопустимо. Для этого случая есть две новости - хорошая и плохая. Для начала сообщим плохую - технически, такое поведение изменить невозможно. PowerShell всегда расщепляет коллекции. Хорошая новость в том, что мы можем обойти это поведение, создавая новую коллекцию, которая будет содержать только один элемент - начальную коллекцию. Например, именно так я бы изменил предыдущий пример, чтобы отправить неизменённый массив далее по конвейеру, а не каждый его отдельный элемент: 17 PS> ,$array | Get-TypeName System.Object[] Едва различимое изменение - запятая перед $array. Эта запятая - унарный оператор, заставляющий PowerShell "обернуть" объект, следующий за ней, в новый массив, содержащий единственный элемент - начальный объект. Таким образом, на выходе мы получим именно тот результат, который нам нужен. Еще одна особенность обработки скаляров PowerShell заключается в том, как оператор foreach обрабатывает скаляры. Например, следующий сценарий может удивить разработчиков C#: PS> $vars = 1 PS> foreach ($var in $vars) { "`$var is $var" } $var is 1 В языках типа C# переменная $vars должна представлять коллекцию (IEnumerable), иначе возникнет ошибка выполнения. Для PowerShell это не является проблемой. Если переменная $vars окажется скалярной, PowerShell просто обработает ее так же, как и коллекцию с единственным элементом. Опять же, это хорошая вещь в PowerShell иначе, если бы мы писали код, подобный этому: PS> $files = Get-ChildItem *.sys PS> foreach ($file in $files) { "File is: $file" } File is: C:\config.sys то его бы пришлось модифицировать в случае, если Get-ChildItem обнаружит только один .sys-файл. Наш сценарий не страдает от избытка строк, осуществляющих проверку на тип данных - являются они скаляром или коллекцией. Теперь проницательный читатель может спросить: "Хорошо, а если Get- ChildItem не найдет вообще ни одного .sys-файла?". Немного подождите с ответом. Работа с коллекциями Работа с коллекциями в PowerShell также проста. Все примеры ниже создают коллекции: PS> $nums = 1,2,3+7..20 PS> $strs = "Hi", "Mom" PS> $flts = [Math]::Pi, [Math]::E PS> $procs = Get-Process Иногда необходимо, чтобы результат рассматривался как коллекция, даже если возвращена может быть лишь одна величина (скаляр). PowerShell предлагает для этого случая оператор массива. Давайте посмотрим на нашу команду Get-ChildItem ещё раз. В этот раз мы принудительно заставим результат быть коллекцией: PS> $files = @(Get-ChildItem *.sys) PS> $files.GetType().Fullname System.Object[] PS> $files.length 1 В нашем случае найден один файл. Здесь также важно знать, что и коллекция, и FileInfo в случае единичного файла имеют свойство Length. Это может ввести в заблуждение. Учитывая, что запятая 18 (оператор) заносит объект в новый массив, как с ней работает оператор, создающий массив? Давайте посмотрим: PS> $array = @(1,2,3,4) PS> $array.rank 1 PS> $array.length 4 Как видим, в этом случае запятая не оказывает влияния - размерность массива не изменяется. Ну и всё- таки, а что будет, если Get-ChildItem не вернёт ничего? Работа с пустыми наборами Давайте посмотрим, что произойдет, если команда не возвращает ничего. Это довольно сложная тема, и необходимо понимать несколько вещей, чтобы избежать ошибок в сценариях. Сначала запишем несколько правил: 1. Результатом команды может являться поток вывода, но его может и не быть - то, что я назвал пустым набором 2. Если переменной присваивается результат команды, для представления пустого набора используется $null 3. Оператор forEach будет выполнен с каждым скаляром, даже если его значение - $null Кажется простым, верно? Однако, их сочетания могут быть столь удивительными, что вызовут проблемы при написании сценариев. Вот пример: PS> function GetSysFiles { } PS> foreach ($file in GetSysFiles) { "File: $file" } PS> GetSysFiles не содержит какого-либо результата, и оператору forEach, нечего перебирать, так как вызов GetSysFiles ничего не возвращает. Что ж, давайте попробуем немного изменить задачу. Предположим, что вызов функции содержит длинный список аргументов, и мы решили выделить её вызов в отдельную строку, вот так: PS> $files = GetSysFiles SomeReallyLongSetOfArguments PS> foreach ($file in $files) { "File: $file" } File: Хм, теперь мы получили вывод. Хотя всё, что мы сделали - добавили промежуточную переменную, содержащую вывод функции. Если честно, мне кажется это нарушением принципа " как можно меньше сюрпризов " . Позвольте мне объяснить, что случилось. Используя временную переменную, мы вызвали правило №2. При присваивании переменной результата, который явился пустым множеством, он был конвертирован в $null и назначен $files. Пока кажется разумным, верно? К несчастью, оператор forEach следует правилу №3 - и выполняет итерацию, так как он получил уже скаляр, пусть и со значением $null. В общем, сам PowerShell обрабатывает ссылки на $null довольно правильно. Отметим, что изменение строки кода в вышеприведённом примере не вызвало никаких ошибок при обнаружении $null. Однако методы .NET Framework могут вести себя не столь "безобидно": 19 PS> foreach ($file in $files) { "Basename: $($file.Substring(0,$file.Length-4))" } Нельзя вызвать метод для выражения со значением NULL. В строка:1 знак:16 + $file.Substring( <<<< 0,$file.Length-4) Basename: " Хьюстон, у нас проблема ". Это означает, что необходимо быть осторожным при использовании forEach для перебора результатов команды в том случае, когда не ясно, какое количество элементов будет возвращено, а сценарий не сможет обработать перебор $null. Использование оператора массива может помочь в этом случае, но крайне важно использовать его в правильном месте. Например, вот такая конструкция тоже не работает: PS> foreach ($file in @($files)) { "Basename: $($file.Substring(0,$file.Length-4))" } Нельзя вызвать метод для выражения со значением NULL. В строка:1 знак:16 + $file.Substring( <<<< 0,$file.Length-4) Basename: Так как $files уже имеет значение $null, оператор создания массива просто создает массив с одним элементом - $null, который forEach "успешно" обрабатывает. Я рекомендую помещать вызов функции полностью в конструкцию forEach, если её вызов достаточно лаконичен. Оператор forEach прекрасно знает что делать, если функция не возвратит результат. Если же вызов функции достаточно длинный, его можно сделать таким образом: PS> $files = @(GetSysFiles SomeReallyLongSetOfArguments) PS> foreach ($file in $files) { "Basename: $($file.Substring(2))" } PS> При применении оператора массива непосредственно к функции, которая ничего не возвращает, вы получите пустой массив, а не массив с $null в нём. Если вы хотите, чтобы ваши функции, имели возможность возвращать пустые массивы, используйте оператор-запятую, как показано ниже, для обеспечения возврата результатов в форме массива. function ReturnArrayAlways { $result = @() # Здесь что-то, что может добавить 0, 1 или более элементов к массиву $result # $result = 1 # or # $result = 1,2 ,$result } 20 Часть 5: Используй объекты, Люк. Используй объекты! Использование Windows PowerShell требует изменений в вашем отношении к тому, как оболочка обрабатывает информацию. В большинстве других оболочек вы имеете дело в первую очередь с информацией в виде текста. PowerShell предоставляет много полезных функций для манипуляций с текстом: -like -notlike -match -notmatch -replace -eq -ne -ceq (case-sensitive) -cne (case-sensitive) По умолчанию, PowerShell обрабатывает текст (точнее, объекты типа System.String), не обращая внимания на регистр в операциях сравнения, поиска или замены регулярных выражений. Из-за удобств этих функций легко продолжать пользоваться методами разбора и сравнения строк. Иногда даже этого не избежать. Но лучше стараться использовать свойства предоставляемых объектов. Вот основные причины для этого: • простое понимание кода • легче избежать ошибок (связанных с изменением формата, неправильных регулярных выражений, неверной техники сравнения) • более высокая производительность Вот, например, вопрос из группы новостей public.microsoft.windows.powershell: "Как проверить вывод dir или Get-ChildItem, так, чтобы отфильтровать каталоги, а файлы отправить на конвейер?" Вот подход, основанный на прежнем методе: PS> Get-ChildItem | Where {$_.mode -ne "d"} Во-первых, скажу сразу, что он не работает. Во-вторых, и это более важно, он полагается на сравнение строк при определении, отправлять или нет элемент на конвейер. Если вы уж хотите осуществлять проверку таким способом, можно попробовать следующий вариант: PS> Get-ChildItem | Where {$_.mode -notlike "d*"} Однако в случае неосторожного использования легко получить неверные результаты. Более лучший подход к решению подобных проблем - способ PowerShell. PowerShell присваивает каждому элементу на выходе командлета Get-ChildItem (или других *-Item командлетов) дополнительные свойства. Это не зависит от используемого провайдера: файловой системы, реестра, функций и т. д. Мы можем увидеть эти дополнительные свойства, которые содержат приставку PS, с помощью "нашего старого друга" Get- Member таким образом: 21 PS> cd function: PS Function:\> New-Item -type function "foo" -value {} | Get-Member TypeName: System.Management.Automation.FunctionInfo Name MemberType Definition ---- ---------- ---------- Equals Method System.Boolean Equals(Object obj) GetHashCode Method System.Int32 GetHashCode() GetType Method System.Type GetType() ToString Method System.String ToString() PSDrive NoteProperty System.Management.Automation.PSDriveInfo PSDrive=Function PSIsContainer NoteProperty System.Boolean PSIsContainer=False PSPath NoteProperty System.String PSPath=Microsoft.PowerShell.Core\Function::foo PSProvider NoteProperty System.Management.Automation.ProviderInfo PSProvider=Microsof CommandType Property System.Management.Automation.CommandTypes CommandType {get;} Definition Property System.String Definition {get;} Name Property System.String Name {get;} Options Property System.Management.Automation.ScopedItemOptions Options {get;s ScriptBlock Property System.Management.Automation.ScriptBlock ScriptBlock {get;} Одно из этих дополнительных свойств - PSIsContainer, сообщает нам, что объект является контейнером. В случае с реестром это значит ключ реестра, для файловой системы - это означает каталог (объект DirectoryInfo). Поэтому задача может быть решена так: PS> Get-ChildItem | Where {!$_.PSIsContainer} Это значительно короче и менее подвержено ошибкам. А что у нас с производительностью? Давайте сравним оба метода (я даже добавлю ещё один с проверкой регулярного выражения и параметром -notmatch) и измерим производительность: PS> $oldWay1 = 1..20 | Measure-Command {Get-ChildItem | Where {$_.mode -notlike "d*"}} PS> $oldWay2 = 1..20 | Measure-Command {Get-ChildItem | Where {$_.mode -notmatch "d"}} PS> $poshWay = 1..20 | Measure-Command {Get-ChildItem | Where {!$_.PSIsContainer}} Вот результат: PS> $oldWay1 | Measure-Object TotalSeconds -ave Count : 1 Average : 169.2571743 Sum : Maximum : Minimum : Property : TotalSeconds PS> $oldWay2 | Measure-Object TotalSeconds -ave 22 Count : 1 Average : 181.929144 Sum : Maximum : Minimum : Property : TotalSeconds PS> $poshWay | Measure-Object TotalSeconds -ave Count : 1 Average : 61.5349126 Sum : Maximum : Minimum : Property : TotalSeconds Немного математики, в PowerShell конечно же, и мы получим: PS> "{0:P0}" -f ((169.26 – 61.53) / 61.53) 175 % Надо же! Использование сравнения строк медленнее на 175%, чем использование свойства PSIsContainer. С помощью PowerGadgets, разработанных SoftwareFX, это можно представить наглядно: PS> $data = @{ >> 'Mode-Notlike' = $oldWay1.TotalSeconds >> 'Mode-Notmatch' = $oldWay2.TotalSeconds >> PSIsContainer = $poshWay.TotalSeconds >> } >> PS> $data.Keys | Select @{n='Method';e={$_}},@{n='TotalSeconds';e={$data[$_]}} | >> Out-Chart -Title "PSIsContainer vs Mode" >> 23 PowerGadgets - очень удобный инструмент. Я использую его для презентации использования управлениями версиями менеджерам проекта. Консоль PowerShell создаёт иллюзию, что вы работаете с текстом - на самом деле вы имеете дело с .NET- объектами, даже если они представлены текстом. Вы часто имеете дело с объектами, содержащими больше информации, чем System.String и часто эти объекты содержат необходимую вам информацию в виде свойств. Можно извлекать эту информацию, не прибегая к помощи синтаксического разбора текста. Дополнительный пример работы со свойствами объектов вместо операций с текстом я описал здесь - ( http://tinyurl.com/PsSortIP ). 24 Часть 6: Как форматируется вывод Я уже говорил ранее, практически всё в Windows PowerShell возвращает объекты .NET почти для всего. Get-ChildItem выводит последовательность объектов System.IO.FileInfo и System.IO.DirectoryInfo. Get-Date выводит объект System.DateTime. Get-Process выводит объекты System.Diagnostics.Process, Get-Content выводит объект System.String (или их массив, в зависимости от параметра -ReadCount). PowerShell всегда имеет дело с .NET-объектами. Отображение текста в консоли делает это не всегда очевидным. Представим себе на минуту, что нам бы самим понадобилось преобразовать объект в текст, который необходимо вывести на экран. Вероятно, первым делом мы бы рассмотрели метод ToString(), имеющийся для каждого .NET-объекта. Это отлично работает для некоторых объектов .NET: PS> (Get-Date).ToString() 9/3/2007 10:21:23 PM Но не очень подходит для других: PS> (Get-Process)[0].ToString() System.Diagnostics.Process (audiodg) Как видим, нам предоставляется очень мало информации. Давайте посмотрим, как разработчики PowerShell решали эту проблему. Они придумали понятие "вида" для популярных типов .NET, которые могут отображать объекты таблицей, списком, столбцами или пользовательским способом. Для известных типов .NET у PowerShell есть описание по умолчанию, позволяющее выводить текстовую информацию в подходящем виде без необходимости указания форматирования. Для типов, с которыми PowerShell "не знаком", необходимо выбрать способ форматирования. Если вы не зададите форматирование, PowerShell сам выберет один из стандартных видов форматирования. Краткое определение для типов и объектов. Класс System.DateTime это лишь один из типов .NET. Командлет Get-Date выводит объект, который является экземпляром типа System.DateTime. Может существовать много объектов DateTime, основанных на System.DateTime. PowerShell определяет вид для всех экземпляров объектов этого типа. Что делать, если PowerShell не сможет определить вид для какого-либо типа? Это вполне возможно, поскольку типов в .NET может быть бесконечное множество. Я могу прямо сейчас создать тип Plan9FromOuterSpace, скомпилировать его в сборку .NET и загрузить её в PowerShell. Как будет работать с ним PowerShell, если он ничего не знает о таком типе? Давайте посмотрим: @' public class Plan9FromOuterSpace { public string Director = "Ed Wood"; public string Genre = "Science Fiction B Movie"; public int NumStars = 0; } '@ > C:\temp\Plan9.cs PS> csc /t:library Plan9.cs PS> [System.Reflection.Assembly]::LoadFrom('c:\temp\Plan9.dll') PS> New-Object Plan9FromOuterSpace Director Genre NumStars -------- ----- -------- Ed Wood Science Fiction B Movie 0 |