Publi

Cómo listar archivos de forma recursiva en C, y un mundo de posibilidades en nuestros programas

3042638653_f14c62a4f7_bPuede que seas un hacha con el comando find o incluso con locate; pero hay veces que nuestro software tiene que ser capaz de realizar la búsqueda de un archivo en el árbol de directorios, puede que queramos hacer inclusiones, exclusiones, o analizar las características del archivo.
Podemos tener varias misiones, por ejemplo calcular el tamaño que ocupan todos los archivos a partir de una ruta dada (como hace du -s), copiarlos a otra ruta como haría un gestor de archivos o, incluso buscar archivos repetidos. Lo que todos estos programas tienen en común es que exploran los archivos contenidos dentro de un directorio de manera recursiva, y esto es que si encontramos un directorio entramos en él y repetimos la operación.
Llegará un punto en el que no encontraremos más directorios, sólo archivos. En ese caso, hacemos lo que tengamos que hacer con ellos, y salimos de la función. El caso es que, como en todos los algoritmos recursivos, si entramos 15 veces dentro de la función, tendremos que salir otras 15 veces hasta que nuestro caso base se cumpla en todas las ejecuciones de la función.

En el caso de los archivos, siempre se va a cumplir, el número de archivos en el sistema es limitado, a no ser que encontremos un directorio que se contenga a sí mismo, haya alguna inconsistencia en disco o estemos realizando una operación no válida. De todas formas, podemos limitar la recursividad (el número de ejecuciones que podrá haber de una función, o el número de directorios dentro del directorio base en el que podremos entrar).

Aunque hace años ya publiqué un código parecido voy a extender un poco los ejemplos por petición de varios usuarios por Facebook.

Buscando un archivo

Con este programa sólo tendremos que hacer:

./listar . archivo.jpg

Donde . es la ruta actual, a partir de la que empezamos a leer, podría ser /home/portatil/ si queremos; y archivo.jpg es el archivo que estamos buscando.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <dirent.h>
#include <string.h>
#include <errno.h>

/* Podemos usar malloc() para reservar sólo lo necesario, como en
   https://poesiabinaria.net/2011/09/listar-archivos-dentro-de-un-directorio-o-carpeta-en-c/
*/

#define MAXSTR 1000

/* Función para devolver un error en caso de que ocurra */
void panic(const char *s);

/* Sacamos el tipo de archivo haciendo un stat(), es como el stat de la línea de comandos */
unsigned char statFileType(char *fname);

/* Intenta sacar el tipo de archivo del ent */
unsigned char getFileType(char *ruta, struct dirent *ent);

/* Obtiene el nombre del fichero con la ruta completa */
void getFullName(char* dest, char *ruta, struct dirent *ent);

/* Genera una cadena de espacios, para dibujar el árbol */
void generaPosStr(char* dest, int niv);

/* Función principal, que cuenta archivos */
int buscaArchivo(char *ruta, char* busca, int maxNiv, int niv);

int main(int argc, char *argv[])
{
  int veces;

  if (argc != 3)
    {
      panic("Uso: ./listar <ruta> <archivo>\n");
    }

  printf("Entrando en: %s\n", argv[1]);
  veces = buscaArchivo(argv[1], argv[2], 10, 1);

  if (veces)
    printf ("Encontrados %d archivos\n", veces);
  else
    printf ("Archivo no encontrado\n");

  return EXIT_SUCCESS;
}

void panic(const char *s)
{
  /* perror() devuelve la cadena S y el error (en cadena de caracteres) que tenga errno */
  perror (s);
  exit(EXIT_FAILURE);
}

void getFullName(char *dest, char *ruta, struct dirent *ent)
{
  int tmp = strlen(ruta);

  tmp=strlen(ruta);

  if (ruta[tmp-1]=='/')
    snprintf(dest, MAXSTR ,"%s%s", ruta, ent->d_name);
  else
    snprintf(dest, MAXSTR ,"%s/%s", ruta, ent->d_name);
}

void generaPosStr(char* dest, int niv)
{
  int i;

  for (i=0; i<niv*2; ++i)
    dest[i]=' ';

  dest[niv*2]='\0';
}

int buscaArchivo(char *ruta, char* busca, int maxNiv, int niv)
{
  /* Con un puntero a DIR abriremos el directorio */
  DIR *dir;
  /* en *ent habrá información sobre el archivo que se está "sacando" a cada momento */
  struct dirent *ent;
  int veces = 0;
  unsigned char tipo;       /* Tipo: fichero /directorio/enlace/etc */
  char nombrecompleto[MAXSTR];     /* Nombre completo del fichero */
  char posstr[MAXSTR];         /* Cadena usada para posicionarnos horizontalmente */

  if (niv == maxNiv)
    return 0;

  dir = opendir (ruta);

  /* Miramos que no haya error */
  if (dir == NULL)
    return ;            /* No entramos, puede que no tengamos permiso */
 
  while ((ent = readdir (dir)) != NULL)
    {
      if ( (strcmp(ent->d_name, ".")!=0) && (strcmp(ent->d_name, "..")!=0) )
    {
      getFullName(nombrecompleto, ruta, ent);
      generaPosStr(posstr,niv); /* Espacios para dibujar mejor esto */
      tipo=getFileType(nombrecompleto, ent);
      if (strcmp (ent->d_name, busca) == 0)
        {
          veces++;
          printf ("%sArchivo encontrado: %s\n", posstr, nombrecompleto);
        }

      if (tipo==DT_DIR)
        {
          /* Si es un directorio, entramos, ya que es la condición
         que hace de nuestro algoritmo algo recursivo. */

          printf("%sEntrando en: %s\n", posstr, nombrecompleto);          
          veces+=buscaArchivo(nombrecompleto, busca, maxNiv, niv+1);
        }
    }
    }
  closedir (dir);
 
  return veces;
}

unsigned char getFileType(char *nombre, struct dirent *ent)
{
  unsigned char tipo;

  tipo=ent->d_type;
  /* Puede que hayamos visto un enlace, pero queremos saber
     si ese enlace es de un archivo o de un directorio, para
     seguirlo o no.
  */

  if ( (tipo==DT_UNKNOWN) || (tipo == DT_LNK) )
    {
      tipo=statFileType(nombre);
    }

  return tipo;
}

/* stat() vale para mucho más, pero sólo queremos el tipo ahora */
unsigned char statFileType(char *fname)
{
  struct stat sdata;

  /* Intentamos el stat() si no funciona, devolvemos tipo desconocido */
  if (stat(fname, &sdata)==-1)
    {
      return DT_UNKNOWN;
    }

  switch (sdata.st_mode & S_IFMT)
    {
    case S_IFBLK:  return DT_BLK;
    case S_IFCHR:  return DT_CHR;
    case S_IFDIR:  return DT_DIR;
    case S_IFIFO:  return DT_FIFO;
    case S_IFLNK:  return DT_LNK;
    case S_IFREG:  return DT_REG;
    case S_IFSOCK: return DT_SOCK;
    default:       return DT_UNKNOWN;
    }
}

Podemos cambiar el número de niveles máximos a entrar cambiando el argumento maxNiv de la función buscaArchivo().

También hay un pequeño añadido, es que, cuando encontramos un enlace entre los archivos del directorio, analizamos con stat() qué es dicho enlace, así, si es un directorio, podemos entrar dentro del mismo (a veces no nos interesará esto, pero es bueno conocerlo).

Cuenta tamaño de los archivos

Podemos utilizar dos métodos para calcular el tamaño de los archivos. Ahora, siempre haremos stat() para ver información sobre cada archivos. En este caso, veremos el tamaño de cada archivo y lo sumaremos. Copio el código completo para que podamos probarlo con tranquilidad:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <dirent.h>
#include <string.h>
#include <errno.h>

struct DirStats
{
  unsigned totalArchivos;
  unsigned totalDirectorios;
  unsigned long tamTotal;
  unsigned totalEnlaces;
};

typedef struct DirStats DirStats;

/* Podemos usar malloc() para reservar sólo lo necesario, como en
   https://poesiabinaria.net/2011/09/listar-archivos-dentro-de-un-directorio-o-carpeta-en-c/
*/

#define MAXSTR 1000

/* Función para devolver un error en caso de que ocurra */
void panic(const char *s);

/* Sacamos el tipo de archivo haciendo un stat(), es como el stat de la línea de comandos */
unsigned char statFileType(char *fname, long unsigned* size);

/* Obtiene el nombre del fichero con la ruta completa */
void getFullName(char* dest, char *ruta, struct dirent *ent);

/* Genera una cadena de espacios, para dibujar el árbol */
void generaPosStr(char* dest, int niv);

/* Función principal, que cuenta archivos */
void generaStats(char *ruta, DirStats* ds, int maxNiv, int niv);

/* Inicializa DirStats (lo pone todo a 0, por si acaso) */
void inicializaDirStats(DirStats* ds);

/* Tamaño en formato humano https://poesiabinaria.net/2010/03/tamano-de-archivo-para-seres-humanos-phpc-y-c/  */
char *human_size(char *store, long double size);

int main(int argc, char *argv[])
{
  int veces;
  DirStats ds;
  char tamhumano[MAXSTR];

  if (argc != 2)
    {
      panic("Uso: ./contar <ruta> \n");
    }

  inicializaDirStats(&ds);
  printf("Entrando en: %s\n", argv[1]);
  generaStats(argv[1], &ds, 10, 1);

  printf ("Total de archivos: %d\n", ds.totalArchivos);
  printf ("Total de directorios: %d\n", ds.totalDirectorios);
  printf ("Total de enlaces: %d\n", ds.totalEnlaces);
  printf ("Tamaño total: %s (%lu bytes)\n", human_size(tamhumano, ds.tamTotal), ds.tamTotal);

  return EXIT_SUCCESS;
}

void panic(const char *s)
{
  /* perror() devuelve la cadena S y el error (en cadena de caracteres) que tenga errno */
  perror (s);
  exit(EXIT_FAILURE);
}

void inicializaDirStats(DirStats* ds)
{
  ds->totalArchivos = 0;
  ds->totalDirectorios = 0;
  ds->totalEnlaces = 0;
  ds->tamTotal = 0;
}

void getFullName(char *dest, char *ruta, struct dirent *ent)
{
  int tmp = strlen(ruta);

  tmp=strlen(ruta);

  if (ruta[tmp-1]=='/')
    snprintf(dest, MAXSTR ,"%s%s", ruta, ent->d_name);
  else
    snprintf(dest, MAXSTR ,"%s/%s", ruta, ent->d_name);
}

void generaPosStr(char* dest, int niv)
{
  int i;

  for (i=0; i<niv*2; ++i)
    dest[i]=' ';

  dest[niv*2]='\0';
}

void generaStats(char *ruta, DirStats* ds, int maxNiv, int niv)
{
  /* Con un puntero a DIR abriremos el directorio */
  DIR *dir;
  /* en *ent habrá información sobre el archivo que se está "sacando" a cada momento */
  struct dirent *ent;
  int veces = 0;
  unsigned char tipo;       /* Tipo: fichero /directorio/enlace/etc */
  char nombrecompleto[MAXSTR];     /* Nombre completo del fichero */
  char posstr[MAXSTR];         /* Cadena usada para posicionarnos horizontalmente */
  long unsigned filesize;

  if (niv == maxNiv)
    return;

  dir = opendir (ruta);

  /* Miramos que no haya error */
  if (dir == NULL)
    return;         /* No entramos, puede que no tengamos permiso */

  while ((ent = readdir (dir)) != NULL)
    {
      if ( (strcmp(ent->d_name, ".")!=0) && (strcmp(ent->d_name, "..")!=0) )
    {
      getFullName(nombrecompleto, ruta, ent);
      generaPosStr(posstr,niv); /* Espacios para dibujar mejor esto */

      /* Cuenta un enlace */
      if (tipo == DT_LNK)
        ds->totalEnlaces++;
      tipo=statFileType(nombrecompleto, &filesize);
      if (tipo == DT_REG)
        {
          ds->totalArchivos++;
          ds->tamTotal+=filesize;
        }
      if (tipo==DT_DIR)
        {
          ds->totalDirectorios++;
          /* Si es un directorio, entramos, ya que es la condición
         que hace de nuestro algoritmo algo recursivo. */

          printf("%sEntrando en: %s\n", posstr, nombrecompleto);          
          generaStats(nombrecompleto, ds, maxNiv, niv+1);
        }
    }
    }
  closedir (dir);
}


/* stat() vale para mucho más, pero sólo queremos el tipo ahora */
unsigned char statFileType(char *fname, long unsigned* size)
{
  struct stat sdata;

  /* Intentamos el stat() si no funciona, devolvemos tipo desconocido */
  if (stat(fname, &sdata)==-1)
    {
      return DT_UNKNOWN;
    }
  *size = sdata.st_size;

  switch (sdata.st_mode & S_IFMT)
    {
    case S_IFBLK:  return DT_BLK;
    case S_IFCHR:  return DT_CHR;
    case S_IFDIR:  return DT_DIR;
    case S_IFIFO:  return DT_FIFO;
    case S_IFLNK:  return DT_LNK;
    case S_IFREG:  return DT_REG;
    case S_IFSOCK: return DT_SOCK;
    default:       return DT_UNKNOWN;
    }
}

char *human_size(char *store, long double size)
{
  static char units[10][6]={"bytes","Kb","Mb","Gb","Tb","Pb","Eb","Zb","Yb","Bb"};  
  int i= 0;

  while (size>1024) {
    size = size /1024;
    i++;
  }

  sprintf(store,"%.2Lf%s",size, units[i]);

  return store;
}

Ahora, la función principal es generaStats() ya que se generarán algunas estadísticas de archivos encontrados, y se almacenará todo en una estructura de tipo DirStats. Se contarán directorios, archivos y enlaces y, además, se calculará el tamaño de todos los archivos, generándose un resumen de todo al final.

Como véis, es distinto al método utilizado por du -s (o du -sh para verlo en formato humano). Y eso es porque du en lugar de contar el tamaño de los archivos, tal cual, cuenta el número de bloques y lo multiplica por el tamaño de bloque de nuestro sistema de archivos actual. Como norma general (puede haber variaciones según SO), la función stat() nos devolverá un struct donde los elementos:

  • st_size : de tipo unsigned es el tamaño total en bytes del archivo
  • st_blksize : es el tamaño de bloque del sistema de archivos actual.
  • st_blocks : es el número de bloques de 512 bytes para el archivo actual.

Por lo que, aunque internamente se use st_blksize, nosotros usaremos st_blocks y multiplicamos por 512. Aunque normalmente si exploramos los archivos, el número st_blocks*512 sea múltiplo de st_blksize.

De hecho, la función statFileType, la cambiamos por:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
unsigned char statFileType(char *fname, long unsigned* size)
{
  struct stat sdata;

  /* Intentamos el stat() si no funciona, devolvemos tipo desconocido */
  if (stat(fname, &sdata)==-1)
    {
      return DT_UNKNOWN;
    }

  *size = 512 * sdata.st_blocks;
  switch (sdata.st_mode & S_IFMT)
    {
    case S_IFBLK:  return DT_BLK;
    case S_IFCHR:  return DT_CHR;
    case S_IFDIR:  return DT_DIR;
    case S_IFIFO:  return DT_FIFO;
    case S_IFLNK:  return DT_LNK;
    case S_IFREG:  return DT_REG;
    case S_IFSOCK: return DT_SOCK;
    default:       return DT_UNKNOWN;
    }
}

Y nos tiene que dar lo mismo que el comando du -sh.

Actualización 8/12/2016: Pequeñas correcciones en fragmentos que no se veían bien.

Foto principal: Dani

También podría interesarte....

There are 8 comments left Ir a comentario

  1. Pingback: Cómo listar archivos de forma recursiva en C, y un mundo de posibilidades en nuestros programas | PlanetaLibre /

  2. Ismael /
    Usando Mozilla Firefox Mozilla Firefox 50.0 en Windows Windows 7

    Yo estoy utilizando el Codeblocks con GCC y cuando intento compilar me salen errores por no declarar los DT_… y me indica que ent.type no existe.
    ¿Debo añadir algo para que funcione correctamente?
    ¿O debo buscar una alternativa para conocer el tipo del archivo?

    Soy un estudiante que acaba de empezar a programar y aun no tengo mucha idea de esto…

    1. Gaspar Fernández / Post Author
      Usando Mozilla Firefox Mozilla Firefox 50.0 en Ubuntu Linux Ubuntu Linux

      Hola Ismael, ¿me puedes copiar y pegar el error que te da ?

      De todas formas, veo que estás en Windows y esta biblioteca no funciona en ese Sistema Operativo. Hay una versión de dirent.h aquí https://github.com/tronkko/dirent , pero no es cuestión de complicarte la vida si acabas de empezar.

      Para Windows puedes intentar esto: https://msdn.microsoft.com/en-us/library/windows/desktop/aa365200(v=vs.85).aspx

  3. OKBet /
    Usando Google Chrome Google Chrome 116.0.0.0 en Windows Windows NT

    You there, this is really good post here. Thanks for taking the time to post such valuable information.
    OKBet FAQ

  4. Macle Anflorian /
    Usando Google Chrome Google Chrome 122.0.0.0 en Windows Windows NT

    All the time i used to read smaller posts which also clear their motive, and that is also happening with this article which I am reading at this time.

    Trading

  5. Lolaredblog.com /
    Usando Google Chrome Google Chrome 122.0.0.0 en Windows Windows NT

    Hi there! I realize this is somewhat off-topic but I needed to ask.
    Does operating a well-established blog such as yours take a massive amount work?
    I’m brand new to blogging however I do write in my journal everyday. I’d like to start a blog so I will be able to share my experience and views online. Please let me know if you have any kind of recommendations or tips for new aspiring bloggers. Appreciate it!

  6. Skribbl io /
    Usando Google Chrome Google Chrome 123.0.0.0 en Windows Windows NT

    We are able to make use of the data that’s on your blog. Each of the suggestions presented in your article is outstanding.

  7. 토토사이트 /
    Usando Google Chrome Google Chrome 122.0.0.0 en Windows Windows NT

    It’s a game. Five dollars is free. Try it It’s not an easy game
    ->-> 토토사이트.COM

Leave a Reply to Ismael Cancle Reply