Kahibaro
Discord Login Register

Writing tools in C

Why C for Linux Tools

C is the native language of the Linux kernel and most low‑level userland. Writing tools in C gives you:

This chapter focuses on practical patterns for implementing command‑line tools in C on Linux, not generic C language tutorials.

Building a Simple C Tool on Linux

A minimal command‑line program

A conventional Linux command‑line tool has:

#include <stdio.h>
#include <errno.h>
#include <string.h>
int main(int argc, char *argv[])
{
    if (argc < 2) {
        fprintf(stderr, "Usage: %s NAME\n", argv[0]);
        return 1;
    }
    printf("Hello, %s!\n", argv[1]);
    return 0;  // success
}

Compile:

gcc -Wall -Wextra -O2 -o hello hello.c

Key flags:

For serious tools, treat warnings as errors: add -Werror once the codebase stabilizes.

Parsing Command-Line Arguments

For proper command behavior, you rarely look at argv manually. Linux and POSIX provide getopt and getopt_long.

Using `getopt` (short options)

Typical pattern:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
static void usage(const char *prog)
{
    fprintf(stderr,
            "Usage: %s [-n NUMBER] [-v] FILE...\n"
            "  -n NUMBER  process up to NUMBER lines\n"
            "  -v         verbose output\n",
            prog);
}
int main(int argc, char *argv[])
{
    int opt;
    int verbose = 0;
    long limit = -1;
    while ((opt = getopt(argc, argv, "n:v")) != -1) {
        switch (opt) {
        case 'n':
            limit = strtol(optarg, NULL, 10);
            if (limit <= 0) {
                fprintf(stderr, "Invalid value for -n: %s\n", optarg);
                return 1;
            }
            break;
        case 'v':
            verbose = 1;
            break;
        default:
            usage(argv[0]);
            return 1;
        }
    }
    if (optind >= argc) {
        usage(argv[0]);
        return 1;
    }
    if (verbose)
        fprintf(stderr, "Limit = %ld\n", limit);
    for (int i = optind; i < argc; i++) {
        printf("Would process file: %s\n", argv[i]);
    }
    return 0;
}

Concepts:

Using `getopt_long` (GNU long options)

For GNU‑style long options (--verbose, --limit=10):

#define _GNU_SOURCE
#include <getopt.h>
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
    static struct option long_opts[] = {
        {"limit",   required_argument, 0, 'n'},
        {"verbose", no_argument,       0, 'v'},
        {"help",    no_argument,       0, 'h'},
        {0, 0, 0, 0}
    };
    int opt, long_index = 0;
    int verbose = 0;
    long limit = -1;
    while ((opt = getopt_long(argc, argv, "n:vh", long_opts, &long_index)) != -1) {
        switch (opt) {
        case 'n':
            limit = strtol(optarg, NULL, 10);
            break;
        case 'v':
            verbose = 1;
            break;
        case 'h':
        default:
            printf("Usage: %s [--limit N] [--verbose] FILE...\n", argv[0]);
            return 0;
        }
    }
    /* ... main logic ... */
    return 0;
}

Working with Files and Directories

Linux tools often manipulate files and directories directly using POSIX APIs.

Low-level file I/O: `open`, `read`, `write`, `close`

These are thin wrappers over Linux system calls.

#include <fcntl.h>   // open
#include <unistd.h>  // read, write, close
#include <stdio.h>
#include <errno.h>
#include <string.h>
int copy_file(const char *src, const char *dst)
{
    int in_fd  = open(src, O_RDONLY);
    if (in_fd < 0) {
        fprintf(stderr, "open(%s): %s\n", src, strerror(errno));
        return -1;
    }
    int out_fd = open(dst, O_WRONLY | O_CREAT | O_TRUNC, 0644);
    if (out_fd < 0) {
        fprintf(stderr, "open(%s): %s\n", dst, strerror(errno));
        close(in_fd);
        return -1;
    }
    char buf[8192];
    ssize_t n;
    while ((n = read(in_fd, buf, sizeof(buf))) > 0) {
        ssize_t written = 0;
        while (written < n) {
            ssize_t m = write(out_fd, buf + written, n - written);
            if (m < 0) {
                fprintf(stderr, "write(%s): %s\n", dst, strerror(errno));
                close(in_fd);
                close(out_fd);
                return -1;
            }
            written += m;
        }
    }
    if (n < 0) {
        fprintf(stderr, "read(%s): %s\n", src, strerror(errno));
    }
    close(in_fd);
    close(out_fd);
    return (n < 0) ? -1 : 0;
}

Notes:

Directory traversal with `opendir` / `readdir`

Pattern for tools like ls, find, custom scanners:

#include <dirent.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
int list_dir(const char *path)
{
    DIR *dir = opendir(path);
    if (!dir) {
        fprintf(stderr, "opendir(%s): %s\n", path, strerror(errno));
        return -1;
    }
    struct dirent *entry;
    while ((entry = readdir(dir)) != NULL) {
        // Skip "." and ".."
        if (entry->d_name[0] == '.' &&
           (entry->d_name[1] == '\0' ||
           (entry->d_name[1] == '.' && entry->d_name[2] == '\0')))
            continue;
        printf("%s/%s\n", path, entry->d_name);
    }
    closedir(dir);
    return 0;
}

On Linux, struct dirent usually contains d_type (file type), but you must not rely on it being set on all filesystems; use stat if you need portable type detection.

Getting file metadata with `stat`

For file sizes, timestamps, and types:

#include <sys/stat.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
void show_info(const char *path)
{
    struct stat st;
    if (lstat(path, &st) < 0) {
        fprintf(stderr, "lstat(%s): %s\n", path, strerror(errno));
        return;
    }
    printf("Size: %lld bytes\n", (long long)st.st_size);
    if (S_ISREG(st.st_mode))  printf("Type: regular file\n");
    else if (S_ISDIR(st.st_mode)) printf("Type: directory\n");
    else if (S_ISLNK(st.st_mode)) printf("Type: symlink\n");
}

Use lstat when you want to avoid following symlinks, as many tools do.

Using Linux System Calls

Glibc exposes system calls via wrappers; in some cases you may use syscall(2) directly, but typical tools rely on the POSIX API.

Some common syscalls for tools:

Example: running another program (core for many helpers/wrappers):

#include <unistd.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <sys/wait.h>
int run_command(char *const argv[])
{
    pid_t pid = fork();
    if (pid < 0) {
        fprintf(stderr, "fork: %s\n", strerror(errno));
        return -1;
    }
    if (pid == 0) {
        // Child: replace process image
        execvp(argv[0], argv);
        // If execvp returns, it failed
        fprintf(stderr, "execvp(%s): %s\n", argv[0], strerror(errno));
        _exit(127);
    }
    // Parent: wait for child
    int status;
    if (waitpid(pid, &status, 0) < 0) {
        fprintf(stderr, "waitpid: %s\n", strerror(errno));
        return -1;
    }
    if (WIFEXITED(status))
        return WEXITSTATUS(status);
    if (WIFSIGNALED(status))
        fprintf(stderr, "Process killed by signal %d\n", WTERMSIG(status));
    return -1;
}

This pattern is essential if your tool orchestrates other commands (like a simplified timeout, parallel, or custom runners).

Error Handling and `errno`

Linux tools should:

Common pattern:

#include <errno.h>
#include <stdio.h>
#include <string.h>
int safe_unlink(const char *path)
{
    if (unlink(path) < 0) {
        int e = errno;
        fprintf(stderr, "Failed to remove %s: %s\n", path, strerror(e));
        return -1;
    }
    return 0;
}

Avoid using perror if you want more controlled or localized messages, but it’s handy for quick tools:

perror("unlink");

Exit codes:

Writing Robust, Script-Friendly Tools

When your C program is used in shell scripts, certain behaviors matter a lot.

Use stdout vs stderr correctly

This allows:

Avoid interactive prompts by default

Machine‑friendly tools should not block waiting for input unless explicitly requested (e.g. a --interactive flag). Always provide non‑interactive alternatives.

Stable output format

If the tool is parsed by scripts:

Memory Management and Safety

In C, memory bugs can crash your tools or create vulnerabilities.

Basic allocation patterns

#include <stdlib.h>
#include <stdio.h>
char *read_line(FILE *fp)
{
    size_t cap = 128;
    size_t len = 0;
    char *buf = malloc(cap);
    if (!buf) {
        fprintf(stderr, "Out of memory\n");
        return NULL;
    }
    int c;
    while ((c = fgetc(fp)) != EOF) {
        if (len + 1 >= cap) {
            cap *= 2;
            char *tmp = realloc(buf, cap);
            if (!tmp) {
                free(buf);
                fprintf(stderr, "Out of memory\n");
                return NULL;
            }
            buf = tmp;
        }
        if (c == '\n')
            break;
        buf[len++] = (char)c;
    }
    if (len == 0 && c == EOF) {
        free(buf);
        return NULL;
    }
    buf[len] = '\0';
    return buf;
}

For new code on Linux, also consider:

Common pitfalls

Use tools:

Example compile with sanitizers (for debug builds):

gcc -g -O1 -fsanitize=address,undefined -fno-omit-frame-pointer -o mytool mytool.c

Interacting with the Linux Environment

CLI tools often need to read environment variables or work with the process environment.

Environment variables

#include <stdio.h>
#include <stdlib.h>
void show_env_var(const char *name)
{
    const char *val = getenv(name);
    if (val)
        printf("%s=%s\n", name, val);
    else
        printf("%s is not set\n", name);
}

You can also modify the environment using setenv, unsetenv, or by passing a custom environment to execve.

Determining terminal vs pipe

For interactive behavior (like progress bars), detect whether stdout is a terminal:

#include <unistd.h>
int is_terminal_output(void)
{
    return isatty(STDOUT_FILENO);
}

Many tools suppress colors or progress animations when output is not a TTY.

Using `poll` / `select` for I/O Multiplexing

If your tool processes multiple file descriptors (e.g. reading from stdin and a socket), you need multiplexing.

Simple poll example:

#include <poll.h>
#include <stdio.h>
#include <unistd.h>
int wait_for_input(int timeout_ms)
{
    struct pollfd pfd = {
        .fd = STDIN_FILENO,
        .events = POLLIN
    };
    int ret = poll(&pfd, 1, timeout_ms);
    if (ret < 0) {
        perror("poll");
        return -1;
    } else if (ret == 0) {
        printf("Timeout with no input\n");
        return 0;
    }
    if (pfd.revents & POLLIN) {
        char buf[1024];
        ssize_t n = read(STDIN_FILENO, buf, sizeof(buf));
        if (n > 0)
            write(STDOUT_FILENO, buf, n);
    }
    return 1;
}

This is useful for log collectors, custom shells, or monitoring tools.

Concurrency: Threads in Tools

Many Linux tools are single‑threaded, but some benefit from threads (e.g. parallel downloaders, hashers).

Basic pthread pattern:

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
struct worker_arg {
    int id;
    const char *path;
};
void *worker(void *arg)
{
    struct worker_arg *wa = arg;
    printf("Thread %d processing %s\n", wa->id, wa->path);
    return NULL;
}
int main(int argc, char *argv[])
{
    if (argc < 2) {
        fprintf(stderr, "Usage: %s FILE...\n", argv[0]);
        return 1;
    }
    int n = argc - 1;
    pthread_t *threads = calloc(n, sizeof(*threads));
    struct worker_arg *args = calloc(n, sizeof(*args));
    if (!threads || !args) {
        fprintf(stderr, "Out of memory\n");
        free(threads);
        free(args);
        return 1;
    }
    for (int i = 0; i < n; i++) {
        args[i].id = i;
        args[i].path = argv[i + 1];
        if (pthread_create(&threads[i], NULL, worker, &args[i]) != 0) {
            perror("pthread_create");
            // In real code: join already created threads and clean up
        }
    }
    for (int i = 0; i < n; i++) {
        pthread_join(threads[i], NULL);
    }
    free(threads);
    free(args);
    return 0;
}

For tools, keep concurrency simple; race conditions and deadlocks are easy to introduce in C.

Using Linux-Specific APIs

Linux offers extra APIs beyond POSIX. For portable scripts you might avoid them, but many system tools use them heavily.

Examples:

A tiny example of reading /proc (like a simplified pidof):

#include <dirent.h>
#include <stdio.h>
#include <string.h>
int main(void)
{
    DIR *d = opendir("/proc");
    if (!d) {
        perror("opendir /proc");
        return 1;
    }
    struct dirent *de;
    while ((de = readdir(d)) != NULL) {
        // PIDs are numeric directory names
        int is_digit = 1;
        for (char *p = de->d_name; *p; p++) {
            if (*p < '0' || *p > '9') {
                is_digit = 0;
                break;
            }
        }
        if (!is_digit)
            continue;
        printf("PID: %s\n", de->d_name);
    }
    closedir(d);
    return 0;
}

Many Linux utilities (like ps, top) are built largely by parsing /proc.

Logging and Diagnostics for Tools

For small utilities, printing to stderr is enough. For long‑running or system‑level tools, consider:

Minimal syslog usage:

#include <syslog.h>
void init_logging(const char *ident)
{
    openlog(ident, LOG_PID | LOG_NDELAY, LOG_USER);
}
void log_error(const char *msg)
{
    syslog(LOG_ERR, "%s", msg);
}

Systemd‑managed tools may also write to stdout/stderr and rely on journald, but syslog is still widely used.

Small Build System for Tools

For a single file, compiling with gcc manually is fine. For multi‑file tools, use a simple Makefile.

Example Makefile:

CC      := gcc
CFLAGS  := -Wall -Wextra -O2
LDFLAGS :=
OBJS    := main.o util.o
all: mytool
mytool: $(OBJS)
	$(CC) $(CFLAGS) -o $@ $(OBJS) $(LDFLAGS)
clean:
	rm -f $(OBJS) mytool
.PHONY: all clean

Integrate debugging builds:

debug: CFLAGS += -g -O0
debug: clean mytool

This keeps your Linux tools easy to build and install.

Example: A Simple `head`-like Tool

Putting several pieces together:

Requirements:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
static void usage(const char *prog)
{
    fprintf(stderr,
            "Usage: %s [-n LINES] [FILE...]\n"
            "Print the first LINES lines of each FILE (default 10).\n",
            prog);
}
static int do_head(FILE *fp, const char *name, long lines)
{
    char *line = NULL;
    size_t cap = 0;
    long count = 0;
    while (count < lines) {
        ssize_t n = getline(&line, &cap, fp);
        if (n < 0)
            break;
        if (fwrite(line, 1, (size_t)n, stdout) != (size_t)n) {
            fprintf(stderr, "write error: %s\n", strerror(errno));
            free(line);
            return -1;
        }
        count++;
    }
    free(line);
    if (ferror(fp)) {
        fprintf(stderr, "Error reading %s: %s\n",
                name ? name : "stdin", strerror(errno));
        return -1;
    }
    return 0;
}
int main(int argc, char *argv[])
{
    long lines = 10;
    int opt;
    while ((opt = getopt(argc, argv, "n:h")) != -1) {
        switch (opt) {
        case 'n':
            lines = strtol(optarg, NULL, 10);
            if (lines <= 0) {
                fprintf(stderr, "Invalid line count: %s\n", optarg);
                return 1;
            }
            break;
        case 'h':
        default:
            usage(argv[0]);
            return (opt == 'h') ? 0 : 1;
        }
    }
    int status = 0;
    if (optind == argc) {
        // No files: read from stdin
        if (do_head(stdin, NULL, lines) < 0)
            status = 1;
        return status;
    }
    for (int i = optind; i < argc; i++) {
        const char *name = argv[i];
        FILE *fp = fopen(name, "r");
        if (!fp) {
            fprintf(stderr, "Cannot open %s: %s\n", name, strerror(errno));
            status = 1;
            continue;
        }
        if (argc - optind > 1) {
            // Multiple files: print header like GNU head
            if (i > optind)
                putchar('\n');
            printf("==> %s <==\n", name);
        }
        if (do_head(fp, name, lines) < 0)
            status = 1;
        fclose(fp);
    }
    return status;
}

This example demonstrates:

Next Steps

To go further with Linux tools in C, explore:

Working at this level lets you build tools that behave like native parts of the Linux ecosystem.

Views: 23

Comments

Please login to add a comment.

Don't have an account? Register now!