В Windows PowerShell 2.0 предусмотрено несколько способов деления набора команд на блоки. Диапазон решений: от простых до сложных, в зависимости от нужд пользователя. Однако всем решениям свойственны общие черты и правила.

Прежде всего, отдельная функция в идеале должна быть ориентирована на одну задачу. Функция не формирует вывода (как при выполнении какого-либо действия) или должна выдавать единственный вид выходных данных (например, при извлечении информации). Использование соглашения глагол — существительное в виде составных команд — один из способов достижения однозадачности функций.

Кроме того, вывод функций должен формироваться только с использованием команды Write-Output. Если нужно, чтобы функция также вносила какие-нибудь записи в файл (например, об ошибках), используйте Out-File. Для вывода такой информации можно задействовать команды Write-Verbose, Write-Debug, Write-Warning и Write-Error (подробное протоколирование, информация об отладке, предупреждения и ошибки соответственно). Избегайте Write-Host, так как его вывод нельзя использовать так же гибко, как у других команд Write. Недопустимо, чтобы функция обращалась к любым переменным, созданным вне нее.

. Потребуются свойства Caption, ServicePackMajorVersion и BuildNumber класса Win32_OperatingSystem, а также свойство SerialNumber из класса Win32_BIOS. Кроме того, необходимо, чтобы выходные данные содержали имена компьютеров.

Начинаем с команды

Я предпочитаю работать непосредственно из командной строки, а не в файле сценария, чтобы справиться с основной функциональной частью задачи. Следующие две команды получают нужную информацию:

Get-WmiObject -class Win32_OperatingSystem -computername Server-R2 |
   Select __SERVER, ServicePackMajorVersion, BuildNumber, Caption
Get-WmiObject -class Win32_BIOS -computername Server-R2 | Select SerialNumber

Можно добавить в -computername список имен компьютеров с разделительными запятыми (CSV), чтобы обработать дополнительные компьютеры.

Единый вывод

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

Целесообразно дать столбцу __SERVER имя ComputerName, а не просто __SERVER. Исполь­зо­вать имя __SERVER удобно, так как __SERVER — свойство системы WMI, а команда __SERVER всегда возвращает настоящее имя компьютера (независимо от псевдонима или IP-адреса, указанного для обращения к компьютеру). Тем не менее __SERVER выглядит некрасиво.

Как всегда в PowerShell, существуют различные методы для выполнения этой задачи. Одно из решений — создать новый, пустой объект и наделить его лишь необходимыми свойствами из Win32_OperatingSystem и Win32_BIOS. Такой подход представляет собой разновидность решения на основе сценария, но его логика достаточна ясна. Сценарий в листинге 1 обеспечивает сохранение WMI-информации в двух переменных (по одной для каждого класса WMI), а затем указывает фрагменты, вставляемые в новый, пустой объект.

Совершенно иной подход связан с применением команды Select-Object. Она создает новый пользовательский объект с любыми нужными свойствами. Можно воспользоваться особым приемом, именуемым хеш-таблицей или словарем, чтобы добавить свойство из Win32_BIOS. Данное решение — не полный сценарий, а однострочная команда, но разобраться в синтаксисе (и особенно в пунктуации) несколько труднее. Следующая команда переименовывает свойство __SERVER в ComputerName, но имена остальных свойств остаются неизменными.

Get-WmiObject -class Win32_
   OperatingSystem -computername
   Server-r2 | Select @{n=
   'ComputerName'; e={$_
   . __SERVER}}, Caption, BuildNumber,
   ServicePackMajorVersion,
   @{n='BIOSSerial'; e={Get-WmiObject
   — class Win32_BIOS -computername
   $_ | Select -expand SerialNumber}}

Выглядит устрашающе, но работает!

Для данной статьи мы воспользуемся решением, представленным в листинге 1. Я предпочитаю это решение из-за его более простого синтаксиса.

Простые и параметризованные функции

В функциях самого простого типа команды упаковываются в конструкцию функции, как в сценарии листинга 2. Команда Write-Output служит для вывода пользовательского объекта в конвейер. Недостаток функции заключается в том, что кто-то должен ее открыть и отредактировать, изменив имя компьютера.

Если дополнить функцию параметром, посторонний пользователь сможет запускать ее, не внося изменений. В листинге 3 параметр объявлен как строка и указано имя по умолчанию localhost на случай, если кто-то забудет ввести параметр. Параметр подставляется вместо жестко заданного имени компьютера в двух командах Get-WmiObject.

Существует несколько вариантов запуска этого сценария. Можно использовать позиционный параметр, например

Get-OSInventory Server-R2

или именованный параметр, например

Get-OSInventory -computername Server-R2

В любом случае ввести функцию в оболочку для использования ее как команды довольно сложно.

Преобразование функции в команду

Существует два простых способа сделать новую функцию видимой в оболочке (и еще третий способ, о котором будет рассказано далее). Первый вариант — скопировать функцию в сценарий профилирования, выполняемый автоматически каждый раз при открытии нового окна оболочки. Чтобы получить информацию об использовании профилей PowerShell, запустите команду

help about_profiles

Используемые сценарии профилирования не существуют по умолчанию; в файле справки указывается, как их назвать и куда поместить. Чтобы запустить функцию, просто скопируйте ее непосредственно в сценарий профилирования.

В следующем примере показан еще один возможный вариант, вызов с использованием точки:

. c:\Scripts\utilities.ps1

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

Прием конвейерного ввода

Было бы удобно передать конвейерный ввод в функцию, но пока функция может принимать только позиционный или именованный параметр. В листинге 4 присутствует особый вид функции — конвейерная функция, или функция фильтрации, которая передает конвейерный ввод в функцию.

Если имеется текстовый файл, содержащий имена компьютеров, функция обрабатывает по одному имени в каждой строке:

Get-Content names.txt | Get-OSInventory

Когда имена поступают в функцию, PowerShell в первую очередь выполняет блок BEGIN. У меня в этом блоке нет никаких команд, но его можно использовать, чтобы подключиться к базе данных или иному ресурсу, многократно используемому в ходе выполнения функции.

Оболочка исполняет блок PROCESS по одному разу для каждого введенного имени. Каждое имя компьютера из текстового файла помещается в переменную $_, которую рекомендуется скопировать в переменную $computername для удобства чтения.

Наконец, когда все имена прошли через блок PROCESS, выполняется блок END. И опять же мне не обязательно использовать блок END, но можно было бы закрыть соединение с базой данных или выполнить другие завершающие операции перед окончанием выполнения функции. Блоки BEGIN и END можно опустить, но я привел их ради единообразия.

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

Get-ADComputer -filter * -searchbase
   'ou=West, dc=company, dc=com'} |
   Select -expand name |
   Get-OSInventory

В этой команде для получения компьютеров из организационной единицы (OU) West в домене company.com применяется команда Get-ADComputer модуля Windows Server 2008 R2 Active Directory (AD). Команда Select-Object извлекает только содержимое свойства Name этих компьютеров, передавая имена компьютеров в функцию. Поскольку для функции безразлично, где находятся имена компьютеров, ее можно многократного использовать без изменений для различных источников имен компьютеров. Придерживаясь однозадачности функций, можно повторно задействовать функцию в нескольких местах.

Гибкость функции повышается, если настроить ее на вывод пользовательских объектов в конвейер. Например, предположим, нам нужно организовать вывод в CSV-файл:

Get-ADComputer -filter * -searchbase
   'ou=West, dc=company, dc=com'} |
   Select -expand Name |
   Get-OSInventory | Export-CSV
   output.csv

Или требуется фильтровать выходные данные таким образом, чтобы получить только компьютеры Windows XP без пакета обновления SP3:

Get-ADComputer -filter * -searchbase
   'ou=West, dc=company, dc=com'} |
   Select -expand Name | GetOSInventory
   | where {$_.OSBuild -eq
   2600-nd $_.SPVersion -ne 3}

Благодаря однозадачности и выводу объектов функция может работать со многими другими командами PowerShell.

Теперь можно принимать конвейерный ввод. Но обратите внимание, что, поскольку блок параметров был удален, исчезает возможность использовать параметр -computername. В действительности нам нужна функция, пригодная для работы любым способом, совсем как составная команда.

Расширенные функции: команды сценария PowerShell

Было бы удобно иметь функцию, пригодную как для конвейерного ввода, так и для использования позиционных и именованных параметров. В PowerShell 2.0 предусмотрено решение, именуемое расширенной функцией или сценарной командой, поскольку оно работает подобно настоящей составной команде. Существенная проблема заключается в том, что, если направить в функцию конвейерный ввод, блок PROCESS сценария выполняется один раз для каждого входного элемента. Затем каждый элемент помещается по одному в параметр.

При использовании параметра блок PROCESS сценария выполняется только один раз. Однако параметр содержит все входные элементы, которые необходимо перечислить вручную.

Я предпочитаю строить расширенные функции как своеобразную оболочку для обработки обоих вариантов ввода, не выполняющую никаких реальных действий. Оптимальное решение: переместить реальные действия во вспомогательную функцию, предназначенную для поочередной обработки компьютеров по одному. При необходимости расширенная функция обеспечивает разделение входных данных на фрагменты для отдельных компьютеров. Исходный текст расширенной функции приведен в листинге 5.

Подробное описание расширенных функций выходит за рамки данной статьи. Чтобы получить дополнительную информацию об этих конструкциях, выполните команду

help *advanced*

Можно заметить, что реальные действия в сценарии листинга 5 выполняются функцией OSInventoryHelper. Эту функцию можно скрыть от пользователей, заставив их задействовать непосредственно Get-OSInventory. Об этом — в следующем разделе.

Расширенную функцию в листинге 5 можно использовать по-разному. Например:

Get-Content names.txt | Get-OSInventory
   | export-CSV output.csv
Get-OSInventory -computername
   server-r2, server57, dc001 |
   Format-Table
Get-OSInventory localhost
Get-OSInventory

Последняя команда запрашивает ввод одного или нескольких имен компьютеров; нажмите клавишу Enter на пустой строке после того, как будут введены все имена компьютеров.

Распространение результатов

Расширенную функцию нужно распространить таким способом, чтобы другие администраторы могли загрузить ее в оболочку без помощи сценария профилирования или вызовов с использованием точки. Кроме того, стоит скрыть функцию OSInventoryHelper.

Обе задачи можно выполнить, сохранив OSInventoryHelper и собственно функции Get-OSInventory в файле с именем Utilities.psm1, содержащим следующую дополнительную строку программного кода

Export-ModuleMember -function
   Get-OSInventory

чтобы другим администраторам была видна лишь команда Get-OSInventory (функция OSInventoryHelper видна, если открыть файл, но скрыта, если загрузить сценарий в оболочку). Сценарий Utilities.psm1 следует сохранить в папке \My Documents\WindowsPowerShell\Modules\Utilities, так как оболочка автоматически выполняет поиск новых модулей по этому пути.

Чтобы загрузить Get-OSInventory в оболочку как команду, выполните

Import-Module utilities.

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

Дон Джоунз (powershell@concentratedtech.com) — технический инструктор по PowerShell (www.windowsitpro.com/go/DonJonesPowerShell), автор более 35 книг. Имеет звание Microsoft MVP

Листинг 1. Сценарий для хранения информации WMI в двух переменных
$os = Get-WmiObject -class Win32_OperatingSystem -computername Server-R2
$bios = Get-WmiObject -class Win32_BIOS -computername Server-R2
$obj = New-Object PSObject
$obj | Add-Member NoteProperty ComputerName ($os.__SERVER)
$obj | Add-Member NoteProperty OSVersion ($os.Caption)
$obj | Add-Member NoteProperty OSBuild ($os.BuildNumber)
$obj | Add-Member NoteProperty SPVersion ($os.ServicePackMajorVersion)
$obj | Add-Member NoteProperty BIOSSerial ($bios.SerialNumber)
Листинг 2. Простая функция, объединяющая команды в конструкции Function
Function Get-OSInventory {
$os = Get-WmiObject -class Win32_OperatingSystem -computername Server-R2
$bios = Get-WmiObject -class Win32_BIOS -computername Server-R2
$obj = New-Object PSObject
$obj | Add-Member NoteProperty ComputerName ($os.__SERVER)
$obj | Add-Member NoteProperty OSVersion ($os.Caption)
$obj | Add-Member NoteProperty OSBuild ($os.BuildNumber)
$obj | Add-Member NoteProperty SPVersion ($os.ServicePackMajorVersion)
$obj | Add-Member NoteProperty BIOSSerial ($bios.SerialNumber)
Write-Output $obj
}
Листинг 3. Utilities.ps1
Function Get-OSInventory {
 Param([string]$computername = 'locahost')
 $os = Get-WmiObject -class Win32_OperatingSystem -computername $computername
 $bios = Get-WmiObject -class Win32_BIOS -computername $computername
 $obj = New-Object PSObject
 $obj | Add-Member NoteProperty ComputerName ($os.__SERVER)
 $obj | Add-Member NoteProperty OSVersion ($os.Caption)
 $obj | Add-Member NoteProperty OSBuild ($os.BuildNumber)
 $obj | Add-Member NoteProperty SPVersion ($os.ServicePackMajorVersion)
 $obj | Add-Member NoteProperty BIOSSerial ($bios.SerialNumber)
 Write-Output $obj
}
Листинг 4. Конвейерная функция
Function Get-OSInventory {
 BEGIN {}
 PROCESS {
 $computername = $_
  $os = Get-WmiObject -class Win32_OperatingSystem -computername $computername
  $bios = Get-WmiObject -class Win32_BIOS -computername $computername
  $obj = New-Object PSObject
  $obj | Add-Member NoteProperty ComputerName ($os.__SERVER)
  $obj | Add-Member NoteProperty OSVersion ($os.Caption)
  $obj | Add-Member NoteProperty OSBuild ($os.BuildNumber)
  $obj | Add-Member NoteProperty SPVersion ($os.ServicePackMajorVersion)
  $obj | Add-Member NoteProperty BIOSSerial ($bios.SerialNumber)
  Write-Output $obj
 }
 END {}
}
Листинг 5. Расширенная функция для сбора информации об операционной системе
Function OSInventoryHelper {
 Param([string]$computername)
 $os = Get-WmiObject -class Win32_OperatingSystem -computername $computername
 $bios = Get-WmiObject -class Win32_BIOS -computername $computername
 $obj = New-Object PSObject
 $obj | Add-Member NoteProperty ComputerName ($os.__SERVER)
 $obj | Add-Member NoteProperty OSVersion ($os.Caption)
 $obj | Add-Member NoteProperty OSBuild ($os.BuildNumber)
 $obj | Add-Member NoteProperty SPVersion ($os.ServicePackMajorVersion)
 $obj | Add-Member NoteProperty BIOSSerial ($bios.SerialNumber)
 Write-Output $obj
}
Function Get-OSInventory {
 [CmdletBinding()]
 Param(
  [Parameter(Mandtory=$True,ValueFromPipeline=$true)]
  [String[]]$ComputerName
 )
 BEGIN {
  $inputWasFromPipeline = -not $PSBoundParameters.ContainsKey('computername')
 }
 PROCESS {
  If ($inputWasFromPipeline) {
   OSInventoryHelper $computername
  } else {
   foreach ($computer in $computername) {
    OSInventoryHelper $computer
   }
  }
 }
}

Дополнительно

Другие советы Дона Джоунза по PowerShell можно найти в его блоге Purpose по адресу www.windowsitpro.com/blogs/PowerShellwithaPurpose.aspx.