Table of Contents
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:
- Direct access to Linux system calls and kernel interfaces
- Fine‑grained control over memory and performance
- Small, dependency‑free binaries suitable for scripts and system environments
- The ability to use the same APIs that core tools (
ls,cp,mount, etc.) use
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:
- A
mainentry point - Return codes (
0= success, non‑zero = error) - I/O via stdin/stdout/stderr
- Use of
errnofor error reporting
#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.cKey flags:
-Wall -Wextra: enable useful warnings-O2: optimize for typical use-o: set output binary name
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:
getoptparses-n 5 -vstyle options.optargcontains the value for options that require an argument.optindis the index of the first non-option argument (e.g. file names).
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:
- Always handle short writes:
writemay write fewer bytes than requested. - Use proper file modes (here
0644) when creating files.
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:
- Process control:
fork,execve,waitpid - Signals:
kill,sigaction - Files:
unlink,rename,link,symlink,truncate - Time:
clock_gettime,nanosleep - Resource limits:
getrlimit,setrlimit
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:
- Check return values of all system/library calls
- Use
errnoto describe errors - Report errors to
stderr, notstdout - Return meaningful exit codes
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:
0— success1— general error2— misuse of shell builtins / bad usage (convention from POSIX utilities)- Other values — tool‑specific meanings (document them in
--helpor man page).
Writing Robust, Script-Friendly Tools
When your C program is used in shell scripts, certain behaviors matter a lot.
Use stdout vs stderr correctly
- Normal output:
stdout(printf,puts,fwrite) - Messages, progress, errors:
stderr(fprintf(stderr, ...))
This allows:
- Redirection:
mytool input >result.txt 2>errors.log - Pipelining:
mytool | grep pattern
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:
- Avoid unnecessary decoration (colors, headers) unless requested via options
- Consider a
--quietand/or--jsonmode for easier parsing
Memory Management and Safety
In C, memory bugs can crash your tools or create vulnerabilities.
Basic allocation patterns
- Use
malloc,calloc,realloc,free - Always check for allocation failure
- Ensure every successful allocation is freed along all code paths
#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:
getline(GNU extension, but widely available) to read lines safelystrndup,asprintf(GNU extensions) for safer string handling
Common pitfalls
- Off‑by‑one errors in indexing and buffer sizes
- Using uninitialized variables
- Using memory after
free - Forgetting NUL terminator for strings
Use tools:
-fsanitize=address,undefined(with GCC/Clang) for runtime checksvalgrindto detect leaks and memory errors
Example compile with sanitizers (for debug builds):
gcc -g -O1 -fsanitize=address,undefined -fno-omit-frame-pointer -o mytool mytool.cInteracting 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:
inotify: file change notificationsepoll: scalable I/O multiplexingsignalfd,timerfd,eventfd: file descriptor based system events/procand/sys: kernel information and control via pseudo‑filesptrace: debugging/tracing
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:
- A verbosity flag (
-v,-q) controlling diagnostic output - Syslog logging (
syslog(3)) so messages appear in system logs
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 cleanIntegrate debugging builds:
debug: CFLAGS += -g -O0
debug: clean mytoolThis keeps your Linux tools easy to build and install.
Example: A Simple `head`-like Tool
Putting several pieces together:
Requirements:
- Options:
-n LINES(default 10) - Read from one or more files; or stdin if no files
#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:
- Argument parsing with
getopt - Safe line reading using
getline - Correct use of exit codes and error messages
- Script‑friendly behavior (reads stdin if no files)
Next Steps
To go further with Linux tools in C, explore:
- Parsing
/procand/sysfor system utilities - Using
inotifyfor file‑watching tools - Implementing network tools using
socket,connect,bind,send,recv - Adding manual pages (
manentries) for your tools - Studying source of existing tools such as
coreutils,busybox, ortoyboxfor real‑world patterns
Working at this level lets you build tools that behave like native parts of the Linux ecosystem.