3

Hello a client of mine has a lot of VBSripts (VBS) that do various things. At the top of each script they reference a library of subs and functions like this:

Const ForReading = 1
Dim objFSO, objFile
Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("Z:\path\VbsProcedures.vbs", ForReading)
Execute objFile.ReadAll()
'more code that uses the subs and functions in VbsProcedures.vbs

This way the subs and functions are available to the code below the lines. That is all good. I can work on the procedures all in one place. The issue is that when a procedure in VbsProcedures.vbs produces an error I don't get a line number to help me debug it. I just returns line 0 character 1, or something like that.

How do I get an error line number in this case?

I am wondering if there is a simple solution.

I can put If Err.Number <> 0 Then after every suspect line and echo out an error description like this https://technet.microsoft.com/en-us/library/ee692852.aspx, but that seems quite inefficient. You can't use GoTo somelabel statements in VBS to handle errors for large blocks of code like you can in VBA.

Edit: I can't post the VbsProcedures.vbs because it is my clients. I am looking for a general solution to the issue so I can debug better in general. I'm not looking for help debugging a specific issue right now. In fact there are no errors in it right now. Generally speaking, what is in the VbsProedures.vbs is routine Functions and Subs we use. Like this:

Option Explicit

'create formatted dates easily
Function sfFormatDate(sDateOld, sFormat)

'code omitted

sfFormatDate = sDateNew
End Function

'progress  type of message
Function fScriptIsRunningNoticeStart()

'code omitted

fScriptIsRunningNoticeStart = objExplorer
End Function

TEMP EDIT2: (Attempting BuvinJ's suggestion.)

I think I have it working finally....

Contents of file test.vbs:

'Option Explicit 

Const bINJECT_LOGGING=True

Sub Include( sRelativeFilePath )    
    ' Get the library script
    Dim oFs : Set oFs = CreateObject("Scripting.FileSystemObject")
    Dim sThisFolder : sThisFolder = oFs.GetParentFolderName( WScript.ScriptFullName )
    Dim sAbsFilePath : sAbsFilePath = oFs.BuildPath( sThisFolder, sRelativeFilePath )

    Dim sLibrary : sLibrary = oFs.OpenTextFile( sAbsFilePath ).readAll()
    ' Perform compilation debugging
    On Error Resume Next
    Dim oSC : Set oSC = CreateObject("MSScriptControl.ScriptControl")
    With oSC
        .Language = "VBScript"
        .UseSafeSubset = False
        .AllowUI = False
        .AddCode sLibrary
    End With
    With oSC.Error
        If .Number <> 0 then
            WScript.Echo sAbsFilePath & "(" & .Line & "," & .Column & ")" & _
                " Microsoft VBScript compilation error: " & _
                "(" & .Number & ") " & .Description
            WScript.Quit
        End If
    End With
    On Error Goto 0
    ' Implement optional runtime debugging via logging injections
    If bINJECT_LOGGING Then 
        InjectRoutineLogging sLibrary, _
            oFs.OpenTextFile( sAbsFilePath ), sRelativeFilePath
    End If
    ' Import the Library
    ExecuteGlobal sLibrary
End Sub

Sub InjectRoutineLogging( ByRef sLibrary, ByRef oFile, sFilePath )
    sLibrary = ""        
    Dim sLine, sParseLine, sAppendLine, sPrependLine
    Dim bIsRoutineBody : bIsRoutineBody = False
    Dim sRoutineName   : sRoutineName = ""
    Dim aStartKeywords : aStartKeywords = Array( "SUB", "FUNCTION" )
    Dim aEndKeywords   : aEndKeywords   = Array( "END SUB", "END FUNCTION" )    
    Do Until oFile.AtEndOfStream
        sLine        = oFile.ReadLine            
        sParseLine   = Trim(sLine)            
        sPrependLine = ""
        sAppendLine  = ""                
        ' Find routine signature starts (and the save name)
        If sRoutineName = "" Then                                 
            For Each sKeyword In aStartKeywords                
                If Left( UCase(sParseLine), Len(sKeyword) ) = sKeyword Then
                    sParseLine = Right( sParseLine, _
                                        Len(sParseLine)-Len(sKeyword) )
                    sRoutineName = Trim(Split( sParseLine, "(" )(0))
                End If
            Next            
        End If                
        If sRoutineName <> "" Then            
            If Not bIsRoutineBody Then                   
                ' Find end of routine signature 
                ' (figuring in line continuations and eol comments)                
                ' Inject start log
                sParseLine = Trim(Split(sParseLine, "'")(0)) 
                If Right( sParseLine, 1 ) = ")" Then                    
                    sAppendLine = "WScript.Echo" & _
                        """Start " & sRoutineName & _
                        " (" & sFilePath & ")..." & """" & vbCrLF
                    bIsRoutineBody = True
                End If    
            Else                    
                ' Find routine end
                ' Inject end log 
                For Each sKeyword In aEndKeywords                
                    If Left( UCase(sParseLine), Len(sKeyword) ) = sKeyword Then
                        sPrependLine = "WScript.Echo ""...End " & _
                                        sRoutineName & " "" " 
                        sRoutineName = ""
                        bIsRoutineBody = False
                    End If
                Next                                        
            End If                            
        End If
        ' Append lines
        If sPrependLine <> "" Then sLibrary = sLibrary & sPrependLine & vbCrLF
        sLibrary = sLibrary & sLine & vbCrLF
        If sAppendLine  <> "" Then sLibrary = sLibrary & sAppendLine  & vbCrLF
    Loop        
End Sub

WScript.Echo "test1"

Include "test_VbsProcedures.vbs" '"Z:\mypath\test_VbsProcedures.vbs"

WScript.Echo "test2"

call test_sub

WScript.Echo "test3"

Contents of file test_VbsProcedures.vbs is just:

sub test_sub()
WScript.Echo "test_sub"
end sub

Using the command in the cmd prompt:

"C:\Windows\SysWOW64\cscript.exe" "Z:\mypath\test.vbs"

I get:

test1
test2
Start test_sub (test_VbsProcedures.vbs)...
test_sub
...End test_sub
test3

I really appreciate your help BuvinJ's! Let me know if there is more to do here, but I think it is running! Next I'll break it to test it.

EDIT 3: If the lib has this:

option explicit

sub test_sub()
msgbox "testing msg"
WScript.Echo "test_sub"
s = 1
end sub

sub stack1()
call test_sub()
end sub

I get:

test1
test2
Start stack1 (test_VbsProcedures.vbs)...
Start test_sub (test_VbsProcedures.vbs)...
test_sub
Z:\mypath\test.vbs(0,
 1) Microsoft VBScript runtime error: Variable is undefined: 's'

... so it looks like error line number is (0,1), which isn't correct.

EDIT 4: with lib as:

option explicit
sub test_sub()
msgbox "testing msg"
WScript.Echo "test_sub"
's = 1
dim x : x=(1
end sub
sub stack1()
call test_sub()
end sub

line number is working now!!

test1
Z:\mypath\test_VbsProcedures.vbs(11,12) Microsoft VBScript compilation error: (1006) Expected ')'
test1
mountainclimber11
  • 1,339
  • 1
  • 28
  • 51
  • Can you post the script VbsProcedures.vbs ? – Hackoo Apr 24 '15 at 16:29
  • @Hackoo - I added an edit. – mountainclimber11 Apr 24 '15 at 16:53
  • Nice! I see fiel is being included, and the auto logging is working. If you revise your test function now to produce a compile error, it should spit that out for you (saying the actual file and line number in that lib!). If you change it instead to produce a run time error, the logging will reveal that it entered your function, but never left it. – BuvinJ May 16 '18 at 13:56
  • Here's simple compile error to test (missing a parenthesis): `dim x : x=(1` – BuvinJ May 16 '18 at 14:00
  • And here's a run time error (division by zero): `dim x : x=(1/0)` – BuvinJ May 16 '18 at 14:01
  • thanks. I added an edit 3. I created a compile error with `option explicit` and `s = 1`. I'll try yours now. No line number though. – mountainclimber11 May 16 '18 at 14:04
  • 1
    Your test in fact works. That shows a run time error. There is no way to get the lib file path and line number on one of those. Instead, you can trace through the log to find the ballpark of the error. That's the point of all that. If, however, you have a compile error, this code will id that exact. (which doesn't naturally occur otherwise) – BuvinJ May 16 '18 at 14:06
  • 1
    Note that most of the time, you will probably want to disable the logging. That's why I added the global switch that you can set at the top of the main script. You just turn that on when you have a run time error you can't track down. – BuvinJ May 16 '18 at 14:09
  • picking this up slowly! Thanks! See edit 4. Line numbers! Rolling your own high end code! thanks! – mountainclimber11 May 16 '18 at 14:12
  • Does that make sense? You'll get the compile error details exactly output all the time. When you have a nasty run time error that can't be found, turn on the logging switch and read through it to figure out which routine was started, but never ended. – BuvinJ May 16 '18 at 14:13
  • testing that now. – mountainclimber11 May 16 '18 at 14:14
  • In every other translated language (like Java, or Python, PHP...) they "throw exceptions" on run time errors which have details like the location of the error and even full "stack traces". VBS "raises errors", but those objects are weak and don't have much trace back info. Thus, one has to resort to old time logging, which is a pain to retro fit. That's why I automated it. – BuvinJ May 16 '18 at 14:17
  • where do I actually see the logging? On the cmd prompt? Or in some file? Looks like some file, but I don't see it. – mountainclimber11 May 16 '18 at 14:19
  • 1
    Yes, by "logging" I mean what you are seeing spit out on the console. "Logging" == messages you can read to trace through the execution of your program. – BuvinJ May 16 '18 at 14:23
  • 1
    If you want an actual, hard log file, then pipe your output into one like this: `"C:\Windows\SysWOW64\cscript.exe" "Z:\mypath\test.vbs" > result.log` – BuvinJ May 16 '18 at 14:24

2 Answers2

4

You don't get line numbers for runtime errors

It was not meant to load libraries.

This checks syntax.

Sub VBSCmd
    RawScript = LCase(Arg(1))
    'Remove ^ from quoting command line and replace : with vbcrlf so get line number if error
    Script = Replace(RawScript, "^", "")
    Script = Replace(Script, "'", chr(34))
    Script = Replace(Script, ":", vbcrlf)
    'Building the script with predefined statements and the user's code
    Script = "Dim gU" & vbcrlf & "Dim gdU" & vbcrlf & "Set gdU = CreateObject(" & chr(34) & "Scripting.Dictionary" & chr(34) & ")" & vbcrlf & "Function UF(L, LC)" & vbcrlf & "Set greU = New RegExp" & vbcrlf & "On Error Resume Next" & vbcrlf & Script & vbcrlf & "End Function" & vbcrlf

    'Testing the script for syntax errors
    On Error Resume Next
    set ScriptControl1 = wscript.createObject("MSScriptControl.ScriptControl",SC)
        With ScriptControl1
            .Language = "VBScript"
            .UseSafeSubset = False
            .AllowUI = True
        .AddCode Script
    End With
    With ScriptControl1.Error
        If .number <> 0 then
            Outp.WriteBlankLines(1)
            Outp.WriteLine "User function syntax error"
            Outp.WriteLine "=========================="
            Outp.WriteBlankLines(1)
            Outp.Write NumberScript(Script)
            Outp.WriteBlankLines(2)
            Outp.WriteLine "Error " & .number & " " & .description
            Outp.WriteLine "Line " & .line & " " & "Col " & .column
            Exit Sub
        End If
    End With

    ExecuteGlobal(Script)

    'Remove the first line as the parameters are the first line
    'Line=Inp.readline  
    Do Until Inp.AtEndOfStream
        Line=Inp.readline
        LineCount = Inp.Line 

        temp = UF(Line, LineCount)
        If err.number <> 0 then 
            outp.writeline ""
            outp.writeline ""
            outp.writeline "User function runtime error"
            outp.writeline "==========================="
            Outp.WriteBlankLines(1)
            Outp.Write NumberScript(Script)
            Outp.WriteBlankLines(2)
            Outp.WriteLine "Error " & err.number & " " & err.description
            Outp.WriteLine "Source " & err.source

            Outp.WriteLine "Line number and column not available for runtime errors"
            wscript.quit
        End If
        outp.writeline temp
    Loop
End Sub

Here's help for it

filter vbs "text of a vbs script"
filter vb "text of a vbs script"

Use colons to seperate statements and lines. Use single quotes in place of double quotes, if you need a single quote use chr(39). Escape brackets and ampersand with the ^ character. If you need a caret use chr(136).

The function is called UF (for UserFunction). It has two parameters, L which contains the current line and LC which contains the linecount. Set the results of the script to UF. See example.

There are three global objects available. An undeclared global variable gU to maintain state. Use it as an array if you need more than one variable. A Dictionary object gdU for saving and accessing previous lines. And a RegExp object greU ready for use.

Example

This vbs script inserts the line number and sets the line to the function UF which Filter prints.

filter vbs "uf=LC ^& ' ' ^& L"< "%systemroot%\win.ini"

This is how it looks in memory

Dim gU
Set gdU = CreateObject("Scripting.Dictionary")
Set greU = New RegExp

Function UF(L, LC)

---from command line---
uf=LC & " " & L
---end from command line---

End Function

If there is a syntax error Filter will display debugging details.

User function syntax error
==========================


1 Dim gU
2 Dim gdU
3 Set greU = CreateObject("Scripting.Dictionary")
4 Function UF(L, LC)
5 On Error Resume Next
6 uf=LC dim & " " & L
7 End Function

Error 1025 Expected end of statement
Line 6 Col 6


User function runtime error
===========================


1 Dim gU
2 Dim gdU
3 Set greU = CreateObject("Scripting.Dictionary")
4 Function UF(L, LC)
5 On Error Resume Next
6 uf=LC/0 & " " & L
7 End Function

Error 11 Division by zero
Source Microsoft VBScript runtime error
Line number and column not available for runtime errors

Other examples

Reverse each line

filter vbs "uf=StrReverse^(L^)"< "%systemroot%\win.ini"

Make each line lower case

filter vbs "uf=LCase^(L^)"< "%systemroot%\win.ini"

Make each line upper case

filter vbs "uf=UCase^(L^)"< "%systemroot%\win.ini"

Number each line in a file

filter vbs "uf=LC ^& ' ' ^& L"< "%systemroot%\win.ini"

Go to top of page

Trigger
  • 681
  • 3
  • 4
  • It is not clear to me how I would use this. For example, how do I use `VBSCmd`? – mountainclimber11 Sep 11 '17 at 13:04
  • The example code using the ScriptControl here was useful to me, but the rest of this a mess. I upvoted for the value of this as something to pull pieces from and rewrite. – BuvinJ May 10 '18 at 22:47
3

I've been dealing with the same issues. I have a pile of libraries in play, and thousands of lines of code. I need to know where my errors occur. It's awful that VBScript does not have it's own include / import statement. Since it does not though, it's no surprise you have to roll your own compilation debugging for such.

It's crazy to me though that the run time error object doesn't track line numbers or provide any way to stack trace with it. What a pain! As such, I'm rolling my solution for that too...

This is what I'll use for now. The auto logging could be improved in many ways, this is just the starting point. Probably, I'll keep improving this and posting the evolution of it.

Getting to the point - here's an "Include" sub routine for VBScript which will import your library and output the details of any compilation errors within them. Additionally, you can set a global switch to inject logging code into your library that will output the start and end of each of your subs & functions as they are executed.

Just paste this into your "main" / entry point script (and set bINJECT_LOGGING as desired).

(Note the scrollbar here, as this is rather long if you want the logging portion too.)

Const bINJECT_LOGGING=True

Sub Include( sRelativeFilePath )    
    ' Get the library script
    Dim oFs : Set oFs = CreateObject("Scripting.FileSystemObject")
    Dim sThisFolder : sThisFolder = oFs.GetParentFolderName( WScript.ScriptFullName )
    Dim sAbsFilePath : sAbsFilePath = oFs.BuildPath( sThisFolder, sRelativeFilePath )
    Dim sLibrary : sLibrary = oFs.OpenTextFile( sAbsFilePath ).readAll()
    ' Perform compilation debugging
    On Error Resume Next
    Dim oSC : Set oSC = CreateObject("MSScriptControl.ScriptControl")
    With oSC
        .Language = "VBScript"
        .UseSafeSubset = False
        .AllowUI = False
        .AddCode sLibrary
    End With
    With oSC.Error
        If .Number <> 0 then
            WScript.Echo sAbsFilePath & "(" & .Line & "," & .Column & ")" & _
                " Microsoft VBScript compilation error: " & _
                "(" & .Number & ") " & .Description
            WScript.Quit
        End If
    End With
    On Error Goto 0
    ' Implement optional runtime debugging via logging injections
    If bINJECT_LOGGING Then 
        InjectRoutineLogging sLibrary, _
            oFs.OpenTextFile( sAbsFilePath ), sRelativeFilePath
    End If
    ' Import the Library
    ExecuteGlobal sLibrary
End Sub

Sub InjectRoutineLogging( ByRef sLibrary, ByRef oFile, sFilePath )
    sLibrary = ""        
    Dim sLine, sParseLine, sAppendLine, sPrependLine
    Dim bIsRoutineBody : bIsRoutineBody = False
    Dim sRoutineName   : sRoutineName = ""
    Dim aStartKeywords : aStartKeywords = Array( "SUB", "FUNCTION" )
    Dim aEndKeywords   : aEndKeywords   = Array( "END SUB", "END FUNCTION" )    
    Do Until oFile.AtEndOfStream
        sLine        = oFile.ReadLine            
        sParseLine   = Trim(sLine)            
        sPrependLine = ""
        sAppendLine  = ""                
        ' Find routine signature starts (and the save name)
        If sRoutineName = "" Then                                 
            For Each sKeyword In aStartKeywords                
                If Left( UCase(sParseLine), Len(sKeyword) ) = sKeyword Then
                    sParseLine = Right( sParseLine, _
                                        Len(sParseLine)-Len(sKeyword) )
                    sRoutineName = Trim(Split( sParseLine, "(" )(0))
                End If
            Next            
        End If                
        If sRoutineName <> "" Then            
            If Not bIsRoutineBody Then                   
                ' Find end of routine signature 
                ' (figuring in line continuations and eol comments)                
                ' Inject start log
                sParseLine = Trim(Split(sParseLine, "'")(0)) 
                If Right( sParseLine, 1 ) = ")" Then                    
                    sAppendLine = "WScript.Echo" & _
                        """Start " & sRoutineName & _
                        " (" & sFilePath & ")..." & """" & vbCrLF
                    bIsRoutineBody = True
                End If    
            Else                    
                ' Find routine end
                ' Inject end log 
                For Each sKeyword In aEndKeywords                
                    If Left( UCase(sParseLine), Len(sKeyword) ) = sKeyword Then
                        sPrependLine = "WScript.Echo ""...End " & _
                                        sRoutineName & " "" " 
                        sRoutineName = ""
                        bIsRoutineBody = False
                    End If
                Next                                        
            End If                            
        End If
        ' Append lines
        If sPrependLine <> "" Then sLibrary = sLibrary & sPrependLine & vbCrLF
        sLibrary = sLibrary & sLine & vbCrLF
        If sAppendLine  <> "" Then sLibrary = sLibrary & sAppendLine  & vbCrLF
    Loop        
End Sub

Then, to implement it, call the routine like with any of these examples:

Include "SomeLibrary.vbs"
Include ".\MyLibrary.vbs"
Include ".\MySubFolder\MyOtherLibrary.vbs"

Note that you are probably best off to place your "main" script into a library file itself, so you can load it this way and have these awesome debugging tools.

BuvinJ
  • 10,221
  • 5
  • 83
  • 96