Эффективное программирование в Windows PowerShell Разбираться в Windows PowerShell и получать от него больше
Скачать 0.88 Mb.
|
37 PS> Get-Process | Select -first (1.5 * 2) Handles NPM(K) PM(K) WS(K) VM(M) CPU(s) Id ProcessName ------- ------ ----- ----- ----- ------ -- ----------- 239 9 64840 65644 189 34,59 224 AcroRd32 36 2 328 1184 14 0,06 496 ADCDLicSvc 107 6 1136 3500 33 0,06 2440 alg Используя возможность начать новый режим, можно вложить команды в команды. Это очень эффективная возможность. В следующем примере PowerShell успешно анализирует строку в режиме команд, пока не встретит выражение '@('. В этом месте начнётся новое определение режима разбора, но будет обнаружена следующая команда. Эта команда будет получать имя файла для переименования из его первой строки. Я использую подвыражение массива для того, чтобы он гарантированно вернул массив строк, даже если файл содержит только одну строку. Если вместо него использовать простое подвыражение и файл будет содержать только одну строку, то PowerShell требование вернуть элемент с индексом [0] расценит так - "вернуть первый символ, встреченный в строке". Тогда это будет соответствовать символу "f" в нижеприведённом примере. PS> Get-ChildItem [a-z].txt | >>Foreach{Rename-Item $_ -NewName @(Get-Content $_)[0] -WhatIf} What if: Performing operation "Rename File" on Target "Item: C:\a.txt Destination: C:\file_a.txt". What if: Performing operation "Rename File" on Target "Item: C:\b.txt Destination: C:\file_b.txt". Последняя тонкость, на которую я бы хотел обратить ваше внимание - различие между использованием оператора исполнения & и вызовом команды с помощью точки. Рассмотрим вызов простого скрипта, который определяет переменную $foo = 'PowerShell Rocks!'. Давайте выполним этот сценарий, используя оператор вызова и посмотрим его воздействие на глобальную сессию: PS> $foo PS> & .\script.ps1 PS> $foo Как видим, оператор вызова запускает команду в дочерней области видимости, которая будет утеряна по окончании работы команды (сценария, функции и т. д.). То есть, такой способ не оказывает влияния на значение переменной $foo в глобальной области видимости. Теперь попробуем с точкой: PS> $foo PS> . C:\Users\Keith\script.ps1 PS> $foo PowerShell Rocks! Если мы ставим в начале строки точку, сценарий будет выполнен в текущей области видимости. В результате переменная $foo из скрипта script.ps1 становится ссылкой на глобальную переменную $foo (если сценарий вызван из командной строки с точкой), и сценарий успешно изменяет значение глобальной переменной $foo. Такое поведение не несёт сюрпризов - оно сходно с поведением в других командных оболочках. Эти же правила применяются к вызову функций. Однако для внешних exe-файлов неважно, производится их вызов точкой или оператором вызова. Выполнение exe-файлов происходит в отельном потоке и не оказывает влияния на текущую область видимости. 38 Вот полезная таблица, которая поможет вам запомнить правила, определяющие режимы синтаксического анализа PowerShell Первый непробельный символ Режим [_aA-zZ], &, . или \ Режим команд [0-9], ', ", $, (, @ и любой другой символ, не являющийся символом режима команд Режим выражений Поняв тонкости режимов анализа, вы сможете избежать сюрпризы, которые получают начинающие - например, как выполнить exe-файл, в пути которого содержатся пробелы. 39 Часть 8: Параметры привязки элементов конвейера ByPropertyName (по имени) Нам всем нравится решать проблемы эффективными способами. Кульминацией эффективности в PowerShell являются команды в одну строку. В целях обучения, я считаю, гораздо лучше расширить эту лаконичность, составляя команды в несколько строк. Вместе с тем нельзя отрицать, что когда вам необходимо что-то быстро набрать в консоли PowerShell с её возможностями редактирования, менее утомительно использовать однострочные команды. Это не вина PowerShell - он использует антикварную консольную подсистему Windows, которая не менялась со врёмен выхода NT в 1993 году. Одна из возможностей, позволяющих сократить длину вводимых команд - использование привязки параметров конвейера. Я часто вижу людей, которые пишут команды вроде этой: PS> Get-ChildItem . *.cs -r | Foreach { Get-Content $_.fullname } | ... Это вполне работает, но использование Foreach-Object не является здесь технически необходимым. Многие командлеты PowerShell связывают свой "первичный" параметр с тем, что передаётся по конвейеру. Это явно указывается в справке для Get-Content: ПАРАМЕТРЫ -path Определяет путь к элементу. Командлет Get-Content извлекает содержимое элемента. Подстановочные знаки разрешены. Имя параметра ("-Path" или "-FilePath") можно не указывать. Требуется? true Позиция? 1 Значение по умолчанию Невозможно - должен быть указан путь Принимать входные данные конвейера? true (ByPropertyName) Принимать подстановочные знаки? true -literalPath Определяет путь к элементу. В отличие от значения Path, значение LiteralPath используется точно так, как оно введено. Никакие знаки не интерпретируются как подстановочные знаки. Если путь включает знаки управляющих последовательностей, его нужно заключить в одинарные кавычки. Одинарные кавычки указывают оболочке Windows PowerShell, что никакие знаки не следует интерпретировать как знаки управляющих последовательностей. Требуется? true Позиция? 1 Значение по умолчанию Принимать входные данные конвейера? true (ByPropertyName) Принимать подстановочные знаки? false Примечание: для такой подробной детализации используйте Get-Help с параметром -Full. 40 Из четырёх параметров Get-Content, которые принимают входные данные конвейера (ByPropertyName), я привёл лишь два. Ещё два - ReadCount и TotalCount. Квалификатор ByPropertyName просто означает, что если объект, поступающий на вход элемента конвейера, имеет свойство с тем же именем, оно "привязывается" как входной параметр. Это действует в том случае, если типы совпадают или могут быть преобразованы. Например, вышеприведённую команду можно записать и так - убрав командлет Foreach-Object: PS> Get-ChildItem . *.cs -r | Get-Content | ... Интуитивно понятно, что Get-Content вполне способен оперировать с объектами System.IO.FileInfo, которые выведет Get-ChildItem. Тем не менее, это не явное использование только что упомянутого правила ByPropertyName. Почему? Объект FileInfo, выводимый командлетом Get-ChildItem не имеет свойств Path или LiteralPath похожих на PSPath. Каким же образом Get-Content определяет путь файла в этом сценарии? Есть два способа узнать это. Первый, наиболее простой - использование командлета Trace-Command, показывающего, как привязываются параметры. Второй - с помощью сборки Red Gate .NET Reflector. Давайте сначала попробуем Trace-Command. Trace-Command - это встроенное средство, показывающее часть внутренних процессов, производимых PowerShell. Я хочу предупредить, что этот командлет выводит достаточно много данных. Одним из полезных применений его может быть трассировка привязки параметров. Например, для команды, указанной выше, это будет выглядеть так: PS> Trace-Command -Name ParameterBinding -PSHost -Expression { Get-ChildItem log.txt | Get-Content } Он выводит значительное количество текста, и, к сожалению, выводит его в отладочный поток, который не так легко найти или перенаправить в файл. Ну да ладно. Нам более всего интересна вот эта часть: BIND PIPELINE object to parameters: [Get-Content] PIPELINE object TYPE = [System.IO.FileInfo] RESTORING pipeline parameter's original values Parameter [ReadCount] PIPELINE INPUT ValueFromPipelineByPropertyName NO COERCION Parameter [TotalCount] PIPELINE INPUT ValueFromPipelineByPropertyName NO COERCION Parameter [Path] PIPELINE INPUT ValueFromPipelineByPropertyName NO COERCION Parameter [Credential] PIPELINE INPUT ValueFromPipelineByPropertyName NO COERCION Parameter [ReadCount] PIPELINE INPUT ValueFromPipelineByPropertyName NO COERCION Parameter [TotalCount] PIPELINE INPUT ValueFromPipelineByPropertyName NO COERCION Parameter [LiteralPath] PIPELINE INPUT ValueFromPipelineByPropertyName NO COERCION BIND arg [Microsoft.PowerShell.Core\FileSystem::C:\Users\Keith\log.txt] to parameter [LiteralPath] Здесь я привожу лишь часть той информации, которую выводит Trace-Command. Из неё следует, что PowerShell пытался связать объект FileInfo, с параметрами командлета Get-Content. Попытки преобразования были безуспешными (NO COERCION), за исключением параметра LiteralPath. Это показывает, как Get-Content получает путь к файлу, однако никак не проясняет самой ситуации. Объект FileInfo не имеет свойства LiteralPath или расширенных свойств с именем LiteralPath. Можно использовать второй способ - .NET Reflector и просмотреть исходный код PowerShell. После запуска .NET Reflector и загрузки сборки Microsoft.PowerShell.Commands.Management.dll необходимо найти GetContentCommand и посмотреть параметр LiteralPath: 41 [ Alias ( new string [] { "PSPath" })] [ Parameter (Position = 0, ParameterSetName = " LiteralPath ", Mandatory = true , ValueFromPipeline = false , ValueFromPipelineByPropertyName = true )] public string [] LiteralPath { } Обратите внимание на атрибут Alias этого параметра. Он создаёт ещё одно верное имя для параметра LiteralPath - PSPath. Оно соответствует расширенному свойству PSPath, которое PowerShell добавляет к объектам FileInfo. Именно это и позволяет передать ByPropertyName по конвейеру. Свойство FileInfo для PSPath соответствует параметру LiteralPath, хотя и является псевдонимом. Очень часто объект может быть прямо передан по конвейеру следующему командлету, потому что PowerShell ищет наиболее подходящий параметр передаваемого объекта для привязки в качестве входного параметра. Ещё один пример конвейеризации без использования командлета Foreach-Object: PS> Get-ChildItem *.txt | Rename-Item -NewName {$_.name + '.bak'} Теперь вы знаете, как PowerShell осуществляет привязку свойств входного объекта конвейера к параметрам командлета. А благодаря .NET Reflector мы знаем, что некоторые параметры имеют псевдонимы, наподобие PSPath для упрощения процесса привязки. Мы говорили о передаче объекта по конвейеру с использованием ByPropertyName. Существует ещё один тип передачи, с использованием не имени объекта, а его значения - ByValue, и сейчас мы поговорим и о нём. 42 Часть 9: Параметры привязки элементов конвейера ByValue (по значению) Параметр привязки ByValue получает сам объект, а не его свойства, и пытается привязать его тип (и преобразуя его, если необходимо) к параметрам, которые отмечены как ByValue. Например, большинство командлетов *-Object связывают ByValue с любым объектом, который передаётся им по конвейеру. Справка по Where-Object показывает это: -inputObject Указывает объекты, которые необходимо отфильтровать. Если сохранить вывод команды в переменной, то можно использовать параметр InputObject, чтобы передать ее командлету Where-Object. Однако в большинстве случаев параметр InputObject не указывается в этой команде. Вместо этого при передаче объекта по конвейеру оболочка Windows PowerShell связывает его с параметром InputObject. Требуется? false Позиция? named Значение по умолчанию Принимать входные данные конвейера? true (ByValue) Принимать подстановочные знаки? false Выглядит менее понятно, чем ByPropertyName. Спрашивается, как выполняется такая инструкция? Это одна из вещей, которые мне очень нравятся в PowerShell. Объект предоставляет много метаданных о самом себе, содержит много описывающей его информации. Вы можете легко просмотреть каждый параметр любого командлета, который в настоящее время загружен в PowerShell. Во-первых, давайте взглянем на информацию, доступную для параметров: PS> Get-Command -CommandType cmdlet | Select -Expand ParameterSets | >>Select -Expand Parameters -First 1 | Get-Member TypeName: System.Management.Automation.CommandParameterInfo Name MemberType Definition ---- ---------- ---------- Aliases Property System.Collections.ObjectModel.Rea... Attributes Property System.Collections.ObjectModel.Rea... HelpMessage Property System.String HelpMessage {get;} IsDynamic Property System.Boolean IsDynamic {get;} IsMandatory Property System.Boolean IsMandatory {get;} Name Property System.String Name {get;} ParameterType Property System.Type ParameterType {get;} Position Property System.Int32 Position {get;} ValueFromPipeline Property System.Boolean ValueFromPipeline {... ValueFromPipelineByPropertyName Property System.Boolean ValueFromPipelineBy... ValueFromRemainingArguments Property System.Boolean ValueFromRemainingA... нас интересуют здесь свойства Name и все ValueFromPipeline. Основываясь на этой информации, легко подсчитать количество для каждого типа: 43 PS> (Get-Command -CommandType cmdlet | Select -Expand ParameterSets | >> Select -Expand Parameters | >> Where {$_.ValueFromPipeline -and !$_.ValueFromPipelineByPropertyName} | >> Measure-Object).Count >> 55 PS> (Get-Command -CommandType cmdlet | Select -Expand ParameterSets | >> Select -Expand Parameters | >> Where {!$_.ValueFromPipeline -and $_.ValueFromPipelineByPropertyName} | >> Measure-Object).Count >> 196 PS> (Get-Command -CommandType cmdlet | Select -Expand ParameterSets | >> Select -Expand Parameters | >> Where {$_.ValueFromPipeline -and $_.ValueFromPipelineByPropertyName} | >> Measure-Object).Count >> 66 Вот результат: Тип привязки в конвейере Количество ValueFromPipeline (по значению, ByValue) 55 ValueFromPipelineByPropertyName (по имени) 196 Оба типа 66 Как видно, привязка по имени используется более часто. Привязка по значению в конвейере применяется в основном с командлетами, обрабатывающими объекты общим способом — наподобие фильтрации и сортировки. В запросе, приведённом ниже, видно, что параметр InputObject используется наиболее часто в конвейере при привязке по значению: PS> Get-Command -CommandType cmdlet | Select -Expand ParameterSets | >> Select -Expand Parameters | >> Where {$_.ValueFromPipeline -and !$_.ValueFromPipelineByPropertyName} | >> Group Name -NoElement | Sort Count -Desc >> Count Name ----- ---- 40 InputObject 4 Message 3 String 2 SecureString 1 ExecutionPolicy 1 Object 1 AclObject 44 1 DifferenceObject 1 Id 1 Command Дальнейшие изыскания позволяют увидеть командлеты, которые используют привязку по значению к параметру InputObject. Примечание: Один параметр может встречаться в разных наборах параметров одного командлета. Это объясняет, почему здесь показано лишь 36 командлетов, при 40 упоминаниях InputObject. PS> $CmdletName = @{Name='CmdletName';Expression={$_.Name}} PS> Get-Command -CommandType cmdlet | Select $CmdletName -Expand ParameterSets | >> Select CmdletName -Expand Parameters | >> Where {$_.ValueFromPipeline -and !$_.ValueFromPipelineByPropertyName} | >> Group Name | Sort Count -Desc | Select -First 1 | Foreach {$_.Group} | >> Sort CmdletName -Unique | Format-Wide CmdletName -AutoSize >> Add-History Add-Member ConvertTo-Html Export-Clixml Export-Csv ForEach-Object Format-Custom Format-List Format-Table Format-Wide Get-Member Get-Process Get-Service Get-Unique Group-Object Measure-Command Measure-Object Out-Default Out-File Out-Host Out-Null Out-Printer Out-String Restart-Service Resume-Service Select-Object Select-String Sort-Object Start-Service Stop-Process Stop-Service Suspend-Service Tee-Object Trace-Command Where-Object Write-Output Как можно увидеть, большинство из этих командлетов предназначается для работы с объектами в целом, без привязки к каким то конкретным типам. Примечание: Разработчикам командлетов следует помнить о том, что именно с помощью привязки параметров ваш командлет получает объекты из конвейера. Когда вы создаёте командлет на C# вам не доступен эквивалент переменной $_. Если вы хотите чтобы ваш командлет работал с конвейером, в атрибутах параметра (ParameterAtribute), свойство ValueFromPipeline и/или ValueFromPipelineByPropertyName должно быть установлено в True как минимум на одном из параметров командлета. Как уже сказано выше, большинство атрибутов ByValue приходится на параметр InputObject (тип PsObject или PsObject[]), так что они принимают практически всё. Однако не все командлеты работают таким способом. Параметр –Id (тип long[]) у командлета Get-History привязывается к объектам из конвейера по значению. Вывод команды Trace-Command показывает, как тяжело приходится потрудиться PowerShell’у, чтобы конвертировать передаваемый объект в нужный тип. В данном примере он превращает скалярное строковое значение '1' в массив Int64. PS> Trace-Command -Name ParameterBinding -PSHost -Expression {'1' | Get-History} BIND NAMED cmd line args [Get-History] BIND POSITIONAL cmd line args [Get-History] MANDATORY PARAMETER CHECK on cmdlet [Get-History] CALLING BeginProcessing BIND PIPELINE object to parameters: [Get-History] PIPELINE object TYPE = [System.String] RESTORING pipeline parameter's original values Parameter [Id] PIPELINE INPUT ValueFromPipeline NO COERCION BIND arg [1] to parameter [Id] 45 |