Lewati ke konten

Membaca dan Menulis File di Rust

Membaca dan menulis file adalah salah satu tempat pertama di mana semua yang sudah kamu pelajari tentang Result dan operator ? tidak lagi sekadar teori. Hampir setiap operasi file bisa gagal (file-nya mungkin tidak ada, atau kamu tidak punya izin), jadi standard library memaksamu menangani kemungkinan itu secara langsung.

Cara paling sederhana untuk membaca isi file ke dalam sebuah String:

use std::fs;
fn main() {
let contents = fs::read_to_string("hello.txt");
match contents {
Ok(text) => println!("{text}"),
Err(e) => println!("Tidak bisa membaca file: {e}"),
}
}

fs::read_to_string mengembalikan Result<String, std::io::Error>: Ok berisi teksnya kalau berhasil, Err berisi alasannya kalau gagal (file tidak ada, tidak punya izin, dan sebagainya).

Memakai ? untuk menyederhanakan penanganan error

Section titled “Memakai ? untuk menyederhanakan penanganan error”

Melakukan match di setiap operasi file cepat jadi berulang-ulang. Kalau kamu berada di dalam fungsi yang juga mengembalikan Result, operator ? (dari halaman Result) bisa merapikannya:

use std::fs;
use std::io;
fn read_file(path: &str) -> Result<String, io::Error> {
let contents = fs::read_to_string(path)?; // langsung return kalau gagal
Ok(contents)
}
fn main() {
match read_file("hello.txt") {
Ok(text) => println!("{text}"),
Err(e) => println!("Error: {e}"),
}
}
use std::fs;
fn main() {
let result = fs::write("output.txt", "Halo, file!");
match result {
Ok(()) => println!("Berhasil ditulis"),
Err(e) => println!("Gagal menulis: {e}"),
}
}

fs::write membuat file-nya kalau belum ada, dan menimpa seluruh isinya kalau sudah ada. Ingat ini kalau kamu ingin menambahkan isi ke file, bukan menggantinya.

Untuk menambahkan isi ke file yang sudah ada tanpa menimpanya, pakai OpenOptions:

use std::fs::OpenOptions;
use std::io::Write;
fn main() -> std::io::Result<()> {
let mut file = OpenOptions::new()
.append(true)
.create(true)
.open("log.txt")?;
writeln!(file, "Baris log baru")?;
Ok(())
}

Perhatikan bahwa di sini main sendiri mengembalikan Result. Pola ini penting untuk diketahui: kalau main mengembalikan Result, kamu bisa memakai ? langsung di dalamnya, tanpa membungkus semuanya dengan match.

Untuk file yang besar, membaca seluruh isinya ke memori sekaligus tidak selalu ideal. Sebagai gantinya, baca baris demi baris:

use std::fs::File;
use std::io::{BufRead, BufReader};
fn main() -> std::io::Result<()> {
let file = File::open("hello.txt")?;
let reader = BufReader::new(file);
for line in reader.lines() {
println!("{}", line?);
}
Ok(())
}

Sejauh ini, semua error ditangani dengan cara yang sama: dicetak. Tapi sering kali kamu ingin bereaksi berbeda tergantung apa yang salah. File yang tidak ada biasanya tidak masalah (mungkin ini pertama kali program dijalankan, jadi kamu cukup mulai dari kosong). Sedangkan “Permission denied” (akses ditolak) adalah masalah sungguhan yang perlu dilaporkan.

Sebuah io::Error bisa memberi tahu kategorinya lewat .kind(), yang mengembalikan enum ErrorKind (bahasa Inggris) yang bisa kamu match:

use std::fs;
use std::io::ErrorKind;
fn main() {
// file di folder sementara sistem yang tidak ada
let path = std::env::temp_dir().join("rusttutorial-missing-notes.txt");
match fs::read_to_string(&path) {
Ok(text) => println!("Memuat {} baris", text.lines().count()),
Err(e) => match e.kind() {
ErrorKind::NotFound => println!("Belum ada catatan, mulai dari awal"),
ErrorKind::PermissionDenied => println!("Tidak diizinkan membaca file catatan"),
_ => println!("Ada masalah lain: {e}"),
},
}
}

Output:

Belum ada catatan, mulai dari awal
▶ Coba sendiri

std::env::temp_dir() memberimu path folder sementara milik sistem (misalnya /tmp di Linux), dan .join(...) menambahkan nama file ke path itu. Fungsi-fungsi file menerima path seperti ini sama baiknya dengan &str.

Selalu sisakan cabang _ di akhir: ErrorKind punya banyak varian, dan varian baru terus ditambahkan seiring waktu. Cabang _ menangkap semua yang tidak kamu sebutkan.

Kedua latihan menulis file-nya sendiri ke folder sementara terlebih dahulu, jadi bisa dijalankan di mana saja, termasuk di Playground.

Tulis tiga item to-do ke sebuah file dengan fs::write, satu item per baris. Lalu baca kembali file-nya dengan fs::read_to_string dan cetak setiap baris dengan nomor di depannya, diikuti jumlah totalnya.

Petunjuk: taruh \n di antara item supaya jadi baris terpisah. text.lines() memberimu baris satu per satu. Buat main mengembalikan std::io::Result<()> supaya kamu bisa memakai ?.

Lihat solusi
use std::fs;
fn main() -> std::io::Result<()> {
let path = std::env::temp_dir().join("rusttutorial-todo.txt");
fs::write(&path, "belajar ownership\nmenulis modul\nmembaca file\n")?;
let text = fs::read_to_string(&path)?;
let mut count = 0;
for line in text.lines() {
count += 1;
println!("{count}. {line}");
}
println!("{count} item");
Ok(())
}

Output:

1. belajar ownership
2. menulis modul
3. membaca file
3 item
▶ Coba sendiri

Karena fs::write mengganti seluruh isi file, kamu selalu mendapat tiga baris yang sama, berapa kali pun kamu menjalankannya. Setiap ? mengembalikan error dari main kalau proses menulis atau membaca gagal.

Mulai file log dari awal dengan satu baris judul, lalu tambahkan (append) dua baris lagi dengan OpenOptions dan writeln!. Terakhir, baca kembali seluruh file-nya dan cetak.

Petunjuk: pakai fs::write untuk baris judul (supaya setiap kali dijalankan, file-nya mulai bersih), lalu OpenOptions::new().append(true).open(&path)? untuk baris-baris tambahannya. print! (bukan println!) mencegah baris kosong ekstra di akhir, karena file-nya sudah diakhiri dengan baris baru.

Lihat solusi
use std::fs::{self, OpenOptions};
use std::io::Write;
fn main() -> std::io::Result<()> {
let path = std::env::temp_dir().join("rusttutorial-log.txt");
fs::write(&path, "=== log ===\n")?; // mulai dari awal
let mut file = OpenOptions::new().append(true).open(&path)?;
writeln!(file, "dimulai")?;
writeln!(file, "selesai")?;
print!("{}", fs::read_to_string(&path)?);
Ok(())
}

Output:

=== log ===
dimulai
selesai
▶ Coba sendiri

use std::fs::{self, OpenOptions}; membawa masuk modul fs itu sendiri sekaligus OpenOptions. Tanpa fs::write yang pertama, setiap kali dijalankan program akan terus menambahkan ke file lama, dan log-nya akan bertambah panjang selamanya. Itu memang yang kamu inginkan untuk log sungguhan, tapi tidak untuk latihan dengan output yang tetap.

  • fs::read_to_string(path) membaca seluruh file ke dalam String, dan mengembalikan Result.
  • fs::write(path, contents) menulis file, dan menimpanya kalau file itu sudah ada.
  • OpenOptions::new().append(true) memungkinkanmu menambahkan isi ke file, bukan menggantinya.
  • main() -> std::io::Result<()> memungkinkanmu memakai ? langsung di main, tanpa harus match setiap pemanggilan.
  • e.kind() memberi tahu jenis error yang terjadi, misalnya ErrorKind::NotFound, supaya kamu bisa menangani masing-masing secara berbeda.
  • Contoh yang memakai file di folder proyekmu paling baik dicoba secara lokal; std::env::temp_dir() memberimu folder yang bisa dipakai di mana saja.

Kuis singkat

1. Apa yang terjadi kalau kamu memanggil fs::write pada path yang sudah ada?

2. Bagaimana cara menambahkan isi ke file tanpa menimpanya?

3. Apa yang jadi mungkin kalau main sendiri mengembalikan std::io::Result<()>?

4. Apa yang dilakukan operator ? ketika fs::read_to_string(path)? gagal?

5. Kenapa membaca file besar baris demi baris dengan BufReader, bukan dengan fs::read_to_string?

Skor: 0 / 5