Directories and file metadata
A directory is a file that maps names to entries, and the C library reads it as a stream of names with opendir and readdir. The metadata of an entry, its size, type and permissions, comes from stat. These functions are POSIX rather than ISO C, so the lesson includes the headers that declare them.
Listing a directory
readdir returns the next entry or NULL at the end, and the order is whatever the file system stores. The program collects the names, sorts them, and prints a stable list. The dot entries are skipped, and the name is copied because the buffer belongs to the directory stream.
#include <dirent.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
static int compare(const void *left, const void *right)
{
const char *a = *(const char *const *) left;
const char *b = *(const char *const *) right;
return strcmp(a, b);
}
int main(void)
{
const char *files[3] = {"a.txt", "b.txt", "c.txt"};
char path[64];
mkdir("demo", 0755);
for (int i = 0; i < 3; i++) {
snprintf(path, sizeof path, "demo/%s", files[i]);
FILE *file = fopen(path, "w");
if (file != NULL) {
fclose(file);
}
}
DIR *directory = opendir("demo");
char *names[16];
int count = 0;
if (directory == NULL) {
return 1;
}
struct dirent *entry;
while ((entry = readdir(directory)) != NULL && count < 16) {
if (entry->d_name[0] == '.') {
continue;
}
names[count++] = strdup(entry->d_name);
}
closedir(directory);
qsort(names, (size_t) count, sizeof names[0], compare);
for (int i = 0; i < count; i++) {
printf("%s\n", names[i]);
free(names[i]);
}
return 0;
}
gcc -Wall -Wextra list.c -o list
rm -rf demo
./list
a.txt
b.txt
c.txt
The sort makes the output independent of the directory order. strdup allocates one copy per name; freeing them in the loop keeps the program leak-free, the rule from lesson 32.
File metadata
stat fills a struct with the file type, the size, the permissions and the timestamps. The size is an off_t, printed with %lld after a cast to long long, and the permission bits are the low nine of st_mode, printed in octal.
#include <stdio.h>
#include <sys/stat.h>
int main(void)
{
struct stat info;
if (stat("demo/a.txt", &info) != 0) {
perror("demo/a.txt");
return 1;
}
printf("size: %lld\n", (long long) info.st_size);
printf("regular: %d\n", S_ISREG(info.st_mode) != 0);
printf("permissions: %o\n", info.st_mode & 0777);
return 0;
}
gcc -Wall -Wextra statfile.c -o statfile
./statfile
size: 0
regular: 1
permissions: 644
The size is zero because the file was created empty by the listing program. 644 is the usual mode: the owner may read and write, everyone else may read. S_ISDIR, S_ISLNK and the other macros ask the same struct about the type.
Creating, renaming, removing
The four calls return zero on success and -1 with errno set on failure. The example creates a directory, moves one file into it, deletes the file and removes the directory. Every call is printed with its return value, which is the smallest possible check.
#include <stdio.h>
#include <sys/stat.h>
#include <unistd.h>
int main(void)
{
printf("mkdir: %d\n", mkdir("later", 0755));
printf("rename: %d\n", rename("demo/a.txt", "later/a.txt"));
printf("unlink: %d\n", unlink("later/a.txt"));
printf("rmdir: %d\n", rmdir("later"));
return 0;
}
gcc -Wall -Wextra files.c -o files
./files
mkdir: 0
rename: 0
unlink: 0
rmdir: 0
The listing program must have run first, because the rename needs demo/a.txt. All four calls succeed on the same file system; a rename across devices fails with EXDEV, and the portable fix is to copy and unlink.
Wrong size type, unchecked open
st_size is not an int, and printing it with %d is a type mismatch the compiler reports. The format hint at the end names the conversion that matches, %ld for a long off_t.
#include <stdio.h>
#include <sys/stat.h>
int main(void)
{
struct stat info;
if (stat("demo/b.txt", &info) != 0) {
return 1;
}
printf("size: %d\n", info.st_size);
return 0;
}
gcc -Wall -Wextra sizeformat.c -o sizeformat
sizeformat.c: In function 'main':
sizeformat.c:11:20: warning: format '%d' expects argument of type 'int', but argument 2 has type 'off_t' {aka 'long int'} [-Wformat=]
11 | printf("size: %d\n", info.st_size);
| ~^ ~~~~~~~~~~~~
| | |
| int off_t {aka long int}
| %ld
The second mistake is an unchecked opendir. The function returns NULL for a missing directory, and passing that to readdir dereferences the null pointer:
#include <dirent.h>
#include <stdio.h>
int main(void)
{
DIR *directory = opendir("missing");
struct dirent *entry = readdir(directory);
if (entry != NULL) {
printf("%s\n", entry->d_name);
}
closedir(directory);
return 0;
}
gcc -Wall -Wextra unchecked.c -o unchecked
./unchecked; echo "exit: $?"
exit: 139
The fix is the check from the first section: if directory == NULL, report the error and return before the first readdir. The shell reports 139 for the segmentation fault; no compiler warning appears because the null value is only known at run time.
Exercises
- Print only the entries of the current directory whose name ends in .c.
- Walk the demo directory recursively and print the path of every regular file with its size.
- Create a file with mode 0600 and check the permissions with stat; then change them with chmod and check again.
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
A directory name returned by
readdiris reused by the next call. Why copy it?Answer
The buffer belongs to the directory stream
The string lives in the directory stream, so a copy is needed when the names outlive the next call.
-
Which macro asks whether a
statresult is a regular file?Answer
S_ISREG(info.st_mode)The macro tests the mode bits, while the size and inode fields describe other properties.
-
What do
mkdir,rename,unlinkandrmdirreturn on success?Answer
Zero, with -1 and
errnoon failureThe POSIX file calls report success as zero, and the caller checks for -1 and reads
errno. -
Why sort the names after reading a directory?
Answer
The file system order is arbitrary
The directory stream has no promised order, so sorting makes the output stable between runs.
-
What does this program print?
#include <dirent.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/stat.h> static int cmp(const void*a,const void*b){return strcmp(*(const char**)a,*(const char**)b);} int main(void){mkdir("/tmp/listdemo",0755);const char*f[3]={"a.txt","b.txt","c.txt"};for(int i=0;i<3;i++){char p[64];snprintf(p,sizeof p,"/tmp/listdemo/%s",f[i]);FILE*x=fopen(p,"w");fclose(x);}DIR*d=opendir("/tmp/listdemo");char*n[3];int c=0;struct dirent*e;while((e=readdir(d)))if(e->d_name[0]!='.')n[c++]=strcpy(malloc(strlen(e->d_name)+1),e->d_name);closedir(d);qsort(n,c,sizeof n[0],cmp);for(int i=0;i<c;i++)printf("%s\n",n[i]);return 0;}Answer
a.txt / b.txt / c.txt
The names are collected, sorted, and printed, so the order is stable.
-
What does this program print for an empty file?
#include <stdio.h> #include <sys/stat.h> int main(void){FILE*f=fopen("/tmp/empty.txt","w");fclose(f);struct stat s;stat("/tmp/empty.txt",&s);printf("%lld %d\n",(long long)s.st_size,S_ISREG(s.st_mode)!=0);return 0;}Answer
0 1
The file is empty and regular, so the size is zero and the type check is true.
-
What does this program print?
#include <stdio.h> #include <sys/stat.h> #include <unistd.h> int main(void){mkdir("/tmp/op1",0755);rename("/tmp/op1","/tmp/op2");rmdir("/tmp/op2");printf("%d %d %d\n",0,0,0);return 0;}Answer
0 0 0
All three calls succeed on the same file system and return zero.
-
What does GCC warn about in
printf("size: %d", info.st_size)?Answer
That
%dexpectsint, but the size isoff_tst_sizeis anoff_t, usually a long, so the warning suggests%ldor a cast to a matching type. -
What happens when
opendirreturns NULL andreaddiris called anyway?Answer
The program crashes with status 139
readdirdereferences the null stream pointer, and the shell reports the segmentation fault as 139. -
What does this program print?
#include <stdio.h> #include <sys/stat.h> int main(void){mkdir("demo",0755);FILE*f=fopen("demo/a.txt","w");if(!f)return 1;fclose(f);printf("created\n");return 0;}Answer
created
mkdircreates the directory, and the followingfopenwrites a file inside it.
The code on this page was run with gcc (Alpine 15.2.0) 15.2.0 on Alpine Linux 3.24.3 (x86_64) in a Docker container.
Article author: Arthur Isaev