In the previous post, we built a utility to find text inside files within a single folder. Today we’re upgrading that utility to support recursive searching with depth control. We’ll also refactor the code to keep it maintainable as it gets more complex.
The Goal
We want to:
- Search through subfolders automatically.
- Add a
maxDepthparameter so we don’t accidentally crawl the entire hard drive. - Keep the simple version working using method overloading.
Step 1: Move File Reading to its own Method
The original FindInFiles function did everything: validated the folder, looped through children, read file content, and matched text. Adding recursion to that would make the code look ugly, so let’s refactor our code.
We’ll start by moving the file-reading logic into its own private method: SearchFile. This keeps the “search” logic separate from the “looping” logic.
Private Sub SearchFile(file As FolderItem, searchTerm As String, hits() As SearchHit)
Try
Var input As TextInputStream = TextInputStream.Open(file)
input.Encoding = Encodings.UTF8
Var lineNumber As Integer = 0
While Not input.EndOfFile
Var line As String = input.ReadLine
lineNumber = lineNumber + 1
If line.Contains(searchTerm, ComparisonOptions.CaseInsensitive) Then
hits.Add(New SearchHit(file.NativePath, lineNumber, line))
End If
Wend
input.Close
Catch error As IOException
// Skip unreadable files
End Try
End Sub
Step 2: Recursive Folder Traversal
Now we create SearchFolder. This method loops through a folder. If it finds a file, it calls SearchFile. If it finds a folder, it calls itself.
To keep this safe, we Use maxDepth:
maxDepth = 0: Search only this folder.maxDepth > 0: Search this folder and N levels of subfolders.maxDepth = -1: Search everything (unlimited).
Private Sub SearchFolder(folder As FolderItem, searchTerm As String, hits() As SearchHit, maxDepth As Integer)
Try
For Each item As FolderItem In folder.Children
If item Is Nil Or Not item.Exists Or Not item.IsReadable Then Continue
If item.IsFolder Then
If maxDepth <> 0 Then
Var nextDepth As Integer = If(maxDepth > 0, maxDepth - 1, maxDepth)
SearchFolder(item, searchTerm, hits, nextDepth)
End If
Else
SearchFile(item, searchTerm, hits)
End If
Next
Catch error As IOException
// Skip inaccessible folders
End Try
End Sub
The logic in nextDepth lets us count down if there’s a limit, or stay at -1 if there isn’t.
Step 3: Support Both Versions with Method Overloading
Overloading is a feature that lets you have multiple methods with the same name, as long as they have different parameters. In our case, it allows us to provide a simple version for one folder, and a recursive version with a depth limit. Xojo will automatically pick the right one based on the arguments you pass. You can read more about it in the Xojo Documentation.
The default version (depth 0):
Public Function FindInFiles(targetFolder As FolderItem, searchTerm As String) As SearchHit()
Return FindInFiles(targetFolder, searchTerm, 0)
End Function
The recursive version:
Public Function FindInFiles(targetFolder As FolderItem, searchTerm As String, maxDepth As Integer) As SearchHit()
Var hits() As SearchHit
If targetFolder Is Nil Or Not targetFolder.Exists Or Not targetFolder.IsFolder Or searchTerm.IsEmpty Then
Return hits
End If
Try
SearchFolder(targetFolder, searchTerm, hits, maxDepth)
Catch error As IOException
// Skip inaccessible folders
End Try
Return hits
End Function
By separating the validation, traversal, and processing, the code is much easier to modify. If you want to add Regex support, you only change SearchFile. If you want to filter by file extension, you only change SearchFolder.
Summary
Now, you can search a single folder just like before, or crawl a directory tree with one extra parameter:
// Search up to 3 levels deep
Var results() As SearchUtils.SearchHit = SearchUtils.FindInFiles(myFolder, "TODO", 3)
The Complete Recursive Code
The SearchUtils Module
Module SearchUtils
Class SearchHit
Public Property FilePath As String
Public Property LineNumber As Integer
Public Property LineText As String
Public Sub Constructor(filePath As String, lineNumber As Integer, lineText As String)
Self.FilePath = filePath
Self.LineNumber = lineNumber
Self.LineText = lineText
End Sub
End Class
Public Function FindInFiles(targetFolder As FolderItem, searchTerm As String) As SearchHit()
// Simple usage: depth = 0
Return FindInFiles(targetFolder, searchTerm, 0)
End Function
Public Function FindInFiles(targetFolder As FolderItem, searchTerm As String, maxDepth As Integer) As SearchHit()
// Recursive usage: maxDepth ( -1 is unlimited )
Var hits() As SearchHit
If targetFolder Is Nil Or Not targetFolder.Exists Or Not targetFolder.IsFolder Or searchTerm.IsEmpty Then
Return hits
End If
Try
SearchFolder(targetFolder, searchTerm, hits, maxDepth)
Catch error As IOException
// Skip inaccessible folders
End Try
Return hits
End Function
Private Sub SearchFolder(folder As FolderItem, searchTerm As String, hits() As SearchHit, maxDepth As Integer)
Try
For Each item As FolderItem In folder.Children
If item Is Nil Or Not item.Exists Or Not item.IsReadable Then Continue
If item.IsFolder Then
If maxDepth <> 0 Then
Var nextDepth As Integer = If(maxDepth > 0, maxDepth - 1, maxDepth)
SearchFolder(item, searchTerm, hits, nextDepth)
End If
Else
SearchFile(item, searchTerm, hits)
End If
Next
Catch error As IOException
// Skip inaccessible folders
End Try
End Sub
Private Sub SearchFile(file As FolderItem, searchTerm As String, hits() As SearchHit)
Try
Var input As TextInputStream = TextInputStream.Open(file)
input.Encoding = Encodings.UTF8
Var lineNumber As Integer = 0
While Not input.EndOfFile
Var line As String = input.ReadLine
lineNumber = lineNumber + 1
If line.Contains(searchTerm, ComparisonOptions.CaseInsensitive) Then
hits.Add(New SearchHit(file.NativePath, lineNumber, line))
End If
Wend
input.Close
Catch error As IOException
// Skip unreadable files
End Try
End Sub
End Module
Happy coding!
Gabriel is a digital marketing enthusiast who loves coding with Xojo to create cool software tools for any platform. He is always eager to learn and share new ideas!
