Cómo crear una función recursiva para buscar archivos

A continuación encontrarás traducido al castellano el artículo escrito por Gabriel Ludosanu y publicado originalmente en el Blog oficial de Xojo.

En el anterior artículo creamos una utilidad para buscar texto en el contenido de los archivos de una carpeta. En este artículo actualizaremos dicha utilidad para que soporte la búsqueda recursiva con la capacidad de controlar su profundidad (niveles de recursión).

También refactorizaremos el código para que resulte sencillo su mantenimiento a medida que se torne más complejo.

El objetivo

Esto es lo que queremos obtener:

  • Buscar automáticamente en las sub-carpetas.
  • Añadir el parámetro maxDepth de modo que no busquemos por accidente en todo el disco duro.
  • Mantener la versión sencilla utilizando la sobrecarga de métodos.

Paso 1: Mover la lectura de archivos a su propio método

La función FindInFiles original se encargaba de todo: validar la carpeta, recorrer su contenido, leer el contenido del archivo y encontrar el texto coincidente. Al añadir la recursión resultaría en un código difícil de leer y mantener; de modo que refactorizaremos dicho código.

Comenzaremos moviendo la lógica de lectura de archivos a su propio método privado: SearchFile. Esto mantendrá la lógica de “búsqueda” separada de la lógica encargada de recorrer el contenido de la carpeta.

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

Paso 2: Recorrido trasversal de Carpetas

Ahora crearemos SearchFolder. Este método recorre los contenidos de una carpeta. Si encuentra un archivo, entonces llamará al método SearchFile. Si encuentra una carpeta, se llamará a sí mismo.

Para mantener la seguridad en cuanto a la profundidad de recursión utilizaremos maxDepth:

  • maxDepth = 0: Buscar sólo en la carpeta actual.
  • maxDepth > 0: Buscar esta carpeta y N niveles de sub-carpetas.
  • maxDepth = -1: Buscar todo (sin límites).
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

La lógica en maxDepth nos permite finalizar la recursión en el caso de que exista un límite, o bien realizar una búsqueda completa en caso de que su valor sea -1.

Paso 3: Soportar ambas versiones con la sobrecarga de métodos

La sobrecarga es una característica que nos permite contar con múltiples métodos con el mismo nombre, siempre y cuando cuenten con parámetros diferentes. En nuestro caso nos permite proporcionar una versión simple para una carpeta, y una versión recursiva con un límite de niveles de profundidad. Xojo seleccionará automáticamente el método correcto basándose en los argumentos proporcionados. Puedes leer más sobre este aspecto en la documentación de Xojo.

La versión por omisión (depth 0):

Public Function FindInFiles(targetFolder As FolderItem, searchTerm As String) As SearchHit()
  Return FindInFiles(targetFolder, searchTerm, 0)
End Function

La versión recursiva:

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

Al separar la validación, el recorrido trasversal y el procesado, el código resulta más sencillo de modificar. Si quieres añadir soporte RegExt, sólo has de cambiar SearchFile. Si quieres filtrar los contenidos en base a la extensión de archivo, sólo has de cambiar SearchFolder.

En resumen

Ahora puedes buscar en una sola carpeta tal y como hacías con anterioridad, o bien recorrer un directorio utilizando un parámetro adicional:

// 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

¡Feliz programación con Xojo!

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *