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 Read and Write as universal abstractions — write functions that accept impl Read or impl Write to work with files, stdin, memory buffers, and other types without modification.
  • Always flush() after BufWriter — unflushed data may be lost if the program crashes. Drop BufWriter does flush automatically but the error cannot be caught.
  • fs::write and fs::read_to_string for simple cases — convenient shortcut for small files that don’t need streaming.
  • OpenOptions for full control — combination of read, write, append, create, create_new, truncate for 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 PathBuf and Path, not strings for paths — handles automatic cross-platform / vs \ separator, and provides APIs like parent(), extension(), join().
  • ErrorKind for granular I/O error handling — distinguish NotFound, PermissionDenied, AlreadyExists, TimedOut for proper response.
  • env::var always returns Result — use .unwrap_or_else(|_| "default".into()) for fallback value or ? for error propagation.
  • Command::output() to capture outputCommand::status() only gets exit code, Command::output() captures both stdout and stderr at the same time.


← Previous: Strings   Next: Math →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact