Pada Bagian 4 ini kita membangun modul file_processor yang membaca file input, menghitung statistik sederhana (jumlah baris, kata, dan karakter), dan menampilkan hasil ke stdout. Modul ini juga menangani error umum seperti file tidak ditemukan dan ukuran terlalu besar, dengan mengembalikan kode error yang dapat ditangani pemanggil.

Konsep File Processing #

Pemrosesan file teks biasanya mengikuti pola: buka file, baca baris per baris dengan fgets, proses setiap baris, lalu tutup file. Untuk statistik kata, kita iterasi karakter dan mendeteksi transisi dari whitespace ke non-whitespace sebagai awal kata baru. Pendekatan ini lebih efisien daripada memori-mapped seluruh file untuk file besar.

Implementasi File Processor #

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

typedef struct {
    long lines;
    long words;
    long chars;
} FileStats;

int file_process(const char *path, FileStats *stats) {
    if (!path || !stats) return -1;
    FILE *f = fopen(path, "r");
    if (!f) return -2;

    stats->lines = 0;
    stats->words = 0;
    stats->chars = 0;

    int c;
    int in_word = 0;
    while ((c = fgetc(f)) != EOF) {
        stats->chars++;
        if (c == '\n') {
            stats->lines++;
        }
        if (isspace(c)) {
            in_word = 0;
        } else if (!in_word) {
            in_word = 1;
            stats->words++;
        }
    }
    fclose(f);
    return 0;
}

void file_stats_print(const FileStats *stats) {
    printf("Baris: %ld\n", stats->lines);
    printf("Kata:  %ld\n", stats->words);
    printf("Karakter: %ld\n", stats->chars);
}

int main(int argc, char *argv[]) {
    if (argc < 2) {
        fprintf(stderr, "Penggunaan: %s <file>\n", argv[0]);
        return 1;
    }

    FileStats stats;
    int rc = file_process(argv[1], &stats);
    if (rc == -2) {
        fprintf(stderr, "Error: tidak bisa membuka file %s\n", argv[1]);
        return 1;
    } else if (rc != 0) {
        fprintf(stderr, "Error: kode %d\n", rc);
        return 1;
    }

    printf("Statistik untuk %s:\n", argv[1]);
    file_stats_print(&stats);
    return 0;
}

Output yang Dihasilkan (dengan file input sample.txt berisi 4 baris teks):

text
Statistik untuk sample.txt:
Baris: 4
Kata:  18
Karakter: 87

Mari kita telaah bagian kunci dari kode di atas:

  1. struct FileStats: menyimpan hasil pemrosesan dalam struct yang jelas. Pemanggil menerima pointer ke struct dan mengisi nilainya.
  2. file_process: fungsi utama yang membaca file karakter per karakter dengan fgetc. Mengembalikan kode error:
    • -1 jika parameter tidak valid (NULL pointer).
    • -2 jika file tidak bisa dibuka.
    • 0 jika sukses.
  3. Deteksi kata dengan in_word flag: setiap transisi dari whitespace ke non-whitespace menandakan awal kata baru. Flag in_word melacak apakah kita sedang di dalam kata.
  4. isspace: mendeteksi whitespace generik (spasi, tab, newline, carriage return, form feed, vertical tab).

Pengujian #

Buat file sample.txt:

text
Belajar C menyenangkan.
Bahasa C lahir di Bell Labs.
Proyek ini menggabungkan banyak konsep.
Selamat belajar.

Kompilasi dan jalankan:

bash
gcc -std=c11 -Wall -Wextra -o fileproc main.c
./fileproc sample.txt
./fileproc /nonexistent

Output yang diharapkan:

text
Statistik untuk sample.txt:
Baris: 4
Kata:  18
Karakter: 87

Error: tidak bisa membuka file /nonexistent

Memproses File Besar #

Untuk file besar, baca dengan buffer bukan karakter per karakter:

c
#define BUF_SIZE 4096

int file_process_buffered(const char *path, FileStats *stats) {
    FILE *f = fopen(path, "r");
    if (!f) return -2;

    stats->lines = stats->words = stats->chars = 0;
    char buf[BUF_SIZE];
    int in_word = 0;
    size_t n;

    while ((n = fread(buf, 1, sizeof(buf), f)) > 0) {
        stats->chars += n;
        for (size_t i = 0; i < n; i++) {
            if (buf[i] == '\n') stats->lines++;
            if (isspace((unsigned char)buf[i])) {
                in_word = 0;
            } else if (!in_word) {
                in_word = 1;
                stats->words++;
            }
        }
    }
    fclose(f);
    return 0;
}

Versi buffered ini membaca 4 KB sekaligus, mengurangi syscall dan lebih cepat untuk file besar.

Pada Bagian 5 kita akan menambahkan modul logger yang menulis pesan ke stderr dengan level INFO/WARN/ERROR.

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;
}