Kita telah mempelajari seluruh materi Course C, mulai dari sintaks dasar hingga topik lanjutan seperti function pointer dan modularitas. Pada proyek terakhir ini, kita membangun satu program utuh: command line tool yang dapat memproses file, menerima konfigurasi, dan menghasilkan log. Proyek ini adalah tempat di mana seluruh pengetahuan tersebut dipakai secara nyata untuk membangun CLI tool yang siap dipakai sehari-hari.
Program yang akan kita bangun bekerja dari terminal dengan beberapa subcommand:
tool run <file>— memproses file input dan menampilkan hasil ke stdouttool list <dir>— mendaftar file dalam direktori dengan filtertool config show— menampilkan konfigurasi yang aktiftool --help— menampilkan penggunaan
Tampilan Program Akhir #
Sesi pemakaian berikut menunjukkan hasil akhir proyek:
$ ./tool run sample.txt
[INFO] Memproses file: sample.txt
[INFO] Membaca 4 baris
[INFO] Total kata: 18
[INFO] Selesai dalam 0.001 detik
$ ./tool config show
Konfigurasi aktif:
log_level: info
output_format: text
max_file_size: 1048576
$ ./tool --help
Penggunaan: tool <command> [options]
Perintah yang tersedia:
run <file> Proses file input
list <dir> Daftar file di direktori
config show Tampilkan konfigurasi
help Tampilkan bantuan ini
Opsi global:
-v, --verbose Tingkatkan detail log
-q, --quiet Hanya tampilkan error
--version Tampilkan versiTujuan Pembelajaran #
Setelah menyelesaikan proyek ini, kita mampu:
- mem-parsing
argc/argvsecara manual dengan pola yang rapi, - mengimplementasikan subcommand dengan dispatch berbasis string,
- membaca file konfigurasi INI sederhana,
- memproses file dengan error handling yang baik,
- membangun logger dengan level INFO/WARN/ERROR ke stderr,
- menyusun modul terpisah (cli, config, file_processor, logger) dengan header publik,
- menulis unit test untuk setiap modul,
- mengemas program untuk distribusi dengan Makefile.
Prasyarat #
Proyek ini mempergunakan materi dari banyak Chapter sebelumnya:
- Function Declaration dan Pointer Parameters — deklarasi API modul,
- Array of Strings — parsing argv,
- File Handling — membaca file input dan konfigurasi,
- Error Handling — return code dan strerror,
- Memory Management — malloc/free untuk buffer dinamis,
- Preprocessor dan Modularitas — pemisahan .h/.c,
- Build System dan Toolchain — Makefile.
Tahapan Pengerjaan #
Proyek dikerjakan dalam enam bagian berurutan:
- Bagian 1: Argument Parsing — parsing
argc/argv, handle flag-v/--verbosedan--version. - Bagian 2: Subcommands — dispatch
run,list,config,help. - Bagian 3: Konfigurasi — baca file
.toolrcformat INI sederhana. - Bagian 4: File Processing — baca file, hitung statistik, cetak hasil.
- Bagian 5: Logging dan Error Reporting — logger dengan level ke stderr.
- Bagian 6: Testing dan Distribusi — unit test untuk modul dan Makefile distribusi.
Mulailah dari Bagian 1 dan ikuti urutannya. Setiap bagian membangun di atas bagian sebelumnya, dan akhir dari bagian terakhir menghasilkan program utuh yang siap dipakai.
Parse argc/argv manual dan tangani flag -h --help dan --version.
Buka → # Bagian 2: SubcommandsImplementasikan subcommand seperti git: tool run, tool list, tool config.
Buka → # Bagian 3: KonfigurasiBaca file konfigurasi INI atau JSON sederhana.
Buka → # Bagian 4: File ProcessingProses file input dan tulis output.
Buka → # Bagian 5: Logging dan Error ReportingImplementasikan logger dengan level INFO/WARN/ERROR ke stderr.
Buka → # Bagian 6: Testing dan DistribusiTulis test dan paketkan untuk distribusi.
Buka → # Buka →Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
cli.c
Unduh berkas/* 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/* 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/* 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/* 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/* 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/* 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;
}