C CLI Tool Building, Exit Codes & Environment Variables Masterclass

โšก C (C17 / C23 Standard) ๐ŸŸข Lesson 43 ๐Ÿ“‚ Phase 16: Command-Line Arguments ๐Ÿ“… 2026 Comprehensive Master Edition
๐Ÿ“Œ Covered in this in-depth guide: Exit Codes (EXIT_SUCCESS/FAILURE) ยท atexit() Cleanup Handlers ยท getenv() / putenv() ยท Environment Variables ยท Shell Piping ยท Building Real CLI Utilities

Welcome to Phase 16 (Chapter 43): C CLI Tool Building, Exit Codes & Environment Variables Masterclass! Production CLI tools communicate with the shell through exit status codes and can read environment variables for configuration. In this guide you master exit(), atexit(), getenv(), and patterns used by real Unix utilities like grep, ls, and wc.

1Exit Status Codes & Shell Integration

Every C program returns an integer exit status to the operating system shell. This status code is the primary IPC mechanism between programs in Unix shell pipelines.

Exit Code Convention (POSIX standard): 0 โ†’ SUCCESS (Program completed its task correctly) 1 โ†’ GENERAL ERROR (Catch-all failure) 2 โ†’ MISUSE of command or invalid arguments 126 โ†’ Permission denied (cannot execute file) 127 โ†’ Command not found 128+N โ†’ Fatal signal N (e.g. 139 = SIGSEGV Segfault) Shell check: $ ./app && echo "Success!" || echo "Failed!"

exit() vs return from main():

โ€ข return 0; from main() โ€” Normal clean exit. Calls static destructors and fflush on all open streams.

โ€ข exit(EXIT_SUCCESS); โ€” Same as return 0 from main(). Can be called from ANY function.

โ€ข _Exit(0); โ€” Immediate process termination. Does NOT flush buffers or call atexit handlers. Use after fork() in child processes.

2atexit() Cleanup Handlers

atexit() registers functions called automatically when the program exits normally (via exit() or return from main). Up to 32 handlers may be registered (POSIX minimum). They execute in LIFO (Last-In, First-Out) order โ€” reverse registration order.

3Environment Variables
FunctionSignaturePurpose
getenv()char *getenv(const char *name)Read environment variable. Returns NULL if not set.
putenv()int putenv(char *string)Set/modify env var. POSIX only. Unsafe (string ownership issues).
setenv()int setenv(const char *name, const char *value, int overwrite)Safer POSIX alternative to putenv(). Makes internal copy.
4Complete Production CLI Utility
C โ€” Production-grade CLI Word Counter (like wc)โ–ถ Try in Compiler
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <string.h>

static FILE *logfp = NULL;

static void cleanup(void) {
    if (logfp) { fclose(logfp); printf("[atexit] Log file closed.\n"); }
}

typedef struct { long lines, words, chars; } WcResult;

static WcResult count_file(FILE *fp) {
    WcResult r = {0, 0, 0};
    int ch, in_word = 0;
    while ((ch = fgetc(fp)) != EOF) {
        r.chars++;
        if (ch == '\n') r.lines++;
        if (isspace(ch)) { in_word = 0; }
        else if (!in_word) { in_word = 1; r.words++; }
    }
    return r;
}

int main(int argc, char *argv[]) {
    atexit(cleanup);

    /* Read log path from environment variable */
    const char *log_path = getenv("WC_LOG");
    if (log_path) {
        logfp = fopen(log_path, "a");
        if (!logfp) perror("Cannot open WC_LOG");
        else fprintf(logfp, "wc invoked with %d args\n", argc);
    }

    FILE *fp = (argc > 1) ? fopen(argv[1], "r") : stdin;
    if (!fp) { perror(argv[1]); return EXIT_FAILURE; }

    WcResult r = count_file(fp);
    printf("Lines: %ld  Words: %ld  Chars: %ld\n", r.lines, r.words, r.chars);

    if (fp != stdin) fclose(fp);
    return EXIT_SUCCESS;
}
5Technical FAQs

Q1: How does the shell check exit codes?

After any command, $? holds its exit code. Zero means success; non-zero means failure. Shell conditionals like if ./app; then... use exit codes automatically.

Q2: What order do atexit handlers run?

Handlers are called in LIFO order โ€” the last registered handler runs first. This mirrors C++ destructor ordering for cleanup safety.

Q3: Is getenv() thread-safe?

No โ€” getenv() returns a pointer to internal static storage that may be modified by setenv()/putenv() in another thread. Use with caution in multithreaded programs.

Q4: How do shell pipelines use exit codes?

In cmd1 | cmd2, by default only cmd2's exit code is checked. With set -o pipefail in Bash, any failing command in the pipeline causes pipeline failure.

Q5: What is the difference between exit() and abort()?

abort() sends SIGABRT signal, generating a core dump for debugging. It does NOT call atexit handlers or flush streams. Used when program detects a fatal internal inconsistency.