(Последнее редактирование: код, который я собрал, работает, находится ниже, вероятно, это окончательный ответ в этой теме. :-))
Я пытаюсь написать общий код копирования и вставки, который будет работать в автономном VBScript (файл .vbs), в файле .hta и как VBA (например, в файле Excel). Для этого мне нужен какой-то способ, чтобы сам код сообщал, на каком движке он работает.
Лучшая идея, которую я слышал до сих пор, заключалась в проверке того, существуют определенные объекты или нет, но в VBA это дает сбой во время компиляции (поэтому я не могу обойти это с помощью On Error), так что это не сработало. Попытка узнать имя файла, в котором он работает, не увенчалась успехом; это одна из вещей, которая выполняется по-разному в зависимости от того, в каком из трех скриптовых движков выполняется код. Я хотел бы иметь что-то простое, как это, но не уверен, чем его заполнить:
Обновлено: большинство ответов до сих пор включают проверку возможно несуществующих объектов, которые напрямую не работают в VBA с включенным параметром Explicit (он выдает ошибку времени компиляции, поэтому On Error не работает, а отключение Option Explicit не работает). опция). Есть ли какой-то другой обходной/нестандартный способ узнать, что здесь нужно?
Option Explicit
'--- Returns a string containing which script engine this is running in,
'--- either "VBScript", "VBA", or "HTA".
Function ScriptEngine()
If {what goes here?} Then ScriptEngine = "VBS"
If {what goes here?} Then ScriptEngine = "VBA"
If {what goes here?} Then ScriptEngine = "HTA"
End Function
Если это заполнено правильно, вы сможете скопировать и вставить эту функцию в любой файл VBA, VBS или HTA без изменений, вызвать ее и получить результат вместо ошибки, даже если опция Explicit включена. Как лучше всего это сделать?
Проблема в том, что есть несколько основных вещей, которые различаются между движками (например, как определить, из какого файла выполняется код, или как сделать задержку). Если у меня есть способ определить, какой движок работает, я могу сделать код, который разветвляется, чтобы обрабатывать подобные вещи автоматически без модификации.


У всех программ есть хост, который предоставляет как минимум глобальный объект Application. WScript предоставляет глобальный WScript, Word/Excel — объект Application, HTA — объект IE Window (который через родительское свойство дает доступ к объекту InternetExplorer.Application.
В COM мы вызываем QueryRef для интерфейса IUnknown, чтобы узнать, какие у него есть объекты. Это происходит под капотом. Принципиал заключается в том, что вы пытаетесь использовать его и ищете ошибку, которая говорит о свойстве E_Not_Implemented.
Это стандарт того, что должно быть в объекте приложения https://docs.microsoft.com/en-au/previous-versions/windows/desktop/automat/using-the-application-object-in-a-type-library.
Итак, wscript.name, InternetExplorer.Application.Name и Word.Application.Name.
В Wscript этот код печатает Windows Scripting Host. В Word печатает Normal 424 Object Required. В HTA Microsoft VBScript runtime error 424 Object required.
On Error Resume Next
x = wscript.name
If err.Number = 0 then
Msgbox x
Else
Msgbox err.source & " " & err.number & " " & err.description
End If
Аналогично в Word Microsoft Word, VBS Microsoft VBScript runtime error 424 Object required и HTA Microsoft VBScript runtime error 424 Object required.
On Error Resume Next
x = Application.Name
If Err.Number = 0 Then
MsgBox x
Else
MsgBox Err.Source & " " & Err.Number & " " & Err.Description
End If
Также обратите внимание, что тестирование приложения определено или не является надежным. В Excel также есть объект Application. Вы должны пройти тест на name.
Хм, интересно! Как бы я на самом деле сделал это с точки зрения кода? У вас есть несколько строк кода, которые я мог бы протестировать и поэкспериментировать?
Ааа, я вижу насчет проверки имени, это отличный способ обойти, если кто-то переопределил объект. К сожалению, попытка такого кода в VBA с Option Explicit вылетает с ошибкой компиляции: переменная не определена при первом упоминании WScript.
Возможно, есть обходной способ идентифицировать двигатель без проверки объектов, которые могут существовать или не существовать?
В этом вся идея COM, проверяющая, доступны ли объекты, поэтому вы можете выбрать другой, если нет. Обычно Basic скрывает от нас детали.
Dim wscript as object.
Dim WScript as Object выделяет 16 байт памяти 1 x 32-битный счетчик ссылок, 1 x 32-битный указатель на VTable (таблица виртуальных функций - адрес памяти всех методов и свойств) и 2 x 32-битных неиспользуемых числа. Все они установлены на 0 (или ничего), пока вы не используете команду set. Так что Option Explicit будет счастлив.
Попробуйте проверить наличие объектов контекста, например
Function ScriptEngine()
dim tst
on error resume next
Err.Clear
tst = WScript is Nothing
if Err=0 then ScriptEngine = "WScript" : exit function
Err.Clear
' similar way check objects in other environments
End Function
Это определенно на правильном пути, однако On Error Resume Next заставит функцию работать не так, как предполагалось. Проголосовал за идею, но нужно доработать.
Не уверен, что вы имели в виду под «заставит функцию работать не так, как предполагалось». О.Э.Р.Н. это именно то, что здесь нужно, без этого код просто вылетит. Это похоже на использование блоков try{}catch() в других языках.
Я имею в виду, что то, что следует за токеном Then, будет выполняться безоговорочно в случае ошибки, заставляя функцию возвращать неправильное значение; см. этот ответ.
OERN не обращает внимания на ошибку RTE 424 «требуется объект», которая отключается проверкой Is Nothing на соответствие Variant/Empty во время выполнения. Вы не можете безоговорочно вернуть «WScript», вам нужно сначала проверить, не вызвала ли нулевая проверка ошибку. Как есть, эта логика возвращает то, что решит вернуть первая проверка.
Очень хороший момент. Я обновил свой пример кода. Сейчас работает нормально, только что проверил.
Любая необъявленная переменная будет Variant/Empty, поэтому VarType(something) будет vbEmpty (или 0), если something не определена.
Если VarType не существует вне стандартной библиотеки VBA (я понятия не имею, TBH), то нет необходимости перехватывать/пропускать/обрабатывать какие-либо ошибки, чтобы это работало - проверено в VBA:
Function GetHostType()
If VarType(wscript) <> vbEmpty Then
GetHostType = "VBS"
Exit Function
End If
If VarType(Application) <> vbEmpty Then
GetHostType = "VBA"
Exit Function
End If
GetHostType = "HTA"
End Function
Обратите внимание, что это приведет к неожиданным результатам, если, например. Application или WScriptявляется определен где-то, а хост не является VBA или VBScript соответственно.
В качестве альтернативы это будет работать независимо от того, определен ли VarType или нет:
Function GetHostType()
On Error Resume Next
If Not WScript Is Nothing Then
If Err.Number = 0 Then
GetHostType = "VBS"
Exit Function
End If
End If
Err.Clear ' clear error 424 if not VBS
If Not Application Is Nothing Then
If Err.Number = 0 Then
GetHostType = "VBA"
Exit Function
End If
End If
Err.Clear ' clear error 424 if not VBA
GetHostType = "HTA"
End Function
Снова предполагается, что ни один объект с этими именами не определен; механизм основан на том, что WScript/Application является Variant/Empty и, таким образом, выдает ошибку времени выполнения 424 «Требуется объект» при тестировании с Is Nothing.
Обратите внимание, что для этого нельзя указать Option Explicit. Причина в том, что если вы сделаете Dim WScript и Dim Application, чтобы удовлетворить компилятор, то во время выполнения эти переменные будут затенять глобальные идентификаторы, которые вы проверяете, и функция последовательно вернет тот хост, который вы проверили первым.
К сожалению, при попытке использовать этот код в VBA с Option Explicit происходит сбой с сообщением «Ошибка компиляции: переменная не определена» в первой ссылке WScript.
@CyberTaco да, он полагается на Variant/Empty поведение необъявленных переменных: у вас не может быть и того, и другого — если вы объявляете WScript или Application, вы затемняете глобальные объявления и делаете все бесполезным. Вы не можете указать Option Explicit для этого. Я отредактировал ответ соответственно.
Действительно, и это именно моя точка зрения - я ищу способ обойти проблему, и этот способ должен работать с включенной опцией Explicit. Таким образом, хитрость заключается в том, какие разное способы обнаружения движка могут быть, прямые или косвенные, помимо попытки протестировать объекты, которые могут не существовать? :-)
(И да, я полностью признаю, что это сложно - поэтому я здесь. :-))
@CyberTaco, тогда я бы прислушался к это допрос, сделал бы шаг назад и спросил бы себя, почему это вообще имеет значение.
Я бы посоветовал вам подойти к проблеме в обратном направлении. Вместо того, чтобы спрашивать «кто здесь хозяин?», у вас должен быть общий интерфейс для задач. Например, у вас будет модуль - назовем его MyAPI:
Public Function MySleep(Milliseconds As Long)
End Function
Затем вы можете реализовать один для VBA, один для VBS, другой для HTA (хотя я сомневаюсь, есть ли реальная разница. Тогда ваш код, не зависящий от хоста, потребует включения того модуля, который все должны разрешать. Например, см. здесь для включения файла в VBS. Это также выглядит как В HTA есть что-то подобное В VBA это просто еще один модуль.
Затем в коде независимого хоста вы должны вызывать MySleep вместо объявленных в API Sleep или WScript.Sleep и позволить включенному модулю предоставлять специфичную для хоста реализацию без какого-либо ветвления и, следовательно, без необходимости отключать Option Explicit или тестировать несуществующие объекты.
На самом деле это похоже на конечный результат, за исключением того, что я пытаюсь использовать его как один фрагмент кода, который я могу использовать для всех трех движков. Поддержание трех отдельных наборов кодов для моих общих функций стало чем-то вроде медведя. Если я смогу отключить эту функцию обнаружения, то я могу написать эту функцию MySleep (среди прочего) один раз, заставить ее обрабатывать сон в зависимости от того, какой двигатель работает, и пуф, проблема решена. :-)
Ограничение на требование Option Explicit в реализации VBA делает это немного сложнее, чем было бы в противном случае (без него это однострочник) ... По иронии судьбы, это также оказывается ключом к решению.
Если вы не ограничиваете себя одной функцией, вы можете обойтись без нее, выполнив что-то вроде этого:
Dim hta
Sub window_onload()
hta = True
End Sub
Function HostType()
On Error Resume Next
If hta Then
HostType = "HTA"
Else
Dim foo
Set foo = foo
If Err.Number = 13 Then
HostType = "VBA"
Else
HostType = "VBS"
End If
End If
End Function
Это работает следующим образом: если он загружается через HTA-файл, запускается обработчик события window_onload, устанавливая для переменной hta значение True. Это первое испытание. Второй «тест» — на ошибку, которую выдает строка Set foo = foo. Это несоответствие типов в VBA, где оно интерпретируется как попытка Set преобразовать Variant в Empty, что не является совместимым типом. Та же строка кода выдает ошибку 424 (требуется объект) в VBScript, потому что это не строго типизированный язык. Это означает, что проверка типа VBA пропускается, и он пытается фактически выполнить назначение (что не удается). Остальное просто выяснить, как он бросил и вернуть результат.
VBA
Option Explicit
Dim hta
Sub Test()
Debug.Print HostType 'VBA
End Sub
Sub window_onload()
hta = True
End Sub
Function HostType()
On Error Resume Next
If hta Then
HostType = "HTA"
Else
Dim foo
Set foo = foo
If Err.Number = 13 Then
HostType = "VBA"
Else
HostType = "VBS"
End If
End If
End Function
VBScript
WSCript.Echo HostType
Dim hta
Sub window_onload()
hta = True
End Sub
Function HostType()
On Error Resume Next
If hta Then
HostType = "HTA"
Else
Dim foo
Set foo = foo
If Err.Number = 13 Then
HostType = "VBA"
Else
HostType = "VBS"
End If
End If
End Function
ОМТ
<HTML>
<BODY>
<script type = "text/vbscript">
Dim hta
Sub Test()
MsgBox HostType
End Sub
Sub window_onload()
hta = True
End Sub
Function HostType()
On Error Resume Next
If hta Then
HostType = "HTA"
Else
Dim foo
Set foo = foo
If Err.Number = 13 Then
HostType = "VBA"
Else
HostType = "VBS"
End If
End If
End Function
</script>
<button onclick = "vbscript:Test()">Click me</button>
</BODY>
</HTML>
Обновлено:
FWIW, однострочник, упомянутый выше, если Option Explicit не нужен, просто так:
Function HostString()
HostString = Application & document & WScript
End Function
Все три объекта имеют свойство по умолчанию, которое возвращает String. В VBScript это вернет «Хост сценариев Windows». В VBA он вернет имя хоста (например, «Microsoft Excel» в Excel). В HTA он вернет «[объект]».
Вау, это какой-то хитрый хак.
Очень умный! Спасибо, это довольно гладко!
Подождите минуту - VBA останавливается с «Ошибка времени выполнения« 13 »: несоответствие типов» в строке «Set foo = foo», несмотря на то, что там присутствует ошибка «При ошибке».
@CyberTaco - Э... не должно - я не копирую это в Excel 2013 x32. У вас есть настройка VBE для «Перерыва при всех ошибках»?
Ха, кажется, да, да! Вау, я поставил это сто лет назад, я давно забыл об этом! Хорошо, теперь это работает намного лучше, спасибо! :-)
Забавный факт: я узнал, как отличить VBScript от HTA в одной и той же функции — если вы попробуете ReDim WScript(0) , VBScript выдаст ошибку, но у HTA нет никаких проблем с этой идеей! :-)
@CyberTaco Рад, что это работает на вас. Причина, по которой ReDim WScript(0) работает в HTA, а не в VBScript, заключается в том, что в VBScript это объект приложения, поэтому вы получите несоответствие типов. В HTA это будет необъявленная переменная (по умолчанию она равна Variant). Redim просто переопределяет его как массив Variant с одним (пустым) элементом.
На самом деле, это странно. В HTA и VBA, если вы попытаетесь преобразовать объект, который уже существует в этих движках (например, «Окно» или «Приложение»), это позволит вам! В итоге вы получаете новую локальную переменную, которая переопределяет встроенный объект до тех пор, пока он не выйдет за пределы области видимости. VBScript — единственный из трех, который жалуется. Интересно и, как ни странно, полезно!
@CyberTaco - это нормальное разрешение области. Оператор ReDim считается объявлением (его можно использовать отдельно даже без Dim, если указан Option Explicit). Новая переменная «скрывает» ту, которая находится в области, в которой она определена. См. второй маркер в разделе «Статическая сематика» для ReDim в Спецификация VBA.
Хотя я согласен с @это, вот мой Option Explicit-безопасный метод без On Error утверждений, который использует прослушиватель Window_OnLoad для HTA (как это делал Коминтерн) и трюк с меткой строки, чтобы отличить VBScript от VBA.
Dim IsInHTA, IsInVBScript
Sub Window_Onload()
IsInHTA = True
End Sub
Sub LineLabelTest()
'VBA and VB6 (maybe VB5 too, IDK) treats "DummyLabel:" as a line label
'VBScript treats "DummyLabel" as an expression to call and treats ":" as a statement separator.
DummyLabel:
End Sub
Sub DummyLabel()
'this is called by the LineLabelTest subroutine only in VBScript
IsInVBScript = True
End Sub
Function HostType()
LineLabelTest
If IsInVBScript Then
If IsInHTA Then
HostType = "HTA"
Else
HostType = "VBS" 'Other hosts incuding WSH, ASP, Custom
End If
Else
HostType = "VBA" 'VBA or VB6 (maybe VB5 too, don't know)
End If
End Function
Хороший! Я поймал метку VBScript, когда играл с ней, но не подумал сделать из нее вызов функции.
Для тех, кто столкнется с этим в будущем, это окончательный код, который я закончил, и он работает! Особая благодарность всем в этой теме, которые поделились идеями, которые объединились, чтобы сделать эту работу! :-)
'------------------------------------------------------------------
'--- Function ScriptEngine
'---
'--- Returns a string containing which script engine this is
'--- running in.
'--- Will return either "VBScript","VBA", or "HTA".
Function ScriptEngine
On Error Resume Next
ScriptEngine = "VBA"
ReDim WScript(0)
If Err.Number=501 Then ScriptEngine = "VBScript"
Err.Clear
ReDim Window(0)
If Err.Number=501 Then ScriptEngine = "HTA"
On Error Goto 0
End Function
'-------------------------------------------------------------------
Я думаю, что я что-то упускаю --- какое это имеет значение? Если вы пишете код, предназначенный для выполнения где угодно, вам все равно, какой у него движок, верно?