cpointersmultidimensional-arrayreturn

C 'String Array' Function Returns Empty


I'm writing this program to enumerate every file in the directory to practice using pointers because I am new to the concept and the C language. The return statement to **getFiles() comes back empty for some reason. Weirdly, in the getFiles() function, when I print my 'array of strings' (dirs), it prints 1 of the 3 files I have in my directory. However, when I print the results in main(), nothing happens.

#include <stdio.h>
#include <stdlib.h>
#include <dirent.h>
#include <string.h>

int main(){
    char **getFiles();
    printf("\nFUNCTION RETURN:\n%s", *getFiles());
    return 0;
}
#define BUFFER_SIZE 4096

char **getFiles(){

    char **dirs;

    int total = 10; //Need to do this
    dirs = malloc(total*sizeof(char *));

    char buffer[BUFFER_SIZE];

    struct dirent *de;  
  
    DIR *dr = opendir("."); 
  
    if (dr == NULL)  
    { 
        printf("Error"); 
        return 0; 
    } 
  
    int x=0;
    while ((de = readdir(dr)) != NULL){ 
            int length = strlen(de->d_name);
            dirs[x] = malloc(length*sizeof(char));
            strcpy(buffer, de->d_name);
            strcpy(dirs[x], buffer);
            dirs[x] = buffer;
            x++;
    }

    closedir(dr);     
    printf("\nPRINT DIRS\n:\n%s", *dirs);
    return dirs;
    for(int i =0; i<x;i++){
        free(dirs[i]);
    }
    free(dirs);
}

As I said I'm still new to this whole pointers and addresses thing so I tried de-referencing a bunch of stuff and printing that but nothing really helped.


Solution

  • Another answer, hopefully more complete.

    If you are going to read a directory’s list of names, do it all at once. The directory content can change at any time, even when you are reading its contents! So the more quickly you can get through it, the more likely the listing you get is correct.

    You can monitor a directory for changes to be sure you got it right, but that is a subject I will only mention in passing here. Here’s a quickly-Googled link to start learning more: What is the proper way to use inotify?

    Get the Directory Contents

    To collect just a list of names relative to a given directory (using your code template):

    // getFileNames()
    // freeFileNames()
    // countFileNames()
    
    #include <stdlib.h>
    #include <string.h>
    
    #include <sys/types.h>
    #include <dirent.h>
    
    char **getFileNames(const char *dirname){
    
        // Our final result will be a dynamically-allocated list
        // of dynamically-allocated filenames (relative to dirname).
        // The list ends with a NULL filename.
        char **filenames = NULL;
    
        // We wish to make a single pass through the directory, so we
        // will use a linked list to collect filenames AND count them.
        struct node {char *filename; struct node *next;};
        struct node * head = NULL;
        size_t count = 0;
    
        // Scan through the directory,
        // pushing each new filename to the head of the list
        DIR *dir = opendir(dirname);
        if (!dir) return NULL;
    
        struct dirent *de;
        while ((de = readdir(dir))){
            struct node *node = malloc(sizeof(struct node));
            if (!node) goto lerror;
            node->filename = strdup(de->d_name);
            node->next = head;
            head = node;
            count += 1;
        }
        closedir(dir);
    
        // Now allocate the resulting array
        // and move all the collected filenames to it,
        // dismantling the linked list as we go
        filenames = malloc((count+1) * sizeof(char*));
        if (!filenames) goto lerror;
        filenames[count] = NULL;
    
        while (head){
            struct node *next = head->next;
            filenames[--count] = head->filename;
            free(head);
            head = next;
        }
        return filenames;
    
    lerror:
        // If something went wrong, we'll need to clean up
        while (head){
            struct node *next = head->next;
            free(head->filename);
            free(head);
            head = next;
        }
        return NULL;
    }
    
    char **freeFileNames(char **filenames){
        // Free each filename
        for (size_t n = 0;  filenames[n];  n++)
            free(filenames[n]);
        // And free the list
        free(filenames);
        return NULL;
    }
    
    size_t countFileNames(char **filenames){
        size_t count = 0;
        while (filenames[count])
            count += 1;
        return count;
    }
    

    Notice that I changed the function to take as argument the path (relative or absolute) to the directory you wish to examine. This is a good practice! Let the caller determine which directory we want to read without having to dink with first changing the CWD and then restoring it afterwards.

    Because the function does not return the number of files in the list, the list itself is terminated with a NULL string. This is a common design in C when dealing with lists of this type.

    ⟶ For example, main()’s argc is not really anything more than a convenience, because argv ends with a NULL string! That is to say, argv[argc] == NULL!

    And once again, it is important to note that the returned list of filenames is relative to the argument directory. You could return the full path of each file (== absolute path name). This would require help concatenating the directory name and filename before strdup()ing it into the resulting list.

    Or you could leave it as-is and simply compose the full file path whenever you needed it later. It’s easy enough to write a little function to do that:

    #include <linux/limits.h>
    #include <string.h>
    
    char *JoinDirAndFileName(const char *dirname, const char *filename){
        char path[PATH_MAX] = "";
        strcpy(path, dirname);
        strcat(path, "/");
        strcat(path, filename);
        return strdup(path);
    }
    

    This, of course, requires the dirname argument to be a full path name itself, lol.

    Is It A Directory?

    Additional useful information can be obtained about the files using the stat() family of functions. For example, we might like to know whether a filename refers to a directory or not:

    #include <stdbool.h>
    
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <unistd.h>
    
    bool isDirectory(const char *filename){
        struct stat sb;
        if (stat( filename, &sb )) return false;  // Failure == not a directory
        return (sb.st_mode & S_IFMT) == S_IFDIR;
    }
    

    Keep in mind that this requires either:

    Sort the Filenames

    You may wish to use that information to do things like sort the filenames.

    #include <stdlib.h>
    #include <string.h>
    
    int FileNameCompare(const void *a, const void *b){
        // Sort directory names before filenames
        // Then sort using strcmp()
        bool is_dir_a = isDirectory(*(const char **)a);
        bool is_dir_b = isDirectory(*(const char **)b);
        if (is_dir_a == is_dir_b) 
            return strcmp(*(const char **)a, *(const char **)b);
        return is_dir_a ? -1 : 1;
    }
    
    void SortFileNames(char **filenames){
        qsort(filenames, countFileNames(filenames), sizeof(char *), FileNameCompare);
    }
    

    Again, the sort only works if isDirectory() gets a filename it can use stat() on, meaning either

    An Example Program

    Now we can put it all together as an example program:

    #include <stdio.h>
    
    int main(void){
    
        // Get the list of filenames in the CWD
        char **filenames = getFileNames(".");
        if (!filenames)
        {
            fprintf( stderr, "%s\n", "fooey!" );
            return 1;
        }
    
        // Might as well sort them
        SortFileNames(filenames);
    
        // Print the list
        puts("FILENAMES:");
        for (size_t n = 0;  filenames[n];  n++)
            if (isDirectory(filenames[n]))
                printf("  %s/\n", filenames[n]);
            else
                printf("  %s\n", filenames[n]);
    
        // Clean up
        filenames = freeFileNames(filenames);
        return 0;
    }
    

    To compile and run the example program, just concatenate all the code blocks into a text file and save it as dirlist.c, then compile it with something like:

    $ clang -Wall -Wextra -O3 dirlist.c -o dirlist
    $ ./dirlist
    FILENAMES:
      ./
      ../
      dirlist
      dirlist.c
    $
    

    Windows ≠ Linux

    Alas, Windows users, getting a directory’s content requires different functions, but it all works very much the same way. You can read more starting with some convenient Microsoft documentation: FindFirstFile(), FindNextFile(), FindClose(), and other File Management Functions.