IO #
std::io is the foundation of all input/output operations in Rust. Two core traits — Read and Write — define universal abstractions implemented by file, network, memory buffer, stdin/stdout, and many other types. Code that accepts impl Read can work with file, stdin, or Vec<u8> without changes. This article covers std::io thoroughly as part of the standard library — from stdin/stdout to file operations, directory management, environment variables, and running child processes. For more specific topics such as sockets and async I/O, see the I/O articles in the Advanced section.
Traits Read and Write
#
Before discussing concrete implementations, it is important to understand two traits that are the foundation of the entire I/O ecosystem:
use std::io::{self, Read, Write};
// Generic function — works with ALL types that 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)
}
// Generic function — works with ALL types that 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<()> {
// The same function works for 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 — Read Input #
use std::io::{self, BufRead, Write};
fn main() -> io::Result<()> {
// Read one line from stdin
print!("Masukkan nama: ");
io::stdout().flush()?; // must flush before reading to display a prompt
let mut nama = String::new();
io::stdin().read_line(&mut nama)?;
let nama = nama.trim(); // remove '\n' at the end
println!("Halo, {}!", nama);
// Read multiple lines until 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; // or use 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(())
}
Reading Input and 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 and Stderr #
use std::io::{self, Write};
fn main() {
// println! and print! — to stdout
println!("Output normal ke stdout");
print!("Tanpa newline ");
println!("lanjut di sini");
// eprintln! and eprint! — to stderr (for errors and diagnostics)
eprintln!("Error: koneksi gagal");
eprint!("Warning: ");
eprintln!("memori hampir penuh");
// Write directly to stdout/stderr (more control)
let stdout = io::stdout();
let mut handle = stdout.lock(); // lock for performance in loops
for i in 0..5 {
writeln!(handle, "Baris {}", i).unwrap();
}
// Redirect stdout to a file (usually from the shell, but can also be from code)
// Program: cargo run > output.txt
// All println! will go to output.txt
// eprintln! stick to the terminal
// Buffer stdout manually
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 and BufWriter
#
Every call to read() or write() is a syscall — expensive. Buffering reduces the number of syscalls drastically:
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 — sufficient for most cases
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;
// Transformation: line number + uppercase
writeln!(writer, "{:6}: {}", nomor + 1, baris.to_uppercase())?;
}
writer.flush()?; // is a must!
Ok(baris_count)
}
// BufReader with custom buffer size
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)
}
File Operations #
Simple Read and Write #
use std::fs;
use std::io;
fn main() -> io::Result<()> {
// Write string to file (create new or overwrite)
fs::write("halo.txt", "Baris pertama\nBaris kedua\n")?;
// Read entire file as String
let isi = fs::read_to_string("halo.txt")?;
println!("Isi:\n{}", isi);
// Read as bytes
let bytes = fs::read("halo.txt")?;
println!("Ukuran: {} byte", bytes.len());
// Copy files
fs::copy("halo.txt", "salinan.txt")?;
// Rename / move
fs::rename("salinan.txt", "dipindahkan.txt")?;
// Metadata files
let meta = fs::metadata("halo.txt")?;
println!("Ukuran: {} byte", meta.len());
println!("Readonly: {}", meta.permissions().readonly());
println!("Modifikasi: {:?}", meta.modified()?);
// Delete files
fs::remove_file("halo.txt")?;
fs::remove_file("dipindahkan.txt")?;
Ok(())
}
OpenOptions — File Open Mode Control
#
use std::fs::OpenOptions;
use std::io::{self, Write};
fn main() -> io::Result<()> {
// Append — add at the end without overwriting
{
let mut file = OpenOptions::new()
.append(true)
.create(true) // make it if it doesn't exist yet
.open("log.txt")?;
writeln!(file, "[{}] Event terjadi", chrono_now())?;
}
// Create new, error if existing
{
let _ = OpenOptions::new()
.write(true)
.create_new(true) // error if file already exists
.open("baru.txt");
// Ignore errors for demo
}
// Read and write at the same time
{
let _ = OpenOptions::new()
.read(true)
.write(true)
.open("log.txt");
}
// Clean file (truncate to 0 bytes)
{
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 for timestamp
String::from("2024-08-24 10:30:00")
}
Seek — File Cursor Position #
use std::fs::File;
use std::io::{self, Read, Seek, SeekFrom, Write};
fn main() -> io::Result<()> {
// Write a file with structured content
{
let mut file = File::create("terstruktur.bin")?;
file.write_all(&100u32.to_le_bytes())?; // header: 4 bytes
file.write_all(b"DATA1234")?; // data: 8 bytes
file.write_all(&200u32.to_le_bytes())?; // footer: 4 bytes
}
// Read only certain sections
{
let mut file = File::open("terstruktur.bin")?;
// Read header
let mut header = [0u8; 4];
file.read_exact(&mut header)?;
let nilai_header = u32::from_le_bytes(header);
println!("Header: {}", nilai_header); // 100
// Skip to footer (skip 8 bytes of data)
file.seek(SeekFrom::Current(8))?;
let mut footer = [0u8; 4];
file.read_exact(&mut footer)?;
println!("Footer: {}", u32::from_le_bytes(footer)); // 200
// Back to the beginning
file.seek(SeekFrom::Start(0))?;
// Current position
let posisi = file.seek(SeekFrom::Current(0))?;
println!("Posisi: {}", posisi); // 0
// From the end of the 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(())
}
Directory Management #
use std::fs;
use std::io;
fn main() -> io::Result<()> {
// Create directory
fs::create_dir("direktori-baru")?;
// Create nested directory (non-existing parent is created automatically)
fs::create_dir_all("a/b/c/d")?;
// Create multiple files
fs::write("direktori-baru/file1.txt", "isi 1")?;
fs::write("direktori-baru/file2.rs", "fn main() {}")?;
fs::write("direktori-baru/data.json", "{}")?;
// Read directory contents
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()
);
}
// Sort entries
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());
// Check path existence
println!("Ada direktori: {}", fs::metadata("direktori-baru").is_ok());
println!("Ada file: {}", fs::metadata("tidak-ada.txt").is_ok());
// Delete directory (must be empty)
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")?;
// Delete directory and its contents (recursive)
fs::remove_dir_all("a")?;
Ok(())
}
std::path — Path Management
#
use std::path::{Path, PathBuf};
fn main() {
// PathBuf: owned, can be modified (like String)
let mut path = PathBuf::from("/home/budi");
path.push("dokumen");
path.push("laporan.pdf");
println!("{}", path.display()); // /home/budi/document/report.pdf
// Path: borrowed, cannot be modified (like &str)
let ref_path: &Path = Path::new("/etc/hosts");
// Path component
println!("file_name: {:?}", path.file_name()); // Some("report.pdf")
println!("extension: {:?}", path.extension()); // Some("pdf")
println!("file_stem: {:?}", path.file_stem()); // Some("report")
println!("parent: {:?}", path.parent()); // Some("/home/budi/document")
// join — join paths
let base = Path::new("/var/log");
let log = base.join("app").join("error.log");
println!("{}", log.display()); // /var/log/app/error.log
// Check type
println!("exists: {}", path.exists());
println!("is_file: {}", path.is_file());
println!("is_dir: {}", path.is_dir());
println!("is_absolute: {}", path.is_absolute());
// Convert to String
if let Some(s) = path.to_str() {
println!("Sebagai &str: {}", s);
}
let s = path.to_string_lossy(); // does not fail, replace invalid character
println!("Lossy: {}", s);
// Relative path
let rel = Path::new("src/main.rs");
println!("Relatif: {}", rel.display());
// Canonical path (resolve symlink, clear ../)
if let Ok(abs) = Path::new(".").canonicalize() {
println!("CWD absolut: {}", abs.display());
}
}
Environment Variables #
use std::env;
fn main() {
// Take one env var
match env::var("HOME") {
Ok(val) => println!("HOME = {}", val),
Err(e) => println!("HOME tidak ada: {}", e),
}
// With 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);
// Check all 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 (only for current process and child processes)
env::set_var("APP_MODE", "production");
println!("APP_MODE: {}", env::var("APP_MODE").unwrap());
// Remove env var
env::remove_var("APP_MODE");
// Command line arguments
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());
// Executable directory
if let Ok(exe) = env::current_exe() {
println!("Executable: {}", exe.display());
}
}
std::process — Running Child Process
#
use std::process::{Command, Stdio};
use std::io::{self, Write};
fn main() -> io::Result<()> {
// Execute a simple command
let status = Command::new("ls")
.arg("-la")
.status()?;
println!("Exit code: {}", status.code().unwrap_or(-1));
// Capture 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());
// With environment variables
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 to 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));
// Exit with specified code
// process::exit(0); // exit with code 0 (success)
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 does not exist — use 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(()), // already exists — OK
Err(e) => Err(e),
}
}
fn main() -> io::Result<()> {
// ErrorKind to handle different error types
let jenis_error = [
ErrorKind::NotFound,
ErrorKind::PermissionDenied,
ErrorKind::AlreadyExists,
ErrorKind::ConnectionRefused,
ErrorKind::TimedOut,
ErrorKind::UnexpectedEof,
ErrorKind::WouldBlock,
];
// Read configuration with fallback
let config = baca_atau_default("config.toml", "[default]\nport = 8080\n");
println!("Config:\n{}", config);
// Create directory idempotently
pastikan_direktori_ada("logs")?;
pastikan_direktori_ada("logs")?; // does not error even though it already exists
fs::remove_dir("logs").ok();
Ok(())
}
Summary #
- Trait
ReadandWriteas universal abstractions — write functions that acceptimpl Readorimpl Writeto work with files, stdin, memory buffers, and other types without modification.- Always
flush()afterBufWriter— unflushed data may be lost if the program crashes. DropBufWriterdoes flush automatically but the error cannot be caught.fs::writeandfs::read_to_stringfor simple cases — convenient shortcut for small files that don’t need streaming.OpenOptionsfor full control — combination ofread,write,append,create,create_new,truncatefor all file opening scenarios.SeekFrom::Start,SeekFrom::Current,SeekFrom::End— three cursor position modes;SeekFrom::Current(0)to get the current position without moving.- Use
PathBufandPath, not strings for paths — handles automatic cross-platform/vs\separator, and provides APIs likeparent(),extension(),join().ErrorKindfor granular I/O error handling — distinguishNotFound,PermissionDenied,AlreadyExists,TimedOutfor proper response.env::varalways returnsResult— use.unwrap_or_else(|_| "default".into())for fallback value or?for error propagation.Command::output()to capture output —Command::status()only gets exit code,Command::output()captures both stdout and stderr at the same time.