So I'm maintaining some legacy code, in which there is a GoTo leading right before the End If of an if block. My predicament is that I now need to add an Else clause to that block. Will the Else function correctly if it is reached via the GoTo rather than failing the If?
GoTo is completely unstructured, and will continue execution wherever you tell it to, regardless of the block of code that it takes you to. If you need the Else condition to execute based on a change in the parameters that happens after your If test but before the GoTo, it won't work. If you don't care how the If statement is evaluated, it may work, but this would be an ideal time to fix the underlying control flow issue. You can see this behavior with the following sample code by stepping through it with the debugger:
Private Sub DontTryThisAtHome()
Dim test As Long
Dim doneThat As Boolean
If test = 0 Then
Debug.Print "If condition tested."
Spaghetti:
Debug.Print "This always executes even if test = " & test & "."
If doneThat Then GoTo Pasta
Else
Debug.Print "test > 0"
End If
test = 1
doneThat = True
Debug.Print "Pasta express..."
GoTo Spaghetti
Pasta:
End Sub
Output:
If condition tested.
This always executes even if test = 0.
Pasta express...
This always executes even if test = 1.
Just don't do it.
Legacy code is hard enough to understand, even without the previous guy taking advantage of some language dark corner. Don't make life confusing for the next guy.
Rewrite the code in question so it is clean and obvious.
Related
I have a function that does the same operation for all of my scripts, but only the variable in which the Pass-Fail value is stored, would change.
For example, in one script -> the status is stored in Envrionment.Value("Current_Status")
in another script -> the status is stored in DataTable.Value("Status",1)
in another script -> the status is stored in objRS("AddCriteria_Status").Value
So i am trying to make a function in which i pass on these parameters as strings and then later use them as variable names. Here is the sample code:
Envrionment.Value("Current_Status") = "none"
Environment.Value("Fail_text") = "none"
Call AddCriteria("Environment.Value(""Current_Status"")","Environment.Value(""Fail_text"")")
Pubic Function AddCriteria(varStatus,varActual)
varTemp = ""
Execute(varStatus+ "=InProgress") 'change status to InProgress by the time execution is done
Execute(varActual + "=not_defined") 'this will have the reason the case failed
....code
If varTemp = "FAIL" Then
Execute(varStatus+ "=PASS")
Execute(varActual + "=PASS")
Else
Execute(varStatus+ "=FAIL")
Execute(varActual + "=Criteria did not get added")
End If
End Function
On calling the sub-routine i want the value of Environment.Value("Current_Status") to change from "none" to "InProgress" and then to "PASS"
But after the "Execute" command is executed, the Environment variable become empty.
Since CVar is not supported in VBScript, i cannot use it.
I tried Eval, but it doesn't work in the other direction i.e.:
If you change the value of Environment.Value("Current_Status"), then the value Eval(varStatus) changes, but I could not find a way to change the value of Eval(varStatus) so that the value of Environment.Value("Current_Status") changes.
Please help out. I am stuck at this for a week.
!!!What I'm trying to accomplish!!!
In a .vbs file, pass on any string to a function as a parameter; and convert it into a variable name in that function. Simple example: pass a string "abc" as a parameter to a function -> and within that function, convert the string to a variable name to store value [say, abc = "PASS"]
!!!How I attempt to do it!!!
I tried using Execute command as that is a solution that I got from a previous post
[vbscript Eval a string to a Variable in a loop?
Using "CVar" is a way but that is not supported in VBScript. So I ran out of ideas
!!!Problems that I faced!!!
Honestly, I didn't understand the logic of using "Execute", but i tried it nevertheless. Sadly, it didn't work out. When using execute command (as mentioned in the code), the environment variables become empty.
Ideas:
Use ExecuteGlobal to execute the assignment you want to execute --
if that´s what you want. Eval and especially Execute have subtle limitations regarding the scope they live in.
The target variable (i.e. the variable that receives a value in the
assignment that is evaluated by ExecuteGlobal) must be a global
variable.
If the ExecuteGlobal call happens on an Action's global scope, the
target variable must be declared there, too. (I think.)
If the ExecuteGlobal call happens in a routine in a function
library, the target variable must be declared there, too. (I know that for sure. But read on.)
To further help you, I'd need an update on your question because it is not clear what you want to accomplish, and what problems you see. Because -- Eval does not change values, it just evaluates an expression supplied as a string, and returns its value. If the expression has side-effects, like setting a global variable, then you might be out of luck because...well...it depends on where that global variable is declared, and initialized (if at all), and where the ExecuteGlobal call happens. Actions and libraries do NOT share one global scope, even if it looks like they do, and that can create a lot of strange behavior.
But as I said, if you clarify what you are trying to accomplish (got 90% of that), how you attempt to do it (got 40% of it), and what problems you face (got 10% of it), I´m sure I can update this answer so it approaches a solution.
** Update **
I use this library code for all runtime expression evaluation, be it from within a library or Action:
' Interpret (execute) a piece of VSH source code consisting of statements -- success?
' Code: String containing VBS source code. Passed by reference for performance reasons only
Public Function ExecCode (ByRef Code)
Dim ErrNumber
Dim ErrDescription
On error resume next ' Avoid getting kicked out by errors in the code contained in Code
ExecuteGlobal Code
ErrNumber=Err.Number
ErrDescription=Err.Description
On error goto 0 ' Re-enable RTE handling
If ErrNumber <> 0 Then
ExecCode=false
Print "Code execution failed ('" & ErrDescription & "'), code:" & vbNewline & Code & "<eof>"
else
ExecCode=true
End If
End Function
Dim GlobalVar
' Interpret (execute) a piece of VSH source code consisting of a single expression -- success?
' Expr; String containing a VBS expression. Passed by reference for performance reasons only.
' Target: Variable receiving the value to which the expression evaluates
Public Function EvalCodeAndAssign (ByRef Expr, ByRef Target)
' In order to force "Option explicit", we don´t use Eval, but ExecCode (and thus ExecuteGlobal):
Dim Code: Code="Option Explicit: GlobalVar=(" & Expr & ")"
Dim Result: Result=ExecCode (Code)
If Result Then
Target=GlobalVar
End If
EvalCodeAndAssign=Result
End Function
Update 2: if the statement that you pass to ExecuteGlobal contains quotes (which I think are missing in your code), the must be quoted, I.e. you must use double-quotes, like in
ExecuteGlobal "x=""This is a string"""
Because what ExecuteGlobal/Execute/Eval do is: take a string and interpret it as VBScript code. The code you are trying to use is not valid due to missing quotes.
I read somewhere that exiting out of an error handling block is not recommended. Sadly they did not give an explanation why?
I was curious why is it so. I did find something related to this in the discussion at "On Error Goto 0" before Exit Function but not sure if that is the definitive answer. They talk about the Err object not being cleared. Is that the only issue and if so is there a work around?
Is this wrong? If it is, then what is the recommended way of exiting the function? I am not a huge supporter of go to statements, they make the code harder to follow.
Private Sub Foo2()
On Error Resume Next
'Multiple lines of code
If Not Foo(arg1,arg2) Then
Exit Sub 'Can I exit here?
End If
'Multiple lines of code
On Error GoTo 0
End Sub
If I understand your question correctly, the better way would be is to do something like:
Private Sub Foo2()
On Error Goto ErrHandler
'Multiple lines of code
If Not Foo(arg1,arg2) Then
Exit Sub 'Can I exit here?
End If
Multiple lines of code
ErrHandler:
Error handling mechanism here.
End Sub
Apart from the warnings about On Error Resume Next (heed them!!) there are no problems with this code. The scope of the error handling statement is the Foo2 Sub, it will expire when you Exit it. The dangers arise when you call another Sub within the For block, which in itself may call other Subs. Those subs will inherit the On Error Resume Next and will give you nightmares for years on end when Errors raised from them are swallowed...
In my past experience with VB6, using Resume Next wasn't a good idea. I never used it. My code did its best to avoid errors at all, and in the case where one happened, it would always go down to an error handler block. The question you linked to illustrates the use of MZTools, which I used liberally.
So, my point is that the Resume Next code is most likely just an exercise, but not real production code. If that is true, then you wouldn't have to clear the Err object before Exit Sub, because the Err object wouldn't have anything in it anyway. If you DID encounter an error, it would Goto the ErrorHandler label and continue from there. I that handler, yes, you would ideally want to do something with the Err object, then return appropriately to the caller.
I would rearrange your code line this and ensure that the exit sub or exit function always comes just before the ErrorHandler:
Private Sub Foo2()
On Error Goto ErrHandler
'Multiple lines of code
If Foo(arg1,arg2) Then
'Multiple lines of code
End If
Exit Sub
ErrHandler:
Error handling mechanism here.
End Sub
How can I do something similar to the following C code in VB 6?
#ifdef _DEBUG_
// do things
#else
// do other things
#end if
It's pretty much the same as the other languages that you're used to. The syntax looks like this:
#If DEBUG = 1 Then
' Do something
#Else
' Do something else
#End If
It's easy to remember if you just remember that the syntax is exactly the same as the other flow-control statements in VB 6, except that the compile-time conditionals start with a pound sign (#).
The trick is actually defining the DEBUG (or whatever) constant because I'm pretty sure that there isn't one defined by default. There are two standard ways of doing it:
Use the #Const keyword to define the constant at the top of each source file. The definition that you establish in this way is valid throughout the entire source module. It would look something like:
#Const DEBUG = 1
Set the constant in the project's properties. This would define a constant that is valid throughout the entire project (and is probably what you want for a "Debug" mode indicator).
To do this, enter something like the following in the "Conditional Compilation Constants" textbox on the "Make" tab of the "Project Properties" dialog box:
DEBUG = 1
You can define multiple constants in this dialog by separating each of them with a colon (:):
DEBUG = 1 : VERSION2 = 1
Remember that any constant which is not defined is assumed to be 0.
Cody has told you about conditional compilation. I'd like to add that if you want different behaviour when debugging on the IDE (e.g. turn off your own error handling so that the IDE traps errors) you don't need conditional compilation. You can detect the IDE at runtime like this.
On Error Resume Next
Debug.Print 1/0
If Err=0 then
'Compiled Binary
Else
'in the IDE
End if
This works because Debug.Print is omitted in the compiled EXE.
EDIT Remember to turn off On Error Resume Next !
EDIT You can wrap the check in a function like this (thanks CraigJ)
To achieve the same effect as MarkJ, but with error handling, you can use the following code.
Public Function GetRunningInIDE() As Boolean
Dim x As Long
Debug.Assert Not TestIDE(x)
GetRunningInIDE = x = 1
End Function
Private Function TestIDE(x As Long) As Boolean
x = 1
End Function
When you are running from within the IDE, there will be an extra overhead of calling a function (which is ridiculously small). When compiled, this evaluates to a simple number comparison.
This is my short and stable code. I think it is better than conditional constants, because you don't need to change it every complication time.
Public Function InIDE() As Boolean
On Error Resume Next
Debug.Print 0 / 0
InIDE = Err.Number <> 0
End Function
I have the following VB.NET code (but for each loops are in most languages, thus the language-agnostic tag):
Public Function VerifyServiceName(ByRef sMachineName As String, ByRef sServiceName As String) As Boolean
Dim asServices As System.ServiceProcess.ServiceController() = System.ServiceProcess.ServiceController.GetServices(sMachineName)
Dim bVerified As Boolean = False
For Each sService In asServices
If sService.DisplayName = sServiceName Then bVerified = True
Next
Return bVerified
End Function
If I have X number of services to loop through, and my service name is #3. Is it better to have multiple return statements or an exit for? Or is there a more efficient way of writing this function?
I know that the time difference between looping X times and looping through 3 times could be marginal for what I am doing, but I always have performance on the brain.
I personally believe having one return at the bottom is far more readable and easier to debug than if you have return statements everywhere, as you can never tell when the function is going to exit so you end up putting breakpoints on every return statement instead of just once at the end, for example.
I think it's all down to preference though, as there are valid arguments for both ways.
Found more discussion here about the subject. I guess I am not that proficient at searching.
I would never use a goto as the target of another goto, so if there's some additional processing at the end of the function, use "break / Exit For", otherwise just return early. Otherwise you end up with lines that mean "return" but say "break"... that doesn't help maintainability.
I have a batch file that calls a VBScript (.vbs) program. After calling it, my batch script checks %errorlevel% to see if the .vbs program failed. I can signal failure with an exit code in the .vbs program with WScript.Quit(1).
However, I can only do that explicitly. If some unexpected run-time error happens, the .vbs quits with an error dialog box, however the exit code is zero so my batch file thinks it suceeded! How can I change that behavior?
And if you are thinking of saying, use on error goto, don't bother... that syntax is available in regular VB, but not in VBScript.
I thought of an out-of-the-box solution... Who says 0 has to mean success? VBScript sometimes returns a 0 return code for failures, so why not embrace that? Adopt 0 as (at least one possible) failure code and make up another number (e.g. 10) as the "success code".
At the end of the script, put WScript.Quit(10). That will only be hit if everything succeeded up to that point. Then instead of "if errorlevel 1" in the calling batch file, use "if %errorlevel% == 10"
EDIT : Having tentatively (see caveats) proposed this, I am rapidly beginning to think that it is a very bad idea, but I leave it here for posterity. The most compelling reason to not use this comes from Eric Lippert at Microsoft, who worked on the design & implementation of VBScript. He states, in answer to another question: VBScript does not make any guarantee that terminators always run. This can mean that this sometimes does not return a non-0 exit code in the case of an unhandled error.
I think I personally will use a 'wrapper batch file that subtracts 1 from the cscript exit code' solution in future.
I like the solution linked to by fmunkert, but I think it requires you to put your code in a particular Class_Initalize, which is clumsy at best. I've devised a related solution that does not require this; you simply "Commit" a successful result at the end of your code; if it's not called, any exception causes the ExitCodeHandler's Class_Terminate instance to set a non-zero exit code.
Option Explicit
Class ExitCodeHandler
private exit_code
Public Sub Commit()
exit_code = 0
End Sub
Private Sub Class_Initialize()
exit_code = -1 ' this exit code will be returned if Commit is never called
End Sub
Private Sub Class_Terminate()
if exit_code<>0 then WScript.Quit(exit_code)
End Sub
Public Sub Quit(exitCode)
Commit
WScript.Quit(exitCode) ' exit code will be respected since we have committed
End Sub
End Class
' create one of these at the start:
Dim ech: Set ech = New ExitCodeHandler
WSCript.StdOut.WriteLine "Hello"
s = "" ' undeclared variable causes runtime error - comment out to see success.
' WScript.Quit(-4) ' before a commit, -1 is returned due to the Class_Terminate
' Commit at the end
ech.Commit
' WScript.Quit(-5) ' after a commit, -5 is returned
Note that this idiom is used heavily in C++, where it is called RAII (Resource Acquisition Is Initialization)
You could of course embellish the class this to support other exit codes, error messages etc. You may want to put this in a common vbs file and use a mechanism for includes in vbscript to share it.
Caveats
I don't know the full details of downsides to calling WScript.Quit during stack unwinding due to an exeption in VBScript. I've disovered the following:
Use with caution. I have come up with this and poked around with it when I saw fmunkert's linked suggestion, not used it extensively.
If you explicitly call WScript.Quit(n), the ExitCodeHandler will replace n with its own exit code. The workaround is to either always call ExitCodeHandler.Commit before calling WScript.Quit, or call the supplied ExitCodeHandler.Quit instead which does it for you. However, relying on either of these methods may not always be practical/possible, and it is fairly non-idiomatic and may not be ovbious to maintainers.
If any other object with a Class_Terminate is terminated (i.e. after ExitCodeHandler's Class_Terminate calls WScript.Quit), you seem to get an error. You may get similar behaviour with any COM objects that are being destroyed. I don't know in what order VBScript destroys objects (or even if it's guaranteed), so I've asked about it in another question.
As you say, all that's available is On Error Resume Next, so your forced to use the pattern:
On Error Resume Next
ThingWithAChanceOfThrowingAnError ...
If (Err.number <> 0) then PrintErrorAndQuitWith1(Err.Description)
You could, if it's an option, use jscript instead which has better support for exception handling, including an easy way to return a non-zero exit code on any exception. See the solution to why does my JScript (windows script host) exit with 0 on an uncaught exception?
This is the #1 reason we're choosing jscript over vbscript (when we have to use one of the two!)
You might use the technique described in this article.
It requires you to wrap your script inside a VBScript class.