vbscript recursion programming-techniques - scripting

I am looking for some expert insight concerning recursion within vbscript.
From various examples found online I created the following code, which works by the way.
http://saltwetbytes.wordpress.com/2010/05/04/vbscript-grabbing-subfolders-recursively/
http://technet.microsoft.com/en-us/library/ee198872.aspx
Function GetAllSubFolders(RootFolder, ByRef pSubfoldersList)
Dim fso, SubFolder, root
Set fso = CreateObject("scripting.filesystemobject")
set root = fso.getfolder(RootFolder)
For Each Subfolder in root.SubFolders
If pSubFoldersList = "" Then
pSubFoldersList = Subfolder.Path
Else
pSubFoldersList = pSubFoldersList & "|" & Subfolder.Path
End If
GetAllSubFolders Subfolder, pSubFoldersList
Next
GetAllSubFolders = pSubFoldersList
End Function
My question is: Is this a good aproach when it comes to creating a recursive function (using a parameter for storing previous results)?
I prefer putting this in a (self-contained) "function", so the procedure returns the subsubfolders as the result. But most of the examples found use a "sub" I always get confused when it comes to "sub" vs "function" (I understand when you want a procedure that needs to return something you use a function, imho this seems to be the case in this example)
But I could also use a "sub" and just simple reference the output parameter (ByRef pSubfoldersList)
So what is the best practise or is it better to use a whole different approach all together?
(the function is this examples is also very slow compared to [shell.exec "cmd /c dir RootFolder /s /b /a:d"], I guess this is a side effect from the recursion or maybe the FSO is just really slow?)

whether it is good practice to pass the result in a recursive function, i don't really know, you can test this out by doing it this way and the other and comparing the time and memory taken. Haven't tried this with your version cause i get the error "Microsoft VBScript runtime error: Permission denied" if I start from the root of the c:
The real problem with your solution is the concatenation, that takes time because the in your case BIG variable gets created every time. Better were to store the result in an array or in the case of VBscript in a dictionary. I'll post an example.
What the difference between sub and function concerns: you are right about the main difference, the returning of a result but that is optional so I always use functions, the only drawback is that if you don't assign the value to a variable and you use more than 2 parameters you have to use "call". When you use your approach with ByRef you could also define the var in the main global context, it's perhaps less encapsulated but more readable and you can more easily reuse or debug the results.
What the speed concerns: vbscript is VERY slow in file handling, if you used WMI perhaps you could speed up a bit but not much, indeed for some operations it is better to shell out and let the OS take care of it. I now program in Ruby and there most jobs like this you can write in one line of code and it is much faster.
Speaking about fast, if your only purpose is to have a list of your files, get to know the tool "search everything", in less than a second you can search millions of files, if you don't know it check it out !
Here is an example using the dictionary
set fso = CreateObject("Scripting.FileSystemObject")
set filelist = CreateObject("Scripting.Dictionary")
iCount = 0
ShowSubfolders fso.GetFolder("C:\Documents and Settings\peter")
PrintFilelist(filelist)
'--- ---
Function ShowSubFolders(Folder)
For Each Subfolder in Folder.SubFolders
on error resume next
wscript.echo Subfolder.Path 'show some progress
Set fFolder = fso.GetFolder(Subfolder.Path)
if err.number <> 0 then wscript.echo err.description
For Each File in fFolder.Files
iCount = iCount+1
filelist.add iCount, File.Path
Next
ShowSubFolders Subfolder
Next
End Function
'--- ---'
Function PrintFilelist(ByRef dic)
Dim index, allKeys, allItems, msg
allKeys = dic.Keys
' allKeys is an array to all the keys
allItems = dic.Items
' allItems is an array to all the items
wscript.echo "There are " & dic.Count & " number of files in the dictionary"
For index = 0 To dic.Count-1
' Notice, dictionary range goes from 0 to count-1
wscript.echo "Key=" & allKeys(index) & " Filename=" & allItems(index)
Next
End Function

Related

Random File Selector?

It's been years since I've used Visual Basic. I downgraded from 2017 to 2010 (The version I was using while I was in school). I figured VB would be the best way to attempt a solution. (Although I'm sure there are other languages that would do it as well.)
I'm looking to get back into programming. Let me get to the problem.
My friend has an ever growing amount of text documents in a folder, and he wants a program to choose one at random, and open it.
I thought I'd put a TextBox with a Button that would let him open the folder where he stores his files. Then this program would read the number of text files in that folder, and randomly generate a number between one and that number, select, and open the document with its default program (if it's text, notepad; if it's DocX then word.)
I've been sitting at a blinking cursor for 45 minutes. I've gone on YouTube for help with this project.
Any advice, or help you guys can give me? Does this need to be simplified?
That sounds like a reasonable strategy to me.
It might be worth displaying some sort of progress to the user, say by putting the name of current file name being read into the status bar, in case there's a long delay reading the file names due to the large number of files in the folder, and/or a slow-running network drive. If you do this, remember to put a DoEvents into your loop to allow screen updates to display.
There's a separate thread on how to open files in their native handler here.
Hope this helps - good luck!
Option Explicit
Public oFSO As Object
Public arrFiles()
Public lngFiles As Long
Sub Main()
Dim sPath As String
sPath = InputBox("Enter folder path", "Folder path")
' clear starting point
lngFiles = 0
Erase arrFiles
Set oFSO = CreateObject("Scripting.FileSystemObject")
Call recurse(sPath)
Randomize
Dim lngRandomFileNumber As Long
lngRandomFileNumber = CLng(lngFiles * Rnd) + 1
MsgBox "This is random file, that will be opened: " & arrFiles(lngRandomFileNumber)
Call CreateObject("Shell.Application").Open(arrFiles(lngRandomFileNumber))
End Sub
Sub recurse(sPath As String)
Dim oFolder As Object
Dim oSubFolder As Object
Dim oFile As Object
Set oFolder = oFSO.GetFolder(sPath)
'Collect file information
For Each oFile In oFolder.Files
lngFiles = lngFiles + 1
ReDim Preserve arrFiles(lngFiles + 1)
arrFiles(lngFiles) = sPath & "\" & oFile.Name
Next oFile
'looking for all subfolders
For Each oSubFolder In oFolder.SubFolders
'recursive call
Call recurse(oSubFolder.path)
Next oSubFolder
End Sub
You can paste this code in any VBA supporting application (MS Access, MS Excel, MS Word), call VBA editor (Shift + F11) and paste this code. After that press F5 and select Main() function. You'll see prompt to enter folder path, and after that you would get random file path.
I think it should be understandable in practice to see what program do
Updated: #Belladonna mentioned it clearly, to open file in default program.
NB: This code is passes through subfolders also, if you want to exclude subfolders, you should comment the recursive call block in recurce function

Is there a way to use Items.find() in VBA to extract certain text from Outlook?

I need to extract specific text from outlook in bulk (through 9000 emails)
I was wondering would something like this work
Dim Folder as Outlook.MAPIFolder
Dim sFolders As Outlook.MAPIFolder
Dim iRow As Integer, oRow As Integer
Dim MailBoxName As String, Pst_Folder_Name As String, Destination As String
ThisWorkbook.sheets(1).Cells(1,1) = "Destinations"
For iRow = 1 To Folder.Items.Count
ThisWorkbook.Sheets(1).Cells(oRow, 1) = Folder.Items.Find(Destination)
I have only some experience in VBA from years ago and I need am trying to create system like this for my job so I can pull out the information needed from the Body of an Email instead of scanning through thousands of emails seprately.
Does anyone know some good source/tutorials I can look at? as every one keeps leading me back to the same place
Thankyou
Is this what you are trying (Tested from within Outlook)? Please amend it to run from MS-Excel.
Sub Sample()
Dim myFilter As String, SearchString As String
Dim OutlookTable As Table
Dim OutlookRow As Row
'~~> This is your search string. Change as applicable
SearchString = "Siddharth"
'~~> Create Query
myFilter = "#SQL=" & _
Chr(34) & _
"urn:schemas:httpmail:textdescription" & _
Chr(34) & _
" ci_phrasematch '" & _
SearchString & _
"'"
Set OutlookTable = Application.ActiveExplorer.CurrentFolder.GetTable(myFilter)
Do Until OutlookTable.EndOfTable
Set OutlookRow = OutlookTable.GetNextRow
'~~> Print Subject (For example) of that email
'~~> which has the search string
Debug.Print OutlookRow("Subject")
Loop
End Sub
Does anyone know some good source/tutorials I can look at?
Tutorial: See this MSKB Article
The Outlook object model provides the Find/FindNext, Restrict, GetTable and AdvancedSearch methods for filtering items in Outlook. I'd suggest using the Restrict emthod in your case. The Find or FindNext methods are faster than filtering if there are a small number of items. The Restrict method is significantly faster if there is a large number of items in the collection, especially if only a few items in a large collection are expected to be found.
You can read about them and find the sample code in the following articles:
How To: Use Find and FindNext methods to retrieve Outlook mail items from a folder (C#, VB.NET)
How To: Use Restrict method to retrieve Outlook mail items from a folder
Advanced search in Outlook programmatically: C#, VB.NET
The Filtering Items section in MSDN describes all possible methods in depth.
Application.ActiveExplorer.CurrentFolder.GetTable(myFilter)
Don't ever use multiple dots in the single line of code. It may bring another issue into your code. I always recommend breaking the chain property and methods calls and declaring them on separate lines of code. Thus, you will be able to see under the debugger what each property and method returns and find the cause of the issue easily (if any).

Outlook VBA how to improve speed when looking thru a folders list

I'm currently writing some code so that when I send an email based upon certain criteria it will save it in a specific folders. I know that rules do something like this, but believe me, it will take forever and wouldn't be practical to set that many rules. So here's what I'm trying to do. When an email is sent, code looks into the title of the email to find a project number. Then it pops up a userform where the user can make multiple filing choice based on it's need.
It first save the project number using the VBA.SaveSetting method
It then returns object "objX" which is in fact the email object that is being send.
So in the code below I'm looping thru a list of public folders. The first part of the folders name is the project number. The project number I'm looking for is saved in "proj_folder" local variable.
So I'm looping thru all the folders to find the folder name that is beginning with "proj_folder" variable.
It's working fine except for the fact that it can get pretty slow if there's a lot of folders to loop thru.
Looking at the code below, would someone be kind enough to share a way to improve my looping speed. Right now it can take around 2 seconds to loop thru let's say 30 folders. Sometime it can get up to more than 200 folders.
Sub MoveProject(objX)
Dim objNS As Outlook.NameSpace
Dim projectParentFolder As Outlook.MAPIFolder
Dim objFolder As Outlook.MAPIFolder
Dim proj_folder As String
Dim intX As Long
'recall of the name of the folder saved from a previouly filed userform
proj_folder = VBA.GetSetting("mail filing", "num_projet", "num_proj", vbNullString)
sub_folder_1 = "Quebec"
sub_folder_2 = Left(proj_folder, 3)
Set objNS = Application.GetNamespace("MAPI")
Set projectParentFolder = objNS.Folders("Public Folder - UserAdress#server.com").Folders("All Public Folders").Folders(sub_folder1).Folders(sub_folder2)
'=============THIS IS THE PART WHERE I WOULD LIKE TO IMPROVE THE SPEED==============================
For intX = 1 To projectParentFolder.Folders.Count 'searching for folder name beginning
If Left(projectParentFolder.Folders.item(intX).Name, Len(proj_folder)) = proj_folder Then
Set objFolder = projectParentFolder.Folders.item(intX)
Exit For
End If
Next
objX.Move objFolder 'moving mail to objFolder
Set objX = Nothing
Set objFolder = Nothing
Set projectParentFolder = Nothing
Set objNS = Nothing
End Sub
You have too many dots in the loop for one. Here's a general VBA speed post of mine.
When setting properties or calling methods, each is a function call in the CPU. That means stack setup overhead. Function calls are slower than inline code. Use loops rather than functions in VBA for the same reason.
For a start don't specify all those properties over and over again. Unless you change them they don't change.
With Selection.Find
.ClearFormatting
.Replacement.ClearFormatting
.Forward = True
.Wrap = wdFindContinue
.Format = False
.MatchCase = False
.MatchWholeWord = False
.MatchByte = False
.MatchAllWordForms = False
.MatchSoundsLike = False
.MatchWildcards = False
.MatchFuzzy = False
For loop to go through each word pair
.Text = SrcText
.Replacement.Text = DestText
.Find.Execute Replace:=wdReplaceAll
Next
End With
Minimise Dots
So if you are interested in performance minimise dots (each dot is a lookup), especially in loops.
There are two ways. One is to set objects to the lowest object if you are going to access more than once.
eg (slower)
set xlapp = CreateObject("Excel.Application")
msgbox xlapp.worksheets(0).name
(faster because you omitt a dot every time you use the object)
set xlapp = CreateObject("Excel.Application")
set wsheet = xlapp.worksheets(0)
msgbox wsheet.name
The second way is with. You can only have one with active at a time.
This skips 100 lookups.
with wsheet
For x = 1 to 100
msgbox .name
Next
end with
String Concatination
And don't join strings one character at a time. See this from a VBScript programmer. It requires 50,000 bytes and many allocation and deallocation to make a 100 character string.
http://blogs.msdn.com/b/ericlippert/archive/2003/10/20/53248.aspx
Reading Properties
Don't reread properties that don't change especially if out of process or late bound. Put them into a variable. Reading variables is quick compared to an object(s) lookup (which is also a function call or at least two if late bound) and then a function call.
Variables
Constants and Literals are pretty much the same once compiled.
Const x = 5
msgbox x
is the same as
msgbox 5
Literals are inserted direct in code. String and object variables have managers, incuring overhead. Avoid creating variables for no reason. This is an example of a pointless and slow variable.
x = "This is a string"
msgbox x
compared to
const x = "This is a string"
msgbox x
or
msgbox "This is a string"
Object Types
Two concepts here - in or out of process and early or late binding.
exefiles are connected to out of process. All calls are marshalled over RPC (a networking protocol). Dllfiles are in process and function calls are made direct with a jump.
Early binding is set x = objecttype. Functions are looked up when you write the program. On execution the program is hard coded to jump to address stored in the vtable for that function.
Late binding is set x = createobject("objecttype"). Each function call goes like this. "Hi object do you have a print command". "Yes", it replies, "command number 3". "Hi object can you please do command number 3". "Sure, here's the result".
From Visual Basic Concepts (part of Help)
You can make your Visual Basic applications run faster by optimizing the way Visual Basic resolves object references. The speed with which Visual Basic handles object references can be affected by:
Whether or not the ActiveX component has been implemented as an in-process server or an out-of-process server.
Whether an object reference is early-bound or late-bound. In general, if a component has been implemented as part of an executable file (.exe file), it is an out-of-process server and runs in its own process. If it has been implemented as a dynamic-link library, it is an in-process server and runs in the same process as the client application.
Applications that use in-process servers usually run faster than those that use out-of-process servers because the application doesn't have to cross process boundaries to use an object's properties, methods, and events. For more information about in-process and out-of-process servers, see "In-Process and Out-of-Process Servers."
Object references are early-bound if they use object variables declared as variables of a specific class. Object references are late-bound if they use object variables declared as variables of the generic Object class. Object references that use early-bound variables usually run faster than those that use late-bound variables.
Excel Specific
See this link from a Microsoft person. This is excel specific rather than VBA. Autocalc and other calc options/screenupdating etc.
http://blogs.office.com/2009/03/12/excel-vba-performance-coding-best-practices/
.
Edit
I don't have Outlook installed so something like this.
Removed dots, changed to an enumerated For Each, and moved the len function outside the loop so it's not called over and over again.
Set projectParentFolder = objNS.Folders("Public Folder - UserAdress#server.com").Folders("All Public Folders").Folders(sub_folder1).Folders(sub_folder2)
prog_folder_len = Len(Prog_Folder)
For Each Fldr in ProjectParentFolder.Folders
If Left(Fldr.Name, prog_folder_len) = proj_folder Then
Set objFolder = Fldr
Exit For
End If
Next
A lot of collections can be accessed by name.
Does something like set objfolder = Fldr(Prog_Folder) or set objfolder = Fldr.item(Prog_Folder) not work?
Collections are implemented by the object. So one cannot know without installing the object the capabilities of a collection.
Also unlike For x = n to n, For each is also object implemented and might be faster than For x = n to n.
For the messages, you can use MAPIFolder.Items.Find/FindNext/Restrict or MAPIFolder.GetTable to find an item using a custom condition. Unfortunately there is nothing like that for the MAPIFolder.Folders collection in the Outlook Object Model - it was assumed that the number of subfolders is always small.
The best you can do with Outlook Object Model is pass the full name of the child folder to Folders.Item() - if there is an exact match (case insensitive), MAPIFolder.Folders.Item() will be able to return it without looping through all subfolders.
If you need a substring (or any other) match, you can either switch to Extended MAPI (C++ or Delphi only, not an option in VBA) and use MAPIFolder.Folders.RawTable to retrieve the IMAPITable MAPI interface that you can use to search for a subfolder. Or you can use Redemption (I am its author) and its MAPITable object. Your code would look something like the following:
set Table = CreateObject("Redemption.MAPITable")
Table.Item = projectParentFolder.Folders
Set Recordset = Table.ExecSQL("SELECT EntryID from Folder where Name like '" & proj_folder & "%' ")
If not Recordset.EOF Then
strEntryID = Recordset.Fields(0).Value
set objFolder = Application.Session.GetFolderFromID(strEntryID)
end If
Redemption version of the folder object (RDOFolder) also exposes Folders.Find/FindNext and Folders.Restrict methods (similar to the methods exposed by the Items collection in Outlook) that allow to specify an arbitrary search clause:
set Session = CreateObject("Redemption.RDOSession")
Session.MAPIOBJECT = Application.Session.MAPIOBJECT
set Folder = Session.GetFolderFromID(Application.ActiveExplorer.CurrentFolder.EntryID)
set subFolder = Folder.Folders.Find("Name LIKE 'MAPI%'")
if subFolder Is Nothing Then
MsgBox "No such subfolder"
else
MsgBox "Found subfolder named '" & subFolder.Name & "'"
end if

In VBScript, how do I manage "Scripting.FileSystemObjects" like objFSO and objFolder for multiple folders/files?

In VBScript, how do I manage "Scripting.FileSystemObjects" like objFSO and objFolder for multiple folders/files ?
In the "Main" code section, I declare an instance (global) of "Scripting.FileSystemObject"
Set objFSO = CreateObject("Scripting.FileSystemObject")
Then, I perform some operations, like:
If objFSO.FileExists(strOutputFilename) Then
WScript.Echo "Deleting File: " & strOutputFilename
objFSO.DeleteFile strOutputFilename
End If
Then, in a loop, I get a folder, and pass it to a function:
For gintLoop = 0 to (ubound(arraySearchPath))
wscript.echo "Processing folder:" & arraySearchPath(gintLoop)
Set objFolderX = objFSO.GetFolder(arraySearchPath(gintLoop))
Call DoWork (objFolderX, arrayParam1, arrayParam2)
Next
So far everything is clear...
Now, within the function, I do things like:
a) collect filenames from objFolder
Set lobjFolder = objFSO.GetFolder(objFolderX.Path)
Set lcolFiles = lobjFolder.Files
b) check for existance of files in other (unrelated) paths
c) get the size of various files:
lcurInputFileSize = CCur(lobjFile.Size)
d) delete various files
e) open files for reading
For Each lobjFile in lcolFiles
lstrTargetFile = lobjFolder.Path & "\" & lobjFile.Name
Set lobjInputFile = objFSO.OpenTextFile(lstrTargetFile, ForReading)
...
f) open files for writing
Set lobjOutputFile = objFSO.OpenTextFile(strOutputFilename, ForAppending, True)
g) call other subs/functions passing various object
h) recursively call the (same) function to process other folders
For Each lobjSubfolderY in objFolderX.SubFolders
Call DoWork (lobjSubfolderY, arrayParam1, arrayParam2)
Next
My concern is that I need to make sure the various uses of FileSystemObjects like folder paths, open files, etc, are not "Stepped-on" by later uses of FileSystemObjects.
Question 1:
Do I need (or is it advised) to have a seperate instance of "Scripting.FileSystemObject" (objFSO) for "Main" and each (or some) sub/function ?
Question 2:
Similarly, how do I manage the various other objects to avoid loosing data ?
Kevin
Q1: No, you do not need multiple instances of Scripting.FileSystemObject.
The methods on the object are all static.
In fact, the documentation for the Scripting Runtime Reference indicates that the FSO is a singleton, although it does not use the word:
You can create only one instance of the FileSystemObject object, regardless of how many times you try to create another.
from: http://msdn.microsoft.com/en-us/library/2z9ffy99(v=vs.84).aspx
In my experience, calling WScript.CreateObject("Scripting.FileSystemObject") multiple times does not result in an error. Likely the return value on subsequent calls is just a copy of the originally created FSO.
As for your Question 2, I don't get it. I think you are referring to objects that are returned by FSO, objects of type Folder, File, TextStream and so on.
Treat these like any other stateful object. You can have multiple instances, and you need to pass them as stack-based arguments if you want to do recursion.

Deleting a file in VBA

Using VBA, how can I:
test whether a file exists, and if so,
delete it?
1.) Check here. Basically do this:
Function FileExists(ByVal FileToTest As String) As Boolean
FileExists = (Dir(FileToTest) <> "")
End Function
I'll leave it to you to figure out the various error handling needed but these are among the error handling things I'd be considering:
Check for an empty string being passed.
Check for a string containing characters illegal in a file name/path
2.) How To Delete a File. Look at this. Basically use the Kill command but you need to allow for the possibility of a file being read-only. Here's a function for you:
Sub DeleteFile(ByVal FileToDelete As String)
If FileExists(FileToDelete) Then 'See above
' First remove readonly attribute, if set
SetAttr FileToDelete, vbNormal
' Then delete the file
Kill FileToDelete
End If
End Sub
Again, I'll leave the error handling to you and again these are the things I'd consider:
Should this behave differently for a directory vs. a file? Should a user have to explicitly have to indicate they want to delete a directory?
Do you want the code to automatically reset the read-only attribute or should the user be given some sort of indication that the read-only attribute is set?
EDIT: Marking this answer as community wiki so anyone can modify it if need be.
An alternative way to code Brettski's answer, with which I otherwise agree entirely, might be
With New FileSystemObject
If .FileExists(yourFilePath) Then
.DeleteFile yourFilepath
End If
End With
Same effect but fewer (well, none at all) variable declarations.
The FileSystemObject is a really useful tool and well worth getting friendly with. Apart from anything else, for text file writing it can actually sometimes be faster than the legacy alternative, which may surprise a few people. (In my experience at least, YMMV).
I'll probably get flamed for this, but what is the point of testing for existence if you are just going to delete it? One of my major pet peeves is an app throwing an error dialog with something like "Could not delete file, it does not exist!"
On Error Resume Next
aFile = "c:\file_to_delete.txt"
Kill aFile
On Error Goto 0
return Len(Dir$(aFile)) > 0 ' Make sure it actually got deleted.
If the file doesn't exist in the first place, mission accomplished!
The following can be used to test for the existence of a file, and then to delete it.
Dim aFile As String
aFile = "c:\file_to_delete.txt"
If Len(Dir$(aFile)) > 0 Then
Kill aFile
End If
In VB its normally Dir to find the directory of the file. If it's not blank then it exists and then use Kill to get rid of the file.
test = Dir(Filename)
If Not test = "" Then
Kill (Filename)
End If
set a reference to the Scripting.Runtime library and then use the FileSystemObject:
Dim fso as New FileSystemObject, aFile as File
if (fso.FileExists("PathToFile")) then
aFile = fso.GetFile("PathToFile")
aFile.Delete
End if
Here's a tip: are you re-using the file name, or planning to do something that requires the deletion immediately?
No?
You can get VBA to fire the command DEL "C:\TEMP\scratchpad.txt" /F from the command prompt asynchronously using VBA.Shell:
Shell "DEL " & chr(34) & strPath & chr(34) & " /F ", vbHide
Note the double-quotes (ASCII character 34) around the filename: I'm assuming that you've got a network path, or a long file name containing spaces.
If it's a big file, or it's on a slow network connection, fire-and-forget is the way to go.
Of course, you never get to see if this worked or not; but you resume your VBA immediately, and there are times when this is better than waiting for the network.
You can set a reference to the Scripting.Runtime library and then use the FileSystemObject. It has a DeleteFile method and a FileExists method.
See the MSDN article here.
A shorter version of the first solution that worked for me:
Sub DeleteFile(ByVal FileToDelete As String)
If (Dir(FileToDelete) <> "") Then
' First remove readonly attribute, if set
SetAttr FileToDelete, vbNormal
' Then delete the file
Kill FileToDelete
End If
End Sub