Before you read this post, go through Part 1: Building the 'ls' Command in C — it covers opendir(), the DIR structure, and debugging with GDB.

I'm back on my journey of learning systems programming with C. In the last post we covered the basics of the directory stream and peeked inside it with gdb. Now for the real work — reading from a directory and printing its content.

What we did in Part 1

In the previous post we opened a directory and got a DIR pointer:

//myls.c

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

int main(int argc, char **argv) {
  // DIR এর একটা pointer define করি
  DIR *dir_stream;

  // current directory (".") open করি
  dir_stream = opendir(".");

  // opendir error হলে NULL return করে, তাই error handle করি...
  if (dir_stream == NULL) {
    perror("Error while trying to open directory \n");
    return 1;
  }

  return 0;
}

We used gdb to inspect the inside of dir_stream, which showed us the details of the internal structure. Now let's move forward and actually read from the directory.

Using readdir()

To read the things inside a directory we'll use the readdir function, which is part of the C standard library (included via dirent.h). Let's check the documentation:

man readdir

So, the readdir function takes a DIR pointer (which is dir_stream in our code) and returns a pointer to a dirent struct. This struct represents the next entry of dir_stream. To read every entry, we just have to keep calling readdir in a loop until it returns NULL.

The dirent Structure

The documentation also tells us what the dirent struct looks like:

struct dirent {
  ino_t d_ino;             // inode number
  off_t d_off;             // offset to next entry
  unsigned short d_reclen; // length of this record
  unsigned char d_type;    // type of file
  char d_name[256];        // filename
};

Here's the fun part: this struct gives us some useful information about every file in the directory:

  • d_name: The name of the file.
  • d_reclen: The size of the directory entry.

So, if we iterate over dir_stream, call readdir on each entry, and print these values — we'll have built our own basic ls function!

Using closedir()

When we're done, we mustn't forget to close the directory either. For that we'll use the closedir function from the C standard library (it's in dirent.h too).

man closedir

To close the directory we just have to pass the pointer to our directory stream dir_stream. On success it returns 0, and on error -1.

The Complete Implementation

Let's write it out!

// myls.c

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

int main(int argc, char **argv) {
  // DIR এর একটা pointer define করি
  DIR *dir_stream;
  // dirent struct এর একটা pointer define করি
  struct dirent *dir_read;

  // current directory (".") open করি
  dir_stream = opendir(".");

  // opendir error হলে NULL return করে, তাই error handle করি...
  if (dir_stream == NULL) {
    perror("Error while trying to open directory\n");
    return 1;
  }

  // এবার ভেতরের জিনিস পড়ি:
  while ((dir_read = readdir(dir_stream)) != NULL) {
    printf("%s\t%d bytes\n", dir_read->d_name, dir_read->d_reclen);
  }

  // কাজ শেষে directory close করি
  if (closedir(dir_stream) == -1) {
    perror("Can't close the dir \n");
    return -1;
  }

  return 0;
}

Now let's compile and see the result:

gcc -Wall -Werror -g myls.c -o myls

What the code does, step by step

We saw the whole thing together above, now let's break it down a bit:

1. Declare variables:

DIR *dir_stream;
struct dirent *dir_read;

Two pointers — one for the directory stream, another for reading each entry.

2. Open the directory:

dir_stream = opendir(".");

We learned this in Part 1 — we open the current directory and get a DIR*.

3. Read entries with a loop:

while ((dir_read = readdir(dir_stream)) != NULL) {
    printf("%s\t%d bytes\n", dir_read->d_name, dir_read->d_reclen);
}

This is the core part. Each time readdir() is called, the pointer to the next entry comes back. NULL means all entries have been read. We access the fields inside the struct using the arrow operator (->).

4. Close the directory:

if (closedir(dir_stream) == -1) {
    perror("Can't close the dir \n");
    return -1;
}

Resource cleanup — if you open something, you have to close it. The Unix rule: whatever you open, you close.

Let's see the output

This is what the built-in ls command gives:

$ ls
myls  myls.c  test_folder  notes.txt

And our own myls gives:

$ ./myls
.             32 bytes
..            32 bytes
myls          32 bytes
myls.c        32 bytes
test_folder   32 bytes
notes.txt     32 bytes

Notice — our version also shows . (the current directory) and .. (the parent directory), because these are directory entries too. The real ls command hides these by default (hidden files — the ones starting with .). We'll handle this in the future!

What I learned today

TopicWhat I learned
readdir()Reads an entry from the directory stream, returns a struct dirent*
struct direntHolds the file's name (d_name), size (d_reclen), type (d_type), and so on
closedir()Closes the directory stream, does resource cleanup
Arrow operator (->)The way to access a struct's fields through a pointer
Hidden filesFiles starting with . — ls hides them by default

Wrapping up

Our code lists the directory's content just like the built-in ls command — pretty satisfying to look at, isn't it? The built-in ls offers a lot more functionality, but we're now at a point where we can start adding new features ourselves. That'll happen in future posts.

Thanks for sticking around. Happy coding, see you next time!


The Full Series

PartTopic
Part 1opendir(), DIR structure, debugging with GDB
Part 2 (this post)readdir(), dirent struct, closedir(), the complete ls