Билл Стюарт (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. Выполните в калькуляторе следующие действия:

  1. Введите число 3 для основного номера версии
  2. Нажмите кнопку Lsh, введите значение 10 (шестнадцатеричное для 16) и нажмите «Ввод». Это действие передвинет разряды числа 16 влево.
  3. Нажмите +, затем 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)

 

Листинг 3. Функции VBScript

' 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

 

Листинг 4. Функции PowerShell

# 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);