IO #
std::io adalah fondasi semua operasi input/output di Rust. Dua trait inti — Read dan Write — mendefinisikan abstraksi universal yang diimplementasikan oleh file, jaringan, buffer memori, stdin/stdout, dan banyak tipe lainnya. Kode yang menerima impl Read bisa bekerja dengan file, stdin, atau Vec<u8> tanpa perubahan. Artikel ini membahas std::io secara menyeluruh sebagai bagian dari standard library — mulai dari stdin/stdout hingga operasi file, manajemen direktori, environment variable, dan menjalankan proses anak. Untuk topik yang lebih spesifik seperti socket dan async I/O, lihat artikel I/O di section Lanjutan.
Trait Read dan Write
#
Sebelum membahas implementasi konkret, penting memahami dua trait yang menjadi fondasi seluruh ekosistem I/O:
use std::io::{self, Read, Write};
// Fungsi generik — bekerja dengan SEMUA tipe yang impl Read
fn baca_semua<R: Read>(mut reader: R) -> io::Result<Vec<u8>> {
let mut buffer = Vec::new();
reader.read_to_end(&mut buffer)?;
Ok(buffer)
}
// Fungsi generik — bekerja dengan SEMUA tipe yang impl Write
fn tulis_semua<W: Write>(mut writer: W, data: &[u8]) -> io::Result<()> {
writer.write_all(data)?;
writer.flush()?;
Ok(())
}
fn main() -> io::Result<()> {
// Fungsi yang sama bekerja untuk file, stdin, Vec<u8>
let isi_file = baca_semua(std::fs::File::open("Cargo.toml")?)?;
let isi_vec = baca_semua(b"data dari byte slice" as &[u8])?;
tulis_semua(std::fs::File::create("output.txt")?, b"konten")?;
tulis_semua(io::stdout(), b"ke layar\n")?;
let mut buffer: Vec<u8> = Vec::new();
tulis_semua(&mut buffer, b"ke memori")?;
Ok(())
}
Stdin — Membaca Input #
use std::io::{self, BufRead, Write};
fn main() -> io::Result<()> {
// Baca satu baris dari stdin
print!("Masukkan nama: ");
io::stdout().flush()?; // wajib flush sebelum read untuk tampilkan prompt
let mut nama = String::new();
io::stdin().read_line(&mut nama)?;
let nama = nama.trim(); // hapus '\n' di akhir
println!("Halo, {}!", nama);
// Baca banyak baris sampai EOF
println!("Masukkan teks (Ctrl+D untuk selesai):");
let stdin = io::stdin();
let mut baris_list = Vec::new();
for baris in stdin.lock().lines() {
let baris = baris?;
if baris.is_empty() {
break; // atau gunakan EOF (Ctrl+D)
}
baris_list.push(baris);
}
println!("Kamu memasukkan {} baris:", baris_list.len());
for (i, b) in baris_list.iter().enumerate() {
println!(" {}: {}", i + 1, b);
}
Ok(())
}
Membaca Input dan Parsing #
use std::io::{self, Write};
fn baca_angka(prompt: &str) -> i64 {
loop {
print!("{}", prompt);
io::stdout().flush().unwrap();
let mut input = String::new();
io::stdin().read_line(&mut input).unwrap();
match input.trim().parse::<i64>() {
Ok(n) => return n,
Err(_) => println!("Input tidak valid, coba lagi."),
}
}
}
fn baca_pilihan(prompt: &str, opsi: &[&str]) -> usize {
loop {
println!("{}", prompt);
for (i, opsi) in opsi.iter().enumerate() {
println!(" {}. {}", i + 1, opsi);
}
print!("Pilihan (1-{}): ", opsi.len());
io::stdout().flush().unwrap();
let mut input = String::new();
io::stdin().read_line(&mut input).unwrap();
if let Ok(n) = input.trim().parse::<usize>() {
if n >= 1 && n <= opsi.len() {
return n - 1; // 0-indexed
}
}
println!("Pilihan tidak valid.");
}
}
fn main() {
let a = baca_angka("Masukkan angka pertama: ");
let b = baca_angka("Masukkan angka kedua: ");
println!("{} + {} = {}", a, b, a + b);
let pilihan = baca_pilihan("Pilih operasi:", &["Tambah", "Kurang", "Kali", "Bagi"]);
println!("Kamu memilih operasi ke-{}", pilihan + 1);
}
Stdout dan Stderr #
use std::io::{self, Write};
fn main() {
// println! dan print! — ke stdout
println!("Output normal ke stdout");
print!("Tanpa newline ");
println!("lanjut di sini");
// eprintln! dan eprint! — ke stderr (untuk error dan diagnostik)
eprintln!("Error: koneksi gagal");
eprint!("Warning: ");
eprintln!("memori hampir penuh");
// Tulis langsung ke stdout/stderr (lebih kontrol)
let stdout = io::stdout();
let mut handle = stdout.lock(); // lock untuk performa di loop
for i in 0..5 {
writeln!(handle, "Baris {}", i).unwrap();
}
// Redirect stdout ke file (biasanya dari shell, tapi bisa juga dari kode)
// Program: cargo run > output.txt
// Semua println! akan masuk ke output.txt
// eprintln! tetap ke terminal
// Buffer stdout secara manual
use std::io::BufWriter;
let stdout = io::stdout();
let mut writer = BufWriter::new(stdout.lock());
for i in 0..10_000 {
writeln!(writer, "Baris {}", i).unwrap();
}
writer.flush().unwrap();
}
BufReader dan BufWriter
#
Setiap panggilan read() atau write() adalah syscall — mahal. Buffering mengurangi jumlah syscall secara drastis:
use std::fs::File;
use std::io::{self, BufRead, BufReader, BufWriter, Write};
fn proses_file_besar(path_masuk: &str, path_keluar: &str) -> io::Result<u64> {
let file_masuk = File::open(path_masuk)?;
let file_keluar = File::create(path_keluar)?;
// Default buffer 8KB — cukup untuk kebanyakan kasus
let reader = BufReader::new(file_masuk);
let mut writer = BufWriter::new(file_keluar);
let mut baris_count = 0u64;
for (nomor, baris_result) in reader.lines().enumerate() {
let baris = baris_result?;
baris_count += 1;
// Transformasi: nomor baris + uppercase
writeln!(writer, "{:6}: {}", nomor + 1, baris.to_uppercase())?;
}
writer.flush()?; // wajib!
Ok(baris_count)
}
// BufReader dengan ukuran buffer kustom
fn baca_binary_besar(path: &str) -> io::Result<Vec<u8>> {
use std::io::Read;
let file = File::open(path)?;
let mut reader = BufReader::with_capacity(64 * 1024, file); // 64KB buffer
let mut konten = Vec::new();
reader.read_to_end(&mut konten)?;
Ok(konten)
}
Operasi File #
Baca dan Tulis Sederhana #
use std::fs;
use std::io;
fn main() -> io::Result<()> {
// Tulis string ke file (buat baru atau timpa)
fs::write("halo.txt", "Baris pertama\nBaris kedua\n")?;
// Baca seluruh file sebagai String
let isi = fs::read_to_string("halo.txt")?;
println!("Isi:\n{}", isi);
// Baca sebagai bytes
let bytes = fs::read("halo.txt")?;
println!("Ukuran: {} byte", bytes.len());
// Salin file
fs::copy("halo.txt", "salinan.txt")?;
// Rename / pindahkan
fs::rename("salinan.txt", "dipindahkan.txt")?;
// Metadata file
let meta = fs::metadata("halo.txt")?;
println!("Ukuran: {} byte", meta.len());
println!("Readonly: {}", meta.permissions().readonly());
println!("Modifikasi: {:?}", meta.modified()?);
// Hapus file
fs::remove_file("halo.txt")?;
fs::remove_file("dipindahkan.txt")?;
Ok(())
}
OpenOptions — Kontrol Mode Buka File
#
use std::fs::OpenOptions;
use std::io::{self, Write};
fn main() -> io::Result<()> {
// Append — tambah di akhir tanpa menimpa
{
let mut file = OpenOptions::new()
.append(true)
.create(true) // buat jika belum ada
.open("log.txt")?;
writeln!(file, "[{}] Event terjadi", chrono_now())?;
}
// Buat baru, error jika sudah ada
{
let _ = OpenOptions::new()
.write(true)
.create_new(true) // error jika file sudah ada
.open("baru.txt");
// Abaikan error untuk demo
}
// Baca dan tulis sekaligus
{
let _ = OpenOptions::new()
.read(true)
.write(true)
.open("log.txt");
}
// Bersihkan file (truncate ke 0 byte)
{
let _ = OpenOptions::new()
.write(true)
.truncate(true)
.open("log.txt");
}
std::fs::remove_file("log.txt").ok();
std::fs::remove_file("baru.txt").ok();
Ok(())
}
fn chrono_now() -> String {
// Placeholder untuk timestamp
String::from("2024-08-24 10:30:00")
}
Seek — Posisi Kursor File #
use std::fs::File;
use std::io::{self, Read, Seek, SeekFrom, Write};
fn main() -> io::Result<()> {
// Tulis file dengan konten terstruktur
{
let mut file = File::create("terstruktur.bin")?;
file.write_all(&100u32.to_le_bytes())?; // header: 4 byte
file.write_all(b"DATA1234")?; // data: 8 byte
file.write_all(&200u32.to_le_bytes())?; // footer: 4 byte
}
// Baca hanya bagian tertentu
{
let mut file = File::open("terstruktur.bin")?;
// Baca header
let mut header = [0u8; 4];
file.read_exact(&mut header)?;
let nilai_header = u32::from_le_bytes(header);
println!("Header: {}", nilai_header); // 100
// Loncat ke footer (skip 8 byte data)
file.seek(SeekFrom::Current(8))?;
let mut footer = [0u8; 4];
file.read_exact(&mut footer)?;
println!("Footer: {}", u32::from_le_bytes(footer)); // 200
// Kembali ke awal
file.seek(SeekFrom::Start(0))?;
// Posisi saat ini
let posisi = file.seek(SeekFrom::Current(0))?;
println!("Posisi: {}", posisi); // 0
// Dari akhir file
file.seek(SeekFrom::End(-4))?;
let posisi = file.seek(SeekFrom::Current(0))?;
println!("Posisi dari akhir -4: {}", posisi); // 12
}
std::fs::remove_file("terstruktur.bin")?;
Ok(())
}
Manajemen Direktori #
use std::fs;
use std::io;
fn main() -> io::Result<()> {
// Buat direktori
fs::create_dir("direktori-baru")?;
// Buat nested direktori (parent yang belum ada dibuat otomatis)
fs::create_dir_all("a/b/c/d")?;
// Buat beberapa file
fs::write("direktori-baru/file1.txt", "isi 1")?;
fs::write("direktori-baru/file2.rs", "fn main() {}")?;
fs::write("direktori-baru/data.json", "{}")?;
// Baca isi direktori
println!("Isi direktori:");
for entri in fs::read_dir("direktori-baru")? {
let entri = entri?;
let path = entri.path();
let meta = entri.metadata()?;
let tipe = if meta.is_dir() { "DIR" } else { "FILE" };
println!(" [{:4}] {} ({} byte)",
tipe,
path.file_name().unwrap().to_string_lossy(),
meta.len()
);
}
// Urutkan entri
let mut entri_list: Vec<_> = fs::read_dir("direktori-baru")?
.filter_map(|e| e.ok())
.collect();
entri_list.sort_by_key(|e| e.file_name());
// Cek keberadaan path
println!("Ada direktori: {}", fs::metadata("direktori-baru").is_ok());
println!("Ada file: {}", fs::metadata("tidak-ada.txt").is_ok());
// Hapus direktori (harus kosong)
fs::remove_file("direktori-baru/file1.txt")?;
fs::remove_file("direktori-baru/file2.rs")?;
fs::remove_file("direktori-baru/data.json")?;
fs::remove_dir("direktori-baru")?;
// Hapus direktori beserta isinya (rekursif)
fs::remove_dir_all("a")?;
Ok(())
}
std::path — Manajemen Path
#
use std::path::{Path, PathBuf};
fn main() {
// PathBuf: owned, bisa dimodifikasi (seperti String)
let mut path = PathBuf::from("/home/budi");
path.push("dokumen");
path.push("laporan.pdf");
println!("{}", path.display()); // /home/budi/dokumen/laporan.pdf
// Path: borrowed, tidak bisa dimodifikasi (seperti &str)
let ref_path: &Path = Path::new("/etc/hosts");
// Komponen path
println!("file_name: {:?}", path.file_name()); // Some("laporan.pdf")
println!("extension: {:?}", path.extension()); // Some("pdf")
println!("file_stem: {:?}", path.file_stem()); // Some("laporan")
println!("parent: {:?}", path.parent()); // Some("/home/budi/dokumen")
// join — gabungkan path
let base = Path::new("/var/log");
let log = base.join("app").join("error.log");
println!("{}", log.display()); // /var/log/app/error.log
// Cek tipe
println!("exists: {}", path.exists());
println!("is_file: {}", path.is_file());
println!("is_dir: {}", path.is_dir());
println!("is_absolute: {}", path.is_absolute());
// Konversi ke String
if let Some(s) = path.to_str() {
println!("Sebagai &str: {}", s);
}
let s = path.to_string_lossy(); // tidak gagal, ganti karakter tidak valid
println!("Lossy: {}", s);
// Path relatif
let rel = Path::new("src/main.rs");
println!("Relatif: {}", rel.display());
// Canonical path (resolve symlink, bersihkan ../)
if let Ok(abs) = Path::new(".").canonicalize() {
println!("CWD absolut: {}", abs.display());
}
}
Environment Variables #
use std::env;
fn main() {
// Ambil satu env var
match env::var("HOME") {
Ok(val) => println!("HOME = {}", val),
Err(e) => println!("HOME tidak ada: {}", e),
}
// Dengan default value
let port = env::var("PORT").unwrap_or_else(|_| "8080".to_string());
let debug = env::var("DEBUG").map(|v| v == "true").unwrap_or(false);
println!("Port: {}, Debug: {}", port, debug);
// Cek semua env var
println!("\nSemua env var (pertama 5):");
for (kunci, nilai) in env::vars().take(5) {
println!(" {} = {}", kunci, &nilai[..nilai.len().min(30)]);
}
// Set env var (hanya untuk proses saat ini dan child processes)
env::set_var("APP_MODE", "production");
println!("APP_MODE: {}", env::var("APP_MODE").unwrap());
// Hapus env var
env::remove_var("APP_MODE");
// Argument baris perintah
let args: Vec<String> = env::args().collect();
println!("\nArgumen: {:?}", args);
println!("Program: {}", args[0]);
// Current working directory
let cwd = env::current_dir().unwrap();
println!("CWD: {}", cwd.display());
// Direktori executable
if let Ok(exe) = env::current_exe() {
println!("Executable: {}", exe.display());
}
}
std::process — Menjalankan Proses Anak
#
use std::process::{Command, Stdio};
use std::io::{self, Write};
fn main() -> io::Result<()> {
// Jalankan perintah sederhana
let status = Command::new("ls")
.arg("-la")
.status()?;
println!("Exit code: {}", status.code().unwrap_or(-1));
// Tangkap output
let output = Command::new("echo")
.arg("Halo dari child process!")
.output()?;
println!("stdout: {}", String::from_utf8_lossy(&output.stdout));
println!("stderr: {}", String::from_utf8_lossy(&output.stderr));
println!("success: {}", output.status.success());
// Dengan environment variable
let output = Command::new("printenv")
.arg("MY_VAR")
.env("MY_VAR", "nilai-kustom")
.output()?;
println!("MY_VAR: {}", String::from_utf8_lossy(&output.stdout).trim());
// Stdin ke child process
let mut child = Command::new("cat")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()?;
if let Some(stdin) = child.stdin.take() {
let mut stdin = stdin;
stdin.write_all(b"Halo dari Rust!\n")?;
}
let output = child.wait_with_output()?;
println!("cat output: {}", String::from_utf8_lossy(&output.stdout));
// Keluar dengan kode tertentu
// process::exit(0); // keluar dengan kode 0 (sukses)
Ok(())
}
Error Handling I/O #
use std::io::{self, ErrorKind};
use std::fs;
fn baca_atau_default(path: &str, default: &str) -> String {
match fs::read_to_string(path) {
Ok(isi) => isi,
Err(e) if e.kind() == ErrorKind::NotFound => {
// File tidak ada — gunakan default
default.to_string()
}
Err(e) => {
eprintln!("Error membaca {}: {}", path, e);
default.to_string()
}
}
}
fn pastikan_direktori_ada(path: &str) -> io::Result<()> {
match fs::create_dir(path) {
Ok(_) => Ok(()),
Err(e) if e.kind() == ErrorKind::AlreadyExists => Ok(()), // sudah ada — OK
Err(e) => Err(e),
}
}
fn main() -> io::Result<()> {
// ErrorKind untuk menangani jenis error berbeda
let jenis_error = [
ErrorKind::NotFound,
ErrorKind::PermissionDenied,
ErrorKind::AlreadyExists,
ErrorKind::ConnectionRefused,
ErrorKind::TimedOut,
ErrorKind::UnexpectedEof,
ErrorKind::WouldBlock,
];
// Baca konfigurasi dengan fallback
let config = baca_atau_default("config.toml", "[default]\nport = 8080\n");
println!("Config:\n{}", config);
// Buat direktori dengan idempoten
pastikan_direktori_ada("logs")?;
pastikan_direktori_ada("logs")?; // tidak error meski sudah ada
fs::remove_dir("logs").ok();
Ok(())
}
Ringkasan #
- Trait
ReaddanWritesebagai abstraksi universal — tulis fungsi yang menerimaimpl Readatauimpl Writeagar bisa bekerja dengan file, stdin, buffer memori, dan tipe lain tanpa modifikasi.- Selalu
flush()setelahBufWriter— data yang belum di-flush bisa hilang jika program crash. DropBufWritermemang flush otomatis tapi error-nya tidak bisa ditangkap.fs::writedanfs::read_to_stringuntuk kasus sederhana — shortcut yang nyaman untuk file kecil yang tidak butuh streaming.OpenOptionsuntuk kontrol penuh — kombinasiread,write,append,create,create_new,truncateuntuk semua skenario pembukaan file.SeekFrom::Start,SeekFrom::Current,SeekFrom::End— tiga mode posisi kursor;SeekFrom::Current(0)untuk mendapat posisi saat ini tanpa bergerak.- Gunakan
PathBufdanPath, bukan string untuk path — menangani separator/vs\lintas platform otomatis, dan menyediakan API sepertiparent(),extension(),join().ErrorKinduntuk penanganan error I/O yang granular — bedakanNotFound,PermissionDenied,AlreadyExists,TimedOutuntuk respons yang tepat.env::varselalu mengembalikanResult— gunakan.unwrap_or_else(|_| "default".into())untuk nilai fallback atau?untuk propagasi error.Command::output()untuk tangkap output —Command::status()hanya dapat kode keluar,Command::output()tangkap stdout dan stderr sekaligus.