Билл Стюарт (bill.stewart@frenchmortuary.com) – системный и сетевой администратор компании French Mortuary, Нью-Мехико
Системным администраторам часто бывает нужна информация о версии файла, например для того, чтобы определить, применено ли программное исправление. Эта информация — метаданные, встроенные в исполняемый файл Windows (например,. exe,. dll) в виде набора чисел, разделенных точками, например 12.1.42.0. Слева направо эти числа указывают основной номер версии, дополнительный номер версии, номер сборки и номер редакции. Каждое число это 16-разрядное целое число без знака. Первые два числа (основной номер версии и дополнительный) представляют наиболее значимые 32 разряда номера версии. Последние два числа (номер сборки и номер редакции) представляют менее значимые 32 разряда номера версии.
В сценариях среды сценариев Windows Script Host (WSH) версию файла можно получить с помощью метода GetFileVersion объекта FileSystemObject. Например, код в листинге 1 это небольшой сценарий на VBScript, который выводит версию файла Notepad.exe.
В Windows PowerShell можно использовать свойство VersionInfo объекта «файл», это свойство является объектом. NET FileVersionInfo. У данного объекта четыре свойства — FileMajorPart, FileMinorPart, FileBuildPart и FilePrivatePart, которые соответствуют четырем частям версии файла. В коде в листинге 2 приведена небольшая функция Powershell, которая использует оператор –f для вывода версии Notepad.exe. В этом листинге также показано, как в Powershell можно задействовать FileSystemObject для получения этой же информации.
Неплохо для начала. Но получение версии файла — это только полдела. Как сравнить полученные числа? В VBScript нет типа для 64-разрядных целых чисел без знака. В Powershell есть тип UInt64, но не все операторы могут работать с ним. Чтобы решить эти проблемы, я написал несколько функций VBScript и PowerShell, которые облегчают сравнение версий. Прежде чем продемонстрировать их, я хочу объяснить принцип, по которому они работают.
Шаг 1. Разделяем разряды
Мы не можем использовать 64-разрядные числа в VBScript напрямую, также как не можем использовать эти числа в Powershell без потери поддержки части операторов. Поэтому мы начнем с разделения числа, определяющего версию файла, на пару 32-разрядных чисел. Поскольку мы не можем сравнить два 64-разрядных числа, мы вместо этого сравним две пары 32-разрядных чисел.
Для начала вам нужно знать, как конвертировать строку a.b, где a — более значимые 16 разрядов, а b — менее значимые 16 разрядов, в одну 32-разрядную величину. Мы не можем просто работать со строкой как с числом с плавающей точкой, потому что сравнение не будет производиться корректно. Например, в случае с числом с плавающей точкой, 5,20 равно 5,2. В данном случае 5,15 будет большим числом (потому что 15 больше 2). В случае с номером версии это не так: 5,20 больше, чем 5,15.
Чтобы корректно сконвертировать строку a.b в 32-разрядное число, мы сделаем сдвиг влево для первого числа и затем добавим к нему второе число. Чтобы проиллюстрировать это, воспользуемся калькулятором Windows 7 в режиме программиста.
Откройте калькулятор, выберите «Вид», «Программист», а затем переключатели Hex и Dword, расположенные вдоль левого края (эффект проще заметить в шестнадцатеричном режиме, чем в десятичном). В этом примере мы создадим 32-разрядное число, которое представляет номер версии 3,7. Выполните в калькуляторе следующие действия:
- Введите число 3 для основного номера версии
- Нажмите кнопку Lsh, введите значение 10 (шестнадцатеричное для 16) и нажмите «Ввод». Это действие передвинет разряды числа 16 влево.
- Нажмите +, затем 7 и после этого нажмите «Ввод». Это действие добавит дополнительный номер версии.
Результатом будет шестнадцатеричное 30007, как показано на экране.
Экран. Шестнадцатеричный пример конвертированной строки |
Таким образом, формула для конвертирования строки вида a.b в 32-разрядное число следующая:
(a Lsh 16) + b
Поскольку в VBScript и PowerShell нет оператора Lsh, мы будем использовать следующий эквивалент данной формулы:
(a * 216) + b
Выше я упоминал, что собираюсь разделить 64 разряда номера версии на два 32-разрядных числа. В сценарии я решил представить два 32-разрядных числа как массив из двух элементов. Например, номер версии 3.7.5.1 был бы представлен как массив, содержащий значения 196615 (30007 шестнадцатеричное для 3,7) и 327681 (50001 шестнадцатеричное для 5.1).
Шаг 2. Сравнение версий
Пока мы разобрали, как представить 64-разрядное значение в виде пары двух 32-разрядных значений. Вопрос в том, как сравнить эти значения? Предположим, что у нас есть два номера версии a.b.c.d и w.x.y.z. В таблице 1 показаны возможные результаты сравнения.
Таблица 1. Сравнение номеров версий |
Шаг 3. Написание кода
Теперь, когда вы понимаете природу номеров версий и то, как их сравнивать, мы можем взглянуть на код, который выполняет работу. В листинге 3 показаны функции VBScript, а в листинге 4 — те же функции, написанные на Powershell. Эти функции описаны в таблице 2.
Таблица 2. Функции, необходимые для сравнения версий |
Обратите внимание на разницу в синтаксисе VBScript и Powershell, когда будете использовать функции в своих сценариях. В VBScript вызов функции требует наличия круглых скобок и запятой между параметрами, тогда как в Poweshell скобки и запятая должны быть опущены. Например, в VBScript нужно написать:
Result = CompareVersions(Ver1, Ver2)
Тогда как в Powershell эквивалентное выражение будет выглядеть так:
$Result = CompareVersions $Ver1 $Ver2
Чтобы увидеть эти функции в действии, просмотрите код в листинге 5 и листинге 6. Versions.vbs и Versions.ps1 — самостоятельные сценарии, которые используют эти функции, для сравнения версий файлов из командной строки. Сценарии также включают дополнительные возможности: ValidateVersionStrin проверяет, корректна ли указанная строка версии, а GetVersionArrayAsString является обратной для GetVersionStringAsArray. Кроме того, я включил JScript версию сценария, Versions.js, в листинг 7 на тот случай, если Jscript — ваш любимый язык.
Листинг 1. Получение версии файла в VBScript
Dim FSO, FileName Set FSO = CreateObject(«Scripting.FileSystemObject») FileName = «C:\Windows\system32\notepad.exe» WScript.Echo FSO.GetFileVersion(FileName)
Листинг 2. Получение версии файла в PowerShell
$fileName = «C:\Windows\system32\notepad.exe» function get-fileversion { param([System.IO.FileInfo] $fileItem) $verInfo = $fileItem.VersionInfo «{0}.{1}.{2}.{3}» -f $verInfo.FileMajorPart, $verInfo.FileMinorPart, $verInfo.FileBuildPart, $verInfo.FilePrivatePart } # Outputs the file's version get-fileversion $fileName # You can also use the FileSystemObject object's GetFileVersion method. $fso = new-object -comobject «Scripting.FileSystemObject» # Same output as previous get-fileversion function. $fso.GetFileVersion($fileName)
' Bitwise left shift Function Lsh(ByVal N, ByVal Bits) Lsh = N * (2 ^ Bits) End Function ' Returns a version string «a.b.c.d» as a two-element numeric ' array. The first array element is the most significant 32 bits, ' and the second element is the least significant 32 bits. Function GetVersionStringAsArray(ByVal Version) Dim VersionAll, VersionParts, N VersionAll = Array(0, 0, 0, 0) VersionParts = Split(Version, «.") For N = 0 To UBound(VersionParts) VersionAll(N) = CLng(VersionParts(N)) Next Dim Hi, Lo Hi = Lsh(VersionAll(0), 16) + VersionAll(1) Lo = Lsh(VersionAll(2), 16) + VersionAll(3) GetVersionStringAsArray = Array(Hi, Lo) End Function ' Compares two versions»a.b.c.d«. If Version1 < Version2, ' returns -1. If Version1 = Version2, returns 0. ' If Version1 > Version2, returns 1. Function CompareVersions(ByVal Version1, ByVal Version2) Dim Ver1, Ver2, Result Ver1 = GetVersionStringAsArray(Version1) Ver2 = GetVersionStringAsArray(Version2) If Ver1(0) < Ver2(0) Then Result = -1 ElseIf Ver1(0) = Ver2(0) Then If Ver1(1) < Ver2(1) Then Result = -1 ElseIf Ver1(1) = Ver2(1) Then Result = 0 Else Result = 1 End If Else Result = 1 End If CompareVersions = Result End Function
# Bitwise left shift function Lsh([UInt32] $n, [Byte] $bits) { $n * [Math]::Pow(2, $bits) } # Returns a version number»a.b.c.d«as a two-element numeric # array. The first array element is the most significant 32 bits, # and the second element is the least significant 32 bits. function GetVersionStringAsArray([String] $version) { $parts = $version.Split(».«) if ($parts.Count -lt 4) { for ($n = $parts.Count; $n -lt 4; $n++) { $parts +=»0« } } [UInt32] ((Lsh $parts[0] 16) + $parts[1]) [UInt32] ((Lsh $parts[2] 16) + $parts[3]) } # Compares two version numbers»a.b.c.d«. If $version1 < $version2, # returns -1. If $version1 = $version2, returns 0. If # $version1 > $version2, returns 1. function CompareVersions([String] $version1, [String] $version2) { $ver1 = GetVersionStringAsArray $version1 $ver2 = GetVersionStringAsArray $version2 if ($ver1[0] -lt $ver2[0]) { return -1 } elseif ($ver1[0] -eq $ver2[0]) { if ($ver1[1] -lt $ver2[1]) { return -1 } elseif ($ver1[1] -eq $ver2[1]) { return 0 } else { return 1 } } else { return 1 } }
Листинг 5. Код сценария Versions.ps1
param($version1, $version2) # Bitwise left shift. function Lsh([UInt32] $n, [Byte] $bits) { $n * [Math]::Pow(2, $bits) } # Returns a version number»a.b.c.d«as a two-element numeric # array. The first array element is the most-significant 32 bits, # and the second element is the least-significant 32 bits. function GetVersionStringAsArray([String] $version) { $parts = $version.Split(».«) if ($parts.Count -lt 4) { for ($n = $parts.Count; $n -lt 4; $n++) { $parts +=»0« } } [UInt32] ((Lsh $parts[0] 16) + $parts[1]) [UInt32] ((Lsh $parts[2] 16) + $parts[3]) } # Compares two version numbers»a.b.c.d«. If $version1 < $version2, # returns -1. If $version1 = $version2, returns 0. If # $version1 > $version2, returns 1. function CompareVersions([String] $version1, [String] $version2) { $ver1 = GetVersionStringAsArray $version1 $ver2 = GetVersionStringAsArray $version2 if ($ver1[0] -lt $ver2[0]) { return -1 } elseif ($ver1[0] -eq $ver2[0]) { if ($ver1[1] -lt $ver2[1]) { return -1 } elseif ($ver1[1] -eq $ver2[1]) { return 0 } else { return 1 } } else { return 1 } } # Bitwise right shift. function Rsh([UInt32] $n, [Byte] $bits) { [Math]::Truncate($n / [Math]::Pow(2, $bits)) } # Returns the specified two-element array containing a version # number as a string»a.b.c.d«. function GetVersionArrayAsString([UInt32[]] $version) { $hi = $version[0] $lo = $version[1] »{0}.{1}.{2}.{3}«-f ((Rsh $hi 16) -band 65535), ($hi -band 65535), ((Rsh $lo 16) -band 65535), ($lo -band 65535) } # Returns $TRUE if the version string»a.b.c.d«is valid, # or $FALSE if not. function ValidateVersionString([String] $version) { $ok = $FALSE $parts = $version.Split(».«) if ($parts.Count -le 4) { for ($n = 0; $n -lt $parts.Count; $n++) { $ok = ($parts[$n] -as [UInt16]) -ne $NULL if (-not $ok) { break } } } return $ok } if (-not ($version1 -and $version2)) { write-host»Specify two versions numbers (a.b.c.d) to compare« exit } if (-not (ValidateVersionString $version1)) { throw»Invalid version string — $version1« } if (-not (ValidateVersionString $version2)) { throw»Invalid version string — $version2« } $OFS =»,« $ver1Array = GetVersionStringAsArray $version1 $ver1String = GetVersionArrayAsString $ver1Array »$ver1String = $ver1Array« $ver2Array = GetVersionStringAsArray $version2 $ver2String = GetVersionArrayAsString $ver2Array »$ver2String = $ver2Array« $result = CompareVersions $ver1String $ver2String if ($result -eq -1) { »$ver1String < $ver2String« } elseif ($result -eq 0) { »$ver1String = $ver2String« } else { »$ver1String > $ver2String« }
Листинг 6. Код сценария Versions.vbs
' Bitwise left shift. Function Lsh(ByVal N, ByVal Bits) Lsh = N * (2 ^ Bits) End Function ' Returns a version string»a.b.c.d«as a two-element numeric ' array. The first array element is the most-significant 32 bits, ' and the second element is the least-significant 32 bits. Function GetVersionStringAsArray(ByVal Version) Dim VersionAll, VersionParts, N VersionAll = Array(0, 0, 0, 0) VersionParts = Split(Version,».«) For N = 0 To UBound(VersionParts) VersionAll(N) = CLng(VersionParts(N)) Next Dim Hi, Lo Hi = Lsh(VersionAll(0), 16) + VersionAll(1) Lo = Lsh(VersionAll(2), 16) + VersionAll(3) GetVersionStringAsArray = Array(Hi, Lo) End Function ' Compares two versions»a.b.c.d«. If Version1 < Version2, ' returns -1; if Version1 = Version2, returns 0; ' If Version1 > Version2, Returns 1. Function CompareVersions(ByVal Version1, ByVal Version2) Dim Ver1, Ver2, Result Ver1 = GetVersionStringAsArray(Version1) Ver2 = GetVersionStringAsArray(Version2) If Ver1(0) < Ver2(0) Then Result = -1 ElseIf Ver1(0) = Ver2(0) Then If Ver1(1) < Ver2(1) Then Result = -1 ElseIf Ver1(1) = Ver2(1) Then Result = 0 Else Result = 1 End If Else Result = 1 End If CompareVersions = Result End Function ' Bitwise right shift. Function Rsh(ByVal N, ByVal Bits) Rsh = N \ (2 ^ Bits) End Function ' Returns the specified two-element array containing a version ' number as a string»a.b.c.d«. Function GetVersionArrayAsString(ByVal Version) Dim Hi, Lo Hi = Version(0) Lo = Version(1) GetVersionArrayAsString = CStr(Rsh(Hi, 16) And 65535) &».«_ & CStr(Hi And 65535) &».«_ & CStr(Rsh(Lo, 16) And 65535) &».«_ & CStr(Lo And 65535) End Function ' Returns True if the version string»a.b.c.d«is valid, ' or False if not. Function ValidateVersionString(ByVal Version) Dim OK, VersionParts OK = False VersionParts = Split(Version,».«) If UBound(VersionParts) <= 3 Then Dim N, Part For N = 0 To UBound(VersionParts) On Error Resume Next Part = CLng(VersionParts(N)) OK = Err.Number = 0 On Error GoTo 0 If OK Then OK = (Part >= 0) And (Part <= 65535) End If If Not OK Then Exit For End If Next End If ValidateVersionString = OK End Function Dim Args Set Args = WScript.Arguments If Args.Unnamed.Count < 2 Then WScript.Echo»Specify two version numbers (a.b.c.d) to compare« WScript.Quit End If Dim Version1 Version1 = WScript.Arguments.Item(0) If Not ValidateVersionString(Version1) Then WScript.Echo»Invalid version string — '«& Version1 &»'« WScript.Quit End If Dim Version2 Version2 = WScript.Arguments.Item(1) If Not ValidateVersionString(Version2) Then WScript.Echo»Invalid version string — '«& Version2 &»'« WScript.Quit End If Dim Ver1Array, Ver1String Ver1Array = GetVersionStringAsArray(Version1) Ver1String = GetVersionArrayAsString(Ver1Array) WScript.Echo Ver1String &» = «& Ver1Array(0) &»,«& Ver1Array(1) Dim Ver2Array, Ver2String Ver2Array = GetVersionStringAsArray(Version2) Ver2String = GetVersionArrayAsString(Ver2Array) WScript.Echo Ver2String &» = «& Ver2Array(0) &»,«& Ver2Array(1) Dim Result Result = CompareVersions(Ver1String, Ver2String) If Result = -1 Then WScript.Echo Ver1String &» < «& Ver2String ElseIf Result = 0 Then WScript.Echo Ver1String &» = «& Ver2String Else WScript.Echo Ver1String &» > «& Ver2String End If
Листинг 7. Код сценария Versions.js
function getVersionStringAsArray(version) { var parts = version.split(».«); if (parts.length < 4) { for (var n = parts.length; n < 4; n++) parts.push(»0«); } var hi = (parseInt(parts[0], 10) << 16) + parseInt(parts[1], 10); var lo = (parseInt(parts[2], 10) << 16) + parseInt(parts[3], 10); return [hi, lo]; } function getVersionArrayAsString(version) { var hi = version[0]; var lo = version[1]; return ((hi >> 16) & 65535).toString() +».«+ (hi & 65535).toString() +».«+ ((lo >> 16) & 65535).toString() +».«+ (lo & 65535).toString(); } function compareVersions(version1, version2) { var ver1 = getVersionStringAsArray(version1); var ver2 = getVersionStringAsArray(version2); if (ver1[0] < ver2[0]) return -1; else if (ver1[0] == ver2[0]) { if (ver1[1] < ver2[1]) return -1; else if (ver1[1] == ver2[1]) return 0; else return 1; } else return 1; } function validateVersionString(version) { var ok = false; var parts = version.split(».«); if (parts.length <= 4) { for (var n = 0; n < parts.length; n++) { var part = parseInt(parts[n], 10); ok = (part >= 0) && (part <= 65535); if (! ok) break; } } return ok; } var args = WScript.Arguments; if (args.Unnamed.Count < 2) { WScript.Echo(»Specify two version numbers (a.b.c.d) to compare«); WScript.Quit(); } var version1 = args.Unnamed.Item(0); if (! validateVersionString(version1)) { WScript.Echo(»Invalid version string — '«+ version1 +»'«); WScript.Quit(); } var version2 = args.Unnamed.Item(1); if (! validateVersionString(version2)) { WScript.Echo(»Invalid version string — '«+ version2 +»'«); WScript.Quit(); } var ver1Array = getVersionStringAsArray(version1); var ver1String = getVersionArrayAsString(ver1Array); WScript.Echo(ver1String +» = «+ ver1Array); var ver2Array = getVersionStringAsArray(version2); var ver2String = getVersionArrayAsString(ver2Array); WScript.Echo(ver2String +» = «+ ver2Array); var result = compareVersions(ver1String, ver2String); if (result == -1) WScript.Echo(ver1String +» < «+ ver2String); else if (result == 0) WScript.Echo(ver1String +» = «+ ver2String); else WScript.Echo(ver1String +» > " + ver2String);