How can my code find out if it's running as VBScript, .HTA, or VBA?
Asked Answered
L

7

7

(Final edit: The code I ended up putting together that works is way down below, it's probably the final reply in this thread. :-) )

I'm trying to write generic copy-and-paste code that will work in a standalone VBScript (a .vbs file), in a .hta file, and as VBA (for instance, in an Excel file). To do that, I need some way for the code itself to tell what engine it's running in.

The best idea I've heard so far involved testing if certain objects exist or not, but in VBA, that fails at compile time (so I can't bypass it with On Error), so that didn't work out. Trying to find out the name of the file it's running didn't end up being viable; that's one of the things that's done differently depending on which of the three script engines the code's running in. I would love to have something simple like this, but am not sure what to fill it in with:

Edit: Most responses so far involve checking for possibly non-existent objects, which outright does not work in VBA with Option Explicit on (it throws a compile-time error, so On Error doesn't work, and turning off Option Explicit is not an option). Is there some other roundabout / out-of-the-box way to find out what's needed here?

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

If this is filled in right, you should be able to copy and paste that function into any VBA, VBS, or HTA file without modification, call it, and get a result instead of an error, even when Option Explicit is on. What's the best way to go about this?

Lingam answered 13/2, 2019 at 20:47 Comment(2)
I think I'm missing something --- why does that matter? If you are writing a code meant to run anywhere, then you shouldn't care what engine it is, right?Godrich
The problem is that there are a few basic things that differ between the engines (for instance, how to tell what file the code is running from, or how to make a delay). If I have a way to detect which engine is running, I can make code that branches to handle things like that automatically without modification.Lingam
I
10

The restriction on requiring Option Explicit in the VBA implementation makes this a little more difficult than it otherwise would be (it's a one-liner without it)... Ironically it also turns out to be the key to the solution. If you don't limit yourself to a single function, you can get away with it by doing something like this:

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

It works like this - If it's loaded via an HTA file, the window_onload event handler runs, setting the hta variable to True. That's the first test. The second "test" is on the error thrown by the line Set foo = foo. This is a type mismatch in VBA, where it is interpreted as trying to Set a Variant to Empty, which isn't a compatible type. The same line of code throws an error 424 (Object required) in VBScript because it is not a strongly typed language. That means VBA's type check is skipped and it attempts to actually perform the assignment (which fails). The rest is just figuring out how it threw and returning the result.

Test code

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

HTA

<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>

EDIT:

FWIW, the one-liner referenced above if Option Explicit isn't needed is simply this:

Function HostString()
    HostString = Application & document & WScript
End Function

All three objects have a default property that returns a String. In VBScript, this will return "Windows Script Host". In VBA, it will return the name of the host (i.e. "Microsoft Excel" in Excel). In HTA it will return "[object]".

Intumescence answered 14/2, 2019 at 0:36 Comment(9)
Wow, that's some clever hack.Rillings
VERY clever! Thank you, this is pretty slick!Lingam
Wait a minute - the VBA one is stopping with "Run-time error '13': Type mismatch" on the "Set foo=foo" line, despite the On Error being in there.Lingam
@Lingam - Uh... it shouldn't - I'm not replicating that in Excel 2013 x32. Do you have the VBE set up for "Break on All Errors"?Intumescence
Huh, it seems yes, yes I do! Wow, I set that ages ago, I'd long forgotten about that! Ok, that works much better now, thank you! :-)Lingam
Fun fact: I found out how to tell between VBScript and an HTA in the same function - if you try ReDim WScript(0) , VBScript throws an error, but an HTA has no problem at all with that idea! :-)Lingam
@Lingam Glad that's working for you. The reason ReDim WScript(0) works in HTA and not VBScript is because in VBScript it's the application object, so you'd get a type mismatch. In HTA, it would be an undeclared variable (which defaults to Variant). The Redim just redefines it as an array of Variant with one (empty) element.Intumescence
Actually, that's the weird thing. In an HTA and VBA, if you try to ReDim an object that does already exist in those engines (like "Window", or "Application"), it lets you! You end up with a new local variable that overrides the built in object until it goes out of scope. VBScript is the only one of the three that complains. Interesting, and oddly useful!Lingam
@Lingam - That's normal scope resolution. A ReDim statement counts as a declaration (it can be used on its own even without a Dim if Option Explicit is specified). The new variable "hides" the one within the scope that it's defined in. See the second bullet under "Static Sematics" for ReDim in the VBA spec.Intumescence
L
5

Although I agree with @this here's my Option Explicit-safe method with no On Error statements that uses Window_OnLoad listener for HTA (like Comintern did) and a line label trick to distinguish VBScript from 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
Leftover answered 15/2, 2019 at 17:16 Comment(1)
Nice! I caught the VBScript label thing when I was playing around with it, but I didn't think to make it a function call.Intumescence
P
2

Try to check existence of context objects, like

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
Pricecutting answered 13/2, 2019 at 21:11 Comment(5)
That's definitely on the right track, however On Error Resume Next will make the function not work as intended. Upvoted for the idea, but needs more work.Rillings
Not sure what did you mean by "will make the function not work as intended". O.E.R.N. is exactly what needs here, without that the code will just crash. It's like using try{}catch() blocks in other languages.Pricecutting
What I mean is that what follows the Then token will be executed unconditionally in case of an error, making the function return the wrong value; see this answer.Rillings
The error OERN is shrugging off is RTE 424 "object required", which is tripped by the Is Nothing check against what's a Variant/Empty at run-time. You can't unconditionally return "WScript", you need to first check whether the null-check has thrown an error. As-is, this logic returns whatever the first check decides to return.Rillings
Very good point. I updated my sample code. It works properly now, just tested it.Pricecutting
R
2

Any undeclared variable will be Variant/Empty, so VarType(something) will be vbEmpty (or 0) if something is undefined.

Unless VarType doesn't exist outside the VBA standard library (I've no idea TBH), then there's no need to trap/skip/handle any errors for this to work - tested in 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

Note that this will yield unexpected results if e.g. Application or WScript is defined somewhere and the host isn't VBA or VBScript, respectively.

Alternatively, this would work whether VarType is defined or not:

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

Again it assumes that no object by these names are defined; the mechanism relies on WScript/Application being a Variant/Empty and thus throwing run-time error 424 "Object Required" when tested with Is Nothing.

Note that you can't have Option Explicit specified for this. The reason is because if you do Dim WScript and Dim Application to satisfy the compiler, then at runtime these variables will be shadowing the global identifiers you're checking, and the function will consistently return whatever host you checked first.

Rillings answered 13/2, 2019 at 22:9 Comment(5)
Unfortunately, trying this code in a VBA with Option Explicit crashes with "Compile error: Variable not defined" on the first WScript reference.Lingam
@Lingam yes, it's relying on the Variant/Empty behavior of undeclared variables: you can't have it both ways - if you declare WScript or Application then you're shadowing the global declarations and making everything useless. You can't have Option Explicit specified for this. I've edited the answer accordingly.Rillings
Indeed, and that's exactly my point - I'm looking for a way around the problem, and that way has to function with Option Explicit on. So the trick becomes what other ways to detect the engine could there be, direct or indirect, besides trying to test objects that may not exist? :-)Lingam
(And yes, I fully admit this is tricky - hence why I'm here. :-) )Lingam
@Lingam then I would heed this questioning and take a step back and ask myself exactly why this matters at all.Rillings
G
1

I'd suggest that you are approaching the problem backward. Instead of asking "who's the host here?", you should instead have a common interface for tasks. As an example, You would have a module - let's call it MyAPI:

Public Function MySleep(Milliseconds As Long)
End Function

You can then implement one for VBA, one for VBS, other for HTA (though I question whether there's a real difference. Your host-agnostic code then would require inclusion of that module of which all should allow for. For example, see here for including a file to VBS. It also looks like HTA has something similar. In VBA, that's just another module.

Then in your agnostic-host code, you would call MySleep instead of API-declared Sleep or WScript.Sleep, and let the included module provide the host-specific implementation without any branching and therefore any need to disable Option Explicit nor testing for non-existent objects.

Godrich answered 13/2, 2019 at 23:55 Comment(1)
That's actually along the lines of what the end result of this would be, except I'm trying to have it as one piece of code I can use for all three engines. Maintaining three separate sets of sets of code for my common functions has become a bit of a bear. If I can get this detection thing down, then I can write that MySleep funtion (among others) once, have it handle sleeping depending on which engine is running, and poof, problem solved. :-)Lingam
L
1

For anyone coming across this in the future, this is the final code I ended up making, and it works! Special thanks to everyone in this thread that contributed the ideas that came together to make this work! :-)

'------------------------------------------------------------------
'--- 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

'-------------------------------------------------------------------
Lingam answered 13/8, 2019 at 23:18 Comment(0)
R
-1

All programs have a host that provides a global Application object at a minimum. WScript provides the global WScript, Word/Excel an Application object, an HTA an IE Window object (which through the parent property gives access to InternetExplorer.Application object.

In COM we call QueryRef on the IUnknown interface to find what objects it has. This happens under the hood. The principal is you try to use it and look for the error that says for a property E_Not_Implemented.

This is the standard on what should be in an Application object https://learn.microsoft.com/en-au/previous-versions/windows/desktop/automat/using-the-application-object-in-a-type-library

So wscript.name, InternetExplorer.Application.Name, and Word.Application.Name.

In Wscript this code prints Windows Scripting Host. In Word it prints Normal 424 Object Required. In an 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

Likewise in Word Microsoft Word, VBS Microsoft VBScript runtime error 424 Object required, and 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

Also note testing for Application is defined or not is not robust. Excel also has an Application object. You have to test for the name.

Respirator answered 13/2, 2019 at 21:9 Comment(6)
Hmm, interesting! How would I go about actually doing that, code-wise? Do you have a few lines of sample code I could test out and experiment with?Lingam
Ahh, I see about testing for the name, that's a great way to get around if someone redefined the object. Unfortunately, trying this kind of code in a VBA with Option Explicit crashes with "Compile error: Variable not defined" at the first mention of WScript.Lingam
Is there perhaps a roundabout way to identify the engine without checking for objects that may or may not exist?Lingam
That's the whole idea of COM checking if objects are available, so you can choose a different one if not. Normally Basic hides the details from us.Respirator
Dim wscript as object.Respirator
Dim WScript as Object allocates 16 bytes of memory 1 x 32 bit reference count, 1 x 32 bit pointer to the VTable (Virtual Function Table - the memory address of all methods and properties), and 2 x 32 bit unused numbers. They are all set to 0 (or nothing) till you use the set command. So Option Explicit will be happy.Respirator

© 2022 - 2024 — McMap. All rights reserved.