Untuk membaca dan menulis teks baris-demi-baris, pasangan fgets/fputs adalah pilihan paling aman di C. fgets selalu menerima ukuran buffer sebagai argumen, sehingga tidak akan menulis melampaui kapasitas yang kita berikan—berbeda dengan gets legendaris yang sudah dihapus di C11 karena memicu begitu banyak eksploitasi buffer overflow. fputs menulis string apa adanya ke stream tanpa menambahkan apapun, memberi kita kendali penuh atas newline. Pada chapter ini kita akan melihat bagaimana fgets memotong baris panjang menjadi beberapa panggilan, dan kenapa ini justru menjadi fitur keamanan paling penting di library I/O C.

Kontrak fgets dan fputs #

ISO/IEC 9899 §7.21.7.2 dan §7.21.7.4 mendefinisikan:

c
char *fgets(char *restrict s, int n, FILE *restrict stream);
int  fputs(const char *restrict s, FILE *restrict stream);

Kontrak fgets:

  • Membaca sampai salah satu dari tiga kondisi — menemui \n (yang juga ikut disimpan), mengisi n-1 byte, atau menemui EOF.
  • Selalu menambahkan null terminator — total maksimum n byte (termasuk terminator).
  • Mengembalikan s bila berhasil, NULL bila gagal — gagal terutama saat EOF langsung ditemui di awal panggilan.
  • n adalah int, bukan size_t — nilai n <= 0 menyebabkan perilaku undefined pada beberapa implementasi; selalu pakai n >= 1.

Kontrak fputs:

  • Menulis string tanpa null terminator ke stream — karakter \0 di tengah string akan menghentikan penulisan (karena fputs menggunakan strlen internal).
  • Tidak menambahkan \n — berbeda dengan puts yang otomatis menambahkan newline.
  • Mengembalikan non-negatif bila berhasil, EOF bila gagal.

Pola Aman Membaca Baris dengan fgets #

Pola yang selalu kita pakai adalah memanggil fgets di dalam loop while, dengan argumen kedua berupa sizeof(buffer) agar otomatis benar bila ukuran buffer berubah.

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

int main(void) {
    /* Tulis beberapa baris dengan fputs */
    FILE *f = fopen("/tmp/test_lines.txt", "w");
    if (!f) { perror("fopen"); return 1; }
    fputs("Baris pertama\n", f);
    fputs("Baris kedua yang lebih panjang dari biasanya\n", f);
    fputs("Baris ketiga tanpa newline di akhir", f);   /* tanpa \n */
    fclose(f);

    /* Baca dengan fgets, batasi ukuran buffer */
    f = fopen("/tmp/test_lines.txt", "r");
    if (!f) { perror("fopen read"); return 1; }

    char buf[16];   /* sengaja kecil untuk demo pemotongan */
    int line_no = 0;
    while (fgets(buf, sizeof(buf), f) != NULL) {
        line_no++;
        size_t len = strlen(buf);
        int has_newline = (len > 0 && buf[len-1] == '\n');
        printf("Baris %d (len=%zu, newline=%d): \"%s\"\n",
               line_no, len, has_newline, buf);
    }
    fclose(f);
    return 0;
}

Output yang Dihasilkan:

text
Baris 1 (len=14, newline=1): "Baris pertama
"
Baris 2 (len=15, newline=0): "Baris kedua yan"
Baris 3 (len=15, newline=0): "g lebih panjang"
Baris 4 (len=15, newline=1): " dari biasanya
"
Baris 5 (len=15, newline=0): "Baris ketiga ta"
Baris 6 (len=15, newline=0): "npa newline di "
Baris 7 (len=5, newline=0): "akhir"

Perhatikan apa yang terjadi pada baris 2 file asli ("Baris kedua yang lebih panjang dari biasanya\n"). Karena buffer buf hanya berisi 16 byte, fgets hanya membaca 15 karakter ("Baris kedua yan") per panggilan dan tidak sempat menemui \n. Pemanggilan kedua membaca sisa baris ("g lebih panjang"), dan baru pada pemanggilan ketiga (" dari biasanya\n") fgets menemui \n dan berhenti dengan newline di akhir. Inilah sebabnya fgets aman: tidak peduli seberapa panjang baris di file, ia tidak akan pernah menulis melebihi sizeof(buf).

Mari kita telaah baris-baris kode di atas:

  1. Baris 8 (fputs("Baris pertama\n", f);): fputs menulis string apa adanya. Kita sendiri yang menyertakan \n bila ingin baris baru.
  2. Baris 12 (fputs("Baris ketiga tanpa newline di akhir", f);): baris terakhir sengaja tanpa \n untuk mendemonstrasikan bahwa fgets tetap bisa membacanya—berhenti karena EOF, bukan karena \n.
  3. Baris 18 (char buf[16];): buffer sengaja kecil (16 byte) agar kita bisa melihat fgets memotong baris panjang. Pada produksi, gunakan buffer yang cukup besar (misalnya 4096 atau 8192) untuk mengurangi pemanggilan fungsi.
  4. Baris 20 (while (fgets(buf, sizeof(buf), f) != NULL)): argumen kedua adalah sizeof(buf) — otomatis benar bila ukuran buffer berubah. fgets mengembalikan NULL bila langsung menemui EOF (tidak ada karakter yang dibaca).
  5. Baris 23 (int has_newline = (len > 0 && buf[len-1] == '\n');): kita bisa memeriksa apakah baris selesai dibaca dengan melihat apakah karakter terakhir adalah \n. Bila bukan, berarti baris dipotong karena buffer penuh atau EOF.
  6. Baris 24-25 (printf("Baris %d (len=%zu, newline=%d): \"%s\"\n", ...)): kita mencetak nomor baris (yang sebenarnya adalah nomor pemanggilan, bukan nomor baris file), panjang string, dan apakah ada newline. %zu adalah specifier untuk size_t.

Perbandingan: gets (Dihapus di C11) vs fgets #

Sebelum C11, library C menyediakan fungsi gets yang membaca baris dari stdin tanpa parameter ukuran:

c
/* Versi lama - BERBAHAYA, dihapus di C11 */
char *gets(char *s);

Karena gets tidak menerima parameter ukuran buffer, ia akan terus membaca sampai menemui \n—tidak peduli seberapa besar buffer yang kita sediakan. Inilah penyebab eksploitasi klasik stack smashing: penyerang mengirim input yang sangat panjang ke program yang memakai gets, menimpa return address di stack, dan mengarahkan eksekusi ke kode pilihan penyerang. C11 (ISO/IEC 9899:2011) menghapus gets sepenuhnya dan menggantinya dengan gets_s opsional yang menerima parameter ukuran.

Mari kita bandingkan secara langsung: perhatikan bagaimana fgets membatasi penulisan, sementara gets (bila masih ada di compiler lama) tidak.

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

/* Demo: fgets dengan buffer kecil tidak akan overflow */
int main(void) {
    /* Buat input simulasi dari file: kalimat panjang */
    FILE *f = fopen("/tmp/test_long_input.txt", "w");
    if (!f) return 1;
    fputs("Kalimat ini sangat panjang dan akan melebihi buffer kecil\n", f);
    fclose(f);

    f = fopen("/tmp/test_long_input.txt", "r");
    if (!f) return 1;

    char kecil[8];   /* buffer 8 byte */
    if (fgets(kecil, sizeof(kecil), f) != NULL) {
        printf("Isi kecil (len=%zu): \"%s\"\n", strlen(kecil), kecil);
    }

    /* Lanjut baca sisa baris */
    char lanjut[64];
    if (fgets(lanjut, sizeof(lanjut), f) != NULL) {
        printf("Sisa (len=%zu): \"%s\"\n", strlen(lanjut), lanjut);
    }

    fclose(f);
    return 0;
}

Output yang Dihasilkan:

text
Isi kecil (len=7): "Kalimat"
Sisa (len=51): " ini sangat panjang dan akan melebihi buffer kecil
"

Pada output di atas, fgets(kecil, 8, f) hanya membaca 7 karakter pertama ("Kalimat") karena harus menyisakan 1 byte untuk null terminator. Sisa baris tetap ada di stream dan dibaca oleh fgets berikutnya. Bila kita memakai gets(kecil) pada kode lama, seluruh baris (60+ byte) akan ditulis ke kecil yang hanya berkapasitas 8 byte—buffer overflow yang menghancurkan stack.

fputs Tanpa newline Tambahan #

Berbeda dengan puts yang otomatis menambahkan \n, fputs menulis persis apa yang kita berikan. Ini berguna ketika kita ingin menulis string tanpa newline (misalnya menulis header CSV lalu data per baris).

c
#include <stdio.h>

int main(void) {
    FILE *f = fopen("/tmp/test_fputs_demo.csv", "w");
    if (!f) { perror("fopen"); return 1; }

    /* Header CSV tanpa newline di tengah */
    fputs("nama,umur,nilai\n", f);

    /* Data per baris */
    fputs("Budi,20,85.5\n", f);
    fputs("Siti,21,92.3\n", f);
    fputs("Andi,19,78.0\n", f);
    fclose(f);

    /* Baca dan tampilkan untuk verifikasi */
    f = fopen("/tmp/test_fputs_demo.csv", "r");
    if (!f) { perror("fopen read"); return 1; }
    char line[64];
    while (fgets(line, sizeof(line), f) != NULL) {
        printf(">> %s", line);
    }
    fclose(f);
    return 0;
}

Output yang Dihasilkan:

text
>> nama,umur,nilai
>> Budi,20,85.5
>> Siti,21,92.3
>> Andi,19,78.0
c
char buf[256];
if (fgets(buf, sizeof(buf), stdin) != NULL) {
    buf[strcspn(buf, "\n")] = '\0';   /* hapus \n */
    /* gunakan buf... */
}