Pada Bagian 5 ini kita membangun modul logger yang menulis pesan dengan level (INFO, WARN, ERROR, DEBUG) ke stderr. Tujuannya adalah memisahkan output normal program (yang ditulis pengguna ingin lihat) dari pesan diagnostik (yang membantu debugging). Logger juga mendukung konfigurasi level minimum, sehingga pengguna dapat mengatur verbositas dengan flag -v atau konfigurasi.

Konsep Logging yang Baik #

Logger yang baik memenuhi tiga kriteria: (1) setiap pesan memiliki level yang menunjukkan severity, (2) pesan ditulis ke stream yang sesuai (biasanya stderr agar tidak mengganggu output normal di stdout), (3) level minimum dapat dikonfigurasi untuk memfilter pesan yang tidak perlu. Level standar yang dipakai:

  • DEBUG — detail implementasi, hanya aktif saat debugging.
  • INFO — konfirmasi operasi normal, default untuk pengguna.
  • WARN — kondisi tak diharapkan tapi tidak fatal.
  • ERROR — error yang membuat operasi gagal, tapi program tetap berjalan.
  • FATAL — error yang membuat program harus keluar.

Implementasi Logger #

c
#include <stdio.h>
#include <stdarg.h>
#include <time.h>

typedef enum {
    LOG_DEBUG = 0,
    LOG_INFO,
    LOG_WARN,
    LOG_ERROR,
    LOG_FATAL
} LogLevel;

static LogLevel g_level = LOG_INFO;
static const char *level_names[] = {
    "DEBUG", "INFO", "WARN", "ERROR", "FATAL"
};

void logger_set_level(LogLevel level) {
    g_level = level;
}

void logger_log(LogLevel level, const char *file, int line, const char *fmt, ...) {
    if (level < g_level) return;

    time_t now = time(NULL);
    struct tm *tm = localtime(&now);
    char timebuf[32];
    strftime(timebuf, sizeof(timebuf), "%H:%M:%S", tm);

    fprintf(stderr, "[%s] [%s] %s:%d: ",
            level_names[level], timebuf, file, line);

    va_list args;
    va_start(args, fmt);
    vfprintf(stderr, fmt, args);
    va_end(args);
    fprintf(stderr, "\n");
    fflush(stderr);
}

#define LOG_INFO(...)  logger_log(LOG_INFO,  __FILE__, __LINE__, __VA_ARGS__)
#define LOG_WARN(...)  logger_log(LOG_WARN,  __FILE__, __LINE__, __VA_ARGS__)
#define LOG_ERROR(...) logger_log(LOG_ERROR, __FILE__, __LINE__, __VA_ARGS__)
#define LOG_DEBUG(...) logger_log(LOG_DEBUG, __FILE__, __LINE__, __VA_ARGS__)

int main(void) {
    logger_set_level(LOG_DEBUG);

    LOG_INFO("Memulai program");
    LOG_DEBUG("Variabel x = %d", 42);
    LOG_WARN("File konfigurasi tidak ditemukan, pakai default");
    LOG_ERROR("Gagal membuka database: %s", "connection refused");
    LOG_INFO("Selesai");
    return 0;
}

Output yang Dihasilkan:

text
[INFO] [12:34:56] main.c:32: Memulai program
[DEBUG] [12:34:56] main.c:33: Variabel x = 42
[WARN] [12:34:56] main.c:34: File konfigurasi tidak ditemukan, pakai default
[ERROR] [12:34:56] main.c:35: Gagal membuka database: connection refused
[INFO] [12:34:56] main.c:36: Selesai

Mari kita telaah bagian kunci dari kode di atas:

  1. LogLevel enum: mendefinisikan level sebagai konstanta integer. Urutan dari paling rendah (DEBUG) ke paling tinggi (FATAL) memungkinkan perbandingan if (level < g_level) return; untuk filtering.
  2. g_level global: level minimum yang akan dicetak. Default LOG_INFO. Diubah dengan logger_set_level.
  3. logger_log: fungsi inti yang menerima level, lokasi (__FILE__, __LINE__), dan format printf. Memakai va_list untuk menangani argument variadic. Mencetak ke stderr dengan format [LEVEL] [time] file:line: message.
  4. Macro LOG_INFO dll.: otomatis menyisipkan __FILE__ dan __LINE__ compiler, sehingga pengguna tidak perlu menulisnya manual. Macro ini adalah idiom standar di C untuk logging.

Integrasi dengan CLI Tool #

Untuk mengintegrasikan logger dengan CLI tool, hubungkan flag -v/-q ke logger_set_level:

c
// Di main.c setelah parsing args
if (args.verbose) {
    logger_set_level(LOG_DEBUG);
} else if (args.quiet) {
    logger_set_level(LOG_ERROR);
} else {
    logger_set_level(LOG_INFO);
}

LOG_INFO("Memulai tool versi 1.0.0");

Pola Error Reporting yang Konsisten #

Selain logger, sediakan helper untuk melaporkan error fatal:

c
#define LOG_FATAL_AND_EXIT(...) do { \
    logger_log(LOG_FATAL, __FILE__, __LINE__, __VA_ARGS__); \
    exit(1); \
} while (0)

// Pemakaian:
if (!file) {
    LOG_FATAL_AND_EXIT("File input tidak diberikan");
}

Pola ini memastikan error fatal selalu keluar dengan exit code 1 setelah mencatat pesan.

Pengujian Logger #

bash
gcc -std=c11 -Wall -Wextra -o logger_demo main.c
./logger_demo

Pada Bagian 6 terakhir, kita akan menulis unit test untuk setiap modul dan mengemas program dengan Makefile untuk distribusi.

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

cli.c

Unduh berkas
c
/* cli.c - Implementasi parsing argumen */
#include "cli.h"

#include <string.h>
#include <stdlib.h>

#define PROGRAM_NAME "mtool"
#define PROGRAM_VERSION "1.0.0"

void cli_print_help(FILE *out) {
    fprintf(out,
        "Penggunaan: %s [SUBCOMMAND] [OPTION]... [FILE]\n"
        "\n"
        "Alat pemrosesan teks baris perintah sederhana.\n"
        "\n"
        "Subperintah:\n"
        "  (none)             Baca stdin dan cetak jumlah baris.\n"
        "  help               Tampilkan pesan ini.\n"
        "  version            Tampilkan versi program.\n"
        "  count [FILE]       Hitung jumlah baris di FILE (atau stdin).\n"
        "  head [-n N] [FILE] Cetak N baris pertama (default 10).\n"
        "  tail [-n N] [FILE] Cetak N baris terakhir (default 10).\n"
        "  stats [FILE]       Statistik: jumlah baris, kata, karakter.\n"
        "  grep PATTERN [-i] [FILE]\n"
        "                     Cetak baris yang mengandung PATTERN.\n"
        "\n"
        "Opsi:\n"
        "  -n N               Jumlah baris untuk head/tail.\n"
        "  -i                 Case-insensitive untuk grep.\n"
        "  -v                 Verbose log ke stderr.\n"
        "  --help             Tampilkan pesan bantuan.\n"
        "  --version          Tampilkan versi.\n",
        PROGRAM_NAME);
}

void cli_print_version(FILE *out) {
    fprintf(out, "%s %s\n", PROGRAM_NAME, PROGRAM_VERSION);
}

/* Cek apakah string diawali tanda strip (option). */
static int is_option(const char *s) {
    return s != NULL && s[0] == '-' && s[1] != '\0';
}

int cli_parse(int argc, char *argv[], cli_args *out) {
    if (out == NULL) {
        return -1;
    }
    memset(out, 0, sizeof(*out));
    out->count = 10; /* default untuk head/tail */

    if (argc < 2) {
        out->subcommand = CLI_SUB_COUNT;
        return 0;
    }

    int i = 1;
    /* Deteksi subperintah pertama bila bukan option. */
    if (!is_option(argv[i])) {
        const char *cmd = argv[i];
        if (strcmp(cmd, "help") == 0 || strcmp(cmd, "--help") == 0) {
            out->subcommand = CLI_SUB_HELP;
            out->show_help = 1;
            return 0;
        }
        if (strcmp(cmd, "version") == 0 || strcmp(cmd, "--version") == 0) {
            out->subcommand = CLI_SUB_VERSION;
            out->show_version = 1;
            return 0;
        }
        if (strcmp(cmd, "count") == 0) {
            out->subcommand = CLI_SUB_COUNT;
        } else if (strcmp(cmd, "head") == 0) {
            out->subcommand = CLI_SUB_HEAD;
        } else if (strcmp(cmd, "tail") == 0) {
            out->subcommand = CLI_SUB_TAIL;
        } else if (strcmp(cmd, "stats") == 0) {
            out->subcommand = CLI_SUB_STATS;
        } else if (strcmp(cmd, "grep") == 0) {
            out->subcommand = CLI_SUB_GREP;
        } else if (cmd[0] == '-' && cmd[1] == '-') {
            /* long option tanpa subcommand */
            out->subcommand = CLI_SUB_COUNT;
        } else if (is_option(cmd)) {
            /* short option tanpa subcommand: anggap count */
            out->subcommand = CLI_SUB_COUNT;
        } else {
            /* Bukan subperintah yang dikenal, anggap sebagai path file. */
            out->subcommand = CLI_SUB_COUNT;
            out->file_path = cmd;
            i++;
            return 0;
        }
        i++;
    } else {
        out->subcommand = CLI_SUB_COUNT;
    }

    /* Khusus grep: argumen non-option pertama adalah PATTERN. */
    if (out->subcommand == CLI_SUB_GREP) {
        if (i >= argc) {
            return -1; /* grep butuh pattern */
        }
        out->pattern = argv[i++];
    }

    /* Iterasi sisa argumen. */
    while (i < argc) {
        const char *a = argv[i];
        if (strcmp(a, "-n") == 0) {
            if (i + 1 >= argc) {
                return -1;
            }
            out->count = (int)strtol(argv[++i], NULL, 10);
            if (out->count < 0) {
                return -1;
            }
            i++;
        } else if (strcmp(a, "-i") == 0) {
            out->ignore_case = 1;
            i++;
        } else if (strcmp(a, "-v") == 0) {
            out->verbose = 1;
            i++;
        } else if (strcmp(a, "--help") == 0) {
            out->show_help = 1;
            i++;
        } else if (strcmp(a, "--version") == 0) {
            out->show_version = 1;
            i++;
        } else if (is_option(a)) {
            return -1; /* option tidak dikenal */
        } else {
            /* Argumen non-option: file_path. */
            if (out->file_path == NULL) {
                out->file_path = a;
            } else {
                return -1; /* lebih dari satu file (tidak didukung) */
            }
            i++;
        }
    }
    return 0;
}

config.c

Unduh berkas
c
/* config.c - Implementasi loader file konfigurasi */
#include "config.h"

#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <stdlib.h>

void config_init(config *c) {
    if (c == NULL) {
        return;
    }
    c->count = 0;
}

static void strip(char *s) {
    /* Trim whitespace di kiri. */
    char *start = s;
    while (*start != '\0' && isspace((unsigned char)*start)) {
        start++;
    }
    if (start != s) {
        memmove(s, start, strlen(start) + 1);
    }
    /* Trim whitespace di kanan. */
    size_t len = strlen(s);
    while (len > 0 && isspace((unsigned char)s[len - 1])) {
        s[--len] = '\0';
    }
}

static void parse_line(char *line, config *c) {
    /* Lewati komentar dan baris kosong. */
    if (line[0] == '#' || line[0] == ';' || line[0] == '\0') {
        return;
    }
    /* Cari '=' pemisah. */
    char *eq = strchr(line, '=');
    if (eq == NULL) {
        return;
    }
    *eq = '\0';
    char *key = line;
    char *value = eq + 1;
    strip(key);
    strip(value);
    if (key[0] == '\0' || c->count >= CONFIG_ENTRIES_MAX) {
        return;
    }
    config_entry *e = &c->entries[c->count];
    strncpy(e->key, key, CONFIG_KEY_MAX - 1);
    e->key[CONFIG_KEY_MAX - 1] = '\0';
    strncpy(e->value, value, CONFIG_VALUE_MAX - 1);
    e->value[CONFIG_VALUE_MAX - 1] = '\0';
    c->count++;
}

int config_load(config *c, const char *path) {
    if (c == NULL || path == NULL) {
        return -1;
    }
    config_init(c);
    FILE *fp = fopen(path, "r");
    if (fp == NULL) {
        return -1;
    }
    char line[256];
    while (fgets(line, sizeof(line), fp) != NULL) {
        /* Hapus newline. */
        size_t len = strlen(line);
        while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r')) {
            line[--len] = '\0';
        }
        parse_line(line, c);
    }
    fclose(fp);
    return c->count;
}

const char *config_get(const config *c, const char *key) {
    if (c == NULL || key == NULL) {
        return NULL;
    }
    for (int i = 0; i < c->count; i++) {
        if (strcmp(c->entries[i].key, key) == 0) {
            return c->entries[i].value;
        }
    }
    return NULL;
}

int config_get_int(const config *c, const char *key, int default_value) {
    const char *v = config_get(c, key);
    if (v == NULL) {
        return default_value;
    }
    return (int)strtol(v, NULL, 10);
}

file_processor.c

Unduh berkas
c
/* file_processor.c - Implementasi pemrosesan file */
#include "file_processor.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>

FILE *file_open(const char *path, const char *mode) {
    if (path == NULL) {
        if (strcmp(mode, "r") == 0) {
            return stdin;
        }
        if (strcmp(mode, "w") == 0) {
            return stdout;
        }
        return NULL;
    }
    return fopen(path, mode);
}

static void close_file(FILE *fp, const char *path) {
    if (path != NULL && fp != NULL) {
        fclose(fp);
    }
    (void)fp;
}

/* Helper: duplikasi string (pengganti strdup agar portable C11 murni). */
static char *dup_str(const char *s) {
    size_t len = strlen(s) + 1;
    char *copy = malloc(len);
    if (copy != NULL) {
        memcpy(copy, s, len);
    }
    return copy;
}

/* Helper: cek apakah string `s` mengandung substring `pattern`
 * secara case-insensitive. */
static int contains_ci(const char *s, const char *pattern) {
    if (pattern[0] == '\0') {
        return 1;
    }
    for (const char *p = s; *p != '\0'; p++) {
        const char *si = p;
        const char *pi = pattern;
        while (*si != '\0' && *pi != '\0' &&
               tolower((unsigned char)*si) == tolower((unsigned char)*pi)) {
            si++;
            pi++;
        }
        if (*pi == '\0') {
            return 1;
        }
    }
    return 0;
}

int file_stats_compute(const char *path, file_stats *out) {
    if (out == NULL) {
        return -1;
    }
    out->lines = 0;
    out->words = 0;
    out->chars = 0;
    FILE *fp = file_open(path, "r");
    if (fp == NULL) {
        return -1;
    }
    int prev_was_space = 1;
    int c;
    while ((c = fgetc(fp)) != EOF) {
        out->chars++;
        if (c == '\n') {
            out->lines++;
            prev_was_space = 1;
        } else if (isspace(c)) {
            prev_was_space = 1;
        } else {
            if (prev_was_space) {
                out->words++;
            }
            prev_was_space = 0;
        }
    }
    /* Hitung baris terakhir bila tidak diakhiri newline. */
    if (out->chars > 0 && !prev_was_space && out->lines == 0) {
        out->lines = 1;
    }
    close_file(fp, path);
    return 0;
}

int file_head(const char *path, int n) {
    if (n < 0) {
        return -1;
    }
    FILE *fp = file_open(path, "r");
    if (fp == NULL) {
        return -1;
    }
    int printed = 0;
    char buf[4096];
    while (printed < n && fgets(buf, sizeof(buf), fp) != NULL) {
        fputs(buf, stdout);
        printed++;
    }
    close_file(fp, path);
    return printed;
}

int file_tail(const char *path, int n) {
    if (n <= 0) {
        return 0;
    }
    FILE *fp = file_open(path, "r");
    if (fp == NULL) {
        return -1;
    }
    /* Alokasikan buffer sirkular untuk N baris terakhir. */
    char **ring = calloc((size_t)n, sizeof(char *));
    if (ring == NULL) {
        close_file(fp, path);
        return -1;
    }
    int idx = 0;
    int filled = 0;
    char buf[4096];
    while (fgets(buf, sizeof(buf), fp) != NULL) {
        if (ring[idx] != NULL) {
            free(ring[idx]);
        }
        ring[idx] = dup_str(buf);
        idx = (idx + 1) % n;
        if (filled < n) {
            filled++;
        }
    }
    /* Cetak sesuai urutan kronologis. */
    int start = (filled < n) ? 0 : idx;
    for (int k = 0; k < filled; k++) {
        int pos = (start + k) % n;
        if (ring[pos] != NULL) {
            fputs(ring[pos], stdout);
            free(ring[pos]);
        }
    }
    free(ring);
    close_file(fp, path);
    return filled;
}

int file_grep(const char *path, const char *pattern, int ignore_case) {
    if (pattern == NULL) {
        return -1;
    }
    FILE *fp = file_open(path, "r");
    if (fp == NULL) {
        return -1;
    }
    int printed = 0;
    char buf[4096];
    while (fgets(buf, sizeof(buf), fp) != NULL) {
        int match;
        if (ignore_case) {
            match = contains_ci(buf, pattern);
        } else {
            match = (strstr(buf, pattern) != NULL);
        }
        if (match) {
            fputs(buf, stdout);
            printed++;
        }
    }
    close_file(fp, path);
    return printed;
}

logger.c

Unduh berkas
c
/* logger.c - Implementasi logger */
#include "logger.h"

#include <stdarg.h>
#include <string.h>
#include <time.h>

static log_level g_min_level = LOG_INFO;
static FILE     *g_out        = NULL;

static const char *level_name(log_level l) {
    switch (l) {
        case LOG_DEBUG: return "DEBUG";
        case LOG_INFO:  return "INFO";
        case LOG_WARN:  return "WARN";
        case LOG_ERROR: return "ERROR";
        case LOG_OFF:   return "OFF";
        default:        return "?";
    }
}

void log_init(log_level min, FILE *out) {
    g_min_level = min;
    g_out = (out != NULL) ? out : stderr;
}

void log_write(log_level level, const char *file, int line,
               const char *fmt, ...) {
    if (level < g_min_level || g_out == NULL) {
        return;
    }
    /* Timestamp sederhana: detik sejak epoch. */
    time_t now = time(NULL);
    struct tm *tm_info = localtime(&now);
    char ts[20];
    strftime(ts, sizeof(ts), "%Y-%m-%d %H:%M:%S", tm_info);

    /* Ambil basename file agar log ringkas. */
    const char *slash = strrchr(file, '/');
    const char *base = (slash != NULL) ? slash + 1 : file;

    fprintf(g_out, "[%s] [%s] %s:%d ", ts, level_name(level), base, line);

    va_list ap;
    va_start(ap, fmt);
    vfprintf(g_out, fmt, ap);
    va_end(ap);

    fputc('\n', g_out);
    fflush(g_out);
}

main.c

Unduh berkas
c
/* main.c - Program utama command-line tool "mtool" */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "cli.h"
#include "config.h"
#include "file_processor.h"
#include "logger.h"

/* Konfigurasi default bila file config ada. */
static const char *CONFIG_FILE = ".mtoolrc";

static int run_subcommand(const cli_args *args) {
    file_stats stats;

    switch (args->subcommand) {
        case CLI_SUB_HELP:
            cli_print_help(stdout);
            return 0;
        case CLI_SUB_VERSION:
            cli_print_version(stdout);
            return 0;
        case CLI_SUB_COUNT: {
            if (args->file_path == NULL) {
                /* Hitung baris dari stdin. */
                int c;
                long count = 0;
                while ((c = fgetc(stdin)) != EOF) {
                    if (c == '\n') {
                        count++;
                    }
                }
                printf("%ld\n", count);
                return 0;
            }
            if (file_stats_compute(args->file_path, &stats) != 0) {
                LOG_ERROR("tidak bisa membuka file: %s", args->file_path);
                return 1;
            }
            printf("%ld %s\n", stats.lines, args->file_path);
            return 0;
        }
        case CLI_SUB_HEAD:
            if (file_head(args->file_path, args->count) < 0) {
                LOG_ERROR("head gagal");
                return 1;
            }
            return 0;
        case CLI_SUB_TAIL:
            if (file_tail(args->file_path, args->count) < 0) {
                LOG_ERROR("tail gagal");
                return 1;
            }
            return 0;
        case CLI_SUB_STATS:
            if (file_stats_compute(args->file_path, &stats) != 0) {
                LOG_ERROR("tidak bisa membuka file: %s",
                          args->file_path ? args->file_path : "stdin");
                return 1;
            }
            printf("%8ld baris  %8ld kata  %8ld karakter  %s\n",
                   stats.lines, stats.words, stats.chars,
                   args->file_path ? args->file_path : "-");
            return 0;
        case CLI_SUB_GREP:
            if (args->pattern == NULL) {
                fprintf(stderr, "grep butuh PATTERN\n");
                return 1;
            }
            if (file_grep(args->file_path, args->pattern,
                          args->ignore_case) < 0) {
                LOG_ERROR("grep gagal");
                return 1;
            }
            return 0;
        case CLI_SUB_NONE:
        default:
            cli_print_help(stderr);
            return 1;
    }
}

int main(int argc, char *argv[]) {
    /* Inisialisasi logger default: level INFO ke stderr. */
    log_init(LOG_INFO, stderr);

    /* Muat config opsional bila ada. */
    config cfg;
    config_init(&cfg);
    if (config_load(&cfg, CONFIG_FILE) > 0) {
        const char *lvl = config_get(&cfg, "log_level");
        if (lvl != NULL) {
            if (strcmp(lvl, "DEBUG") == 0) {
                log_init(LOG_DEBUG, stderr);
            } else if (strcmp(lvl, "WARN") == 0) {
                log_init(LOG_WARN, stderr);
            } else if (strcmp(lvl, "ERROR") == 0) {
                log_init(LOG_ERROR, stderr);
            }
        }
    }

    cli_args args;
    if (cli_parse(argc, argv, &args) != 0) {
        fprintf(stderr, "Argumen tidak valid. Jalankan '%s help'.\n",
                argv[0]);
        return 2;
    }

    if (args.verbose) {
        log_init(LOG_DEBUG, stderr);
    }

    if (args.show_help) {
        cli_print_help(stdout);
        return 0;
    }
    if (args.show_version) {
        cli_print_version(stdout);
        return 0;
    }

    LOG_DEBUG("subcommand=%d file=%s",
               (int)args.subcommand,
               args.file_path ? args.file_path : "(stdin)");

    return run_subcommand(&args);
}

tests.c

Unduh berkas
c
/* tests.c - Unit test untuk CLI tool */
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>

#include "cli.h"
#include "config.h"
#include "file_processor.h"

static int tests_run = 0;
static int tests_pass = 0;

#define ASSERT(cond, msg) \
    do { \
        tests_run++; \
        if (cond) { \
            tests_pass++; \
        } else { \
            printf("FAIL: %s (line %d)\n", msg, __LINE__); \
        } \
    } while (0)

static void test_cli_parse_count(void) {
    char *argv[] = {"mtool", "count", "data.txt"};
    cli_args args;
    int rc = cli_parse(3, argv, &args);
    ASSERT(rc == 0, "count harus sukses");
    ASSERT(args.subcommand == CLI_SUB_COUNT, "subcommand = COUNT");
    ASSERT(args.file_path != NULL, "file_path = data.txt");
    ASSERT(strcmp(args.file_path, "data.txt") == 0, "file_path benar");
}

static void test_cli_parse_head_with_n(void) {
    char *argv[] = {"mtool", "head", "-n", "5", "data.txt"};
    cli_args args;
    int rc = cli_parse(5, argv, &args);
    ASSERT(rc == 0, "head -n 5 harus sukses");
    ASSERT(args.subcommand == CLI_SUB_HEAD, "subcommand = HEAD");
    ASSERT(args.count == 5, "count = 5");
}

static void test_cli_parse_grep(void) {
    char *argv[] = {"mtool", "grep", "hello", "-i", "data.txt"};
    cli_args args;
    int rc = cli_parse(5, argv, &args);
    ASSERT(rc == 0, "grep harus sukses");
    ASSERT(args.subcommand == CLI_SUB_GREP, "subcommand = GREP");
    ASSERT(strcmp(args.pattern, "hello") == 0, "pattern = hello");
    ASSERT(args.ignore_case == 1, "ignore_case = 1");
}

static void test_cli_parse_help_version(void) {
    char *argv1[] = {"mtool", "help"};
    cli_args args;
    ASSERT(cli_parse(2, argv1, &args) == 0, "help sukses");
    ASSERT(args.subcommand == CLI_SUB_HELP, "subcommand = HELP");

    char *argv2[] = {"mtool", "version"};
    ASSERT(cli_parse(2, argv2, &args) == 0, "version sukses");
    ASSERT(args.subcommand == CLI_SUB_VERSION, "subcommand = VERSION");

    char *argv3[] = {"mtool", "--help"};
    ASSERT(cli_parse(2, argv3, &args) == 0, "--help sukses");
    ASSERT(args.show_help == 1, "show_help = 1");
}

static void test_cli_parse_unknown_option(void) {
    char *argv[] = {"mtool", "head", "-Z"};
    cli_args args;
    int rc = cli_parse(3, argv, &args);
    ASSERT(rc == -1, "opsi tidak dikenal = error");
}

static void test_config_load(void) {
    const char *path = "/tmp/test_mtool.cfg";
    FILE *fp = fopen(path, "w");
    ASSERT(fp != NULL, "bisa membuat file config");
    fprintf(fp, "# komentar\n");
    fprintf(fp, "log_level = DEBUG\n");
    fprintf(fp, "default_count = 25\n");
    fprintf(fp, "; komentar titik koma\n");
    fprintf(fp, "name = mtool\n");
    fclose(fp);

    config c;
    int n = config_load(&c, path);
    ASSERT(n == 3, "harus ada 3 entri");

    const char *v = config_get(&c, "log_level");
    ASSERT(v != NULL && strcmp(v, "DEBUG") == 0, "log_level = DEBUG");

    int i = config_get_int(&c, "default_count", 10);
    ASSERT(i == 25, "default_count = 25");

    /* key tidak ada -> default */
    i = config_get_int(&c, "tidak_ada", 42);
    ASSERT(i == 42, "key tidak ada = default");
}

static void test_file_stats(void) {
    const char *path = "/tmp/test_mtool_data.txt";
    FILE *fp = fopen(path, "w");
    ASSERT(fp != NULL, "bisa membuat file data");
    fprintf(fp, "hello world\n");
    fprintf(fp, "second line here\n");
    fprintf(fp, "third\n");
    fclose(fp);

    file_stats s;
    int rc = file_stats_compute(path, &s);
    ASSERT(rc == 0, "stats harus sukses");
    ASSERT(s.lines == 3, "lines = 3");
    ASSERT(s.words == 6, "words = 6");
    ASSERT(s.chars > 0, "chars > 0");
}

static void test_file_head_tail(void) {
    const char *path = "/tmp/test_mtool_data.txt";
    /* File ini sudah dibuat di test sebelumnya. */

    /* Head 2 baris: kembalikan jumlah baris yang dicetak. */
    int n = file_head(path, 2);
    ASSERT(n == 2, "head harus cetak 2 baris");

    /* Head dengan n melebihi total baris: tetap OK, cetak semua. */
    n = file_head(path, 100);
    ASSERT(n == 3, "head 100 harus cetak 3 baris (semua)");

    n = file_tail(path, 1);
    ASSERT(n == 1, "tail 1 harus cetak 1 baris");

    n = file_tail(path, 100);
    ASSERT(n == 3, "tail 100 harus cetak 3 baris");
}

static void test_file_grep(void) {
    const char *path = "/tmp/test_mtool_data.txt";

    int n = file_grep(path, "line", 0);
    ASSERT(n == 1, "grep 'line' case-sensitive = 1 (second line here)");

    n = file_grep(path, "LINE", 0);
    ASSERT(n == 0, "grep 'LINE' case-sensitive = 0");

    n = file_grep(path, "LINE", 1);
    ASSERT(n == 1, "grep 'LINE' case-insensitive = 1");

    n = file_grep(path, "hello", 0);
    ASSERT(n == 1, "grep 'hello' = 1");
}

int main(void) {
    test_cli_parse_count();
    test_cli_parse_head_with_n();
    test_cli_parse_grep();
    test_cli_parse_help_version();
    test_cli_parse_unknown_option();
    test_config_load();
    test_file_stats();
    test_file_head_tail();
    test_file_grep();

    printf("\n=== Hasil Test ===\n");
    printf("Total : %d\n", tests_run);
    printf("Lulus : %d\n", tests_pass);
    printf("Gagal : %d\n", tests_run - tests_pass);
    return (tests_run == tests_pass) ? 0 : 1;
}