Pathname Utility Functions
This section covers functions for creating symlinks, reading the path inside a symlink, resolving a path to its absolute canonical form, and splitting a path into directory and filename parts.
#include <unistd.h>
int symlink(const char *filepath, const char *linkpath);
/* Returns 0 on success, -1 on error */
Creates a symlink file at linkpath whose content is the string filepath. The target filepath does NOT need to exist — you can create a dangling link. The target path can be absolute or relative.
#include <unistd.h>
ssize_t readlink(const char *pathname, char *buffer, size_t bufsiz);
/* Returns: number of bytes written to buffer, or -1 on error */
To be safe, always allocate PATH_MAX bytes and null-terminate after the call:
char buf[PATH_MAX];
ssize_t n = readlink("/tmp/mylink", buf, sizeof(buf) - 1);
if (n != -1) buf[n] = '\0';
If the symlink content is longer than bufsiz, the string is truncated. There is no way to tell if truncation happened if the returned length exactly equals bufsiz — so always use PATH_MAX.
#include <stdlib.h>
char *realpath(const char *pathname, char *resolved_path);
/* Returns pointer to resolved path on success, NULL on error */
realpath() does everything in one step:
. (current dir) and .. (parent dir)Example: realpath("../../etc/./passwd", buf) → /etc/passwd
#include <libgen.h>
char *dirname(char *pathname); /* Returns directory part */
char *basename(char *pathname); /* Returns filename part */
Given /home/ravi/project/main.c:
| Input path | dirname() | basename() |
|---|---|---|
| /usr/bin/ls | /usr/bin | ls |
| /etc/passwd | /etc | passwd |
| passwd | . | passwd |
| / | / | / |
| /etc/passwd//// | /etc | passwd |
| .. | . | .. |
Code Examples
#include <stdio.h>
#include <unistd.h>
#include <limits.h>
#include <string.h>
int main() {
/* Create the real file */
FILE *f = fopen("/tmp/original.txt", "w");
fputs("real content\n", f);
fclose(f);
/* Create symlink pointing to it */
if (symlink("/tmp/original.txt", "/tmp/link.txt") == -1) {
perror("symlink");
return 1;
}
/* Read what the symlink points to */
char buf[PATH_MAX];
ssize_t n = readlink("/tmp/link.txt", buf, sizeof(buf) - 1);
if (n == -1) { perror("readlink"); return 1; }
buf[n] = '\0'; /* Must null-terminate manually */
printf("Symlink points to: %s\n", buf);
/* Access the file through the link */
f = fopen("/tmp/link.txt", "r");
fgets(buf, sizeof(buf), f);
printf("Content via link: %s", buf);
fclose(f);
/* Cleanup */
unlink("/tmp/link.txt");
unlink("/tmp/original.txt");
return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
int main() {
char resolved[PATH_MAX];
/* Resolve a relative path */
if (realpath("../../etc/hostname", resolved) == NULL)
perror("realpath");
else
printf("Resolved: %s\n", resolved);
/* Resolve a path with . and .. */
if (realpath("/usr/bin/../lib/../bin/ls", resolved) != NULL)
printf("Resolved: %s\n", resolved);
/* Resolve the current directory */
if (realpath(".", resolved) != NULL)
printf("CWD resolved: %s\n", resolved);
/* Using NULL buffer (glibc extension — remember to free!) */
char *p = realpath("/etc/./passwd", NULL);
if (p) {
printf("Resolved (heap): %s\n", p);
free(p);
}
return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <libgen.h>
#include <string.h>
void print_parts(const char *path) {
/* MUST use copies — dirname/basename may modify the string */
char *copy1 = strdup(path);
char *copy2 = strdup(path);
printf("Path : %s\n", path);
printf("dirname : %s\n", dirname(copy1));
printf("basename: %s\n", basename(copy2));
printf("---\n");
free(copy1);
free(copy2);
}
int main() {
print_parts("/home/ravi/projects/app/main.c");
print_parts("/etc/passwd");
print_parts("passwd");
print_parts("/");
print_parts("../data/file.txt");
return 0;
}
Output:
Path : /home/ravi/projects/app/main.c
dirname : /home/ravi/projects/app
basename: main.c
---
Path : /etc/passwd
dirname : /etc
basename: passwd
---
Path : passwd
dirname : .
basename: passwd
---
Path : /
dirname : /
basename: /
---
Path : ../data/file.txt
dirname : ../data
basename: file.txt
---
#include <stdio.h>
#include <unistd.h>
#include <limits.h>
#include <sys/stat.h>
#include <stdlib.h>
void check_path(const char *path) {
struct stat sb;
if (lstat(path, &sb) == -1) {
printf("%-30s: does not exist\n", path);
return;
}
if (S_ISLNK(sb.st_mode)) {
char link_target[PATH_MAX];
ssize_t n = readlink(path, link_target, sizeof(link_target)-1);
link_target[n] = '\0';
char *resolved = realpath(path, NULL);
printf("%-30s: symlink -> %s (resolves to: %s)\n",
path, link_target, resolved ? resolved : "DANGLING");
free(resolved);
} else if (S_ISREG(sb.st_mode)) {
printf("%-30s: regular file\n", path);
} else if (S_ISDIR(sb.st_mode)) {
printf("%-30s: directory\n", path);
}
}
int main() {
check_path("/bin");
check_path("/etc/hostname");
check_path("/tmp");
return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <libgen.h>
/* Given input "/src/utils.c", produce output "/build/utils.o" */
char *make_output_path(const char *input, const char *out_dir) {
char *copy = strdup(input);
char *base = basename(copy);
/* Strip extension */
char *dot = strrchr(base, '.');
if (dot) *dot = '\0';
char *result = malloc(strlen(out_dir) + strlen(base) + 8);
sprintf(result, "%s/%s.o", out_dir, base);
free(copy);
return result;
}
int main() {
const char *inputs[] = {
"/home/ravi/project/src/main.c",
"/home/ravi/project/src/utils.c",
"/home/ravi/project/src/parser.c"
};
for (int i = 0; i < 3; i++) {
char *out = make_output_path(inputs[i], "/tmp/build");
printf("%s -> %s\n", inputs[i], out);
free(out);
}
return 0;
}
Output:
/home/ravi/project/src/main.c -> /tmp/build/main.o
/home/ravi/project/src/utils.c -> /tmp/build/utils.o
/home/ravi/project/src/parser.c -> /tmp/build/parser.o
Interview Questions
Forgetting to null-terminate the buffer. readlink() does NOT add ‘\0’ at the end. You must write buf[n] = ‘\0’ after a successful call. Failing to do so causes undefined behavior when treating the buffer as a string.
readlink() just returns the raw string stored inside the symlink (the link target). It doesn’t resolve the path or follow chains. realpath() fully resolves a pathname: follows all symlinks, resolves . and .., and returns the absolute canonical path of the final target.
Both functions are permitted to modify their input argument (e.g., they may add or remove a trailing null byte). If you pass the original string, it may be corrupted. Always strdup() before calling.
It returns “.” (the current directory), because a relative filename with no directory prefix is in the current directory.
Yes. symlink() doesn’t verify that the target exists. The result is a dangling symlink. This is useful for creating links before the target is ready.
It returns NULL and sets errno to ENOENT. All components except the last must exist for realpath() to succeed.
