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

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.

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

How can I make my VBA Outlook script more efficient

This is my first question on StackExchange ever :-)
I am Running the following script in MS Outlook VBA
Sub export()
On Error resume Next
Dim Ns As Outlook.NameSpace
Dim eitem
Dim oFile As Object
Dim fso As Object
Set Ns = Application.GetNamespace("MAPI")
Set fso = CreateObject("Scripting.FileSystemObject")
Set oFile = fso.CreateTextFile("C:\Users\chakkalakka\Desktop\mails.txt")
'Code
For Each eitem In Ns.Session.Folders.Item(12).Items
oFile.WriteLine eitem.SenderName & "§" & eitem.SentOnBehalfOfName & "§" & eitem.ReceivedTime
Next
oFile.Close
Set Ns = Nothing
Set fso = Nothing
Set oFile = Nothing
Debug.Print "Completed!"
End Sub
The script in general is working fine and the output is correct. My Problem is: I need to run this inside a folder with > 95000 items and it takes ages.
So my question is: What can I do to improve performance?
Thanks in advance for your help
The most inefficient line of code is the following one:
For Each eitem In Ns.Session.Folders.Item(12).Items
You need to break the chain of property and method calls and declare them on separate lines. So each property or method will be declared on a separate line of code. Thus, you will be able to release underlying COM objects instantly. Set a variable to Nothing in Visual Basic to release the reference to the object.
Iterating through all items in the folder is a time-consuming task. Instead, I'd suggest using the Find/FindNext or Restrict methods of the Items class to deal with items that correspond to your conditions. Read more about these methods in the following articles:
How To: Use Restrict method to retrieve Outlook mail items from a folder
How To: Use Find and FindNext methods to retrieve Outlook mail items from a folder (C#, VB.NET)
Also you may consider using the GetTable method of the Folder class which allows to obtain a Table object that contains items filtered by Filter. If Filter is a blank string or the Filter parameter is omitted, GetTable returns a Table with rows representing all the items in the Folder.

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

vbscript recursion programming-techniques

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

How do I programatically add a reference to a VBA project?

I'm deploying an early bound styled VBA module that needs Scripting.Dictionary and RegExp.
The script, predictably, fails when it runs on another computer.
The user has to go to Tools->Reference in the VBA IDE and add a reference to those two libraries manually to make it work.
Hence lies the problem. Asking the non-technical end user to go to the IDE and manually add references is asking way too much of them.
The other alternative is to rewrite the whole (very long script written by someone else) to use late binding. I rather not take this path if there are other methods.
As an altervative, some people suggest adding a reference programatically like so:
Application.VBE.ActiveVBProject.References.AddFromFile [Path to library]
Is this the correct solution and if so are there any downsides of this strategy?
If not, are there other methods that will to enable the code to remain early bound yet does not require references to be added manually by the user.
Suggestions involving direct calls to the Win32/64 API are also welcome.
Thanks.
In my own limited environment (small # of other people using spreadsheets I develop, relatively standard machine setups), if I create the file and add the references, and then give a copy to someone else, they can open it with no problems and not have to do anything, so keep that in mind with this answer. (I'm wondering why that doesn't work for you.) Also, this was with Excel.
Rather than adding a reference from a file path, you might consider using the GUID property instead.
Here is some code I once used to automatically create references in a newly created workbook. (It's part of a script that would export code, references, and unit tests on worksheets to text for use with Subversion and then later reconstitute the workbook from the text files.) You might find it useful to your situation. (EH and cleanup removed to keep it short...)
'Export refs in existing workbook to text file
Private Sub exportRefs_(srcWbk As Workbook)
Dim fs As FileSystemObject
Set fs = New FileSystemObject
Dim tsout As TextStream
Set tsout = fs.CreateTextFile(fs.BuildPath(getTargetPath_(srcWbk), "refs.refs"))
Dim ref As Reference
For Each ref In Application.ThisWorkbook.VBProject.References
Call tsout.WriteLine(ref.GUID)
Next ref
'<EH + cleanup...>
End Sub
'Add refs to newly created workbook based on previously exported text file
Private Sub importRefs_(wbk As Workbook, path As String)
Dim fs As FileSystemObject
Set fs = New FileSystemObject
Dim tsin As TextStream
Set tsin = fs.OpenTextFile(path)
Dim line As String
Dim ref As Reference
While Not tsin.AtEndOfStream
line = tsin.ReadLine()
Set ref = Nothing
On Error Resume Next
Set ref = wbk.VBProject.References.AddFromGuid(line, 0, 0)
On Error GoTo 0
If ref Is Nothing Then
Debug.Print "add failed: " & line
End If
Wend
'<EH + cleanup...>
End Sub
Like, I said, limited environment, but hopefully it helps.