Retrieve ERRORLEVEL From Executed Script [duplicate] - batch-file

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.

Related

How do you know when PowerShell script finished?

I'm trying to wrap a piece of code into a WPF application so that a user can just hit the button and the code will run. However, because the script can take time to run, I would like to give a status to the end user so that they know it finished. Is there a way to do that with powershell and this style of code posted below?
1..255 | %{
$I = "192.168.2.$_"
Get-MacAddress($I);
function Get-MacAddress {
...
}
}
TL;DR: You can send an exit 1 to exit with a return code of 1 (or any number other than 0), which means it failed for some reason.
Normally (and under specific circumstances) PowerShell scripts exit with a code of 0. Most other applications return an exit code of 0 whenever they are 'successfully completed,' and without any sort of exception or error that the program is consciously aware occurred.
If you're trapping the exit code in the WPF, you could report whether it was successful (exit 0 inserted at some point in your code, or let it finish as expected), or if it failed (exit of any other number) - at which point you would want to consider reporting unique exit codes specific to the reasons that occurred wrong.
Consider also looking into try, catch, and throw as they're quite useful, as well.
EDIT: As a final note, take a good look at how %errorLevel% (where the exit code is stored) is handled under some unique situations. %errorLevel% is what you want to focus on, if you use exit codes.

VBS doesn't complete loop when in sub, but does when not

Long story short, I have a VBS script I'm writing with nested For loops.
If I put the code in a sub, it doesn't complete the loop. If the code is out in the main, it does. Example:
Do
For x = 0 to 9
For y = 0 to 9
For z = 0 to 9
want = CStr(x) & "." & CStr(y) & "." & CStr(z)
If want = "5.1.3"
Exit Do
End If
Next
Next
Next
Loop While 1 = 1
If this is out in main, it works. But this:
NestedLoop()
Sub NestedLoop()
Do
For x = 0 to 9
For y = 0 to 9
For z = 0 to 9
want = CStr(x) & "." & CStr(y) & "." & CStr(z)
If want = "5.1.3"
Exit Do
End If
Next
Next
Next
Loop While 1 = 1
End Sub
Will only loop to 2.0.5
Here's the full source code for each:
Nested Loop in Main
Nested Loop in Sub
Long story short, what it does is detects the OS arch (x86 or x64), goes out to VLC's download index (https://download.videolan.org/pub/videolan/vlc/last/win64/ or /win32/) and downloads to the working dir whatever version of the exe that resolves to HTTP status of 200. This also generates a log file at C:\Temp\vlc-installer.txt
So why would it not work in a sub? I know I could just have it in the main, but I'd prefer a sub.
FWIW, this is a "working interview" project. Me and another candidate for a position are tasked with coming up with an automated deployment solution.
Both of your scripts have global On Error Resume Next statements, so I think you need to know more about how it works.
Please pay attention to the bold sentences.
From On Error Statement
[...]
Within any particular procedure, an error is not necessarily fatal as
long as error-handling is enabled somewhere along the call stack. If
local error-handling is not enabled in a procedure and an error
occurs, control is passed back through the call stack until a
procedure with error-handling enabled is found and the error is
handled at that point. If no procedure in the call stack is found to
have error-handling enabled, an error message is displayed at that
point and execution stops or the host handles the error as
appropriate.
On Error Resume Next causes execution to continue with the statement
immediately following the statement that caused the run-time error, or
with the statement immediately following the most recent call out of
the procedure containing the On Error Resume Next statement. This
allows execution to continue despite a run-time error. You can then
build the error-handling routine inline within the procedure.
An On Error Resume Next statement becomes inactive when another
procedure is called, so you should execute an On Error Resume Next
statement in each called routine if you want inline error handling
within that routine. When a procedure is exited, the error-handling
capability reverts to whatever error-handling was in place before
entering the exited procedure.
[...]
In your case, when an error occurs within your sub procedure called FindVLC, it immediately exits (jumps to End Sub) then continues to work on the global scope by ignoring errors.
So, you should execute an On Error Resume Next statement in your sub routine if you want to ignore (or handle) errors within that as the docs say.
Sub FindVLC()
On Error Resume Next
'for loops etc.
End Sub

AppleScript: "file is already open" but "file wasn't open"

Because of my slightly obsessive personality, I've been losing most of my productive time to a single little problem.
I recently switched from Mac OS X Tiger to Yosemite (yes, it's a fairly large leap). I didn't think AppleScript had changed that much, but I encountered a problem I don't remember having in the old days. I had the following code, but with a valid filepath:
set my_filepath to (* replace with string of POSIX filepath, because typing
colons was too much work *)
set my_file to open for access POSIX file my_filepath with write permission
The rest of the code had an error which I resolved fairly easily, but because the error stopped the script before the close access command, and of course AppleScript left the file reference open. So when I tried to run the script again, I was informed of a syntax error: the file is already open. This was to be expected.
I ran into a problem trying to close the reference: no matter what I did, I received an error message stating that the file wasn't open. I tried close access POSIX file (* filepath string again *), close access file (* whatever that AppleScript filepath format is called *), et cetera. Eventually I solved the problem by restarting my computer, but that's not exactly an elegant solution. If no other solution presents itself, then so be it; however, for intellectual and practical reasons, I am not satisfied with rebooting to close access. Does anyone have insights regarding this issue?
I suspect I've overlooked something glaringly obvious.
Edit: Wait, no, my switch wasn't directly from Tiger; I had an intermediate stage in Snow Leopard, but I didn't do much scripting then. I have no idea if this is relevant.
Agreed that restarting is probably the easiest solution. One other idea though is the unix utility "lsof" to get a list of all open files. It returns a rather large list so you can combine that with "grep" to filter it for you. So next time try this from the Terminal and see if you get a result...
lsof +fg | grep -i 'filename'
If you get a result you will get a process id (PID) and you could potentially kill/quit the process which is holding the file open, and thus close the file. I never tried it for this situation but it might work.
Have you ever had the Trash refuse to empty because it says a file is open? That's when I use this approach and it works most of the time. I actually made an application called What's Keeping Me (found here) to help people with this one problem and it uses this code as the basis for the app. Maybe it will work in this situation too.
Good luck.
When I've had this problem, it's generally sufficient to quit the Script editor and reopen it; a full restart of the machine is likely excessive. If you're running this from the Script Menu rather than Script Editor, you might try turning off the Script Menu (from Script Editor) and turning it back on again. The point is that files are held by processes, and if you quit the process it should release any lingering files pointers.
I've gotten into the habit, when I use open for access, of using try blocks to catch file errors. e.g.:
set filepath to "/some/posix/path"
try
set fp to open for access filepath
on error errstr number errnom
try
close access filepath
set fp to open for access filepath
on error errstr number errnom
display dialog errnum & ": " & errstr
end try
end try
This will try to open the file, try to close it and reopen it if it encounters and error, and report the error if it runs into more problems.
An alternative (and what I usually do) is that you can also comment out the open for access line and just add in a close access my_file to fix it.

Is there a way to tell what the file name is where VB6 has only assigned it a number?

I'm working with some old VB6 code and it's still new to me. I know that in VB6 you assign an integer to represent a file. I have a program that uses quite a few files and it's tough to tell what file it's working with when it will only display the number when I mouse over the variable. (pic below).
So in the example above, how do i know what file #5 is?
Thanks
You might have to modify the program to 'register' filenames with their file numbers:
Dim FileRegister as Collection
Dim FileName as String
Dim FileNumber as Integer
...
FileRegister.add FileName, str(FileNumber)
Open FileName For Output as #FileNumber
...
FileRegister.Remove str(FileNumber)
Close #FileNumber
Search the code for the variable name? Do you have MZTools? It's a free plugin with excellent search facilities.
Trace the code execution back to see where the unit number comes from? Use the call stack view when debugging, or use MZTools to list all calls to any routine.
(Last resort) add logging.
Every time a file is opened, log the filename and unit number.
Every time a file is closed, log the unit number.
You could leave the logging in the production code, maybe with a way to turn it on/off at runtime. It could be useful again.

String substitution using tcl API

Is there a way to (ab)use the tcl C-API to 'parse' a string, doing all the replacement (including sub commands in square brackets), but stopping before actually evaluating the resulting command line?
What I'm trying to do is create a command (in C, but I'll consider doing a tcl-wrapper, if there's an elegant way to do it there) which takes a block as a parameter (i.e. curly-braces-quoted-string). I'd like to take that block, split it up and perform substitutions in the same way as if it was to be executed, but stop there and interpret the resulting lines instead.
I've considered creating a namespace, where all valid first-words are defined as commands, however this list is so vast (and pretty much dynamic) so it quickly becomes too cumbersome. I also tried this approach but with the unknown command to intercept the different commands. However, unknown is used for a bunch of stuff, and cannot be bound to a namespace, so I'd have to define it whenever I execute the block, and set it back to whatever it was before when I'm done, which feels pretty shaky. On top of that I'd run the risk (fairly low risk, but not zero) of colliding with an actual command, so I'd very much prefer to not use the unknown command.
The closest I can get is Tcl_ParseCommand (and the rest of the family), which produces a parse tree, which I could manually evaluate. I guess I'll resort to doing it this way if there's no better solution, but I would of course prefer it, if there was an 'official' way..
Am I missing something?
Take a look at Tcl_SubstObj. It's the C equivalent of the [subst] command, which appears to be what you're looking for.
As you indicated in your comment, subst doesn't quite do what you're looking to do. If it helps, the following Tcl code may be what you're looking for:
> set mydata {mylist item $listitem group item {$group item}}
> set listitem {1 2 3}
> subst $mydata ;# error: can't read "group": no such variable
> proc groupsubst {data} {
return [uplevel 1 list $data]
}
> groupsubst $mydata ;# mylist item {1 2 3} group item {$group item}

Resources