Get-InstalledApp.ps1 — это сценарий PowerShell, который показывает информацию (например, имя, версию, сервер публикации) о приложениях, установленных на одном или более компьютерах в сети. При написании этого сценария в 2009 году я использовал PowerShell 1.0, и в моем распоряжении были только 32-разрядные операционные системы Windows (см. статью «Какие приложения установлены на компьютерах вашей сети?», опубликованную в Windows IT Pro/RE № 11 за 2009 год). Сейчас, в 2011 году, я использую 64-разрядную версию Windows 7 со встроенным языком PowerShell 2.0. Я хотел использовать Get-InstalledApp.ps1 для вывода информации о приложениях, установленных на 32- и 64-разрядных компьютерах Windows, и сразу же обнаружил, что, когда я запускаю сценарий из 64-разрядной версии PowerShell 2.0, он выводит информацию только о 64-разрядных приложениях. Я вынужден был запустить 32-разрядную версию PowerShell, чтобы обнаружить 32-разрядные приложения. Надо ли говорить, что я был разочарован наличием такого ограничения.

Я написал новую версию Get-InstalledApp.ps1 (листинг 1). Новый сценарий позволяет обойти данное ограничение и добавляет несколько новых свойств, которые будут очень полезны специалистам, управляющим 32- и 64-разрядными приложениями. Новая версия сценария требует PowerShell 2.0 и предоставляет следующие дополнительные возможности:

• подсказку об использовании в виде комментария вместо встроенной; если поместить сценарий в каталог из системной переменной Path, то, набрав

Get-Help Get-InstalledApp

вы увидите информацию о применении сценария;

• использует ввод по конвейеру, а не только параметр ComputerName;

• обнаруживает разрядность приложения (32- или 64-разрядное).

Использование сценария

Синтаксис командной строки сценария будет выглядеть так:

Get-installedApp [-ComputerName ] [-Appid ] [-Appname ]
   [-Publisher ] [-Version ] [-Architecture ] [-MatchAll]

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

Необязательный параметр -AppID позволяет осуществлять поиск специфического приложения по его ID. ID приложения является именем раздела реестра, который располагается в разделе Uninstall в реестре. Этот параметр особенно полезен для поиска приложений, установленных при помощи Windows Installer, поскольку ID приложения — то же самое, что глобально уникальный идентификатор рабочего кода приложения. Как значения параметра -AppID вы можете использовать групповые символы. Если значение содержит фигурные скобки ({}), нужно заключить его еще и в двойные кавычки (""), иначе PowerShell сочтет, что вы определяете хэш-таблицу.

Необязательные параметры -App Name, -Publisher и -Version ведут себя точно так же, как и в предыдущей версии сценария. Эти параметры позволяют искать приложения по имени, издателю и версии соответственно. Все параметры поддерживают групповые символы и нечувствительны к регистру символов.

Вы можете задать одну из двух строк — 64-bit или 32-bit — для необязательного параметра -Architecture, в зависимости от того, какие приложения будете искать — 64-разрядные или 32-разрядные. Если вы пропустите параметр -Architecture, Get-InstalledApp.ps1 выведет информацию как о 64-разрядных, так и о 32-разрядных приложениях.

Когда вы ищете только одно соответствие (например, при поиске приложения по его ID), в просмотре системного реестра необходимости нет. Таким образом, сценарий возвращает по умолчанию только первое совпадение, для того чтобы свести к минимуму доступ к реестру и сети. Если вам нужны все совпадения, вы можете использовать параметр -MatchAll. Если указать этот параметр, сценарий выведет информацию обо всех соответствующих критерию приложениях, а не остановится после первого совпадения. Например, команда

Get-InstalledApp -Publisher *Microsoft*

выводит информацию о первом приложении, в строке «Издатель» которого содержится имя Microsoft, тогда как команда

Get-InstalledApp -Publisher *Microsoft*
   -MatchAll

выводит информацию обо всех приложениях, в строке «Издатель» которых содержится имя Microsoft. Хотя здесь команда сокращена, нужно вводить ее в одну строчку на консоли PowerShell. То же касается и всех остальных команд.

Для каждого приложения Get-InstalledApp.ps1 выводит свойства, перечисленные в таблице. На приведенном экране показано, откуда эта информация взята. Что касается меня, я беру информацию о приложении из реестра, а не из класса Win32_Product в Windows Management Instrumentation из-за имеющихся ограничений. Об этих ограничениях рассказано в упомянутой выше статье «Какие приложения установлены на компьютерах вашей сети?». Еще об одном ограничении класса Win32_Product речь идет в статье Microsoft «Event log message indicates that the Windows Installer reconfigured all installed applications» (support.microsoft.com/kb/974524). Надо отметить, что класс Win32 Reg_AddRemovePrograms, упомянутый в статье Microsoft, существует только на компьютерах, управляемых Microsoft Systems Management Server (SMS).

 

Поиск информации о приложении в реестре
Экран. Поиск информации о приложении в реестре

 

Таблица. Свойства приложения, возвращаемые Get-InstalledApp.ps1
Свойства приложения, возвращаемые Get-InstalledApp.ps1

Изучение возможностей

Вот несколько команд, которые наглядно покажут, насколько полезен этот сценарий. Для вывода данных обо всех 64-разрядных приложениях, установленных на сервере server1, используйте команду:

Get-InstalledApp server1
   -Architecture 64-it -MatchAll.

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

Get-InstalledApp -Appname *office*
   -MatchAll | Select-Object Appname,
   Version, Architecture |
   Sort-Object Architecture, Version.

Теперь давайте рассмотрим что-нибудь более сложное. Если вы хотите, чтобы сценарий проверил все компьютеры, перечисленные в файле Computers.txt, и выявил, на каких системах установлен LibreOffice 3.3, показав их имена, введите команду:

Get-Content computers.txt |
   Get-InstalledApp -Appid
   "{1A97cF67-FeBB-436e-Bd64-
   431FFEF72EB8}" |
   Select-Object ComputerName

Заметим, что эта команда показывает новую функцию сценария — использование конвейерного вывода данных вместо параметра -ComputerName. Команда также показывает, что значение параметра AppID заключено в кавычки, так как оно содержит фигурные скобки.

Работа с WOW64

Как я уже упоминал ранее, Get-InstalledApp.ps1 отыскивает информацию об установленных приложениях в реестре. Для того чтобы перечислить программное обеспечение, Get-InstalledApp.ps1 читает соответствующие подразделы в разделе реестра HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall, как показано на экране. Однако это как раз то место, где исходная версия сценария встречала затруднения, если была запущена на 64-разрядной версии Windows. Данный раздел содержит информацию только о 64-разрядных приложениях, установленных на 64-разрядной системе. Информация об установке 32-разрядных приложений располагается в другом месте в реестре из-за наличия WOW64.

WOW64 является 32-разрядным эмулятором в 64-разрядной Windows, который позволяет 64-разрядной операционной системе без проблем запускать 32-разрядные приложения. Когда 32-разрядное приложение получает доступ к системному реестру, эмулятор WOW64 перенаправляет приложение в раздел Wow6432 Node. Например, 32-разрядное приложение, которое запрашивает HKLM\SOFTWARE перенаправляется в HKLM\SOFTWARE\Wow6432Node. Это означает, что 32-разрядные приложения, установленные на 64-разрядной системе, находятся в разделе HKLM\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall.

Когда я впервые начал корректировать сценарий, я извлек данные из двух мест реестра, которые работали без проблем, когда он был запущен в 64-разрядной среде PowerShell. Однако когда я запускал сценарий в 32-разрядной среде PowerShell на 64-разрядной системе, я был обескуражен, обнаружив, что сценарий выводит данные для каждого приложения дважды. Причина в том, что WOW64 перенаправлял запрос сценария на просмотр HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall в раздел Wow6432Node. Затем сценарий проверял раздел Wow6432 Node снова, вызывая двойной вывод данных.

Поэтому я решил, что стоит определить, является ли приложение 32- или 64-разрядным, основываясь на месте его раздела установки в системном реестре. Однако вы не можете сказать (во всяком случае, это крайне трудно), перенаправляет ли эмулятор WOW64 в раздел реестра Wow6432 Node, когда вы запускаете 32-разрядную среду PowerShell в 64-разрядной системе Windows.

Новая версия Get-InstalledApp.ps1 обходит эти проблемы, сохраняя пути реестра для обоих местоположений как объекты. NET ArrayList, а затем сравнивая элементы путей для обоих местоположений. Крайний элемент местоположения — это его финальный элемент. Например, крайний элемент пути реестра HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{23170F69-40C1-2702-0920-000001000000} есть {23170F69-40C1-2702-0920-000001000000}. В листинге 2 показана функция сравнения крайних элементов, которая используется сценарием для проверки, содержат ли два пути одинаковые конечные элементы. Если функция возвращает $TRUE, то сценарий обнаружил перенаправление реестра, сделанное WOW64, и игнорирует данные Wow6432 Node.

Выясните, что установлено

.

Листинг 1. Сценарий Get-InstalledApp.ps1

# Outputs installed applications on one or more computers that match one or more criteria.
# Version 2 — This version requires PowerShell 2.0. This version works correctly on 64-bit versions of Windows.

<#
.SYNOPSIS
Outputs installed applications for one or more computers.
.DESCRIPTION
Outputs installed applications for one or more computers.
(64-bit Windows only) If you run Get-InstalledApp.ps1 in 32-bit
   PowerShell on a 64-bit version of Windows, Get-InstalledApp.ps1
   can only detect 32-bit applications.
.PARAMETER ComputerName
Outputs applications for the named computer(s). If you omit this
   parameter, the local computer is assumed.
.PARAMETER AppID
Outputs applications with the specified application ID. An application's
   appID is equivalent to its subkey name underneath the Uninstall registry
   key. For Windows Installer-based applications, this is the application's
   product code GUID (e.g. {3248F0A8-6813-11D6-A77B-00B0D0160060}).
   Wildcards are permitted.
.PARAMETER AppName
Outputs applications with the specified application name. The AppName
   is the application's name as it appears in the Add/Remove Programs
   list. Wildcards are permitted.
.PARAMETER Publisher
Outputs applications with the specified publisher name. Wildcards
   are permitted
.PARAMETER Version
Outputs applications with the specified version. Wildcards are permitted.
.PARAMETER Architecture
Outputs applications for the specified architecture. Valid arguments are:
   64-bit and 32-bit. Omit this parameter to output both 32-bit and 64-bit
   applications. Note that 32-bit PowerShell on 64-bit Windows does not
   output 64-bit applications.
.PARAMETER MatchAll
Outputs all matching applications. Otherwise, output only the first match.
.INPUTS
System.String
.OUTPUTS
PSObjects containing the following properties:
  ComputerName - computer where the application is installed
  AppID - the application's AppID
  AppName - the application's name
  Publisher - the application's publisher
  Version - the application's version
  Architecture - the application's architecture (32-bit or 64-bit)
.EXAMPLE
PS C:\> Get-InstalledApp
This command outputs installed applications on the current computer.
.EXAMPLE
PS C:\> Get-InstalledApp | Select-Object AppName,Version |
  Sort-Object AppName
This command outputs a sorted list of applications on the current computer.
.EXAMPLE
PS C:\> Get-InstalledApp wks1,wks2 -Publisher *microsoft* -MatchAll
This command outputs all installed Microsoft applications
   on the named computers.
.EXAMPLE
PS C:\> Get-Content ComputerList.txt | Get-InstalledApp -AppID
   "{1A97CF67-FEBB-436E-BD64-431FFEF72EB8}" | Select-Object
   ComputerName
This command outputs the computer names named in ComputerList.txt
   that have the specified application installed.
.EXAMPLE
PS C:\> Get-InstalledApp -Architecture "32-bit" -MatchAll
This command outputs all 32-bit applications installed on the current computer.
#>
[CmdletBinding()]
param(
  [parameter(Position=0,ValueFromPipeline=$TRUE)]
    [String[]] $ComputerName=$ENV:COMPUTERNAME,
    [String] $AppID,
    [String] $AppName,
    [String] $Publisher,
    [String] $Version,
    [String] [ValidateSet("32-bit","64-bit")] $Architecture,
    [Switch] $MatchAll
)

begin {
  $HKLM = [UInt32] "0x80000002"
  $UNINSTALL_KEY = "SOFTWARE\Microsoft\Windows\CurrentVersion\
   Uninstall"
  $UNINSTALL_KEY_WOW = "SOFTWARE\Wow6432Node\Microsoft\
   Windows\CurrentVersion\Uninstall"
  # Detect whether we are using pipeline input.
  $PIPELINEINPUT = (-not $PSBOUNDPARAMETERS.ContainsKey("ComputerName")) -and (-not $ComputerName)
  # Create a hash table containing the requested application properties.
  $PropertyList = @{}
  if ($AppID -ne "") { $PropertyList.AppID = $AppID }
  if ($AppName -ne "") { $PropertyList.AppName = $AppName }
  if ($Publisher -ne "") { $PropertyList.Publisher = $Publisher }
  if ($Version -ne "") { $PropertyList.Version = $Version }
  if ($Architecture -ne "") { $PropertyList.Architecture = $Architecture }

  # Returns $TRUE if the leaf items from both lists are equal; $FALSE otherwise.
  function compare-leafequality($list1, $list2) {
    # Create ArrayLists to hold the leaf items and build both lists.
    $leafList1 = new-object System.Collections.ArrayList
    $list1 | foreach-object { [Void] $leafList1.Add((split-path $_ -leaf)) }
    $leafList2 = new-object System.Collections.ArrayList
    $list2 | foreach-object { [Void] $leafList2.Add((split-path $_ -leaf)) }
    # If compare-object has no output, then the lists matched.
    (compare-object $leafList1 $leafList2 | measure-object).Count -eq 0
  }
  function get-installedapp2($computerName) {
    try {
      $regProv = [WMIClass] "\\$computerName\root\default:StdRegProv"
      # Enumerate HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\
         Uninstall
      # Note that this request will be redirected to Wow6432Node
         if running from 32-bit
      # PowerShell on 64-bit Windows.
      $keyList = new-object System.Collections.ArrayList
      $keys = $regProv.EnumKey($HKLM, $UNINSTALL_KEY)
      foreach ($key in $keys.sNames) {
        [Void] $keyList.Add((join-path $UNINSTALL_KEY $key))
      }
      # Enumerate HKLM\SOFTWARE\Wow6432Node\Microsoft\Windows\
         CurrentVersion\Uninstall
      $keyListWOW64 = new-object System.Collections.ArrayList
      $keys = $regProv.EnumKey($HKLM, $UNINSTALL_KEY_WOW)
      if ($keys.ReturnValue -eq 0) {
        foreach ($key in $keys.sNames) {
          [Void] $keyListWOW64.Add((join-path $UNINSTALL_KEY_WOW $key))
        }
      }

      # Default to 32-bit. If there are any items in $keyListWOW64,
         then compare the
      # leaf items in both lists of subkeys. If the leaf items in both lists
         match, we're
      # seeing the Wow6432Node redirection in effect and we can ignore
         $keyListWOW64.
      # Otherwise, we're 64-bit and append $keyListWOW64 to $keyList
         to enumerate both.
      $is64bit = $FALSE
      if ($keyListWOW64.Count -gt 0) {
        if (-not (compare-leafequality $keyList $keyListWOW64)) {
          $is64bit = $TRUE
          [Void] $keyList.AddRange($keyListWOW64)
        }
      }
      # Enumerate the subkeys.
      foreach ($subkey in $keyList) {
        $name = $regProv.GetStringValue($HKLM, $subkey, "DisplayName").
           sValue
        if ($name -eq $NULL) { continue }  # skip entry if empty display
           name
        $output = new-object PSObject
        $output | add-member NoteProperty "ComputerName"
           -value $computerName
        # $output | add-member NoteProperty "Subkey" -value (split-path
           $subkey -parent)  # useful when debugging
        $output | add-member NoteProperty "AppID" -value
           (split-path $subkey -leaf)
        $output | add-member NoteProperty "AppName" -value $name
        $output | add-member NoteProperty "Publisher" -value
           $regProv.GetStringValue($HKLM, $subkey, "Publisher").sValue
        $output | add-member NoteProperty "Version" -value
           $regProv.GetStringValue($HKLM, $subkey, "DisplayVersion").sValue
        # If subkey's name is in Wow6432Node, then the application is 32-bit.
           Otherwise,
        # $is64bit determines whether the application is 32-bit or 64-bit.
        if ($subkey -like "SOFTWARE\Wow6432Node\*") {
          $appArchitecture = "32-bit"
        } else {
          if ($is64bit) {
            $appArchitecture = "64-bit"
          } else {
            $appArchitecture = "32-bit"
          }
        }
        $output | add-member NoteProperty "Architecture" -value
           $appArchitecture
        # If no properties defined on command line, output the object.
        if ($PropertyList.Keys.Count -eq 0) {
          $output
        } else {
          # Otherwise, iterate the requested properties and count
             the number of matches.
          $matches = 0
          foreach ($key in $PropertyList.Keys) {
            if ($output.$key -like $PropertyList.$key) {
              $matches += 1
            }
          }
          # If all properties matched, output the object.
          if ($matches -eq $PropertyList.Keys.Count) {
            $output
            # If -matchall is missing, don't enumerate further.
            if (-not $MatchAll) { break }
          }
        }
      }
    }
    catch [System.Management.Automation.RuntimeException] {
      write-error $_
    }
  }
}
process {
  if ($PIPELINEINPUT) {
    get-installedapp2 $_
  } else {
    $ComputerName | foreach-object {
      get-installedapp2 $_
    }
  }
}

Листинг 2. Функция сравнения качества листа

# Returns $TRUE if the leaf items from both lists are equal.
# Otherwise, it returns $FALSE.
function compare-leafequality($list1, $list2) {
   # Create ArrayLists to hold the leaf items and build both lists.
   $leafList1 = new-object System.Collections.ArrayList
   $list1 | foreach-object { [Void] $leafList1.Add((split-path $_ -leaf)) }
   $leafList2 = new-object System.Collections.ArrayList
   $list2 | foreach-object { [Void] $leafList2.Add((split-path $_ -leaf)) }
   # If compare-object has no output, then the lists match.
   (compare-object $leafList1 $leafList2 | measure-object).Count -eq 0
}

Билл Стюарт (bill.stewart@frenchmortuary.com) — системный и сетевой администратор компании French Mortuary, Нью-Мехико