Part IV — Real-world builds
Theory is done — Parts I–III gave you everything this Part uses. From here it's full, opinionated builds rather than isolated examples: a production CLI, a text editor, a Unix shell, and a bump allocator, each combining memory management, data structures, and I/O into something closer to what you'd actually ship.
24. Building production CLIs: argument parsing, config precedence, exit codes
24.1 getopt — the standard way to parse flags
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char *argv[]) {
int verbose = 0;
char *output = "out.txt";
int opt;
while ((opt = getopt(argc, argv, "vo:h")) != -1) {
switch (opt) {
case 'v': verbose = 1; break;
case 'o': output = optarg; break; // optarg is set by getopt for options that take a value
case 'h':
printf("usage: %s [-v] [-o file] [input...]\n", argv[0]);
return 0;
default:
fprintf(stderr, "usage: %s [-v] [-o file] [input...]\n", argv[0]);
return 2; // convention: 2 for usage errors, 1 for runtime errors, 0 for success
}
}
for (int i = optind; i < argc; i++) { // optind marks where positional args start
printf("input: %s\n", argv[i]);
}
printf("verbose=%d output=%s\n", verbose, output);
return 0;
}
24.2 Config precedence — the convention many serious CLIs follow
defaults < config file < environment variables < command-line flags
(lowest precedence) (highest precedence)
#include <stdlib.h>
#include <string.h>
const char *resolve_output_dir(const char *cli_flag) {
if (cli_flag) return cli_flag; // 1. explicit flag wins
const char *env = getenv("APP_OUTPUT_DIR");
if (env) return env; // 2. then environment
return "./output"; // 3. then a hardcoded default
// a real tool would also check a config file here, between env and default
}
Real-world example. This precedence idea is why git -c user.name=X commit overrides ~/.gitconfig, which overrides git's
built-in defaults. Many serious CLIs (curl, docker, kubectl) use some
version of this layered-override convention, because it lets a script
override one setting for one invocation without touching global config.
24.3 Exit codes are part of your program's API
// Conventionally (see <sysexits.h> on BSD/macOS for the fuller list):
// 0 = success
// 1 = general runtime error
// 2 = usage error (bad arguments)
// 126 = command found but not executable
// 127 = command not found
// 128+N = terminated by signal N
Shell scripts and CI pipelines branch on your exit code — returning 0
after a caught, logged error (instead of 1) is a real and common bug
class that silently turns failures into "success" in automation.