Setiap program C otomatis memiliki tiga stream standar yang dibuka oleh sistem operasi saat program dimulai: stdin untuk input, stdout untuk output normal, dan stderr untuk pesan kesalahan. Memisahkan stdout dan stderr tampak seperti detail yang sepele, tetapi pemisahan inilah yang memungkinkan shell memredirect hasil program ke berkas sambil tetap menampilkan pesan error di terminal. Pada chapter ini kita akan memahami perbedaan ketiga stream, kapan harus memakai fprintf(stderr, ...) alih-alih printf, dan bagaimana exit code memberitahu shell apakah program sukses atau gagal.

Tiga Stream Standar #

Ketiga stream standar didefinisikan di header <stdio.h> sebagai pointer FILE *. Fungsi printf dan scanf sebenarnya adalah shortcut untuk fprintf(stdout, ...) dan fscanf(stdin, ...). Berikut ringkasan tiga stream:

StreamTujuanDefault perangkatBuffering default
stdinInput standarKeyboard (terminal)Line-buffered
stdoutOutput normalLayar terminalLine-buffered (terminal) / Full-buffered (file)
stderrOutput pesan kesalahanLayar terminalUnbuffered
c
#include <stdio.h>

int main(void) {
    /* Pesan normal ke stdout */
    printf("Ini pesan info ke stdout.\n");
    fprintf(stdout, "Pesan info memakai fprintf ke stdout.\n");

    /* Pesan error ke stderr */
    fprintf(stderr, "Ini pesan error ke stderr.\n");

    /* stdin dibaca lewat getchar */
    printf("Masukkan satu karakter: ");
    int ch = fgetc(stdin);
    fprintf(stdout, "Karakter: %c\n", ch);
    return 0;
}

Output yang Dihasilkan (input: A):

text
Ini pesan info ke stdout.
Pesan info memakai fprintf ke stdout.
Ini pesan error ke stderr.
Masukkan satu karakter: Karakter: A

Mari kita telaah baris-baris kode di atas:

  1. Baris 5 (printf("Ini pesan info ke stdout.\n");): printf selalu menulis ke stdout. Tidak ada cara langsung untuk membuat printf menulis ke stderr.
  2. Baris 6 (fprintf(stdout, "...")): fprintf adalah versi umum dari printf yang menerima stream tujuan sebagai argumen pertama. Melewatkan stdout menghasilkan perilaku yang sama persis dengan printf.
  3. Baris 9 (fprintf(stderr, "Ini pesan error ke stderr.\n");): pesan ini ditulis ke stderr, bukan stdout. Saat program dijalankan langsung di terminal, keduanya tampak sama di layar. Perbedaan baru terlihat saat kita melakukan redirect.
  4. Baris 12 (int ch = fgetc(stdin);): fgetc membaca satu karakter dari stream yang diberikan. Melewatkan stdin sama dengan memanggil getchar().
  5. Baris 13 (fprintf(stdout, "Karakter: %c\n", ch);): menulis hasil kembali ke stdout. Boleh juga memakai putchar(ch) atau printf(...) — keduanya ekuivalen untuk kasus ini.

Redirect stdout dan stderr di Shell #

Untuk melihat perbedaan praktis antara stdout dan stderr, kita bisa memredirect hanya stdout ke berkas dan membiarkan stderr tetap mengalir ke terminal. Pola redirect yang umum: > untuk stdout, 2> untuk stderr, dan 2>&1 untuk menggabungkan keduanya ke stdout.

bash
$ ./test_streams1 > /tmp/only_stdout.txt
$ cat /tmp/only_stdout.txt

Output yang Dihasilkan (setelah redirect stdout ke berkas):

text
Ini pesan info ke stdout.
Pesan info memakai fprintf ke stdout.
Masukkan satu karakter: Karakter: A

Sementara itu, di terminal tempat perintah dijalankan, pesan Ini pesan error ke stderr. tetap muncul karena stderr tidak ikut ter-redirect. Inilah inti pemisahan kedua stream: pengguna bisa menyimpan output program ke berkas tanpa kehilangan pesan error yang penting untuk debugging.

Memakai stderr dan exit code dengan Benar #

Saat program menemui kesalahan, kita biasanya ingin melakukan dua hal: mencetak pesan yang menjelaskan masalah ke stderr, dan mengembalikan exit code bukan-nol ke shell. Konstanta EXIT_FAILURE (nilai 1) dan EXIT_SUCCESS (nilai 0) dari <stdlib.h> membuat kode lebih jelas dibanding angka literal.

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

int main(int argc, char *argv[]) {
    if (argc < 2) {
        /* Argumen kurang: tulis pesan error ke stderr lalu exit 1 */
        fprintf(stderr, "Error: argumen tidak cukup.\n");
        fprintf(stderr, "Penggunaan: %s <nama_file>\n", argv[0]);
        return EXIT_FAILURE;
    }

    FILE *fp = fopen(argv[1], "r");
    if (fp == NULL) {
        /* File tidak ada: tulis pesan error ke stderr */
        fprintf(stderr, "Error: tidak bisa membuka file '%s'\n", argv[1]);
        return EXIT_FAILURE;
    }

    /* Sukses: tulis ke stdout */
    printf("Berhasil membuka file: %s\n", argv[1]);
    int ch;
    while ((ch = fgetc(fp)) != EOF) {
        putchar(ch);
    }
    fclose(fp);
    return EXIT_SUCCESS;
}

Output yang Dihasilkan untuk berbagai skenario:

text
$ ./program
Error: argumen tidak cukup.
Penggunaan: ./program <nama_file>
$ echo $?
1

$ ./program /tmp/data.txt
Berhasil membuka file: /tmp/data.txt
isi berkas
$ echo $?
0

$ ./program /tmp/tidak_ada.txt
Error: tidak bisa membuka file '/tmp/tidak_ada.txt'
$ echo $?
1

Mari kita telaah baris-baris kode di atas:

  1. Baris 4 (int main(int argc, char *argv[])): signature main dengan parameter argc (jumlah argumen) dan argv (array string argumen). argv[0] adalah nama program itu sendiri.
  2. Baris 6 (if (argc < 2)): jika pengguna tidak memberikan argumen (hanya menjalankan ./program tanpa nama berkas), argc bernilai 1. Kondisi ini terpenuhi.
  3. Baris 8-9 (fprintf(stderr, ...)): menulis dua baris pesan error ke stderr. Pesan pertama menjelaskan masalah, pesan kedua menunjukkan format penggunaan yang benar.
  4. Baris 10 (return EXIT_FAILURE;): mengembalikan exit code 1 ke shell. EXIT_FAILURE lebih jelas daripada angka literal 1.
  5. Baris 13 (FILE *fp = fopen(argv[1], "r");): membuka berkas yang namanya diberikan sebagai argumen pertama. Mode "r" berarti read-only.
  6. Baris 14 (if (fp == NULL)): jika fopen gagal (misalnya berkas tidak ada), fungsi mengembalikan NULL. Pemeriksaan ini wajib sebelum memakai fp.
  7. Baris 17 (return EXIT_FAILURE;): kembalikan exit code gagal, dengan pesan error yang menjelaskan berkas mana yang gagal dibuka.
  8. Baris 22 (while ((ch = fgetc(fp)) != EOF)): membaca karakter satu per satu sampai akhir berkas. Pola ini adalah idiom standar untuk membaca seluruh isi berkas.
  9. Baris 23 (putchar(ch);): putchar menulis satu karakter ke stdout — ekuivalen dengan fputc(ch, stdout).
  10. Baris 25 (return EXIT_SUCCESS;): mengembalikan exit code 0 (sukses).

Menggabungkan stdin, stdout, dan stderr #

Pada contoh terakhir, kita akan mempraktikkan ketiga stream dalam satu program yang membaca nama dari stdin, menulis sapaan ke stdout, dan menulis peringatan ke stderr jika input kosong atau berakhir lebih awal.

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

int main(void) {
    char nama[64];

    /* Baca dari stdin, tulis hasil ke stdout */
    fprintf(stdout, "Siapa nama Anda? ");
    if (fgets(nama, sizeof(nama), stdin) != NULL) {
        nama[strcspn(nama, "\n")] = '\0';

        if (strlen(nama) == 0) {
            fprintf(stderr, "Peringatan: nama kosong!\n");
            fprintf(stdout, "Halo, orang tanpa nama!\n");
        } else {
            fprintf(stdout, "Halo, %s!\n", nama);
        }
    } else {
        /* stdin berakhir sebelum ada input */
        fprintf(stderr, "Error: input berakhir (EOF).\n");
        return 1;
    }
    return 0;
}

Output yang Dihasilkan untuk berbagai input:

text
$ echo "Budi" | ./program
Siapa nama Anda? Halo, Budi!

$ echo "" | ./program
Siapa nama Anda? Halo, orang tanpa nama!
(perintah ini juga mencetak "Peringatan: nama kosong!" ke stderr)

$ printf "" | ./program
Siapa nama Anda? Error: input berakhir (EOF).
$ echo $?
1

Mari kita telaah baris-baris kode di atas:

  1. Baris 6 (fprintf(stdout, "Siapa nama Anda? ");): prompt tanpa \n agar kursor tetap di baris yang sama. Pemakaian fprintf(stdout, ...) sengaja ditulis eksplisit untuk menekankan bahwa prompt ini ke stdout.
  2. Baris 7 (if (fgets(nama, sizeof(nama), stdin) != NULL)): membaca satu baris dari stdin. Jika fgets mengembalikan NULL, berarti stdin sudah mencapai EOF tanpa ada karakter yang bisa dibaca.
  3. Baris 8 (nama[strcspn(nama, "\n")] = '\0';): idiom penghapusan newline yang sudah kita pelajari di chapter fgets.
  4. Baris 10 (if (strlen(nama) == 0)): jika setelah newline dihapus string menjadi kosong, berarti pengguna hanya menekan Enter tanpa mengetik apa-apa. Kasus ini dilaporkan sebagai peringatan ke stderr.
  5. Baris 11 (fprintf(stderr, "Peringatan: nama kosong!\n");): pesan peringatan ke stderr. Walaupun program tidak benar-benar gagal, peringatan ini berguna untuk debugging karena tidak akan ikut ter-redirect saat pengguna memredirect stdout.
  6. Baris 12 (fprintf(stdout, "Halo, orang tanpa nama!\n");): tetap memberi respons ke stdout agar program tetap ramah. Output ini bisa ter-redirect ke berkas tanpa kontaminasi pesan peringatan.
  7. Baris 16 (fprintf(stderr, "Error: input berakhir (EOF).\n");): jika fgets mengembalikan NULL, kita melaporkan ke stderr dan mengembalikan exit code 1.