18  Entrada/Salida

18.1 Entrada y salida por consola

En los capítulos anteriores hemos utilizado repetidamente las macros print! y println! para escribir información en la consola. En esta sección veremos cómo leer datos introducidos por el usuario mediante el teclado.

use std::io;

fn main() {
    let mut nombre = String::new();

    println!("¿Cómo te llamas?");

    io::stdin()
        .read_line(&mut nombre)
        .expect("Error al leer la entrada");

    println!("Hola, {}.", nombre.trim());
}
  • Las funciones relacionadas con la entrada y salida por consola se encuentran en el módulo std::io. Por ello, el primer paso consiste en importar dicho módulo mediante la sentencia use.

  • La función io::stdin() devuelve un objeto que representa la entrada estándar del programa. El método read_line() lee una línea completa de texto y la añade a un objeto de tipo String.

  • La variable nombre debe declararse como mutable porque read_line() modifica su contenido. El método recibe una referencia mutable (&mut String) en la que almacena los caracteres leídos.

  • El método expect() se aplica sobre el valor devuelto por read_line(), que es de tipo Result. Si la lectura se realiza correctamente, el programa continúa su ejecución. En caso contrario, finaliza mostrando el mensaje indicado. Tal y como vimos en Sección 17.5.

    En un programa interactivo es poco frecuente que read_line() produzca un error. Sin embargo, Rust obliga a tratar esta posibilidad porque la entrada estándar no siempre corresponde al teclado.

    Un programa puede recibir los datos desde un fichero, desde otro programa o incluso ejecutarse en un entorno sin un terminal interactivo, como un servicio o un contenedor. En estas situaciones, la entrada estándar puede no estar disponible o producirse algún error durante la lectura. Por ello, el método read_line() devuelve un valor de tipo Result y Rust exige que el programa trate explícitamente la posibilidad de error.

  • Al pulsar la tecla Intro, el salto de línea también pasa a formar parte de la cadena. En muchas ocasiones interesa eliminarlo antes de utilizar el texto. El método trim() devuelve una vista de la cadena sin los espacios en blanco situados al principio y al final, incluido el salto de línea final.

18.2 Escritura de ficheros

La forma más sencilla de escribir un fichero consiste en utilizar la función std::fs::write(). Esta función crea el fichero si no existe y sobrescribe su contenido si ya existe.

En los ejemplos de este capítulo utilizaremos el directorio /tmp, presente en los sistemas Linux. Se trata de un directorio destinado a almacenar ficheros temporales, por lo que resulta adecuado para realizar pruebas. Su contenido puede eliminarse automáticamente al reiniciar el sistema o mediante tareas periódicas de limpieza.

use std::fs;

fn main() {
    let texto = "Rust es un lenguaje moderno.";

    fs::write("/tmp/mensaje.txt", texto).expect("Error al escribir el fichero");

    println!("Fichero escrito correctamente.");
}

La función write() recibe dos argumentos: el nombre del fichero y los datos que se desean escribir. El valor devuelto es de tipo Result, por lo que es necesario tratar la posible aparición de un error. Por ejemplo, la operación puede fallar si el usuario no tiene permisos de escritura sobre el directorio o si el disco está lleno.

Si el nombre del fichero no incluyera una ruta, por ejemplo "mensaje.txt", el fichero se crearía en el directorio de trabajo actual del programa. Cuando un proyecto se ejecuta mediante cargo run, dicho directorio suele ser el directorio raíz del proyecto, es decir, donde se encuentra el fichero Cargo.toml. Por tanto, el fichero aparecería junto a este.

En el caso de Rust Playground, el programa se ejecuta en un entorno temporal gestionado por el servidor. Aunque el programa puede crear ficheros durante su ejecución, estos no permanecen almacenados una vez finaliza.

18.3 Lectura de ficheros

Para leer el contenido completo de un fichero puede utilizarse la función std::fs::read_to_string().

use std::fs;

fn main() {
    let contenido = fs::read_to_string("/tmp/mensaje.txt")
        .expect("Error al leer el fichero");

    println!("{}", contenido);
}

La función read_to_string() recibe el nombre del fichero y devuelve su contenido en un objeto de tipo String. Al igual que ocurre con write(), el valor devuelto es de tipo Result, ya que la lectura puede fallar por distintos motivos, como la inexistencia del fichero o la falta de permisos para acceder a él.

Si ejecutamos este programa en el Rust Playground, la lectura fallará porque el fichero escrito en una ejecución anterior no se conserva. Cada ejecución tiene lugar en un entorno temporal e independiente.

18.4 Propagación de errores en E/S.

En los ejemplos anteriores hemos utilizado el método expect() para finalizar el programa cuando se produce un error. Como vimos en Sección 17.3, otra posibilidad consiste en propagar el error al código que llamó a la función mediante el operador ?.

use std::fs;
use std::io;

fn main() -> io::Result<()> {
    let contenido = fs::read_to_string("/tmp/mensaje.txt")?;
    println!("{}", contenido);
    Ok(())
}

El operador ? se aplica sobre un valor de tipo Result. Si la operación tiene éxito, extrae el valor contenido en Ok y la ejecución continúa normalmente. Si se produce un error, la función finaliza inmediatamente devolviendo dicho error.

Es importante recordar que una función solo puede utilizar el operador ? si devuelve un tipo de error compatible con el de la operación que está realizando. En este ejemplo, read_to_string() devuelve un io::Result<String>, por lo que main() debe devolver también un io::Result<()> (o un tipo compatible).

En otro caso, el compilador produciría un error. Este es uno de los problemas más habituales cuando se comienza a utilizar el operador ?.

18.5 Resumen

En este capítulo hemos introducido las operaciones básicas de entrada y salida que proporciona la biblioteca estándar de Rust.

Las ideas principales son las siguientes:

  • La entrada por consola se realiza a través de std::io, utilizando normalmente el método read_line().
  • La función read_line() almacena el texto leído en un objeto String y devuelve un valor de tipo Result.
  • El método trim() resulta útil para eliminar el salto de línea que se añade al pulsar la tecla Intro.
  • Las funciones std::fs::write() y std::fs::read_to_string() permiten escribir y leer ficheros de texto de forma sencilla.
  • Las operaciones de entrada y salida pueden fallar, por lo que devuelven un Result que debe tratarse explícitamente.
  • Cuando una función devuelve un tipo de error compatible, el operador ? permite propagar los errores de forma concisa.

Aunque la biblioteca estándar ofrece mecanismos mucho más potentes para trabajar con ficheros y flujos de datos, las operaciones estudiadas en este capítulo son suficientes para una gran parte de los programas y constituyen la base sobre la que se apoyan técnicas más avanzadas.