File teks adalah jenis file paling umum yang kita temui: konfigurasi, log, kode sumber, dokumen, dan banyak format data lain. Karakteristik utama file teks adalah dapat dibaca manusia dan terorganisir per baris dengan newline sebagai pemisah. Pada chapter ini kita akan membaca file teks baris per baris dengan fgets, menghitung statistik sederhana (baris, kata, karakter), dan memahami bagaimana newline direpresentasikan di berbagai sistem operasi. Pengetahuan ini penting karena pemrosesan teks adalah salah satu tugas paling sering dilakukan dalam pemrograman.

Konsep File Teks #

Pada level paling dasar, file teks adalah file biner yang setiap byte-nya merepresentasikan karakter ASCII (atau UTF-8 dalam ekstensi modern). Yang membedakan file teks dari file biner bukanlah format penyimpanannya—keduanya sama-sama urutan byte—melainkan konvensi pembacaan:

  • Mode "r"/"w" (text mode) memungkinkan C standard library menerjemahkan newline sesuai konvensi platform.
  • Mode "rb"/"wb" (binary mode) membaca/menulis newline apa adanya tanpa translasi.

Pada Linux dan macOS (POSIX), newline POSIX adalah satu byte 0x0A (LF), sehingga mode teks dan mode biner identik. Pada Windows, newline konvensional adalah dua byte 0x0D 0x0A (CR+LF), sehingga mode teks menerjemahkan \n C menjadi \r\n saat menulis dan sebaliknya saat membaca.

Pola Klasik: Hitung Baris, Kata, Karakter #

Implementasi mini wc (word count) Unix adalah latihan klasik memproses file teks. Kita baca file baris per baris dengan fgets, lalu iterasi karakter per karakter untuk menghitung kata (rangkaian karakter non-whitespace).

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

typedef struct {
    int baris;
    int kata;
    int karakter;
} Statistik;

Statistik hitung_file(const char *path) {
    Statistik s = {0, 0, 0};
    FILE *f = fopen(path, "r");
    if (!f) return s;

    char line[1024];
    int dalam_kata = 0;
    while (fgets(line, sizeof(line), f) != NULL) {
        s.baris++;
        s.karakter += strlen(line);
        for (int i = 0; line[i] != '\0'; i++) {
            if (isspace((unsigned char)line[i])) {
                dalam_kata = 0;
            } else {
                if (!dalam_kata) {
                    s.kata++;
                    dalam_kata = 1;
                }
            }
        }
    }
    fclose(f);
    return s;
}

int main(void) {
    /* Buat file teks dengan beberapa baris */
    FILE *f = fopen("/tmp/test_teks.txt", "w");
    if (!f) return 1;
    fputs("Belajar C itu menyenangkan.\n", f);
    fputs("File handling memberi kita kemampuan menyimpan data.\n", f);
    fputs("Hari ini kita pelajari statistik teks.\n", f);
    fputs("\n", f);   /* baris kosong */
    fputs("Akhir file.\n", f);
    fclose(f);

    Statistik s = hitung_file("/tmp/test_teks.txt");
    printf("Statistik file /tmp/test_teks.txt:\n");
    printf("  Baris    : %d\n", s.baris);
    printf("  Kata     : %d\n", s.kata);
    printf("  Karakter : %d\n", s.karakter);

    return 0;
}

Output yang Dihasilkan:

text
Statistik file /tmp/test_teks.txt:
  Baris    : 5
  Kata     : 19
  Karakter : 133

Perhatikan statistik di atas. File berisi 5 baris (4 baris teks + 1 baris kosong). Hitungan kata 19 benar karena kita menghitung transisi dari whitespace ke non-whitespace sebagai awal kata baru—baris kosong tidak menambah hitungan kata. Total karakter 133 mencakup spasi, titik, dan newline.

Mari kita telaah baris-baris kode di atas:

  1. Baris 15 (Statistik s = {0, 0, 0};): inisialisasi struct dengan semua field 0. Pada C, inisialisasi {0} menjamin semua field (termasuk yang belum disebut) bernilai nol.
  2. Baris 18 (char line[1024];): buffer untuk satu baris. fgets memotong baris yang lebih panjang dari 1023 karakter menjadi beberapa panggilan—ini aman.
  3. Baris 19 (int dalam_kata = 0;): state flag yang melacak apakah kita sedang berada di dalam kata. Berguna untuk menghitung transisi dari whitespace ke non-whitespace sebagai awal kata.
  4. Baris 20 (while (fgets(line, sizeof(line), f) != NULL)): idiom standar membaca baris per baris. sizeof(line) otomatis benar bila ukuran buffer berubah.
  5. Baris 22 (s.karakter += strlen(line);): menambah jumlah karakter dengan panjang baris yang baru dibaca. strlen menghitung byte hingga null terminator—termasuk \n bila ada.
  6. Baris 23-31 (for (int i = 0; line[i] != '\0'; i++)): iterasi karakter per karakter. isspace dari <ctype.h> memeriksa apakah karakter adalah whitespace (spasi, tab, newline). Cast ke unsigned char diperlukan karena isspace menentukan behavior undefined untuk nilai negatif.
  7. Baris 25 (dalam_kata = 0;): menemui whitespace, keluar dari kata.
  8. Baris 28-29 (s.kata++; dalam_kata = 1;): transisi dari “di luar kata” ke “di dalam kata”—tandai sebagai satu kata baru.

Inspeksi Byte: Bagaimana Newline Disimpan #

Untuk benar-benar melihat bagaimana newline disimpan di file, kita bisa membaca file sebagai byte dan mencetak setiap byte dalam heksadesimal. Pada Linux, setiap \n dalam kode C menjadi tepat satu byte 0x0A di file.

c
#include <stdio.h>

int main(void) {
    /* Tulis dengan mode "w" - di Linux sama dengan "wb" */
    FILE *f = fopen("/tmp/test_crlf.txt", "w");
    if (!f) return 1;
    fputs("baris1\nbaris2\nbaris3\n", f);
    fclose(f);

    /* Tampilkan sebagai byte untuk melihat newline */
    f = fopen("/tmp/test_crlf.txt", "rb");
    if (!f) return 1;
    int c;
    int pos = 0;
    printf("Byte-by-byte (hex) dari file teks Linux:\n");
    while ((c = fgetc(f)) != EOF) {
        printf("  [%2d] 0x%02X", pos, (unsigned)c);
        if (c == '\n') printf("  -> LF (newline)");
        if (c == '\r') printf("  -> CR");
        if (c >= 32 && c < 127) printf("  '%c'", c);
        printf("\n");
        pos++;
    }
    fclose(f);
    return 0;
}

Output yang Dihasilkan:

text
Byte-by-byte (hex) dari file teks Linux:
  [ 0] 0x62  'b'
  [ 1] 0x61  'a'
  [ 2] 0x72  'r'
  [ 3] 0x69  'i'
  [ 4] 0x73  's'
  [ 5] 0x31  '1'
  [ 6] 0x0A  -> LF (newline)
  [ 7] 0x62  'b'
  [ 8] 0x61  'a'
  [ 9] 0x72  'r'
  [10] 0x69  'i'
  [11] 0x73  's'
  [12] 0x32  '2'
  [13] 0x0A  -> LF (newline)
  [14] 0x62  'b'
  [15] 0x61  'a'
  [16] 0x72  'r'
  [17] 0x69  'i'
  [18] 0x73  's'
  [19] 0x33  '3'
  [20] 0x0A  -> LF (newline)

Pada output di atas, kita melihat bahwa setiap \n di kode C ditulis sebagai satu byte 0x0A (LF) pada file. Pada Windows dengan mode "w", byte yang sama akan ditulis sebagai dua byte 0x0D 0x0A (CR+LF). Inilah perbedaan praktis antara text mode dan binary mode di Windows—sedangkan di Linux keduanya identik.

Pola Pemrosesan Teks yang Umum #

Beberapa pola yang sering kita temui saat memproses file teks:

Memisahkan Field dengan strtok #

Untuk file CSV/TSV sederhana, kita bisa membaca baris dengan fgets lalu memisahkan field dengan strtok:

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

int main(void) {
    /* Buat file CSV dengan delimiter koma */
    FILE *w = fopen("/tmp/test_csv.csv", "w");
    if (!w) return 1;
    fputs("nama,umur,kota\n", w);
    fputs("Budi,20,Jakarta\n", w);
    fputs("Siti,21,Bandung\n", w);
    fputs("Andi,19,Surabaya\n", w);
    fclose(w);

    FILE *f = fopen("/tmp/test_csv.csv", "r");
    if (!f) return 1;

    char line[256];
    int baris = 0;
    while (fgets(line, sizeof(line), f) != NULL) {
        baris++;
        /* Hapus \n di akhir */
        line[strcspn(line, "\n")] = '\0';
        printf("Baris %d: ", baris);

        /* Pecah berdasarkan koma */
        char *field = strtok(line, ",");
        int col = 0;
        while (field != NULL) {
            printf("[%s] ", field);
            field = strtok(NULL, ",");
            col++;
        }
        printf("\n");
    }
    fclose(f);
    return 0;
}

Output yang Dihasilkan:

text
Baris 1: [nama] [umur] [kota]
Baris 2: [Budi] [20] [Jakarta]
Baris 3: [Siti] [21] [Bandung]
Baris 4: [Andi] [19] [Surabaya]

Pada pola di atas, kita membaca satu baris dengan fgets, menghapus newline di akhir dengan line[strcspn(line, "\n")] = '\0', lalu memisahkan field dengan strtok. strtok pertama menerima string yang akan dipisah; pemanggilan berikutnya menerima NULL untuk melanjutkan dari posisi sebelumnya. Catatan: strtok memodifikasi string asli (menempatkan \0 di tempat delimiter).

Encoding: ASCII vs UTF-8 #

Pada masa lalu, file teks diasumsikan berisi ASCII 7-bit. Saat ini, mayoritas file teks menggunakan UTF-8 yang kompatibel dengan ASCII untuk karakter 0-127 dan menggunakan beberapa byte untuk karakter Unicode lainnya. Dalam C, kita dapat memproses UTF-8 byte-per-byte dengan aman karena byte ASCII (di bawah 128) tidak pernah muncul sebagai bagian dari multi-byte UTF-8 karakter—ini berarti fgetc, fgets, dan strchr tetap berfungsi.