Leer y escribir archivos en Rust
Leer y escribir archivos es uno de los primeros momentos en que todo lo que aprendiste sobre Result y el operador ? deja de ser teoría. Casi cualquier operación con archivos puede fallar (puede que el archivo no exista o que no tengas permiso), así que la biblioteca estándar te obliga a tratar esos casos directamente.
Leer un archivo completo
Sección titulada «Leer un archivo completo»La forma más sencilla de leer el contenido de un archivo en un String:
use std::fs;
fn main() { let contents = fs::read_to_string("hello.txt");
match contents { Ok(text) => println!("{text}"), Err(e) => println!("No se pudo leer el archivo: {e}"), }}fs::read_to_string devuelve un Result<String, std::io::Error>: Ok con el texto si todo fue bien, o Err con el motivo si algo falló (el archivo no existe, no tienes permiso, etc.).
Usar ? para simplificar el manejo de errores
Sección titulada «Usar ? para simplificar el manejo de errores»Hacer un match en cada operación con archivos se vuelve repetitivo enseguida. Si estás dentro de una función que también devuelve un Result, el operador ? (de la página de Result) lo simplifica mucho:
use std::fs;use std::io;
fn read_file(path: &str) -> Result<String, io::Error> { let contents = fs::read_to_string(path)?; // si falla, sale de la función aquí Ok(contents)}
fn main() { match read_file("hello.txt") { Ok(text) => println!("{text}"), Err(e) => println!("Error: {e}"), }}Escribir en un archivo
Sección titulada «Escribir en un archivo»use std::fs;
fn main() { let result = fs::write("output.txt", "¡Hola, archivo!");
match result { Ok(()) => println!("Se escribió correctamente"), Err(e) => println!("No se pudo escribir: {e}"), }}fs::write crea el archivo si no existe y, si ya existe, lo sobrescribe por completo. Tenlo en cuenta si lo que necesitas es añadir contenido a un archivo en lugar de reemplazarlo.
Añadir al final de un archivo
Sección titulada «Añadir al final de un archivo»Para añadir contenido a un archivo existente sin sobrescribirlo, usa 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, "Una nueva línea en el registro")?; Ok(())}Fíjate en que aquí el propio main devuelve un Result. Es un patrón que conviene conocer: cuando main devuelve un Result, puedes usar ? directamente dentro de él en lugar de envolverlo todo en un match.
Leer línea a línea
Sección titulada «Leer línea a línea»Con un archivo grande, cargarlo entero en memoria de una vez no siempre es buena idea. En su lugar, puedes leerlo línea a línea:
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(())}Comprobar qué error ocurrió
Sección titulada «Comprobar qué error ocurrió»Hasta ahora, todos los errores se trataban igual: imprimiéndolos. Pero muchas veces quieres reaccionar de forma distinta según qué salió mal. Que falte un archivo suele estar bien (quizá es la primera ejecución y simplemente empiezas de cero). “Permiso denegado”, en cambio, es un problema real que deberías comunicar.
Un io::Error puede decirte su categoría con .kind(), que devuelve un enum ErrorKind (en inglés) sobre el que puedes hacer match:
use std::fs;use std::io::ErrorKind;
fn main() { // un archivo en la carpeta temporal del sistema que no existe let path = std::env::temp_dir().join("rusttutorial-missing-notes.txt");
match fs::read_to_string(&path) { Ok(text) => println!("Se cargaron {} líneas", text.lines().count()), Err(e) => match e.kind() { ErrorKind::NotFound => println!("Todavía no hay notas, empezamos de cero"), ErrorKind::PermissionDenied => println!("No tienes permiso para leer el archivo de notas"), _ => println!("Algo más salió mal: {e}"), }, }}Salida:
Todavía no hay notas, empezamos de cerostd::env::temp_dir() te da la ruta de la carpeta temporal del sistema (como /tmp en Linux), y .join(...) le añade un nombre de archivo. Las funciones de archivos aceptan una ruta así igual de bien que un &str.
Deja siempre un brazo _ al final: ErrorKind tiene muchas variantes, y con el tiempo se añaden otras nuevas. El brazo _ atrapa todo lo que no hayas listado.
Práctica
Sección titulada «Práctica»Los dos ejercicios escriben primero su propio archivo en la carpeta temporal, así que funcionan en cualquier sitio, incluido el Playground.
1. Guardar y contar líneas
Sección titulada «1. Guardar y contar líneas»Escribe tres tareas pendientes en un archivo con fs::write, una por línea. Después vuelve a leer el archivo con fs::read_to_string e imprime cada línea con un número delante y, al final, el total.
Pista: pon \n entre las tareas para separarlas en líneas. text.lines() te da las líneas de una en una. Haz que main devuelva std::io::Result<()> para poder usar ?.
Ver solución
use std::fs;
fn main() -> std::io::Result<()> { let path = std::env::temp_dir().join("rusttutorial-todo.txt");
fs::write(&path, "aprender ownership\nescribir un módulo\nleer un archivo\n")?;
let text = fs::read_to_string(&path)?; let mut count = 0; for line in text.lines() { count += 1; println!("{count}. {line}"); } println!("{count} tareas"); Ok(())}Salida:
1. aprender ownership2. escribir un módulo3. leer un archivo3 tareasComo fs::write reemplaza el archivo entero, obtienes las mismas tres líneas sin importar cuántas veces lo ejecutes. Cada ? devuelve el error desde main si falla la escritura o la lectura.
2. Añadir a un registro
Sección titulada «2. Añadir a un registro»Empieza un archivo de registro (log) desde cero con una línea de encabezado y luego añade dos líneas más con OpenOptions y writeln!. Por último, lee el archivo completo e imprímelo.
Pista: usa fs::write para el encabezado (así cada ejecución empieza limpia) y luego OpenOptions::new().append(true).open(&path)? para las líneas extra. print! (no println!) evita una línea en blanco de más al final, porque el archivo ya termina con un salto de línea.
Ver solución
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, "=== registro ===\n")?; // empezar de cero
let mut file = OpenOptions::new().append(true).open(&path)?; writeln!(file, "iniciado")?; writeln!(file, "terminado")?;
print!("{}", fs::read_to_string(&path)?); Ok(())}Salida:
=== registro ===iniciadoterminadouse std::fs::{self, OpenOptions}; trae a la vez el propio módulo fs y OpenOptions. Sin el primer fs::write, cada ejecución seguiría añadiendo líneas al archivo anterior y el registro crecería sin fin. Eso es justo lo que quieres en un registro real, pero no en un ejercicio con una salida fija.
Resumen
Sección titulada «Resumen»fs::read_to_string(path)lee un archivo completo en unStringy devuelve unResult.fs::write(path, contents)escribe un archivo y lo sobrescribe si ya existe.OpenOptions::new().append(true)te permite añadir contenido a un archivo en lugar de reemplazarlo.main() -> std::io::Result<()>te permite usar?directamente enmain, sin hacermatchen cada llamada.e.kind()te dice qué tipo de error ocurrió, comoErrorKind::NotFound, para que puedas tratar cada uno de forma distinta.- Los ejemplos que usan archivos de la carpeta de tu proyecto es mejor probarlos en tu equipo;
std::env::temp_dir()te da una carpeta que funciona en cualquier sitio.
Comprueba lo que aprendiste
Sección titulada «Comprueba lo que aprendiste»Comprueba lo aprendido
1. ¿Qué pasa si llamas a fs::write con una ruta que ya existe?
2. ¿Cómo añades contenido a un archivo en lugar de sobrescribirlo?
3. ¿Qué te permite que main devuelva std::io::Result<()>?
4. ¿Qué hace el operador ? cuando fs::read_to_string(path)? falla?
5. ¿Por qué leer un archivo grande línea a línea con BufReader en lugar de usar fs::read_to_string?
Puntuación: 0 / 5
Para leer más
Sección titulada «Para leer más»std::fs— documentación de la biblioteca estándar (en inglés): todas las funciones para archivos y carpetas, comocreate_dir_all,copy,renameyremove_file.- Reading a File — The Rust Book (en inglés): cómo leer un archivo dentro del proyecto
minigrepdel libro, que combina esta página con los argumentos de línea de comandos. - File I/O — Rust by Example (en inglés): ejemplos ejecutables para abrir, crear y leer archivos línea a línea.
OpenOptions— documentación de la biblioteca estándar (en inglés): todas las formas de abrir un archivo (añadir, crear, truncar, etc.).io::ErrorKind— documentación de la biblioteca estándar (en inglés): la lista completa de categorías de error con las que puedes hacermatch.